Pour information : regardez le tutoriel au complet, en Anglais, ici.
Vous trouverez tout ce tutoriel séparé en plusieurs pages ici.
3. memory pool (apr_pool_t)
La plupart des fonctions de libapr sont dépendants de pools mémoire. Grâce aux pools mémoire, vous pouvez facilement gérer des groupes de portions de mémoire.
Imaginez un cas sans le système de pool mémoire, où vous devez allouer chaque portion de mémoire. Vous devez libérer chacun d’eux. Si vous avez fait dix allocations, vous devez faire dix libérations, sinon vous allez avoir des fuites mémoire. Le principe de pool mémoire résout ce genre de problème. Après avoir crée un pool mémoire, vous pouvez allouer autant de morceaux de mémoire que vous voulez sur ce pool. Pour tout libérer, la seule chose que vous ayez à faire est de détruire le pool mémoire. Le pool s’occupe de libérer le reste.
Il y a deux gros avantages :
- Comme expliqué juste avant, cela évite les fuites mémoire ;
- Ensuite, le coût machine d’allocation est réduit.
D’une certaine façon, vous devez tout de même programmer comme si vous étiez dans une session. Un pool mémoire est un peu comme un contexte de session, c’est à dire que tous les objets qui y sont liés ont la même durée de vie. Vous pouvez contrôler un groupe d’objets dans un contexte de session. Au début d’une session, vous créez un pool mémoire. Ensuite, vous créez des objets sur ce pool mémoire pendant cette session. Notez que vous n’avez pas à vous préoccuper de leur durée de vie. A la fin de la session, la seule chose que vous ayez à faire est de libérer le pool mémoire.
REMARQUE : En général, le contrôle de la durée de vie des objets est la chose la plus difficile en programmation. C’est pour cela qu’il y a plein d’autres techniques pour cela, telles que les « pointeurs intelligents, le ramasse-miettes (GC – garbage collector) etc. Il est difficile d’utiliser de telles techniques en même temps. Comme le pool mémoire est l’une de ces techniques, il vous faire attention aux mélanges !
Il y a trois fonctions principales de l’API, concernant les pools mémoire :
/* extrait de apr_pools.h */
APR_DECLARE(apr_status_t) apr_pool_create(apr_pool_t **newpool, apr_pool_t *parent);
APR_DECLARE(void *) apr_palloc(apr_pool_t *p, apr_size_t size);
APR_DECLARE(void) apr_pool_destroy(apr_pool_t *p);
On crée un pool mémoire via apr_pool_create(). Ce pool existe tant qu’on ne l’a pas libéré via apr_pool_destroy(). Le premier argument de apr_pool_create() est un argument-resultat. Un nouvel objet « pool mémoire », apr_pool_t, est renvoyé par cet appel à l’API. On appelle apr_palloc() pour allouer une portion de mémoire en précisant la taille. Jetez un coup d’oeil à mp-sample.c pour voir un exemple simple.
/* extrait de mp-sample.c */
apr_pool_t *mp;
/* créer un pool mémoire. */
apr_pool_create(&mp, NULL);
/* allouer une portion mémoire sur le pool */
char *buf1;
buf1 = apr_palloc(mp, MEM_ALLOC_SIZE);
En résumé, on peut utiliser apr_palloc() comme malloc(3). On peut aussi appeler apr_pcalloc(). Comme vous l’imaginez, apr_pcalloc() ressemble fortement à calloc(3). apr_pcalloc() renvoie une portion mémoire intégralement initialisée avec des zéro. Si vous vous servez de malloc(3)/calloc(3), vous aurez besoin d’appeler free(3) pour chaque portion allouée. A l’inverse, vous n’avez pas besoin de faire ça pour chaque portion allouée sur un pool mémoire. Vous appelez simplement apr_pool_destroy() avec le pool concerné et tout ce qui a été alloué est libéré.
REMARQUE : Il n’y a pas de limitation sur la taille que vous pouvez allouer avec apr_palloc(). Néanmoins, ce n’est pas une bonne idée d’allouer de gros morceaux de mémoire avec une gestion de mémoire orienté « pool ». C’est simplement parce que la gestion via un pool mémoire a été conçue pour gérer efficacement des petites allocations. En réalité, la taille initiale d’un pool mémoire est de de 8 kb. Si vous avez besoin d’allouer quelque chose de plus large, p.ex. plusieurs méga bytes, vous devriez éviter la gestion via un pool mémoire.
REMARQUE : Par défaut, un manager de pool mémoire ne libère jamais la mémoire allouée au système tant qu’il n’est pas détruit. Si un programme tourne pendant longtemps, il aura des problèmes. Je vous conseille de préciser dès le début une taille limite maximale comme suit :
/* exemple qui applique une limite haute pour forcer le
* manager du pool à libérer de la mémoire au système
* à partir d'un certain seuil
*/
#define YOUR_POOL_MAX_FREE_SIZE 32 /* taille max. du pool */
apr_pool_t *mp;
apr_pool_create(&mp, NULL);
apr_allocator_t* pa = apr_pool_allocator_get(mp);
if (pa) {
apr_allocator_max_free_set(pa, YOUR_POOL_MAX_FREE_SIZE);
}
Il y a deux autres fonctions de l’API que vous devez connaitre. L’une d’elles est apr_pool_clear(), et l’autre est apr_pool_cleanup_register(). apr_pool_clear() ressemble à apr_pool_destroy(), mais le pool mémoire est toujours réutilisable. Le code typique est le suivant :
/* exemple montrant comment fonctionne apr_pool_clear() */
apr_pool_t *mp;
apr_pool_create(&mp, NULL);
for (i = 0; i < n; ++i) {
do_operation(..., mp);
apr_pool_clear(mp);
}
apr_pool_destroy(mp);
Le pool mémoire est utilisé dans do_operation(), qui, imaginons, alloue plein de morceaux mémoire. Si vous n’avez pas besoin de tous les morceaux alloués hors de do_operation(), vous pouvez appeler apr_pool_clear(). Ainsi vous limiterez l’utilisation mémoire. Si vous connaissez bien le principe du fonctionnement du système de la pile locale, vous pouvez imaginer que la gestion de pool mémoire est une « pile mémoire locale ». L’appel de apr_palloc() est similaire au fait de déplacer SP(stack pointer), et appeler apr_pool_clear() est similaire à faire reculer SP. Les deux opérations sont très légères en termes de calcul CPU.
La fonction apr_pool_cleanup_register() offre la possibilité d’appeler automatiquement des fonctions de libération lorsque le pool est nettoyé via apr_pool_clear() ou détruit vie apr_pool_destroy(). Dans les fonctions de callback, vous pouvez implémenter un code de libération de vos objets, ou de finalisation, liés au nettoyage/destruction du pool mémoire.
La dernière chose concernant les pools mémoire est le sous-pool. Chaque pool a la possibilité d’avoir un parent. Pour être plus précise, les pools mémoire sont organisés dans un arbre. Le deuxième argument de apr_pool_create() est le parent du pool mémoire à créer. Si vous passez NULL en tant que parent, le nouveau pool est positionné à la racine de l’arbre. Vous pouvez créer des « sous-pools » mémoire en les liant à ce dernier. Quand vous appelez apr_pool_destroy() pour un pool mémoire dans l’arbre, tous ses descendants sont aussi détruits. Quand vous appelez apr_pool_clear(), le pool mémoire reste en vie, il est simplement nettoyé, mais les enfants sont détruits. Quand un enfant est détruit, les fonctions callback liées au nettoyage/destruction dont on a parlé précédemment sont appelées.
REMARQUE : Le bogue suivant est très courant : vous passez NULL comme paramètre pool à la fonction callback de nettoyage. Il faut plutôt utiliser apr_pool_cleanup_null, comme le montre l’exemple :
/* pseudo code sur le bogue habituel de pool mémoire */
/* apr_pool_cleanup_register(mp, CONTEXTE_DE_VOTRE_CODE, CALLBACK_DE_VOTRE_CODE, NULL); C'EST UN BOGUE */
/* Version corrigée : */
apr_pool_cleanup_register(mp, CONTEXTE_DE_VOTRE_CODE, CALLBACK_DE_VOTRE_CODE, apr_pool_cleanup_null);
4. Error status (apr_status_t)
La plupart des fonctions de la librairie libapr renvoient une valeur de type apr_status_t. La valeur apr_status_t est très souvent, soit APR_SUCCESS soit autre chose. APR_SUCCESS indique que tout s’est correctement déroulé. Le code typique ressemble à cela :
/* pseudo code sur la vérification du retour apr_status_t */
apr_status_t rv;
rv = apr_pool_create(&mp, NULL);
if (rv != APR_SUCCESS) {
/* Gestion de l'erreur */;
}
libapr définit quelques statuts d’erreur tels que APR_EINVAL, et des macros pour vérifier des types d’erreurs telles que APR_STATUS_IS_ENOMEM((). Quelques unes d’entre elles sont vraiment très utiles, surtout pour la gestion des problèmes de portabilité. Un exemple typique est la macro APR_STATUS_IS_EAGAIN(). Historiquement, il y a deux erreurs qui ont le même chiffre mais pas la même signification, EAGAIN et EWOULDBLOCK. La macro APR_STATUS_IS_EAGAIN() gère ce problème.
Néanmoins, c’est presque impossible de gérer toutes les erreurs sans prendre en compte le système sous-jacent. La librairie libapr ne réinvente pas la roue. Ce qu’elle fait est très simple :
- En cas de succès, retour = APR_SUCCESS
- En cas d’erreur concernant uniquement la librarie elle-même, retour = APR_XXX
- En cas d’erreur système commune à tous les systèmes d’exploitation, retour = APR_XXX
- En cas d’erreur spécifique à un système d’exploitation, retour = numéro d’erreur du système plus un offset
Je vous conseille de suivre ces règles simple :
- Comparer la valeur de retour avec APR_SUCCESS
- Si vous avez besoin de plus de détails sur l’erreur, comparer avec d’autres valeurs d’erreur définies dans la librairie
Un fonction extrêmement utile est apr_strerror(). Vous pouvez afficher la chaine décrivant l’erreur de cette façon :
/* pseudo code expliquant apr_strerror() */
apr_status_t rv;
rv = apr_xxx_xxx();
if (rv != APR_SUCCESS) {
char errbuf[256];
apr_strerror(rv, buf, sizeof(buf));
/* afficher la description de l'erreur */
puts(errbuf);
}