Almost every developer who has used the C programming language has at some point sighed at the daunting task of managing memory usage. Allocating enough memory to use, keeping track of those allocations, freeing the memory when you no longer need it—these tasks can be quite complex. And of course, failure to do those things properly can result in a program that crashes itself, or worse, crashes the computer. Fortunately, the APR library that Subversion depends on for portability provides the apr_pool_t type, which represents a pool from which the application may allocate memory.
Quasi ogni sviluppatore che ha utilizzato il linguaggio di programmazione C a qualche punto ha sospirato allo scoraggiante compito di gestire l'uso della memoria. Allocare abbastanza memoria da utilizzare, mantenere traccia di quelle allocazioni, liberare la memoria quando non è più necessaria—questi compiti possono essere abbastanza complessi. E naturalmente, fallire a fare queste cose correttamente può risultare in un programma che va in crash, o peggio, fa andare in crash il computer. Fortunatamente, la libreria APR da cui Subversion dipende per portabilità fornisce il tipo apr_pool_t, che rappresenta un pool dal quale l'applicazione può allocare memoria.
A memory pool is an abstract representation of a chunk of
memory allocated for use by a program. Rather than requesting
memory directly from the OS using the standard
malloc()
and friends, programs that link
against APR can simply request that a pool of memory be created
(using the apr_pool_create()
function).
APR will allocate a moderately sized chunk of memory from the
OS, and that memory will be instantly available for use by the
program. Any time the program needs some of the pool memory, it
uses one of the APR pool API functions, like
apr_palloc()
, which returns a generic
memory location from the pool. The program can keep requesting
bits and pieces of memory from the pool, and APR will keep
granting the requests. Pools will automatically grow in size to
accommodate programs that request more memory than the original
pool contained, until of course there is no more memory
available on the system.
Un pool di memoria è una rappresentazione astratta di un pezzo di
memoria allocata per l'uso da parte di un programma. Piuttosto che richiedere
la memoria direttamente al sistema operativo utilizzando la funzione
standard malloc()
e simili, i porgrammi che si
collegano a APR possono semplicemente richiedere che venga creato un pool
di memoria (utilizzando la funzione apr_pool_create()
).
APR allocherà un pezzo di memoria di moderata grandezza dal sistema operativo,
e questa memoria diverrà istantaneamente disponibile per l'uso da parte del
programma. Ogni volta che il programma necessita di qualche pool di memoria,
utilizza una delle funzioni dell'API per i pool di APR, come
apr_palloc()
, che ritorna una posizione di memoria
generica dal pool. Il programma può continuare a chiedere bit e pezzi di
memoria dal pool, e APR continuerà a
esaudire le richieste. I pool cresceranno automaticamente in grandezza
per accogliere i programmi che richiedono più memoria di quella che il pool
originale conteneva, naturalmente fino a quando non ci sia più memoria
disponibile nel sistema.
Now, if this were the end of the pool story, it would hardly
have merited special attention. Fortunately, that's not the
case. Pools can not only be created; they can also be cleared
and destroyed, using apr_pool_clear()
and
apr_pool_destroy()
respectively. This
gives developers the flexibility to allocate several—or
several thousand—things from the pool, and then clean up
all of that memory with a single function call! Further, pools
have hierarchy. You can make “subpools” of any
previously created pool. When you clear a pool, all of its
subpools are destroyed; if you destroy a pool, it and its
subpools are destroyed.
Ora, se questa fosse stata la fine della storia dei pool, a stento
avrebbe meritato speciale attenzione. Fortunatamente, questo non è il
caso. I pool possono non solo essere creati; possono anche essere puliti
e distrutti, utilizzando rispettivamente apr_pool_clear()
e
apr_pool_destroy()
. Questo
da agli sviluppatori la flessibilità di allocare varie—o varie
migliaia—cose dal pool, e poi pulire tutta
questa memoria con una singola chiamata a funzione! Inoltre, i pool
hanno una gerarchia. Si possono fare dei «sotto pool» di ogni
pool creato precedentemente. Quando si pulisce un pool, tutti i suoi
sotto pool vengono distrutti; se si distrugge un pool, questo e i suoi
sotto pool vengono distrutti.
Before we go further, developers should be aware that they
probably will not find many calls to the APR pool functions we
just mentioned in the Subversion source code. APR pools offer
some extensibility mechanisms, like the ability to have custom
“user data” attached to the pool, and mechanisms
for registering cleanup functions that get called when the pool
is destroyed. Subversion makes use of these extensions in a
somewhat non-trivial way. So, Subversion supplies (and most of
its code uses) the wrapper functions
svn_pool_create()
,
svn_pool_clear()
, and
svn_pool_destroy()
.
Prima di andare oltre, gli sviluppatori dovrebbero essere informati che
probabilmente non troveranno nel codice sorgente di Subversion molte delle
chiamate alle funzioni di APR per i pool che abbiamo appena menzionato.
I pool di APR offrono qualche meccanismo di estendibilità, come l'abilità
di avere «dati utente» personalizzati attaccati al pool, e
meccanismi per registrare funzioni di pulizia che possono essere chiamate
quando il pool viene distrutto. Subversion fa uso di queste estensioni in
un modo piuttosto non banale. Così, Subversion fornisce (e la maggior parte
del proprio codice utilizza) le funzioni di avvolgimento
svn_pool_create()
,
svn_pool_clear()
, e
svn_pool_destroy()
.
While pools are helpful for basic memory management, the pool construct really shines in looping and recursive scenarios. Since loops are often unbounded in their iterations, and recursions in their depth, memory consumption in these areas of the code can become unpredictable. Fortunately, using nested memory pools can be a great way to easily manage these potentially hairy situations. The following example demonstrates the basic use of nested pools in a situation that is fairly common—recursively crawling a directory tree, doing some task to each thing in the tree.
Mentre i pool sono utili per una gestione di base della memoria, la costruzione dei pool brilla realmente negli scenari di ciclo e ricorsione. Poiché i cicli sono spesso illimitati nelle loro iterazioni, e le ricorsioni nella loro profondità, il consumo di memoria in queste aree del codice può diventare non predicibile. Fortunatamente, utilizzando pool di memoria annidati può essere un buon modo per gestire facilmente queste situazioni potenzialmente pericolose. L'esempio seguente dimostra l'uso di base dei pool annidati in una situazione che è ragionevolmente comune—l'esplorazione ricorsiva di un albero di directory, eseguendo qualche compito per ogni cosa nell'albero.
Esempio 8.5. Effective Pool Usage
/* 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; }
The previous example demonstrates effective pool usage in
both looping and recursive situations.
Each recursion begins by making a subpool of the pool passed to
the function. This subpool is used for the looping region, and
cleared with each iteration. The result is memory usage is
roughly proportional to the depth of the recursion, not to total
number of file and directories present as children of the
top-level directory. When the first call to this recursive
function finally finishes, there is actually very little data
stored in the pool that was passed to it. Now imagine the extra
complexity that would be present if this function had to
alloc()
and free()
every single piece of data used!
L'esempio precedente dimostra l'effettivo utilizzo del pool in
entrambe le situazioni di ciclo e ricorsione.
Ogni ricorsione inizia creando un sotto pool del pool passato alla
funzione. Questo sotto pool è utilizzato per la regione di ciclo, e
pulito con ogni iterazione. Il risultato di utilizzo della memoria è
approssimativamente proporzionale alla profondità della ricorsione, non il
numero totale di file e directory presenti come figli della directory
di più alto livello. Quando la prima chiamata a questa funzione ricorsiva
finalmente finisce, attualmente c'è un dato molto piccolo immagazzinato
nel pool che le è stato passato. Ora immaginate la complessità ulteriore
che sarebbe presente se questa funzione dovesse usare
alloc()
e free()
ogni singolo pezzo di dati utilizzati!
Pools might not be ideal for every application, but they are extremely useful in Subversion. As a Subversion developer, you'll need to grow comfortable with pools and how to wield them correctly. Memory usage bugs and bloating can be difficult to diagnose and fix regardless of the API, but the pool construct provided by APR has proven a tremendously convenient, time-saving bit of functionality.
I pool possono non essere l'ideale per ogni applicazione, ma sono estremamente utili in Subversion. Come sviluppatore di Subversion, occorre prendere confidenza con i pool e come maneggiarli correttamente. Gli errori di utilizzo della memoria e la sua crescita possono essere difficili da diagnosticare e correggere malgrado l'API, ma la costruzione del pool fornita da APR è risultata tremendamente conveniente, facendo risparmiare tempo in bit di funzionalità.