Nous revoilà pour cette cinquième édition qui est intégralement consacrée à la version 2.6.19 du noyau Linux. Avec cette nouvelle mouture, nous faisons un peu de cosmétique pour parfaire l’organisation de nos propos afin que vous puissiez vous y retrouver plus facilement ;) Ainsi, chaque brève se verra associée un ou plusieurs tags facilitant le repérage. En voici une liste non exhaustive : "New Feature" pour les brèves développant une nouvelle fonctionnalité, "Technical" pour les brèves techniques détaillant un point particulier du noyau, "Kernel Addict" pour les apprentis sorciers ;) souhaitant mettre la main dans le cambouis, "Kernel Beginner" pour les brèves sur les aspects pratiques concernant la participation au développement noyau, "People" pour la narration de certaines polémiques récentes s’étant déroulées sur la LKML, etc. Vous allez découvrir également avec ce numéro une amélioration de taille ;) La rubrique d’actualité (taguée, comme il se doit, [Actualité]) est dorénavant structurée en ensembles cohérents vous permettant de vous y retrouver plus facilement. Noël avant l’heure ? ;)
Actualité Le noyau 2.6.19
Les périphériques blocs
La série 2.6.19 introduit plusieurs changements majeurs. Le plus célèbre est sans doute la bibliothèque PATA (Parallel ATA) et les drivers associés. C’est un sous-système générique complétant la libATA, qui vise à remplacer à terme le vieillissant système ATA/IDE du noyau. Les périphériques PATA et SATA profitent ainsi de la même infrastructure. Ce sous-système apporte une gestion d’un plus grand nombre de chipsets, de meilleures performances, et un meilleur comportement en cas d’erreurs se produisant au niveau du périphérique (secteurs défectueux, panne matérielle). Concocté par Alan Cox, il repart sur des bases neuves et saines, mettant au placard la collection de hacks accumulés dans la pile ATA/IDE ; nous pouvons ainsi en attendre une fiabilité accrue. Une bonne partie des chipsets les plus fréquemment rencontrés sont gérés (cependant marqués expérimentaux), et vous êtes cordialement invité à tester le fonctionnement du vôtre avec la libATA. A cette occasion, l’entrée Serial ATA (prod) and Parallel ATA (experimental) drivers fait son apparition dans le menu device drivers de configuration du noyau. Elle accueille les différents drivers de chipsets SATA/PATA, qui étaient auparavant situés dans SCSI low-level drivers. Sélectionnez votre chipset dans la liste, compilez votre noyau, et n’oubliez pas que, passant désormais par la libATA, vos disques ATA/IDE qui étaient vus comme HDX suivront désormais la nomenclature des périphériques SCSI (SDX). Pensez à adapter votre boot (option
root=/dev/hdX en particulier ;-)). Enfin, il est plus que recommandé de sauvegarder vos données importantes avant de rebooter.
Allez, même si ça n’a aucune valeur fiable, un petit bench stupide avec la version 2.6.19-rc4, mon disque Maxtor et le sous-système PATA :
|
|
# hdparm -tT /dev/sda
/dev/sda:
Timing cached reads: 2004 MB in 2.00 seconds = 1001.55 MB/sec
Timing buffered disk reads: 170 MB in 3.02 seconds = 56.22 MB/sec |
Retour au "vieux" sous-système IDE : pour les buffered disk reads (lecture directe du disque sans passer par aucun cache), les résultats sont moins constants : j’oscille entre 48 et 54 MB/sec.
Les systèmes de fichiers
La partie "systèmes de fichiers" n’est pas en reste, pas moins de trois petits nouveaux font leur entrée, à commencer par le GFS2 de Redhat. C’est un FS destiné aux clusters, sur le même principe qu’OCFS2 inclus à partir du 2.6.16 (cf. Kernel Corner 85) : c’est un système de fichiers auquel plusieurs nœuds peuvent accéder simultanément, dans le cadre d’architectures SAN par exemple. Un exemple type de son utilisation est la création de clusters de bases de données (fichiers de données partagés par toutes les instances du SGBD). Plus ardu à configurer que son "rival" OCFS2, il offre le support des ACL, des quotas, des direct I/O, est extensible en ligne... Vous vous demandez peut-être quelles différences fondamentales justifient l’utilisation d’un de ces systèmes de fichiers "clusterisés" par rapport à notre traditionnel NFS, ou inversement. Premièrement, deux interlocuteurs NFS discutent par-dessus la couche TCP (ou UDP) pour accéder à des données finales qui seront récupérées en faisant appel aux fonctions d’un système de fichiers "physique" (ext, reiser, XFS...). Nous voyons donc ici l’empilement de couches à traverser. GFS2 ou OCFS2 sont, quant à eux, des FS "natifs", c’est-à -dire assurant eux-mêmes l’organisation et la gestion du disque ; par ailleurs, G(ou OC)FS2 sont très nettement taillés pour converser par le biais d’un protocole de bus (iSCSI, Fiber Channel) spécifiquement dédié aux transferts via des liaisons directes entre équipements, ce qui a une incidence positive sur les performances, mais négative sur le coût des équipements :-) et la flexibilité de leur déploiement. Deuxièmement, ces deux derniers offrent une gestion précise et fiable des verrous lors d’accès concurrents à un même fichier, tandis que la fiabilité de NFS dans ce domaine est aléatoire.
La deuxième nouveauté FS de ce noyau est l’intégration du eCryptFS d’IBM, présenté lors du Ottawa Symposium 2005, qui n’est pas un système de fichiers à proprement parler, mais une surcouche proposant le chiffrage des données lors de leur stockage physique. Une démo pratique vaut souvent mieux qu’un long discours, voyons donc ensemble comment chiffrer un fichier.
1. Munissez-vous au préalable d’un 2.6.19 tout frais, compilez-le avec le support "cryptographic API", Cryptographics options -> AES cipher algorithms, Security Options -> Enable access keys retention et, enfin, Filesystems -> Miscellaneous filesystems -> eCryptfs.
Récupérons, puis compilons les paquets
keyutils (http://people.redhat.com/~dhowells/keyutils/), pui
s ecryptfs-util-4 (http://ecryptfs.sourceforge.net/).
2. Créons un montage
eCryptFS :
|
|
# mkdir /tmp/jesuischiffré <! le dossier des contenus chiffré doit être dédié à ecryptfs >
# mkdir /mnt/jesuisdéchiffré
# mount -t ecryptfs /tmp/jesuischiffré/ /mnt/jesuisdéchiffré/
eCryptfs interactive mount menu
-------------------------------
1. Mount with passphrase
4. Abort mount
Make selection: 1
Mount-wide passphrase: <entrez une passphrase ici ! >
Confirm passphrase:
Using the default salt value
Will attempt mount with authentication token signature [d653285fa968513d]
Press ‘l’ to list available ciphers; enter for the default cipher [AES-128]:
eCryptfs will use cipher [aes]
Performed successful eCryptfs mount using signature [d653285fa968513d]. |
3. Allons-y !
|
|
# echo "rdv au centre commercial a 10h, amène la valise" > /tmp/jesuisdéchiffré/message.txt |
Tant que notre montage est actif, nous pouvons lire notre message (ceci s’applique bien entendu à tout contenu autre que texte) chiffré :
|
|
$ cat /mnt/jesuisdéchiffré/message.txt
rdv au centre commercial a 10h, amène la valise |
Démontons (umount /mnt/jesuisdéchiffré) : il ne nous reste que le fichier réellement écrit sur le disque, donc
/tmp/jesuischiffré/message.txt. Le résultat d’un cat sur celui-ci étant vomitif, je vous laisse vérifier par vous-même que vous n’y retrouverez pas votre message. Le contenu déchiffré sera accessible en recréant le montage avec la même passphrase.
Le troisième et dernier de la liste n’est rien de moins que... ext4. Présenté dans votre Kernel Corner 86, il est voué à devenir le successeur d’ext3, et les changements visibles pour l’instant par rapport à ce dernier sont l’adressage des numéros de blocs disque sur 48 bits (gestion de partitions jusqu’à 16 To) et les extents (manière d’inscrire les gros fichiers sur des blocs contigus, ce qui simplifie leur description). C’est un chantier qui sera en mouvement pendant plusieurs mois et dont les fonctionnalités supplémentaires ne sont pas encore complètement définies.
L’embarqué
A partir de sa version 2.6.19, Linux sera capable de tourner sur une architecture supplémentaire : les microcontrôleurs de type Atmel AVR32. Ces processeurs sont de type RISC et se destinent à l’embarqué, visant une efficacité de traitement par cycle d’horloge améliorée, afin de fonctionner à des fréquences plus basses et une consommation d’énergie moindre. Du côté de l’existant, il faut noter de nombreuses améliorations pour les X86_64 (hotplug mémoire et CPU),
la gestion de plusieurs nouvelles puces de type ARM, le support de
cpufreq pour les G5.
Possibilité utile pour les systèmes embarqués disposant de faibles ressources mémoire, désormais le sous-système de gestion des périphériques de type bloc peut être entièrement désactivé ; ce qui autorise malgré tout l’utilisation de systèmes de fichiers comme NFS ou JFFS.
Les pilotes de périphériques
Suite à des discussions ayant eu lieu depuis cet été au sujet de la (perfectible) gestion du suspend/resume dans le noyau, Linus Torvalds introduit de nouvelles fonctions permettant de dialoguer plus finement avec le matériel lors des phases d’hibernation/réveil. Ainsi la structure
bus_type s’enrichit de deux nouveaux membres :
appelée lorsque le système est presque totalement stoppé (mode monoprocesseur, interruptions désactivées).
agissant très tôt au réveil, également en mode monoprocesseur et avant activation des interruptions.
Ce sont des bases qui vont permettre, une fois que les pilotes les implémenteront, un meilleur fonctionnement de l’hibernation. Signalons également des améliorations de la couche USB dans ce sens, et la création d’un mode debug pour le suspend des processus et périphériques, permettant de mieux analyser leurs éventuels problèmes de mise en veille/réveil.
Une option "parallel PCI probing" (
CONFIG_PCI_MULTITHREAD_PROBE) voit le jour, autorisant la découverte de plusieurs périphériques de type PCI en simultané. Ceci accélère notablement le temps de boot sur les systèmes multiprocesseurs, mais pourrait avoir l’effet inverse si trop de périphériques s’initialisent en même temps : la capacité d’énergie demandée pourrait dépasser les possibilités d’alimentation du bus, ralentissant l’initialisation des matériels connectés.
Parmi les traditionnelles évolutions au niveau des pilotes de périphériques, notons plus particulièrement, pour ce cru 2.6.19 :
- le support du Direct Rendering Manager (DRM) pour les puces Intel 965G ;
- le support des chipsets Intel 965 Express ;
- la gestion des cartes tuner Hauppauge WinTV-HVR3000, Nova-T 500 et HVR 1300 et de nombreux autres matériels d’acquisition analogique ou DVB ;
- la mise à jour des Wireless Extensions en version 21 ; effectuée de mauvaise grâce par les développeurs noyau qui attendent la bascule vers une autre solution comme une gestion via Netlink au-dessus de la pile générique Devicescape (cf. KC85) ;
- la gestion du WPA via les Wireless Extensions pour les puces WiFi Prism54, vous permettant d’en bénéficier depuis des applications comme le Network Manager. Le scan passif est désormais possible avec les matériels de type ipw2200 : contrairement au scan classique dit "actif", toute autre utilisation simultanée de la carte wifi est impossible, mais il a l’avantage de rapporter des informations supplémentaires (comme l’IP de toutes les machines connectées à un réseau, y compris celle de l’access-point).
- le retrait d’un bon nombre de drivers OSS, le support ALSA de ces périphériques étant considéré suffisamment stable.
La gestion mémoire
Plusieurs fonctionnalités au niveau de la gestion de la mémoire ont été ajoutées à cette nouvelle mouture. Une première concerne la traque des pages salies (dirty : comprendre écrites en mémoire mais pas encore sur le disque) dans les régions mémoires partagées avec le droit d’écriture. Cette fonctionnalité de Peter Zijlstra est implémentée en protégeant en écriture les pages concernées lorsqu’elles sont propres (clean : c’est-à -dire que les données en mémoire sont synchronisées avec le disque). Lorsqu’un processus tente une écriture, la faute de page est interceptée. La page est alors positionnée dirty et autorisée en écriture (via son PTE). L’écriture peut donc s’effectuer à ce moment (i.e. après la gestion de l’exception). Lorsque l’écriture sur le disque est effectivement lancée, c’est-à -dire quand les conditions du page write-back sont remplies (par exemple que le nombre de pages salies atteint un seuil critique), les dirty bits des PTE sont effacés et la protection en écriture est réactivée. Pour effectuer ces dernières actions, le reverse mapping est bien entendu utilisé. La nouvelle fonction
page_mkclean() (ajoutée Ã
mm/rmap.c) effectue ce travail.
Cette fonctionnalité étant implémentée, elle permet de réguler le nombre de pages salies en agissant directement sur les "fauteurs de troubles" (les processus écrivains des mappings partagés) en réduisant leur enthousiasme ;) Cela est implémenté dans un deuxième patch qui ajoute en gros l’appel à la primitive
balance_dirty_pages() dans le gestionnaire de fautes de page précédent (
do_wp_fault). Ainsi, en mettant au ralenti les processus fautifs, le page write-back (en gros, l’écriture sur le disque depuis la mémoire des pages non synchronisées) peut se dérouler sans encombre, évitant les situations où il n’y a plus de mémoire disponible à cause d’un nombre trop important de pages salies attendant d’être écrites sur le disque. La congestion de ce trafic pour les processus concernés, au moment opportun, évite ce genre de surprise.
Mel Gorman a ajouté un mécanisme d’enregistrement de régions de mémoire actives indépendant du type d’architecture. Une suite de patchs a converti ensuite les architectures ppc, x86, x86_64 et ia64 (pour le moment) à l’utilisation de ce mécanisme générique, entraînant une diminution flagrante du code spécifique à chaque type d’architecture chargé avant d’effectuer cette tâche. Avant l’introduction de ce dispositif générique, chaque type d’architecture définissait des structures pour enregistrer où se trouvaient les zones de pages actives. Ensuite, le calcul de la taille de ces zones ainsi que des trous s’effectuaient. Maintenant, cette dernière étape est faite de façon générique. L’enregistrement, quant à lui, s’effectue pour chaque type d’architecture avec
add_active_range(), laquelle reçoit les descriptions génériques des différentes régions. Le calcul sur les dimensions s’effectue ensuite génériquement au sein de
free_area_init_nodes().
Martin Schwidefsky a intégré une chaîne de notification au gestionnaire de rupture de mémoire (OOMM : out of memory manager/killer). Celui-ci a pour habitude de tuer des processus trop consommateurs, en cas de pénurie de mémoire. La fonctionnalité ajoutée permet d’enregistrer des callbacks qui peuvent libérer de la mémoire dans de telles situations. Le OOMM retente alors l’allocation responsable de son propre lancement et reporte à plus tard son boulot sanguinaire ;) Le but de cette chaîne de notification est d’ajouter un filet de sûreté quand des réserves de mémoire sont utilisées (par exemple les mempool). Si ces dernières sont agrandies par leur gestionnaire et empêchent une allocation mémoire de se produire ailleurs, il est préférable de les réduire plutôt que de mettre fin aux jours des processus.
La mémoire au sein d’un nœud du système (lié à un bus mémoire) est découpée depuis toujours en zones : les fameuses
ZONE_DMA, ZONE_NORMAL et ZONE_HIGHMEM. Cependant, pour certaines architectures, elles ne sont pas pertinentes. Pour des architectures autres que x86_64, la
ZONE_DMA32 n’a pas de sens par exemple, ou bien encore la
ZONE_HIGHMEM n’est utilisée, entre autres, sur aucune architecture 64 bits (l’espace d’adressage est suffisamment grand pour mapper directement en mémoire l’ensemble de la mémoire physique). Des structures de données sont associées à chacune de ces zones occupant de la place en mémoire. Elles sont toujours présentes au grand complet y compris dans les configurations où certaines zones ne sont pas pertinentes (l’abstraction en zone étant générique, le mécanisme de gestion n’est pas spécifique au type d’architecture). C’est pourquoi un patch a fait son apparition pour permettre la désactivation des
ZONE_DMA32 et
ZONE_HIGHMEM au moment de la compilation du noyau.
Notons également une modification de sémantique des primitives
kmap/kunmap (ainsi que les versions atomiques) censées être utilisées pour mapper temporairement en mémoire des pages situées en mémoire haute (ces dernières ne sont pas mappées directement en mémoire à la différence par exemple des 896 premiers méga-octets sur x86). Pour certaines architectures, des problèmes de cohérence mémoire nécessitent de flusher les pages du cache entre l’appel de
kmap et
kunmap. Maintenant, le développeur de pilote n’a plus à se soucier de cela, car cette gestion fait à présent partie du rôle de
kmap/kunmap.
Un nouveau cas est dorénavant traité dans le gestionnaire de fautes de page avec l’ajout de
do_no_pfn() par Jes Sorensen. Cette fonction gère la situation où le mapping mémoire ayant provoqué la faute n’est pas associé à une
struct page (descripteur de page défini pour chaque page de mémoire physique). Cela évite la création d’entrées fictives dans les tables de pages pour les régions de l’espace d’adressage n’étant pas associées à de la véritable mémoire (le driver MSPEC supportant les opérations en mémoire spéciale pour l’architecture SGI N2 l’utilise par exemple). Des situations existent dans lesquelles il n’est vraiment pas conseillé d’avoir une
struct page associée au mapping, car cette dernière offre un accès à la page en question qui peut être effectué en parallèle de l’accès par le driver. Si l’un utilise, par exemple, un accès en mode cache et l’autre un accès direct, des problèmes de corruption peuvent se produire.
Une nouvelle primitive est dorénavant disponible pour les développeurs noyau. Il s’agit de
void *kmemdup(const void *src, size_t len, gfp_t gfp) qui permet de dupliquer directement une région mémoire. Avant, il était usuel de réserver la zone avec
kmalloc() suivi d’un
memcpy() pour effectuer la duplication (en ayant pris soin de vérifier le code de retour de
kmalloc()). Cette nouvelle primitive effectue donc les deux actions en évitant au programmeur de faire des erreurs dans la saisie des longueurs (une pour le
kmalloc() et l’autre pour le
memcpy()). Bien qu’à première vue cette fonctionnalité ne semble pas transcendante, elle provient de la constatation que de nombreux bugs ont été le résultat de ce type d’erreur. Cette primitive est donc à préférer à l’ancienne méthode. De plus, le code noyau s’en trouvera réduit ;)
La virtualisation
Du côté de la virtualisation, plusieurs travaux sont en cours. Les fonctionnalités de container sont intégrées progressivement. Nous trouvons notamment la séparation d’espaces de noms pour différents types d’information : le "nom" du système (structure
utsname) ; les objets IPC (possibilité de rendre privées les IPC – sémaphores, mémoire partagée, files de messages – via l’utilisation d’espaces de noms différents – création des
ipc_namespace, ajout de l’attribut
CLONE_NEWIPC pour la primitive clone, support de la primitive
unshare()) ; les identifiants des processus (définition d’objets décrivant des espaces de PID) ; etc.
Notons également des patchs préparant l’arrivée prochaine du support pour la paravirtualisation. Avant de clore ce paragraphe, mentionnons la suite de patchs (pas encore intégrée à la branche principale du noyau) de Avi Kivity implémentant
/dev/kvm (KVM : Kernel Virtual Machine) pour la création de systèmes invités. Nous aurons l’occasion de reparler de cela dans les prochains KC.
Les interruptions et les exceptions
Une modification au niveau de l’API de la gestion d’interruption générique a été apportée par David Howells. Elle consiste en la suppression d’un paramètre dans la primitive
generic_handle_irq appelée à chaque interruption.
__do_IRQ() est également modifié de la sorte. Ce changement provient de la constatation que l’argument
struct pt_regs * n’est en fait utilisé que par très peu de gestionnaires d’interruptions. En conséquence, l’enlever de la liste des paramètres permet de gagner de la place sur la pile noyau (pensez par exemple à la chaîne d’appels imbriqués lorsqu’un périphérique USB branché à un HUB lève une interruption, et ce paramètre peut même avoir à passer par quelques couches supplémentaires : pour arriver jusqu’au gestionnaire
sysrq). Nous y gagnons également en performance, puisque le code permettant le passage du paramètre disparaît (l’architecture FRV gagnerait 20% en rapidité sur le chemin de sortie du traitement des IRQ).
Les gestionnaires d’interruptions, ayant cependant besoin de connaître l’état des registres, utilisent dorénavant
get_irq_regs(). Les informations sur les registres sont stockées dorénavant dans une structure
struct pt_regs * qui est placée dans une variable globale pour chaque CPU (via la per-CPU variable
__irq_regs). Pour maintenir cette structure, la fonction
do_IRQ() stocke le pointeur
pt_regs dans la per-CPU variable et conserve l’ancien. A la fin du traitement, l’ancien pointeur est restauré.
Notons enfin que le patch modifie plus de 1000 fichiers ;) Cela n’est pas étonnant au vu du nombre de gestionnaires d’interruptions existant dans le noyau (environ 1800).
Opérations d’entrées/sorties
Les opérations sur les fichiers sont définies en respectant une interface précise. Chaque driver en rapport avec ces opérations (notamment les systèmes de fichiers) doit définir une structure
file_operations. Nous y trouvions jusqu’à présent en plus des opérations classiques, des versions dérivées. Ainsi, il existait une version vectorisée des opérations de lecture et d’écriture
readv() / writev() et une version asynchrone (i. e. ne bloquant pas)
aio_read() / aio_write(). La version asynchrone se voit maintenant étendue en des versions vectorisées (ce qui était un manque). De plus, la modification prépare également à la factorisation de toutes les opérations asynchrones et vectorisées, rendant l’interface
file_operations plus claire et concise.
La sécurité
Un nombre assez important de modifications concernant la sécurité a pris place dans le 2.6.19. Cependant, la majeure partie concerne uniquement SELinux. Nous trouvons notamment parmi ces changements l’extension du système de fichier tmpfs afin de supporter les ACL POSIX (Access Control Lists) et les BSD secure level supprimés des LSM (Linux Security Module).
Voyons, à présent, les modifications au sein de SELinux. Tout d’abord, la version maximale du type de politique de sécurité supportée est dorénavant la 21. Cette version autorise les transitions entre toutes les classes de sécurité et n’est plus limitée uniquement à celle des processus. Elle supporte néanmoins toujours l’ancien format. En outre, la version maximale supportée par le noyau peut être abaissée artificiellement afin de pallier les problèmes de gestion éprouvés en userland, lorsqu’il est installé une nouvelle version d’un noyau supportant une version trop récente du format de la politique de sécurité en regard de celle installée sur le système.
Un important changement apporte un raffinement au niveau des Associations de Sécurité (AS) d’IPSec afin de pouvoir les utiliser dans un environnement MLS (Multi-Level Security). L’approche actuelle pour effectuer l’étiquetage des AS pour les besoins de SELinux utilise une correspondance directe entre les règles XFRM de la politique de sécurité (XFRM est le framework implémenté dans Linux permettant d’empiler les destinations afin qu’IPSec supporte les deux versions d’IP : v4 et v6) et les AS. Cependant, dans les systèmes MLS actuels, une règle XFRM (concernant, par exemple, les niveaux de sécurité "secret" et "top secret") peut avoir besoin d’être associée à plusieurs AS (toujours dans l’exemple : les AS affectées aux contextes de sécurité "secret" et "top secret"). C’est chose faite dans la version 2.6.19 de Linux.
Une autre amélioration permet lors de la négociation IKE (Internet Key Exchange) de prendre en compte le contexte de sécurité afin de créer une AS unique correspondante.
Enfin, une autre modification rend possible l’utilisation des catégories MLS dans le paramètre context lors des opérations de montages de systèmes de fichiers.
Connu pour offrir une pile réseau très complète et étoffée, Linux propose désormais une infrastructure supplémentaire, le packet labelling. C’est une couche générique de sécurité qui permet à des systèmes distants de se mettre en accord et de définir des autorisations lors de communications réseau ; pour ce faire, chaque paquet IP reçoit un label dans son en-tête, contenant des informations de sécurité, ce qui peut par exemple permettre à son destinataire de le refuser s’il tente de transmettre un type d’information non autorisé, d’autoriser sa consultation uniquement par les utilisateurs spécifiés... Cette couche doit être couplée avec une implémentation de protocole de sécurité ; pour l’instant, c’est la solution CIPSO, devenue standard de fait, car déjà implémentée dans d’autres systèmes (Solaris...) qui est fournie.
Une protection intéressante est enfin intégrée : le support de l’option
-fstack-protector de GCC >= 4.2. Proposée lors du Kernel summit 2005 avec une série d’autres patchs appelée "ExecShield", elle transpose à l’informatique la technique des mineurs des siècles passés, qui détectaient grâce à des canaris la présence de grisou dans de nouvelles galeries avant d’y entrer eux-mêmes :-) Le principe de cette technique appelée "stack-smashing protection" est de placer aux endroits stratégiques de la pile (juste avant l’adresse de retour d’une fonction par exemple) un canari, qui est en fait ici une valeur connue. Si un bug/code malveillant cherche à écrire plus loin que ce qui est réservé, il écrase le canari. Ceci sera remonté au noyau qui se considérera alors comme non fiable et préférera se terminer en panic plutôt que continuer.
La synchronisation dans le noyau
Le 2.6.19 voit l’apparition d’un nouveau mécanisme de synchronisation (enfin, pas si nouveau que cela) : le SRCU (Sleepable Read Copy Update). En fait, il s’agit d’un dérivé du système RCU, déjà présent dans les noyaux 2.6. Cette version rend possible la préemption des threads lecteurs (fonctionnalité attendue notamment par la branche realtime du noyau).
Expliquons, à présent, le fonctionnement et le rôle du mécanisme RCU. Cet outil a été introduit dans le noyau afin de permettre un accès concurrent efficace sur une structure de données lorsque de nombreux lecteurs et écrivains sont mis en jeu. RCU est une structure sans verrou adaptée aux situations où de nombreux lecteurs ont besoin d’un accès concurrent. Il permet contrairement aux seqlocks (autre technique utilisée dans ces situations, mais faisant intervenir des verrous) un accès concurrent également pour les écrivains. De plus, le fait qu’aucun verrou ne soit utilisé aboutit à un gain de performance important. En effet, la mise à jour des compteurs au sein des verrous (par exemple pour seqlock) entraîne beaucoup d’invalidation de cache entre les différents processeurs dans un système SMP, impactant évidemment les performances.
Cependant, il ne peut pas y avoir que des avantages. Pour pouvoir utiliser cette technique, deux conditions sont nécessaires. La structure à protéger doit être accédée uniquement à travers un pointeur et il n’est pas autorisé de dormir au sein d’une section critique. Vous l’aurez compris, le nouveau mécanisme SRCU nécessite seulement la première hypothèse.
Expliquons, à présent, la magie de RCU ;) Du côté lecteur, avant d’entreprendre une lecture, nous devons faire appel Ã
rcu_read_lock() qui ne fait que désactiver la préemption. A la fin de la lecture, nous réactivons la préemption via
rcu_read_unlock(). C’est au niveau de l’écrivain qu’une astuce va s’effectuer. Quand nous souhaitons écrire dans la structure protégée, nous effectuons auparavant une copie de cette structure (via le déréférencement du pointeur d’accès) et nous la modifions à la place de l’originale. Une fois l’opération effectuée, nous basculons les structures via la modification du pointeur d’accès. Cette dernière opération étant atomique, l’utilisation de verrous n’est pas nécessaire. Remarquons une subtilité, la suppression de l’ancienne version ne peut pas être effectuée directement après l’opération de permutation des pointeurs. En effet, un lecteur sur une autre CPU peut encore y faire référence. Donc, au lieu de la supprimer, l’écrivain enregistre un callback (implémentant la suppression de l’ancienne version) dans une chaîne de notification. Ce dernier sera exécuté par une tasklet lorsque le système saura qu’il n’y a plus de référence en cours à l’ancienne version. Pour cela, il regarde à chaque tick si toutes les CPU sont passées par un état calme (quiescent state). Si le cas se présente, la tasklet est déroulée entraînant la libération de l’ancienne structure.
La dernière brique nécessaire à la compréhension du mécanisme concerne justement les états calmes. Une CPU est considérée être passée dans un état calme dans trois situations : la CPU effectue un switch de processus, la CPU vient de passer en mode utilisateur (i. e. passage du
ring0 au
ring3), ou la CPU exécute le thread oisif (idle thread de
PID 0). Il est imposé au processus lecteur de faire appel Ã
rcu_read_unlock() avant que l’une de ces trois situations ne se produise. Cela est assuré justement par l’hypothèse de départ interdisant le blocage en lecture.
Nous reviendrons dans un prochain KC sur le fonctionnement détaillé du SRCU qui, je n’en doute pas, est en train de vous tracasser ;)
Comme à l’accoutumée, nous terminons en vous signalant que cette synthèse n’est pas complète, et développe ce qui nous a semblé le plus marquant et/ou utile au plus grand nombre. N’hésitez pas à approfondir le sujet via vos amis LWN, Kernelnewbies, KernelTrap et LKML :-)