La GLib et le support des threads dans NuFW
Signature : , | Mis en ligne le : 28/06/2008
Catégorie(s) :
  • GNU/Linux Magazine
  • | Domaine :
    Commentez

    Retrouvez cet article dans : Linux Magazine 86

    Si NuFW [1], présenté dans LMF 84 et 85, n’existe que grâce aux possibilités offertes par Netfilter, la couche pare-feu de Linux, il n’aurait jamais vu le jour sans l’aide de la GLib. Nous parlons ici de la bibliothèque GLib, fondement des applications GTK et GNOME, et non de la glibc aussi appelée " bibliothèque système ". La GLib offre une abstraction et des fonctions de haut niveau permettant d’accélérer le développement d’applications et elle excelle en particulier dans la gestion des threads.

    Présentation de la GLib

    Une boîte à outils indispensable

    Pour commencer, la GLib est publiée, probablement à tort, comme sous-projet de GTK [3], ce qui lui donne une assez faible visibilité. Glib peut (et doit !) largement être utilisée en dehors de GTK, comme nous allons tâcher de le montrer dans cet article. Bien entendu, la Glib est un Logiciel libre, publié sous licence LGPL. Le principe de base de la GLib est d’offrir une implémentation portable (et donc multi plateforme) de la plupart des objets de bases nécessaires au développement d’un logiciel. Elle apporte donc un ensemble de fonctions qui surchargent les fonctions Unix standards et augmentent les capacités de la bibliothèque système par un ensemble de plus haut niveau varié et hétéroclite : 1. Aide à l’architecture logicielle
    • mutex ;
    • opération atomique ;
    • queue asynchrone ;
    • pool de threads ;
    • gestion de la mémoire.
    2. Structures de données
    • liste chaînée (si si ;-) ;
    • table de hachage ;
    • tableau de taille dynamique.
    3. Manipulation de caractères
    • gestion de chaînes de caractères (substitution, recherche) ;
    • analyseur lexical (parseur de configuration) ;
    • parseur XML élémentaire.

    Intérêts

    Les intérêts de l’utilisation de la GLib sont nombreux :
    • Efficacité : le gain de temps procuré par la réutilisation de composants portables et/ou de haut niveau est évident.
    • Sécurité : la bibliothèque est utilisée par un nombre conséquent de projets importants. Le code est donc validé, audité et débogué.
    • Performances : les développeurs de la GLib sont sans doute meilleurs que vous et ils se sont mis à plusieurs pour optimiser leur code. Et cela fait des années qu’ils y travaillent. Libre à vous d’essayer de faire mieux mais bon courage et d’ailleurs si vous y arrivez, contribuez !
    • Portabilité : la couche d’abstraction de la GLib permet au développeur de travailler loin des spécificités de chaque noyau ou système

    Utilisation

    Pour utiliser les fonctions de la GLib, il suffit d’une part d’ajouter à ses fichiers sources et d’autre part de compléter la ligne de commande de compilation par : qui permet de rajouter ici les includes et les bibliothèques nécessaires à la GLib et aux fonctions liées aux threads. L’heureux utilisateur des autotools pourra rajouter au fichier configure.ac après avoir vérifié que glib-2.0.m4 fait bien partie des extensions m4 disponibles.

    Gestion de la mémoire

    Comme promis, la GLib fournit ses propres fonctions pour allouer ou libérer de la mémoire. Comme son nom l’indique, g_new alloue n_structs éléments de type struct_type et de plus elle retourne un pointeur de type struct_type. Le deuxième bonus offert par l’utilisation de fonctions d’allocations de la GLib est qu’il n’est pas nécessaire de vérifier son code de retour. Si l’allocation de la mémoire échoue, la Glib arrête l’application immédiatement. g_new a une sœur, g_new0, pour initialiser la mémoire allouée avec des zéros, et des cousines, comme g_malloc, pour allouer un nombre d’octets choisi. Pour libérer de la mémoire, on utilise Pas grand chose à commenter, sauf un autre petit bonus immédiat par rapport à free() : si le pointeur mem vaut NULL au moment de l’appel de g_free, la fonction rend la main immédiatement (alors que free() nous génère un segfault). Les avantages induits par l’utilisation des fonctions GLib de gestion de la mémoire peuvent paraître à ce stade légers, mais voici de quoi vous faire changer d’avis. Glib exporte également la fonction Elle écrit sur la sortie standard du programme un résumé de l’utilisation de la mémoire. g_mem_profile peut se montrer très utile lorsqu’on cherche à optimiser, ou déboguer, une application. Sont affichés :
    • la quantité de mémoire allouée depuis le démarrage du programme ;
    • la quantité de mémoire libérée ;
    • la fréquence d’allocation, selon la taille des allocations.
    Le tableau suivant est un extrait de sortie de g_mem_profile : Attention toutefois à appeler g_mem_set_vtable avant toute fonction GLib pour initialiser la génération du profil :

    Émulation de la bibliothèque de threads standard et subtilités associées

    Aperçu général

    Les threads (ou processus légers) sont similaires aux processus en cela qu’ils représentent tous l’exécution d’un ensemble d’instructions du langage machine d’un processeur. Du point de vue de l’utilisateur ces exécutions semblent se dérouler en parallèle. Toutefois, là où chaque processus possède sa propre mémoire virtuelle, les processus légers appartenant au même processus père partagent une même partie de sa mémoire virtuelle.Définition extraite de Wikipedia [5] L’API de gestion des threads avec la GLib est un port quasiment direct de la gestion des threads avec la bibliothèque pthread (bibliothèque native sous Linux). Une des seules différences est qu’il est nécessaire d’initialiser le support des threads au début de l’application par un appel à g_thread_init(NULL). Hormis ce point, l’ensemble des fonctions de base de la bibliothèque pthread se retrouve dans la GLib. Seules quelques subtilités au niveau de l’API différencient les deux bibliothèques. Ainsi la création d’un thread se fait ainsi : Le premier paramètre est la fonction qui sera appelée à la création du thread, elle aura comme seul argument datas. Le troisième permet de définir si le thread peut être joint (en fait de savoir si son créateur peut attendre sa mort avec un appel à g_thread_join). Enfin le dernier paramètre est spécifique à la GLib puisqu’il s’agit d’une structure de gestion d’erreur propre à cette bibliothèque. On retrouve donc quelque chose de similaire à la création avec pthread où on utilise : Les amateurs éclairés auront constaté une faiblesse de l’appel g_thread_create qui permet uniquement de spécifier que le thread est joignable là où pthread_create permet de spécifier beaucoup plus d’attributs (grâce au paramètre attr). Qu’ils se rassurent ! Dans la GLib, les fonctions de création d’objets sont déclinées en une version simple et une version complète (suffixée _full) permettant de gérer finement la création de l’objet en question. Ici, on peut utiliser la fonction g_thread_create_full pour avoir accès à l’ensemble des possibilités. Si le principe des threads est simple, ils restent difficiles à utiliser parce qu’il faut gérer les accès concurrents aux ressources système et en particulier à la mémoire. Le moyen de contrôler l’accès le plus évident est le mutex (ou exclusion mutuelle – MUTual EXclusion).

    Mutex

    Un mutex est une primitive de synchronisation utilisée pour éviter que des ressources non partagées d’un système soient utilisées en même temps. Par exemple, si deux threads accèdent à un même compteur et que, par malchance, les deux processus légers essayent en même temps de l’incrémenter, le compteur va prendre une valeur imprévisible. Il est donc nécessaire en programmation multithread de poser un lock sur la ressource. Dans la GLib, l’implémentation classique, héritée de la bibliothèque pthread, oblige le programmeur à initialiser les structures avant de les utiliser. Dans le cas des mutex, cela impose de tout créer en avance avant la mise en route des threads et il en résulte une certaine lourdeur. Les développeurs de la GLib ont donc implémenté les StaticMutex qui sont initialisés à la compilation et dont l’utilisation est moins contraignante. Une fonction d’incrémentation peut alors s’écrire comme suit : Il suffit donc de déclarer le GStaticMutex au début de la fonction et le tour est joué. Note : Avec un mutex standard, il aurait fallu trouver l’endroit du code situé en amont de tous les appels à give_me_next_number pour initialiser le mutex. Certains vont sans doute s’offusquer en disant : " quoi tout ça pour incrémenter un compteur ! ". Les auteurs de la GLib ont déjà dû avoir la remarque. Aussi ont-ils développé les opérations atomiques.

    Opérations atomiques

    Comme le montre l’exemple précédent, en mode multithread, accéder à un simple compteur est complexe et coûteux (utilisation du mutex et blocage des threads). Il est donc souhaitable de passer par des fonctions plus simples et plus performantes. C’est ce que proposent les opérations atomiques. Elles utilisent des implémentations en assembleur permettant de garantir l’atomicité des opérations et fournissent des fonctions permettant d’implémenter facilement et très efficacement des compteurs. Pour incrémenter, on utilise g_atomic_int_inc(&entier) et pour décrémenter et tester si on est à zéro g_atomic_int_dec_and_test(&entier).

    Locks multiples et blocages de l’application

    Dans le cas où l’on travaille sur plusieurs ressources à locker simultanément, et notamment quand la ressource qui fait l’objet du lock est une structure plus complexe qu’un simple compteur, il faut particulièrement veiller à ordonner les locks. Par exemple, si l’on écrit un thread A qui locke M1 puis M2, fait un travail sur les 2 structures protégées par ces locks, puis libère les locks, et que l’on a également un thread B qui réalise des opérations en posant les locks M2 puis M1, nous avons un problème.

    /img-articles/lm/86/art-2/fig-1.jpg

    Problème du double lock

    Dans le cas potentiellement rare où les threads A et B sont lancés au même instant, A va locker M1, et B va locker M2. Puis les 2 threads seront totalement bloqués : A attendra que B libère M2, et B attendra que A libère M1. La solution évidente est donc de définir une relation d’ordre entre les locks : " Je commence toujours par locker M1 avant M2 ". Une autre solution est de revoir l’architecture du programme pour simplifier et optimiser les performances. C’est ce que nous avons réalisé avec la réécriture de nuauth entre la version 0.8 et 1.0, en utilisant ce qui suit.

    Système de queue asynchrone

    Problématique et résolution

    La gestion des conflits lors des accès mémoire avec des mutex est difficile. Elle devient vite insoluble lorsque la complexité du code augmente. Il est donc nécessaire de trouver un moyen de limiter les accès concurrents aux structures partagées. Dans nuauth, la structure de données, qui est la clef de voûte du programme, est la table de hachage qui contient les connexions à authentifier. On peut donc considérer que toute entité accédant à la table de hachage fait partie du cœur du système. Une seule erreur et c’est la crise cardiaque. Trop de tentatives d’accès simultanées et le programme s’engorge : chaque thread passe la majeure partie de son temps à attendre un lock. La table de hachage est accédée de toutes parts, étant un passage obligé tant pour les paquets utilisateurs que pour les paquets venant du pare-feu. En particulier, le démon nuauth doit corréler des passages de messages en provenance :

    • du pare-feu : les paquets reçus par le pare-feu, en attente d’authentification ;
    • des utilisateurs : chaque utilisateur parle directement à nuauth pour fournir l’authentification de ses connexions.

    À noter que TCP/IP ne garantit pas l’ordre de réception des paquets. Le paquet d’authentification fourni par l’utilisateur peut donc parvenir à nuauth avant ou après le paquet au niveau du pare-feu ; et nous devons donc garder chaque message dans un tampon : le traitement n’est réalisé que lorsque les deux messages ont été reçus, pour un paquet donné. Pour décrire à présent ce qui sort de nuauth, nous avons :

    • les décisions : pour une connexion donnée : nuauth renvoie sa décision au pare-feu ;
    • la journalisation : stocker l’information pour en permettre une analyse ou simplement garder l’historique

    L’architecture avec une table de hachage centrale accessible par l’ensemble des threads engendre un cœur d’application extrêmement large. À tel point que presque tous les composants actifs en font partie.

    /img-articles/lm/86/art-2/fig-2.jpg

    Schéma simplifié de l’architecture de la version 0.8 de nuauth

    La gestion des locks pour préserver la table de hachage centrale est vite devenue un enfer et l’un des buts lors du développement de la version 1.0 a été de modifier le code pour éviter ce casse-tête. Il a donc été décidé de confier la gestion de la table de hachage à un thread dédié chargé de suivre et de faire évoluer son contenu en fonction des interventions des différents sous-systèmes.

    /img-articles/lm/86/art-2/fig-3.jpg

    Schéma simplifié de l’architecture de la version 1.0 de nuauth

    Cette architecture centralisée permet de rationaliser l’utilisation de la table centrale, mais il faut alors trouver un moyen de faire transiter proprement des messages vers ce thread. La GLib l’apporte grâce à un système de messages asynchrones. On se retrouve donc avec un thread central qui récupère les évènements un par un et qui déclenche les traitements appropriés. Le cœur de nuauth est donc réduit et la stabilité de l’ensemble est donc plus élevée.

    Note :

    L’ensemble des exemples suivants est extrait de la version 1.0 de NuFW. La version 2.0 ajoute quelques "finesses" qui nuiraient à la compréhension

    Mise en œuvre

    Pour mettre tout cela en œuvre, il faut d’abord créer une queue asynchrone, ce qui se fait par un appel à g_async_queue_new().

    Envoyer des données sur cette queue est simple, puisque l’on travaille en fait par passage de pointeur. Pour cela, on utilise la fonction g_async_queue_push. user_check_and_decide réalise un traitement sur le paquet reçu par l’utilisateur. Elle remplit avec les informations données par l’utilisateur la structure conn_elt, qui contient l’ensemble des données utiles à la prise de décision sur le paquet. Une fois cette structure remplie, elle l’envoie sur la queue. Pour récupérer les données, on utilise une boucle qui dépile un message, le traite et recommence : La fonction search_and_fill ne rend jamais la main. Dans une boucle while, elle dépile les paquets lors de l’appel à g_async_queue_pop, déclenche les actions nécessaires et retourne ensuite au point de départ. On notera l’appel à g_async_queue_ref, qui permet d’accroître le compteur de référence de la queue. Si celui-ci vaut 0 la queue est détruite, ce qui n’arrive jamais dans le démon nuauth : nous n’appelons pas g_async_queue_deref.

    File de travailleurs

    Problématique et résolution

    Les architectures matérielles s’orientent de plus en plus vers le multiprocesseur : les machines multicore se multiplient et les serveurs professionnels sont fréquemment SMP. Aussi, la parallélisation des tâches au sein d’un logiciel serveur est une excellente voie pour exploiter pleinement la puissance de ces machines. Pour paralléliser au sein d’un même processus, il faut soit découper les tâches à effectuer en entités, soit profiter de la nature même du travail qu’effectue le programme. Dans le cas de nuauth, on peut s’intéresser à l’interaction avec les utilisateurs. Le serveur gère les clients par l’intermédiaire d’une connexion (TLS) propre à chaque client et chaque client communique de manière équivalente avec nuauth. Il pourrait donc sembler simple d’affecter un thread à un client, mais avec 500 (ou 5000) clients cela devient problématique, car chaque thread a un coût (notamment au moment de la bascule entre deux threads). Il est donc nécessaire de limiter le nombre de threads. L’algorithme naturel dans ce cas est de gérer une file d’attente des paquets et d’utiliser des threads pour dépiler les paquets.

    /img-articles/lm/86/art-2/fig-4.jpg

    Ajout d’une file de travailleurs

    Mise en œuvre

    La création du pool de threads se fait grâce à la fonction g_thread_pool_new :

    qui prend comme paramètre la fonction effectuant la tâche de traitement des données : Celle-ci a deux arguments : le premier contient un pointeur vers la donnée de pile, et le deuxième contient l’argument user_data passé lors de l’appel à g_thread_pool_new. La valeur max_threads permet de spécifier le nombre de threads à utiliser pour traiter cette file de donnée. La valeur exclusive permet de basculer entre deux modes :
    • TRUE : les threads créés lors de l’appel sont dédiés à ce pool de threads et leur création se fait une fois pour toute à la création du pool.
    • FALSE : les threads sont partagés entre les différents pools et créés dynamiquement.
    La création des pools est réalisée dans la fonction main : Pousser des éléments sur la pile se fait simplement par un appel à g_thread_pool_push. On spécifie la file à utiliser et les données à envoyer. Le dernier paramètre est optionnel. Il s’agit d’un pointeur vers une structure de gestion d’erreur. On décrypte les données reçues avec gnutls_record_recv et on les envoie au pool user_checkers avec l’appel à g_thread_pool_push. Pour dépiler les paquets, il suffit d’écrire une fonction de traitement qui accepte comme premier paramètre la donnée à traiter. userdata contient les informations posées sur la pile. Elles sont décodées par un appel à la fonction interne à nuauth userpckt_decode et le résultat est du traitement envoyé vers la file asynchrone connections_queue que l’on a évoquée précédemment dans l’article.

    Conclusion

    La GLib permet donc de construire des architectures multithreadées complexes avec une relative facilité. Les briques proposées sont en effet d’un niveau suffisamment élevé pour permettre des enchaînements échevelés de threads et de structures de données. Cet article vous a donné un aperçu rapide d’une petite partie des fonctionnalités de la GLib. Malgré ses qualités, elle est malheureusement peu mise en avant par l’équipe de GTK+ qui ne lui accorde même pas une page dédiée sur son site. Que cela ne vous arrête pas, la documentation de la Glib [2] est excellente ! Au vu de sa puissance tant au niveau du développement que de l’exécution, elle pourrait (et devrait) en effet être valorisée bien plus que comme la bibliothèque de fondation de GTK+. Les tests [4] effectués sur NuFW ont montré qu’avec 500 utilisateurs connectés et 3000 nouvelles connexions authentifiées par seconde en flux constant l’utilisation mémoire n’est que de 24 Mo (pour 3,3 Mo à vide) avec des temps de réponse de quelques millisecondes. La GLib montre donc là qu’elle est rapide, légère et efficace. Bibliographie :

    Retrouvez cet article dans : Linux Magazine 86

    Vous souhaitez commenter cet article ?
    Brèves Flux RSS
    Édito : GNU/Linux Magazine 149
    Édito : GNU/Linux Magazine HS N°60
    Édito : Misc 61
    Édito : Linux Pratique 71
    Édito : Linux Essentiel N°25
    Communication RSS Com. RSS Presse
    Lancement de la plateforme de vente en ligne de PDF des Éditions Diamond ! Un...
    Misc N°61 – Communiqué de presse
    GNU/Linux Magazine N°149 – Communiqué de presse
    GNU/Linux Magazine HS N°60 – Communiqué de presse
    Linux Pratique N°71 – Communiqué de presse
    prochainement moteur de recherches des articles
     
    :
    :
    Jours heures minutes secondes
    En kiosque Flux RSS

    Le tout nouveau GNU/Linux Magazine est disponible dès maintenant chez votre marchand de journaux et sur notre site marchand.

    Découvrez le sommaire de ce numéro et un aperçu de ce magazine...

    Lire la suite...

    Le tout nouveau Misc est disponible dès maintenant chez votre marchand de journaux et sur notre site marchand.

    Découvrez le sommaire de ce numéro et un aperçu de ce magazine...

    Lire la suite...

    Le tout nouveau Linux Pratique est disponible dès maintenant chez votre marchand de journaux et sur notre site marchand.

    Découvrez le sommaire de ce numéro et un aperçu de ce magazine...

    Lire la suite...

    Le tout nouveau GNU/Linux Magazine HS est disponible dès maintenant chez votre marchand de journaux et sur notre site marchand.

    Découvrez le sommaire de ce numéro et un aperçu de ce magazine...

    Lire la suite...

    Le tout nouveau Linux Essentiel est disponible dès maintenant chez votre marchand de journaux et sur notre site marchand.

    Découvrez le sommaire de ce numéro et un aperçu de ce magazine...

    Lire la suite...

    Le tout nouveau Misc HS est disponible dès maintenant chez votre marchand de journaux et sur notre site marchand.

    Découvrez le sommaire de ce numéro et un aperçu de ce magazine...

    Lire la suite...