Logiciels Libres et Systèmes Embarqués

Améliorations et optimisations

Tout le long du développement de ce pilote, nous avons remis en question le plus souvent possible ce que nous étions en train de faire (à tel point que l'utilisation d'un serveur CVS fut indispensable).

Cette manière de travailler nous a permis d'apporter quelques améliorations par rapport au sujet demandé et quelques optimisations par rapport à un code simple. Nous allons donc discuter de ces personnalisations dans cette section.

Intégrité et opérations non-bloquantes

Afin d'assurer l'intégrité de la liste doublement chainée, où sont stockées les données de l'utilisateur du périphérique, nous avons fais en sorte que lorsque le fichier spécial de lecture (ou d'écriture) est ouvert, il n'est pas possible qu'un autre processus puisse l'ouvrir. Cela est réalisé par le tableau d'entiers (utilisés comme booléen) already_opened qui est protégé par le tableau de sémaphores open_critique. Leur taille est égale au double du nombre de périphériques gérés par le pilote (donc un élément par fichier spécial).

Bien évidemment, il est tout à fait possible d'ouvrir le fichier spécial de lecture et celui d'écriture en même temps. En revanche, pour les même raisons que précédemment, lorsqu'il y a une opération de lecture (respectivement d'écriture) sur un périphérique, il n'est pas possible de réaliser une opération d'écriture (respectivement de lecture). Cela est réalisé par la variable in_use de la structure private_dummy (propre à chaque périphérique), protégé par le sémaphore in_use_critique de cette même structure.

On pourrait être amener à penser que cette politique est une limitation, mais en fait, en plus de préserver l'intégrité de la liste doublement chainée où sont stockées les données de l'utilisateur, cela préserve aussi l'intégrité des données elles-même. En effet, on peut facilement imaginer que si deux processus écrivent en même temps dans le périphérique et que celui gère cette situation (en mélangeant le flux), les données ainsi stockées n'auraient pas une grande signification...

Mais qu'en est-t-il vraiment lorsqu'une telle situation arrive, faut-il que le second processus soit bloqué, ou non ? En fait, nous avons fais le choix de laisser le choix à l'utilisateur :) ! Ainsi, en fonction du mode d'ouverture (paramètre flags de la fonction open(2)) le pilote ne se comportera pas de la même manière (bloquant ou non). Pour cela, nous vérifions simplement si l'utilisateur a positionné le drapeau O_NONBLOCK.

Listes et boucles

Un effort particulier a été fait pour l'optimisation des listes. Celles-ci sont doublement chainées afin d'accéder directement au dernier élément lors d'une écriture (concaténation des données). Le double chaînage permet aussi d'assurer une généricité, ce qui nous permet de nous affranchir la gestion d'un pointeur éventuel sur le dernier élément et sur le premier.

Certes l'utilisation d'une liste doublement chainée n'est pas une mauvaise idée, mais ce n'est pas non plus remarquable... En fait, nous nous sommes concentré sur le traitement de ces listes dans les boucles. En effet, nous avons fait en sorte de sortir un maximum de code des boucles afin des les rendre plus rapide, car il est mauvais de rester trop longtemps dans le code noyau sans rendre la main ! Nous avons donc fait en sorte que les tests de sortie de boucle ne soit pas pollués par des cas particuliers. Nous pouvons citer pas exemple le cas d'une écriture, où le test pour savoir si la liste est vide est avant la boucle (et nous alloueons donc un élément en cas de besoin). Il faut bien noter qu'en plus d'enlever des intructions de la boucle, cela enlève surtout des conditions qui peuvent être désastreuse en terme de performances sur certaines architectures...

Réordonnacement du processus

Comme nous l'avons dit précédemment, il est mauvais de rester dans le code noyau sans rendre la main. En effet, supposons que l'on écrive un fichier de 50 Mo en une fois, notre pilote va boucler tant que toutes les données ne seront pas stockées... Cette exemple prend 5 secondes sur un Pentium IV à 2.6 GHz (lorsque l'on met la taille des éléments de la listes à 1 octets). Pendant ces 5 secondes, il est impossible de faire quoique ce soit, il n'est même pas possible de faire bouger le pointeur de la souris !!!

Certe, avec un noyau préemptif cela pourrait être résolu, mais faut-il encore avoir compilé son noyau avec cette fonctionnalité... Nous n'avons fait aucune hypothèse en ce qui concerne cela, et nous avons choisi de gérer cette situation par nous même. C'est ainsi que nous avons rajouté des appels à l'ordonnanceur dans nos boucles. Les résultats sont à la hauteur de nos espérances, puisque pour le même test, le système est complêtement fonctionnel tout le long du stockage.

Il faut tout de même noter que l'opération d'écriture (ou de lecture) est alors plus lente, mais cela ne veut pas dire que l'on reste plus longtemps dans le code noyau, bien au contraire. Ce ralentissement du processus en cours d'opération est du au fait qu' on le réordonnance souvent, mais le système globale est infiniment plus rapide.

Mémoire dynamique

Nous allons parler rapidement dans cette section des petits détails qui font que notre pilote de périphérique fontionne (en théorie) plutôt bien :)

Afin de supprimer les bogues liés à l'utilisation de la mémoire dynamique, nous avons fait en sorte de limiter les appels à kmalloc et kfree. Ainsi, tout ce qui pouvait être fait en static l'a été, et lorsque l'on pouvait grouper ces appels, nous ne nous sommes pas privé. Par exemple, au début nous utilisions un tableau (dynamique) d'entier pour savoir si le péripérique était occupé, puis nous avons remplacé ce système en ajoutant la variable in_use (qui a le même rôle) dans la structure private_dummy . Etant donné qu'il existe un tableau de cette structure, nommé device_fifo, qui a exactement la bonne taille (puisqu'il a la même correspondante entre ces éléments et les périphériques), nous avons donc un code plus propre (tout ce qui concerne un périphérique est dans une unique structure) et surtout nous nous affranchissons d'une allocation et d'une libération (qui sont souvent l'origine d'un bogue).

Toujours en ce qui concerne la mémoire dynamique, nous prenons soin de mettre les pointeurs à NULL lorsqu'on les désalloue. Ainsi, si on le réutilise par mégarde, on est prévenu immédiatement :) !

Il y a encore d'autre détails que l'on a oublié de mentionner (par exemple que l'on vérifie tous les retours de fonctions) ou qui n'ont pas une grande utilité par rapport à la compréhension de notre projet (par exemple le respect du Linux Kernel Coding Style), mais nous préférons ne pas ennuyer le lecteur d'avantage :) !

aller à la section suivante