De retour pour cette nouvelle édition, nous vous proposons un Corner consacré au noyau Linux 2.6.20, qui devrait être sorti au moment où vous lirez ces lignes. Linus a souhaité que la 2.6.20 soit une version de stabilité. Ainsi, il n’hésite pas à retirer (revert) des patchs de fonctionnalité, pourtant inclus dans la -rc1, qui posent problème et préfère les réintégrer dans le cycle suivant, celui du 2.6.21. Adrian Bunk, quant à lui, continue son travail sur la stabilisation du 2.6.16 qui en est à la 38ème révision, et maintient une liste de régression entre le 2.6.16 et la dernière version afin que la fiabilité de la série 2.6 soit progressivement éprouvée. Cependant, il y a de quoi assouvir votre curiosité avec l’arrivée de cette nouvelle mouture qui inclut quelques belles surprises. Nous terminons ce Kernel Corner sur l’explication du bug le plus monopolisant pour la communauté des développeurs du noyau Linux. Sa résolution a pris tout le mois de décembre !
[Actualité] Le noyau 2.6.20
La virtualisation
Avant d’aborder le sujet de la virtualisation du noyau, notons l’introduction du support pour la compilation optimisée pour le processeur Intel Core 2 Duo (processeur pouvant assister matériellement la virtualisation). Pour l’instant, GCC ne gère pas encore l’optimisation possible avec cette architecture (prévue dans la version 4.3). Ainsi, le support est ajouté principalement pour prendre en compte les lignes de cache de 64 bits du Core 2 Duo.
Pour commencer réellement sur la virtualisation, notons l’intégration de KVM (Kernel-based Virtual Machine driver créé par Avi Kivity) dans cette version du noyau. Nous en avions parlé dans le Kernel Corner 90 assez longuement. Cependant, une activité débordante s’est déroulée depuis et se déroule encore au sein de KVM. Nous allons y revenir, mais notons déjà la réimplémentation des shadow page tables, point critique pour les performances. Nous trouvons également en vedette une fonctionnalité de taille, introduite par Ingo Molnar, qui ajoute le support de la paravirtualisation pour KVM ! Un travail de titan est à l’œuvre afin de doter Linux de fonctionnalités évoluées au niveau de la virtualisation.
Nous avions mentionné dans le KVM précédent que l’implémentation des shadow page tables était basique et loin d’être optimale en termes de performance. Maintenant, ces tables sont mises en cache afin d’éviter les reconstructions complètes des tables de pages des invités à chaque changement de contexte. Malgré un certain nombre de difficultés à surmonter, les systèmes invités tournant sur KVM sont plus rapides et réactifs.
Terminons avec l’introduction de la paravirtualisation dans KVM, qui n’est pour l’instant implémentée que pour des systèmes Linux sur Linux. En plus de cela, Ingo Molnar ajoute le support de la fonctionnalité du cache matériel du cr3 (cr3 est le registre de contrôle numéro trois qui contient notamment l’adresse de la PGD – Page Global Directory – du processus s’exécutant) des processeurs Intel de type VT-x, allant une étape au-dessus de l’implémentation du cache logiciel des shadow page tables pour les architectures Intel. Les gains lors de l’utilisation de la paravirtualisation et du cache matériel du cr3 en ce qui concerne les changements de contexte et les flushs du TLB (Translation Lookaside Buffer) sont très impressionnants. Dans cette configuration, KVM est relativement proche d’un système sans virtualisation, en termes de performance sur les aspects mentionnés. Notons que si le système Linux invité intégrant KVM avec la paravirtualisation ne détecte pas les capacités de paravirtualisation du côté de l’hyperviseur, il passe automatiquement au mode de fonctionnement de virtualisation complète. Pour plus de renseignement au sujet de KVM, jetez un œil à http://kvm.sourceforge.net.
Afin de supporter différents types de virtualisation, Rusty Russel a intégré la structure paravirt_ops, contenant l’ensemble des appels critiques qu’un hyperviseur doit implémenter. Dans le cas où CONFIG_PARAVIRT est activé, le fichier d’en-tête paravirt.h est inséré dans le code afin que toutes les opérations critiques qui ont besoin d’être remplacées par des appels à l’hyperviseur, le soient.
Les périphériques
La version 2.6.19 ayant vu apparaître la découverte parallélisée des périphériques PCI, ce sont cette fois les périphériques USB et SCSI qui peuvent en bénéficier. Plus précisément, la découverte de périphériques sur un bus SCSI peut être asynchrone : les périphériques s’initialisent tandis que le système effectue en parallèle d’autres opérations de boot, ceci étant possible simultanément sur plusieurs bus SCSI. Concernant l’USB, l’initialisation de chaque périphérique par son driver peut être effectuée dans un thread noyau (kthread). Une légère amélioration du temps de boot peut être observée sur un système monoprocesseur, l’effet le plus marqué se fera apprécier sur les systèmes multicœurs/multiprocesseurs. Pour tester, il faudra respectivement activer les options SCSI_SCAN_ASYNC (SCSI device support -> Asynchronous SCSI scanning) et/ou USB_MULTITHREAD_PROBE (USB Support -> USB Multi-threaded probe). Cependant, coup de théâtre, cette fonctionnalité aura été de courte durée pour le bus PCI : elle sera marquée broken pour le 2.6.20 et devrait être retirée pour le 2.6.21. En effet, par définition il n’y a pas de support d’opérations asynchrones au niveau du bus PCI, alors que c’est le cas pour l’USB et SCSI. Linus Torvalds et Andrew Morton ont invoqué cette raison, plus le fait que beaucoup de drivers ne fonctionnent plus avec cette option activée.
Ce travail effectué sur le noyau trouve un complément dans les nouveaux projets d’init rapide et/ou parallélisé, tels que InitNG, UpStart... Une fois ceux-ci généralisés dans les distributions, nous ne devrions plus avoir à rougir en démarrant notre distribution favorite face à ses concurrents moins libres.
Toujours au sujet des bus, le développeur Benjamin Herrenschmidt introduit un ensemble de nouvelles structures apportant la " notification des événements de bus ", se déclenchant à quatre occasions : ajout et retrait de périphérique, attribution et désattribution d’un pilote pour un périphérique. Un exemple dans lequel ce nouveau code trouve son utilité est le cas où un pilote est dépendant de la présence d’un autre pilote et/ou périphérique, et doit donc savoir quand ceux-ci apparaissent dans le système.
L’architecture interne du noyau se voit remaniée en ce qui concerne les périphériques d’interaction avec l’utilisateur (dits " HID ", pour Human Input Device), tels que clavier, souris, manette... Le code utilisé jusqu’à maintenant par les périphériques de type USB (module noyau usbhid) a été adapté en une couche générique pouvant être utilisée par les autres catégories : Bluetooth, PS2... Elle assure principalement le dialogue entre le système et les périphériques, et la remontée d’événements déclenchés par ces périphériques.
Le travail sur la libATA générique continue, avec des bogues résolus, le support de nouveaux chipsets (Marvel, Winbond...) et de fonctionnalités supplémentaires sur l’existant (NCQ ou Native Command Queuing, cf. Kernel Corner 86, pour les puces SATA Nvidia). Côté système de fichiers, très peu de changements à noter. Utile pour les administrateurs système, des statistiques sur la consommation d’opérations d’entrées/sorties par processus font leur apparition dans /proc. Il est ainsi possible de savoir simplement les volumes de lecture et écriture effectués par un processus via un cat sur /proc/<le pid>/io. Ces informations sont cumulatives. Pour avoir les chiffres instantanés, il vous faudra donc faire la différence entre la valeur actuelle et une valeur prise peu de temps auparavant. N’oublions pas que depuis le 2.6.17, Linux nous permet de modifier la priorité d’accès aux entrées/sorties d’un processus, ce qui nous permet donc d’effectuer un iorenice sur un processus gourmand en accès disques qui en gênerait d’autres.
Dégradées depuis les débuts du noyau 2.6, les écritures en mode Direct IO (O_DIRECT) sur les périphériques de type bloc (disques) voient leurs performances significativement améliorées ; ceci est valable pour, par exemple, les SGBD, qui préfèrent accéder directement au périphérique (via /dev/{s|d}X) sans passer par le cache du noyau. Ce progrès a pu être obtenu en simplifiant le code exécuté dans le cas d’un transfert vers un périphérique de bloc.
Les systèmes de fichiers
Le système de fichiers OCFS2 peut désormais gérer le atime relatif, c’est-à -dire que la métadonnée de chaque fichier indiquant sa date de dernier accès peut n’être modifiée que si elle est plus ancienne que le ctime (date de création) ou le mtime (date de modification) au lieu de l’être systématiquement. Ceci améliore les performances et est similaire à l’option noatime (ne pas modifier la valeur atime quand le fichier est accédé en lecture).
Le réseau
La partie réseau de Linux bénéficie aussi d’évolutions, quelques-unes sont à signaler en particulier. Tout d’abord, le support du NAT (Network Address Translation ou translation d’adresse) fait son apparition dans le module nf_conntrack, couche de connection tracking (suivi de connection) générique de Netfilter gérant l’IPv4 et IPv6. Ce module est voué à remplacer ip_conntrack à brève échéance (les développeurs Netfilter parlent de son retrait dès Linux 2.6.22). Plusieurs NAT helpers qui n’existaient auparavant que pour ip_conntrack font leur apparition, en particulier pour les protocoles FTP, IRC, PPTP, SNMP, SIP et TFTP. Il s’agit de petits modules dédiés à la gestion de paquets spécifiques à un protocole de couche applicative.
Un protocole de plus est maintenant géré. Il s’agit d’UDP-lite, un protocole dérivé d’UDP. Comparé à ce dernier qui rejette un paquet entier si sa somme de contrôle est incorrecte, UDP-lite autorise un paquet incorrect à être traité, laissant à la couche applicative le soin de décider s’il est exploitable ; moins d’informations perdues peut signifier des améliorations dans la transmission de flux " en direct ", tels que son ou vidéo.
Le monde des pilotes de périphériques ne connaît pas non plus de bouleversements majeurs avec cette nouvelle version, mais on y distingue :
- du côté des webcams, l’ajout du module
usbvisionapporte la gestion par Linux d’une cinquantaine de modèles USB supplémentaires (puces Nogatech), tandis que le nouveau module apporte de son côté la gestion des puces de type OV76xx ; - des améliorations dans la prise en charge des puces Intel i915 par le DRM (Direct Rendering Manager) ;
- le support de l’interface Ethernet Atmel présente dans les microcontrôleurs AVR32 (Cf. Kernel Corner 89) ;
- la gestion des tuners DVB USB Pinnacle 400e ;
- La gestion de l’Apple Motion Sensor ;
- l’apparition de pilotes pour les cartes Ethernet de type NetXen 1-10G et Chelsio ;
- la poursuite du retrait des pilotes de périphériques de son utilisant l’obsolète sous-système OSS.
Les travaux différés
Les Workqueues sont un mécanisme intégré depuis le développement de la 2.6. Ils sont mis à disposition des threads noyau afin qu’ils puissent soumettre des travaux qui seront exécutés de façon différée dans un contexte de processus. En effet, ce travail est exécuté soit par l’idle thread, soit par un thread spécialisé associé à la workqueue. Ainsi, les fonctions différées peuvent être bloquantes. Notons la différence avec les Tasklets, qui sont, quant à elles, exécutées dans un contexte d’interruption et ne peuvent donc pas bloquer. Dernière remarque, les workqueues contiennent toutes une liste de travaux, lesquels sont représentés par des structures work_struct.
David Howells a remanié les work_struct, afin d’éviter des gaspillages de mémoire. En effet, jusqu’à présent le descripteur de travail work_struct embarquait toujours un timer logiciel, lequel pouvait être utilisé pour retarder le travail à effectuer. Cependant pour plus de la moitié du code du noyau, ce timer n’est pas utilisé, occupant ainsi de la place en mémoire pour rien. Le changement principal est d’avoir enlevé le timer de la work_struct, créant ainsi un descripteur primitif. Dans le cas où l’on souhaite utiliser le timer, une nouvelle structure regroupant un timer et une work_struct est utilisée. Ainsi, on obtient des gains significatifs sur la taille des work_struct :

Notons également la diminution en taille dans le cas des work_struct temporisables. Cela est dû au retrait du pointeur (= un mot mémoire) data de la work_struct. La fonction de travail prenait jusqu’à présent ce pointeur en argument. Maintenant, pour que la fonction de traitement puisse accéder à ces données, il suffit d’embarquer la work_struct au sein d’une structure privée et ensuite d’utiliser la primitive container_of (Cf. Kernel Corner 86).
Les timers
Au niveau de la fréquence du timer, la valeur par défaut a été positionnée à 300 Hz (à la place de 250 Hz dans les versions précédentes), afin d’optimiser le traitement multimédia. En effet, le taux de trames par seconde utilisé en Europe est de 25, mais pour les États-Unis de 30. 300 étant un multiple à la fois de 25 et de 30, il a été jugé convenable de faire cette modification (proposée par Alan Cox). De plus, 300 se trouve être également un multiple de 50 et de 60, correspondant aux taux de trames par seconde en travail interlacé dans les deux cas considérés précédemment. Finalement, les performances induites par ce changement dans les autres types d’activité du noyau sont similaires à celles d’avant.
Dans le domaine des timers, une nouveauté importante est l’ajout par Arjan van de Ven des fonctions round_jiffies et round_jiffies_relative. Tout d’abord, expliquons brièvement le rôle de la variable jiffies présente dans le noyau. Elle est incrémentée à chaque interruption – tick – du timer, permettant la création de timers logiciels dont l’échéance peut être donnée relativement à cette variable. Les fonctions introduites arrondissent une valeur de jiffies à la seconde supérieure. Le principal objectif de ces fonctions est de regrouper sur le même jiffy les timers logiciels qui n’ont pas de contraintes fortes dans le temps. Ainsi, on évite l’enclenchement de nombreux timers à des dates différentes dans la même seconde. De plus, avec l’utilisation du patch dynamic ticks (Cf. Kernel Corner 87), les états de profond sommeil de la CPU durent plus longtemps. Cela introduit par conséquent un gain d’énergie significatif. Notons que pour les architectures à plusieurs CPU, le moment de réveil exact des différents CPU est modifié en fonction de leur position, cela afin d’éviter qu’il puisse se réveiller exactement au même moment et toucher les mêmes lignes de cache par exemple (qui introduirait des aller-retours supplémentaires non négligeables).
Un casse-tête pour résoudre un bug de corruption de fichiers
Durant le mois de décembre, une bonne partie des développeurs de plus haut niveau du noyau Linux était occupée à traquer un bug de corruption de fichiers soumis par un utilisateur et affectant le 2.6.19.1 Après de longs efforts, de nombreux tests, un audit minutieux des sous-systèmes suspectés, Linus a pu mettre au point un programme capable de déclencher facilement la corruption. Après avoir élagué les différentes possibilités, Linus a proposé le soir de Noël un patch permettant de corriger le problème.
La corruption de fichier pouvait se produire dans une situation de mapping (i. e. mise en correspondance) partagé de fichier (shared mmap). Le bug était une race condition. Afin d’appréhender correctement le bug, expliquons brièvement le fonctionnement du page writeback (i. e. le traitement des pages mémoires nécessitant une réécriture sur le disque).
Une page de mémoire physique peut être mappée dans l’espace d’adressage de différentes applications (shared mmap). Pour chacune d’elle, une entrée dans une table de page (PTE : page table entry) de l’application est créée. Elle établit la correspondance entre l’adresse physique de la page et l’adresse virtuelle de l’espace d’adressage du programme y faisant référence. Pour chaque PTE, les 12 bits de poids faibles sont utilisés comme bits de contrôle (seulement les 20 bits de poids forts sont nécessaires pour adresser une page de 4 Ko). L’un d’eux est le dirty bit (bit attestant de la modification d’une page) et le processeur le positionne à 1 lorsque la page est modifiée par l’application. Dans la situation où plusieurs PTE pointent sur la même page physique, des différences d’état du dirty bit sont courantes. Ainsi, il est nécessaire de mettre en place un moyen de traque de toutes ces PTE. Le noyau Linux maintient un ensemble de descripteurs de pages correspondant exactement au nombre de pages disponibles dans la mémoire physique. Ces descripteurs contiennent différents champs dont un member flag statuant entre autres sur l’état dirty de la page. L’ensemble des PTE associées à la page physique sont également accessibles au travers de ce descripteur, tout comme les blocs correspondants sur le disque dur, s’il s’agit d’un mapping de fichier.
Venons-en à l’unité d’écriture sur un disque : le bloc dont la taille fait 512 octets. Si une page mémoire mappe une partie d’un fichier, elle peut en contenir 8 blocs. Ces blocs sont représentés dans le noyau par des descripteurs appelés buffer heads. Leur utilisation est maintenant très réduite, car un cache de page est utilisé globalement dans le noyau. Ainsi, ces buffer heads sont un reste datant du 2.4, toujours employé par certains systèmes de fichiers, notamment ext3 (ainsi qu’ext4 actuellement), afin d’effectuer les écritures de page mémoire sur le disque. Ces buffer heads ne sont créées qu’au besoin par le système de fichiers. Ainsi, seuls les blocs dirty sont représentés avant écriture sur le disque. Vous l’aurez deviné, ces buffer heads contiennent également un dirty flag.
Venons-en au problème. Lorsque le gestionnaire de mémoire s’aperçoit qu’une page a été modifiée via la lecture du dirty bit d’une PTE ou une opération explicite d’une application, il va appeler la fonction set_page_dirty. Cette dernière appelle la fonction de rappel définie par le système de fichiers, si elle existe. Cette dernière n’est quasiment jamais définie. Aussi, le gestionnaire de mémoire, dans cette situation courante, traverse la liste de buffer heads et les marque dirty afin que le système de fichier soit au courant de l’état des blocs et les recopie sur le disque (via sa méthode writepage).
Le problème apparaît lorsqu’une même page physique est partagée. À partir de ce moment, deux traitements simultanés peuvent se produire. Une écriture disque induite par le système de fichier via writepage peut s’effectuer alors que le gestionnaire de mémoire est en train de parcourir la liste des buffer heads pour les marquer dirty. A la fin des opérations d’E/S, le système de fichiers va positionner à 0 le dirty flag des buffer heads, car il vient tout juste d’écrire les blocs correspondants sur le disque. Cependant, le gestionnaire de mémoire les avait positionnés à 1. Ainsi, des blocs de données en mémoire nécessitant d’être répercutés sur le disque ne le seront pas. Cette race condition entraîne ainsi la corruption du fichier.
Le patch temporaire proposé par Linus correspond en gros à effectuer un nouvel appel à set_page_dirty, dans la fonction appelée par le système de fichier lorsqu’il entame sa remise à zéro du dirty flag des buffer heads.
Concluons sur le fait que Linus s’insurge contre l’utilisation " de nos jours " des buffer heads dans les systèmes de fichiers comme ext3, car ils passent outre le page cache, et induisent des problèmes de performance significatifs, notamment lors de l’exécution de readdir. Pour l’instant, ext4 n’apporte pas de modification à ce niveau, mais Theodore Tso a proposé, en réponse à Linus, de corriger cela.
Finissons cette section " bug " en mentionnant qu’il est désormais possible, pour l’architecture i386, de charger le noyau à n’importe qu’elle adresse alignée sur un multiple de 4 Ko et en dessous de 1 Go. Cette fonctionnalité est intéressante pour ceux qui testent des noyaux afin de générer des crash dumps (Cf. le sous-système Kexec).


