Programando con áreas de memoria

Casi todo desarrollador que ha usado el lenguaje de programación C se ha exasperado ante la desalentadora tarea de gestionar la memoria. Reservar suficiente memoria para un uso, llevar la pista de estas reservas, liberar la memoria cuando ya no se necesita—estas tareas pueden ser bastante complejas. Y por supuesto, errar en estas operaciones puede llevar a un programa que se cuelga, o peor, que cuelga todo el ordenador. Afortunadamente, la librería APR, de la que depende Subversion para su portabilidad, proporciona el tipo de dato apr_pool_t, el cual representa un área de la cual la aplicación puede reservar memoria.

Un área de memoria es una representación abstracta de un bloque de memoria reservado para ser usado por un programa. En lugar de pedir memoria directamente al SO usando las funciones estándar malloc() y compañía, los programas que enlazan con APR pueden simplemente solicitar que se cree un área de memoria (usando la función apr_pool_create()). APR reservará un trozo de memoria de tamaño moderado del SO, y esa memoria será disponible instantáneamente para ser usada por el programa. En cualquier momento que el programa necesite algo de memoria, usará una de las funciones de área de memoria de la API APR, como apr_palloc(), la cual devuelve una dirección genérica de memoria que apunta dentro del área. El programa puede seguir pidiendo bits y trozos de memoria del área, y APR seguirá accediendo a las demandas. Las áreas de memoria crecen automáticamente en tamaño para acomodarse a los programas que solicitan más memoria que la contenida inicialmente por el área, hasta que por supuesto, no quede más memoria disponible en el sistema.

Ahora bien, si este fuese el final de la historia del área de memoria, difícilmente se hubiese merecido atención especial. Afortunadamente este no es el caso. Las áreas no sólo pueden ser creadas; también pueden ser limpiadas y destruidas, usando apr_pool_clear() y apr_pool_destroy() respectivamente. Esto proporciona a los desarrolladores la flexibilidad de reservar varios—o varios miles—elementos del área, ¡y entonces limpiar toda esa memoria con una simple llamada! Además, las áreas de memoria tienen jerarquía. Puede hacer sub áreas de cualquier área de memoria previamente creada. Cuando limpia un área, todas sus sub áreas son destruidas; si destruye un área, ésta y sus sub áreas serán destruidas.

Antes de continuar, los desarrolladores deben tener presente que probablemente no encontrarán muchas llamadas a las funciones de áreas de memoria APR que acabamos de mencionar en el código fuente de Subversion. Las áreas de memoria APR ofrecen algunos mecanismos de extensión, como la posibilidad de tener datos de usuario personalizados asociados al área de memoria, y mecanismos para registrar funciones de limpieza que serán llamadas cuando el área de memoria sea destruida. Subversion hace uso de estas extensiones de un modo no trivial. Por lo que Subversion proporciona (y la mayoría del código usa) las funciones de envoltorio svn_pool_create(), svn_pool_clear(), y svn_pool_destroy().

Aunque las áreas de memoria son útiles para tareas básicas de gestión de memoria, este tipo de estructura realmente brilla con los bucles y la recursividad. Dado que los bucles a menudo pueden terminar en cualquier momento , y las funciones recursivas volver a cualquier profundidad, el consumo de memoria en este tipo de situaciones puede ser impredecible. Afortunadamente, usar áreas de memoria anidadas puede ser un método excelente para tratar con estas situaciones peliagudas. El siguiente ejemplo demuestra el uso básico de áreas de memoria anidadas en una situación que es bastante común—recorrer de forma recursiva un árbol de directorios realizando cierta tarea sobre cada elemento del árbol.

Ejemplo 8.5. Uso efectivo de áreas de memoria

/* Recursively crawl over DIRECTORY, adding the paths of all its file
   children to the FILES array, and doing some task to each path
   encountered.  Use POOL for the all temporary allocations, and store
   the hash paths in the same pool as the hash itself is allocated in.  */
static apr_status_t 
crawl_dir (apr_array_header_t *files,
           const char *directory,
           apr_pool_t *pool)
{
  apr_pool_t *hash_pool = files->pool;  /* array pool */
  apr_pool_t *subpool = svn_pool_create (pool);  /* iteration pool */
  apr_dir_t *dir;
  apr_finfo_t finfo;
  apr_status_t apr_err;
  apr_int32_t flags = APR_FINFO_TYPE | APR_FINFO_NAME;

  apr_err = apr_dir_open (&dir, directory, pool);
  if (apr_err)
    return apr_err;

  /* Loop over the directory entries, clearing the subpool at the top of
     each iteration.  */
  for (apr_err = apr_dir_read (&finfo, flags, dir);
       apr_err == APR_SUCCESS;
       apr_err = apr_dir_read (&finfo, flags, dir))
    {
      const char *child_path;

      /* Clear the per-iteration SUBPOOL.  */
      svn_pool_clear (subpool);

      /* Skip entries for "this dir" ('.') and its parent ('..').  */
      if (finfo.filetype == APR_DIR)
        {
          if (finfo.name[0] == '.'
              && (finfo.name[1] == '\0'
                  || (finfo.name[1] == '.' && finfo.name[2] == '\0')))
            continue;
        }

      /* Build CHILD_PATH from DIRECTORY and FINFO.name.  */
      child_path = svn_path_join (directory, finfo.name, subpool);

      /* Do some task to this encountered path. */
      do_some_task (child_path, subpool);

      /* Handle subdirectories by recursing into them, passing SUBPOOL
         as the pool for temporary allocations.  */
      if (finfo.filetype == APR_DIR)
        {
          apr_err = crawl_dir (files, child_path, subpool);
          if (apr_err)
            return apr_err;
        }

      /* Handle files by adding their paths to the FILES array.  */
      else if (finfo.filetype == APR_REG)
        {
          /* Copy the file's path into the FILES array's pool.  */
          child_path = apr_pstrdup (hash_pool, child_path);

          /* Add the path to the array.  */
          (*((const char **) apr_array_push (files))) = child_path;
        }
    }

  /* Destroy SUBPOOL.  */
  svn_pool_destroy (subpool);

  /* Check that the loop exited cleanly. */
  if (apr_err)
    return apr_err;

  /* Yes, it exited cleanly, so close the dir. */
  apr_err = apr_dir_close (dir);
  if (apr_err)
    return apr_err;

  return APR_SUCCESS;
}

El ejemplo anterior demuestra un uso efectivo de áreas de memoria en situaciones con bucles y recursividad. Cada recursión comienza creando una sub área de memoria del área pasada a la función. Esta sub área es usada por la región con bucles, y limpiada en cada iteración. Esto da como resultado que el consumo de memoria sea a grandes rasgos proporcional a la profundidad de la recursión, no al número total de ficheros y directorios presentes como hijos del directorio de nivel superior. Cuando la primera llamada a esta función recursiva finalmente termina, hay realmente muy pocos datos almacenados en el área de memoria que recibió como parámetro. ¡Ahora imagine la complejidad adicional que estaría presente en esta función si tuviese que reservar y liberar cada pieza individual de datos usados!

Quizás las áreas de memoria no sean ideales para todas las aplicaciones, pero son extremadamente útiles en Subversion. Como desarrollador de Subversion, tendrá que acostumbrarse a las áreas de memoria y a su uso correcto. Los fallos relacionados con el uso de memoria y exceso de código pueden ser difíciles de diagnosticar y corregir se la API que sea, pero la estructura de área de memoria proporcionada por APR ha resultado ser una funcionalidad tremendamente conveniente y ahorradora de tiempo.