Liens connexes

Dépêche modérée par

Dépêche éditée par

: L'évolution de Fastboot

Posté par fweisbec (). Modéré le 05 janvier 2009.
59
L'idée d'Arjan Van de Ven, un développeur du noyau Linux, de tout faire pour réduire le temps de démarrage d'un système GNU/Linux à 5 secondes, a fait son petit bout de chemin depuis ces derniers mois.

Ayant réfléchi à tout ce qui pouvait être responsable de la lenteur de démarrage du noyau, Arjan en a déduit quelques observations, puis une solution. En juillet celui-ci a révélé un petit projet nommé Fastboot.

Pour expliquer ce qu'est fastboot : ce pourquoi il est né et ce qu'il propose, il faut d'abord faire un petit état des lieux de ce qui se déroule en interne au démarrage du noyau.

> Lire la suite (85 commentaires, moyenne: 2,9).   [dépêche : 12633 caractères]

L'initialisation du noyau

Commençons par le commencement.
Le noyau Linux est compilé au format ELF, qui est un format standard d'exécutables largement utilisé dans le monde des Unix. Dans un exécutable ELF, le compilateur (GCC) ainsi que l'éditeur de liens (linker Ld) utilisent beaucoup les sections pour stocker des informations utiles, telles que les fonctions appelées dans les bibliothèques externes, les informations de débuggage, etc.

Un programme destiné à être compilé au format Elf peut même créer ses propres sections pour y stocker du code ou des données spécifiques et destinées à être regroupées. Par exemple, il est tout à fait possible de regrouper le code d'un ensemble de fonctions dans une section spécifique, puis d'extraire l'adresse de cette section pour parcourir toutes les fonctions qui y sont contenues.

Ce genre d'astuce est très utilisée dans le noyau Linux, et notamment pour résoudre un problème d'envergure : la plupart des composants du noyau ont besoin d'exécuter une fonction pour s'initialiser au démarrage. Par exemple les pilotes ont besoin de signaler au noyau qu'ils attachent telles fonctions à tel périphérique matériel, ils ont besoin de signaler qu'ils écoutent sur les fichiers de périphériques (répertoire /dev), ou même d'initialiser un périphérique, voire d'allouer de la mémoire pour la suite.

En bref, chaque composant du noyau, ou presque, a besoin d'exécuter une fonction d'initialisation au démarrage du système. S'il fallait regrouper un appel à toutes ces fonctions dans un fichier source et les appeler les unes après les autres, ce serait un enfer à maintenir et franchement peu élégant.

Ainsi le noyau Linux crée une section nommée .init.text pour ranger toutes les fonctions d'initialisation dedans. Lorsqu'un pilote décide de soumettre une fonction qui s'exécutera au démarrage, il a juste à faire précéder le nom de sa fonction par l'annotation __init, ce qui rangera le code de la fonction dans la section .init.text. Ensuite, il faudra que ce pilote appelle la macro device_initcall pour ranger l'adresse de sa fonction dans une autre section appelée .initcallxx.init

Cela ne sert pas à grand chose d'expliquer ce que signifie le xx ici, mais il est remplacé dynamiquement pour définir plusieurs niveaux d'initialisation, par exemple le cœur du noyau peut avoir besoin de s'initialiser avant le système de fichiers, etc.

Exemple :
int __init ma_fonction(void);
device_initcall(ma_fonction);

À la compilation, GCC va regrouper toutes ces fonctions dans la section .init.text et leurs adresses dans la section .initcallxx.init
Le noyau, quant à lui, aura pris soin de baliser la section .initcallxx.init avec deux pointeurs :

Au démarrage, lorsque le noyau veut exécuter toutes ces fonctions, il procède en utilisant une boucle :
for (call = __initcall_start; call < __initcall_end; call++)
call();

C'est un schéma grossier de ce qui se passe réellement, mais en réalité les choses ne sont pas beaucoup plus compliquées : tous les pointeurs de fonctions contenus dans la section .initcallxx.init vont s'exécuter.

À la fin de cette séquence, la mémoire utilisée pour stocker le code de ces fonctions sera libérée (la section .init.text).

Le problème qui se pose

Comme vous avez pu le voir, ces fonctions sont exécutées les unes après les autres. Ce sont les résultats de ces fonctions qui sont affichés sur votre écran au démarrage. Il peut y en avoir plusieurs centaines qui s'exécutent, certaines sont plus longues que d'autres.

Ce qui peut chagriner ici, c'est que les appels de ces fonctions sont sérialisés, ou encore synchrones. Si le mot peut faire peur, en réalité il traduit un concept simple : la prochaine fonction ne s'exécutera pas tant que la fonction en cours d'exécution n'est pas terminée.

Cela peut faire tiquer si l'on recense les ressources disponibles à ce stade du démarrage :

Juillet 2008, premier jet, premier Fastboot

En juillet dernier, Arjan Van de Ven a posté une première solution découpée en trois patchs :

Cette solution créait un nouveau niveau d'initialisation appelé asynchronous initcall, ce qui signifie fonction d'initialisation asynchrone. Il était donc possible, par le biais de ces patchs, de définir des fonctions d'initialisation qui pouvaient s'exécuter en parallèle à d'autres fonctions d'initialisation. Ou pour faire simple : plusieurs fonctions d'initialisation pouvaient maintenant s'exécuter en même temps, permettant ainsi de profiter des ressources rendues disponibles par la préemption et le multi-processeur.

Par défaut, les fonctions __init continuaient de s'exécuter de manière synchrone, les unes après les autres. Mais les développeurs aventureux et désireux d'optimiser le démarrage du noyau pouvaient tester leur fonction d'initialisation de manière asynchrone.

Ce travail était implémenté en utilisant le système des workqueues. C'est-à-dire un thread s'exécutant dans le noyau dans lequel réside une file d'attente de tâches à exécuter. Les workqueues sont une solution légère : elles ne nécessitent pas de création de multiples threads, on a juste un seul thread qui possède une liste de fonctions à exécuter.

Un nouveau thread de type workqueue était donc créé, et lorsqu'une fonction d'initialisation asynchrone était trouvée, elle était ajoutée en queue de liste du workqueue.

Ce qui implique une chose : ce workqueue n'exécutant qu'une seule tâche à la fois, seules deux fonctions d'initialisation pouvaient s'exécuter en parallèle: la partie synchrone, donc les fonctions __init habituelles, et une fonction exécutée par le workqueue.

Qu'est-il arrivé à cette solution? Il semble que Linus n'ait pas trop apprécié l'approche. L'idée de l'exécution asynchrone ne semblait pas mauvaise, mais il préférait quelque chose de plus granulé. Somme toute, ramener la partie asynchrone du démarrage à des choses plus fines plutôt que sur tout une fonction d'initialisation.

Arjan est donc revenu en ce 4 janvier 2009 avec une nouvelle approche.

Janvier 2009, un fastboot 2, plus granulé

Le 4 janvier 2009, Arjan revient avec une nouvelle approche. L'idée des initcalls complètement asynchrones a été abandonnée au profit d'une API permettant à quiconque de décider quelles parties de ses fonctions d'initialisation seront asynchrones.

Il ne s'agit plus maintenant de rendre toute une fonction d'initialisation asynchrone, mais de décider quelle(s) partie(s) d'une fonction d'initialisation devra s'exécuter de manière asynchrone.

Il suffit d'appeler la fonction
void async_schedule(async_func_ptr *ptr, void *data)
ptr étant la fonction à exécuter et data, les données à lui passer en paramètre.

C'est une idée beaucoup plus souple, laissant plus de contrôle au développeur et permettant ainsi d'éviter des conditions de concurrence, d'incohérences d'états au niveau du système. Exemple : que se passerait-il si le pilote de votre disque n'avait pas fini de s'initialiser pendant le montage de votre système de fichier ?

Pour éviter ce genre de situation, cette nouvelle API fournit de nouveaux outils de synchronisation. Lorsqu'une fonction asynchrone est créée, celle-ci reçoit un "cookie", permettant ainsi de l'identifier par rapport aux autres. Si cette fonction décide à un moment ou à un autre d'attendre que toutes les fonctions asynchrones qui ont été lancées avant elle se terminent, il lui suffit de lancer la fonction
void async_synchronize_cookie(async_cookie_t cookie) en passant son propre cookie. C'est donc un outil de synchronisation entre fonctions asynchrones.
Par exemple si A et B sont des fonctions asynchrones qui font l'état des lieux de certains périphériques. Et si C a été lancée après A et B, et qu'à un moment C a besoin de la liste de tous les périphériques recensés par A et B, alors il lui suffit d'appeler async_synchronize_cookie pour être sûre que A et B ont bien fini leur travail et ont tout trouvé.

Un autre outil de synchronisation a été prévu pour que les fonctions d'initialisation synchrones puissent attendre que toutes les fonctions asynchrones soient terminées.
Pour reprendre l'exemple de tout à l'heure, lorsque la fonction d'initialisation qui va monter la partition racine (/) va s'exécuter, elle voudra être sûre que l'initialisation des périphériques de stockage est terminée, sans quoi elle n'aurait pas de système de fichier à lire. Si ces périphériques sont encore en cours d'initialisation à cause de fonctions asynchrones, alors il suffira d'appeler void async_synchronize_full(void)pour attendre leur terminaison.

Si async_synchronize_cookie permet une synchronisation entre fonctions asynchrones, async_synchronize_full permet de synchroniser entre fonctions synchrones et asynchrones.

Voilà, mal de crâne mis à part, il semble que l'idée soit en bonne voie. Si des commentaires critiques sont évoqués dans la révision de ces patchs, pour l'instant ils semblent seulement concerner de petits détails et non pas l'idée principale, ce qui est plutôt bon signe et augure une bonne voie quant à l'inclusion de ces patchs dans la branche principale du noyau, avec beaucoup de chance pour la fenêtre d'inclusion (merge-window) en cours du 2.6.29, avec un peu moins de chance pour 2.6.30.

Dans tous les cas, il y a de fortes chances qu'on retrouve les évolutions du développement de fastboot dans la branche -tip maintenue par Ingo Molnar.

Voici de quoi dépend la rapidité de démarrage de vos futures distributions Linux.

Cette discussion est archivée, il n'est plus possible de laisser des commentaires.

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.

Importance

Posté par Pascal Terjan (Jabber id, page perso, ) le 05/01/2009 à 11:29. (lien). Évalué à 10.

Voici de quoi dépend la rapidité de démarrage de vos futures distributions Linux.

Ces optimisations servent à gagner quelques secondes dans le cas ou tu n'as pas d'initrd (ou initramfs...), elles ne se voient pas sinon.

Elles lui ont sans doute permis de passer de 7 ou 8s à 5s, mais la plupart des distros sont à au moins 20 ou 25s, et beaucoup sont plutot à 40s.

Il y a beaucoup de choses qui influencent plus sur une distribution générique, les divers services en particulier, ou le temps passé dans l'initrd.

Il y a donc je pense beaucoup de choses plus visibles pour améliorer le temps de boot.

Je peux citer par exemple l'ajout du cache à modprobe qui fait passer le coldboot udev de 11s à 7s sur mon laptop (mais arjan n'a pas ce problème il a fait une "distrib" specifique à sa machine).

Chapeau

Posté par Loul (page perso, ) le 05/01/2009 à 11:33. (lien). Évalué à 10.

Beau travail du développeur... bien rendu par cette excellente dépêche !

Je trouve étonnant que la parallélisation des initialisations ne soit pas apparue plus tôt, ça ne semble pas une idée si farfelue pourtant...

Et combien de temps avant qu'un nombre significatif de pilotes soit mis à jour avec l'API ? Et quand on utilise du vieux matos avec des pilotes qui n'évoluent plus trop ?

Fort intéressant...

Posté par windu.2b (Jabber id, page perso, ) le 05/01/2009 à 11:41. (lien). Évalué à 9.

Tout d'abord bravo et merci pour ces explications claires et compréhensibles, même pour un néophyte du noyau et des drivers, comme moi.
Un article comme ça, ça fait toujours plaisir à lire :-)
Du "patrick_g" dans le texte !

Par contre, quelques détails m'échappent, quand je lis ceci :
"Par exemple si A et B sont des fonctions asynchrones qui font l'état des lieux de certains périphériques. Et si C a été lancée après A et B, et qu'à un moment C a besoin de la liste de tous les périphériques recensés par A et B, alors il lui suffit d'appeler async_synchronize_cookie pour être sûre que A et B ont bien fini leur travail et ont tout trouvé."
Comment C indique-t-il qu'il n'attend que A et B, et pas autre chose ? En passant les cookies de A et B à la fonction "async_synchronize_cookie" ? Mais comment les connait-il dans ce cas ?
Ou alors est-ce simplement le fait d'avoir été lancé après A et B qui sous-entend qu'il attend ces derniers ? Dans ce cas, Z risque de devoir attendre des fonctions inutilement...

Intéressant

Posté par bluelambda () le 05/01/2009 à 11:42. (lien). Évalué à 6.

Très bonne new bien détaillée et bien expliquée!

Et les scripts de démarrage?

Posté par zakMcKraken () le 05/01/2009 à 12:15. (lien). Évalué à 5.

Le problème ne se pose t il pas aussi pour les scripts de démarrage?
A ma connaissance, tous les scripts de /init.d démarrent séquentiellement. En les parallélisant on gagnerait un temps non négligeable. Le problème est de mettre en place un mécanisme pour savoir quels sont les scripts qui ont besoin d'autres scripts afin que ceux ci démarre en premier. Bref, la problématique est la même que pour le noyau. Il me semble qu'une distribution avait déjà tenté un truc du genre...

Il ne reste plus qu'à...

Posté par Spack () le 05/01/2009 à 14:03. (lien). Évalué à 0.

...attendre que patrick_g annonce l'intégration de fastboot dans les prochaines dépêches sur le noyau...

Sinon, cela semble être la continuité de la chose, le nombre de processeurs/cœurs augmente, il faut donc adapter les programmes pour les utiliser correctement...

bsd ??

Posté par Mehdi Saada (Jabber id, ) le 05/01/2009 à 22:51. (lien). Évalué à 1.

Excusez moi mais quand est-il chez netBSD & co ?
Le script d'initialisation, l'asynchronisation au niveau du noyau ?
Merci

Sujet Intéressant!

Posté par Pime () le 21/01/2009 à 18:40. (lien). Évalué à 1.

Cela fait quelques jours (et nuits) que je m'attaque au sujet.
Sous Debian, je parallèllise avec startpar.
J'ai atteint 9,5 secondes pour booter
Réseau USB kde3.3 + 6 consoles textes, LVM, mais pas sur
partitions système.

Il est vrai que j'ai dégraissé plusieurs services non primordiaux,
en privilégiant le chargement à la demande.
D'après mon analyse
J'ai détecté 3 problèmes :

1 init : passage du niveau S au niveau N : temps mort.
En effet , à ce stade, il relance le script rc qui va boucler
sur le nouveau runlevel, mais la règle est que tous les
scripts du runlevel S doivent être terminés.

2 sourcing : tous les scripts ou presque sourcent des fichiers
et ce sont souvent les même = (redondances + IO inutiles)

3 execution du noyau : j'ai 4 à 6 seconde mortes sans activité.
Plus le noyau est récent et plus c'est long.

Ma meilleure performance est avec un noyau 2.6.8
Je travaille toujours avec des noyaux recompilés SANS l'initrd.
Je n'utilise pas udev, mais je vais peut-être quand-même y revenir
car le gain de temps n' est pas si convainquant que cela.

Si quelqu'un pouvait me dire comment raccourcir l'execution du noyau?
Merci.

Revenir en haut de page