Retrouvez cet article dans : Linux Magazine 86
Présentation de la GLib
Une boîte à outils indispensable
Pour commencer, la- mutex ;
- opération atomique ;
- queue asynchrone ;
- pool de threads ;
- gestion de la mémoire.
- liste chaînée (si si ;-) ;
- table de hachage ;
- tableau de taille dynamique.
- 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- 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
GLibsont 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
GLibpermet au développeur de travailler loin des spécificités de chaque noyau ou système
Utilisation
Pour utiliser les fonctions de la#include <glib.h>à ses fichiers sources et d’autre part de compléter la ligne de commande de compilation par :
`pkg-config --cflags --libs glib-2.0 gthread-2.0`qui permet de rajouter ici les includes et les bibliothèques nécessaires à la
AM_PATH_GLIB_2_0(2.4.0, , AC_MSG_ERROR([glib nécessaire à la compilation]),[gthread])après avoir vérifié que
Gestion de la mémoire
Comme promis, lag_new(struct_type, n_structs);Comme son nom l’indique,
g_free(gpointer mem);Pas grand chose à commenter, sauf un autre petit bonus immédiat par rapport Ã
g_mem_profile(void);Elle écrit sur la sortie standard du programme un résumé de l’utilisation de la mémoire.
- 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.
GLib Memory statistics (successful operations):  blocks of | allocated | freed | allocated | freed | n_bytes  n_bytes | n_times by | n_times by | n_times by | n_times by | remaining  | malloc() | free() | realloc() | realloc() | ===========|============|============|============|============|===========  1 | 3 | 1 | 0 | 0 | +2  2 | 1 | 0 | 0 | 0 | +2  3 | 10 | 6 | 0 | 0 | +12  4 | 26 | 347 | 3307 | 2964 | +88 ...  4001 | 10 | 10 | 0 | 0 | +0 GLib Memory statistics (failing operations):  --- none --- Total bytes: allocated=348074, zero-initialized=13557 (3.89%), freed=331517 (95.24%), remaining=16557Attention toutefois à appeler
g_mem_set_vtable(glib_mem_profiler_table);
É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 lamonthread = g_thread_create ((GThreadFunc)func, datas, TRUE, error);Le premier paramètre est la fonction qui sera appelée à la création du thread, elle aura comme seul argument
int pthread_create(pthread_t * thread, pthread_attr_t * attr, void * (*start_routine)(void *), void * arg);Les amateurs éclairés auront constaté une faiblesse de l’appel
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 int give_me_next_number ()
 {
 static int current_number = 0;
 int ret_val;
 static GStaticMutex mutex = G_STATIC_MUTEX_INIT; g_static_mutex_lock (&mutex);
 ret_val = current_number++;
 g_static_mutex_unlock (&mutex);
 return ret_val;
 }
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 à 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 utiliseLocks 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.
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.

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.

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().
int main(int argc,char * argv[])
{
... connexions_queue = g_async_queue_new();
 if (!connexions_queue)
 exit(1);
...
}
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 void user_check_and_decide (gpointer userdata, gpointer data)
{
 ...
 connection * conn_elt;
 ...
 g_async_queue_push (connections_queue,conn_elt);
 ...
}
void search_and_fill ()
{
 connection * element = NULL;
 connection * pckt = NULL; g_async_queue_ref (connexions_queue);
 ...
 /* wait for message */
 while ( (pckt = g_async_queue_pop(connexions_queue)) ) {
 /* search pckt */
 g_static_mutex_lock (&insert_mutex);
 element = (connection *)
 g_hash_table_lookup(conn_list,&(pckt->tracking_hdrs));
 if (element == NULL) {
 ...
}
La fonction 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.
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 :
 GThreadPool* g_thread_pool_new (GFunc func, gpointer user_data, gint max_threads, gboolean exclusive, GError **error);qui prend comme paramètre la fonction effectuant la tâche de traitement des données :
 void (*GFunc) (gpointer data,
gpointer user_data);
Celle-ci a deux arguments : le premier contient un pointeur vers la donnée de pile, et le deuxième contient l’argument 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.
 int main(int argc,char * argv[])
{
...
user_checkers = g_thread_pool_new (
(GFunc)user_check_and_decide,
NULL,
nbuser_check,
POOL_TYPE,
NULL);
...
}
Pousser des éléments sur la pile se fait simplement par un appel à  static int treat_user_request (user_session_t * c_session)
{
...
/* copie des données de paquets */
datas->buffer = g_new0(char, CLASSIC_NUFW_PACKET_SIZE);
if (datas->buffer == NULL){
g_free(datas);
return -1;
}
g_mutex_lock(c_session->tls_lock);
datas->buffer_len = gnutls_record_recv(*(c_session->tls),
datas->buffer, CLASSIC_NUFW_PACKET_SIZE);
g_mutex_unlock(c_session->tls_lock);
...
debug_log_message(VERBOSE_DEBUG,
AREA_MAIN, „Pushing packet to user_checker“);
g_thread_pool_push (user_checkers,
datas,
NULL
);
...
return 1;
}
On décrypte les données reçues avec  void user_check_and_decide (gpointer userdata, gpointer data)
{
GSList * conn_elts=NULL;
GSList* conn_elt_l;
connection_t* conn_elt;
debug_log_message (VERBOSE_DEBUG,
EA_USER, "entering user_check");
conn_elts = userpckt_decode(userdata);
...
g_async_queue_push (connections_queue,conn_elt);
...
}
Conclusion
La- [1] NuFW : http://www.nufw.org/
- [2] Manuel de la GLib : http://developer.gnome.org/doc/API/2.0/glib/index.html
- [3] Page de GTK : http://www.gtk.org/
- [4] Benchs agressifs sur NuFW : http://www.nufw.org/Tests-de-performance-intensifs-sur.html
- [5] Article Wikipedia sur les threads : http://fr.wikipedia.org/wiki/Thread
Retrouvez cet article dans : Linux Magazine 86





Donnez votre avis
Vous devez avoir ouvert une session pour écrire un commentaire.