La magie des filesystems : 1- Tour d'horizon

Magazine
Marque
GNU/Linux Magazine
Numéro
168
Mois de parution
février 2014
Spécialité(s)


Résumé
Le filesystem... Si notre OS préféré a plusieurs modules intéressants, c'est peut-être le plus sympa ! Cette première partie vous propose de le rencontrer.

Body

1. Le filesystem au début de son histoire

Historiquement, un système de fichiers n'était guère plus qu'un simple « gestionnaire de disques ».

Ce module de l'OS a été conçu comme un passage obligé pour tous les processus qui souhaitent écrire ou lire sur le disque. Avec cette position centrale, on pouvait assurer une cohérence entre les zones allouées pour ces différents processus.

On a alors appelé ces « zones du disque » des « fichiers » et donc, ce module est devenu un « système de fichiers », au sens classique du terme.

Pour repérer les fichiers sur le disque, on a aussi introduit les répertoires, ce qui a permis d'obtenir une vision hiérarchique (arborescente) des données.

2. Les filesystems dits « virtuels »

Cette approche ayant eu un succès certain, on a commencé à étendre l'utilisation des filesystems bien au-delà de cette approche historique. À tel point qu'on dit, depuis un certain temps déjà, que « tout est fichier ».

Ce qu'on a gardé de l'approche classique du système de fichiers, c'est la représentation arborescente. Par contre, pourquoi ne pas représenter autre chose que des zones allouées dans un espace de stockage ?

Par exemple, pourquoi ne pas utiliser un filesystem pour représenter les informations sur les processus en cours d'exécution, avec un répertoire par PID ? C'est ce que fait (entre autres fonctionnalités) le filesystem monté sur /proc.

Et les périphériques connectés ? C'est ce qu'on monte sur /dev.

Et pourquoi ne pas revenir à la notion historique des filesystems et donc, faire de la gestion d'espace, mais en mémoire plutôt que sur disque ? Pour des fichiers temporaires, cela pourrait grandement accélérer les choses ! Eh bien c'est ce que fait tmpfs.

Je vais m'arrêter là, mais il est clair que les exemples ne manquent pas.

Ce qu'il faut bien voir, c'est qu'avec chaque nouveau filesystem de ce genre, on étend d'avantage le concept du « tout est fichier ». Et j'espère bien qu'on continuera à l'étendre. Par exemple, sous Linux, les interfaces réseau ne sont pas (encore) vues comme un fichier. Cela serait pourtant utile pour envoyer ou lire des paquets en mode « raw », en utilisant simplement nos outils UNIX classiques1... Cela dit, rien n'empêche2 de coder un filesystem adéquat, qui remplisse cette fonction et d'autres, et qu'on pourrait monter sur /net...

3. FUSE : codage en espace utilisateur

En réalité, cette explosion de filesystems en tous genres n'en était qu'à ses débuts. Il était clair que le concept de filesystem pouvait être utilisé à des fins très diverses et la demande était bien là. Mais l'offre était finalement limitée, car tout le monde n'a pas l'habitude de la programmation noyau.

Jetons un œil sur l'architecture sur laquelle reposent les filesystems.

Le cas le plus simple est sans doute celui d'un filesystem comme procfs.

archi_proc

Fig. 1 : Architecture de procfs

Sur cette figure, la commande cat /proc/1121/exe est exécutée. Sous Linux, le processus d'analyse des chemins est effectué par le module VFS (Virtual File System) du noyau. Celui-ci analyse donc le chemin /proc/1121/exe à partir de la racine jusqu'au dernier élément. Comme un filesystem de type procfs est monté sur /proc, il transmet la requête de lecture au module dédié à la gestion de ce filesystem (module proc indiqué sur la figure), qui renverra le résultat.

Le cas de ext4 n'est pas vraiment plus complexe.

archi_ext4

Fig. 2 : Architecture de ext4

Cette fois, c'est la commande cat /etc/hosts qu'on exécute. À nouveau, le chemin est analysé par VFS. Comme un filesystem de type ext4 est monté sur /, il transmet la requête de lecture au module dédié à la gestion de ce filesystem (module ext4 indiqué sur la figure). Ce module fera alors une lecture sur le disque (sauf si les données sont en cache) et pourra retourner le résultat.

Avec l'apparition du module FUSE dans le noyau, une nouvelle architecture est apparue (voir figure suivante) et avec elle, une nouvelle méthode pour implémenter les systèmes de fichiers.

archi_fuse

Fig. 3 : Architecture de FUSE

Cette fois, la commande que l'on a exécutée est : cat /tmp/mountpoint/stock. On considère ici qu'un filesystem « charpentefs », utilisé pour gérer les stocks d'un charpentier, est monté sur /tmp/mountpoint. Charpentefs étant un filesystem basé sur FUSE, c'est vers ce module que la requête sera redirigée. Ce module fait alors office de passerelle vers le programme chargé de la gestion de ce filesystem. Ce qui est fondamental ici, c'est que ce programme tourne en espace utilisateur et non dans le noyau...3

Développer un filesystem en utilisant FUSE présente ainsi un avantage important : le programmeur n'a pas à écrire du code noyau. Et, s'agissant de code utilisateur, les risques pour la stabilité du système sont moindres, donc le développement peut se faire sans trop de précautions.

En revanche, cette indirection supplémentaire provoque évidemment une baisse de performances. Linus Torvalds a déclaré un jour à ce sujet : « People who think that userspace filesystems are realistic for anything but toys are just misguided »4. Les avis tranchés de Linus sont bien connus... ;) A mon avis, FUSE est parfait pour un prototype ou même, si on n'a pas besoin de hautes performances, pour un filesystem finalisé. Et même en matière de performances, il faut faire la part des choses. Par exemple, si les données qu'affiche votre filesystem sont stockées sur une autre machine du réseau, alors le temps d'accès à ces données aura sans doute d'avantage d'impact que l'architecture (FUSE ou noyau) que vous choisirez.

4. Les live CD : une arborescence de filesystems

Comme nous allons le voir dans cette partie, les live CD constituent un cas d'utilisation relativement avancée des filesystems.

4.1 Les premiers live CD : ramdisk + filesystem

Le côté magique des filesystems me fascine depuis plusieurs années. Vers 2004, j'ai étudié le fonctionnement de la distribution Damn Small Linux et j'ai fini par la modifier pour les besoins de l'entreprise où je travaillais. Ce qui m'intéressait le plus, c'était l'aspect « live ». Comment peut-on, à partir d'un support en lecture seule (CD), booter un système qui nous autorise à créer ou modifier des fichiers ??

En réalité, dans Damn Small Linux, on ne pouvait pas modifier les fichiers, mais on pouvait déjà en créer. En effet, /home/<user> était le point de montage d'un filesystem créé dans un « ramdisk » (espace réservé en mémoire vive). Les fichiers créés dans ce répertoire étaient ainsi stockés en mémoire. Mais cette technique était relativement limitée : il était impossible d'installer (ou de mettre à jour) un programme, sauf en l'installant dans ce répertoire, tout le reste de l'arborescence étant en lecture seule.

En fait, il restait bien un moyen pour rendre des fichiers existants modifiables : au démarrage de l'OS, on les lit sur le CD et on les copie sur le ramdisk. Mais ce n'est pas très optimal : avec une quantité de mémoire réduite, on ne peut pas tous les copier de cette manière. Et cette copie prend du temps au démarrage (car lire le CD prend du temps).

4.2 tmpfs : stockage de fichiers en RAM

Aujourd'hui, plutôt que d'empiler un filesystem (de son choix, par exemple ext2...) dans un ramdisk, on utilise plutôt tmpfs. Il s'agit d'un filesystem qui stocke les fichiers en mémoire et qui permet donc d'obtenir un résultat comparable en une seule opération. Par exemple :

$ mount -t tmpfs no_device /tmp

À partir de là, ce qu'on fera dans /tmp sera fait en mémoire, ce qui permet d'obtenir une réactivité qu'on n'aura jamais avec un disque (même SSD). (Bien sûr, il faut garder à l'esprit qu'on est limité par la taille de nos barrettes et que, si votre fils appuie sur l'interrupteur de la multiprise, vous perdez votre travail).

Dans cet exemple, on voit aussi que le prototype de la commande mount nous oblige à spécifier un device. Il s'agit d'un héritage de l'époque où les filesystems étaient utilisés uniquement pour les disques. Cela ne s'applique pas à notre cas, j'ai donc mis no_device, mais on peut en réalité mettre n'importe quelle chaîne.5

Faire tourner MySQL en mémoire...

Au labo, j'ai par exemple utilisé tmpfs pour faire tourner MySQL en mémoire, pour les besoins d'un prototype qui simulait plusieurs clients, tous connectés à la même base MySQL et qui faisaient des requêtes assez lourdes.

Sur une Ubuntu, il suffit de créer un script de démarrage (lancé avant MySQL) qui sauvegarde le contenu de /usr/share/mysql, monte ce répertoire avec tmpfs, puis restaure son contenu.

(En fait, l'un des moteurs disponibles pour MySQL est conçu pour tourner en mémoire. Mais nous héritions d'un code largement incompatible avec ce moteur.)

4.3 Les systèmes de fichiers « union »

Les live CD ont pris une autre dimension avec l'apparition des systèmes de fichiers « union ». Un de leurs premiers représentants fut unionfs, puis aufs (les deux ont la même syntaxe). Voici un exemple qui illustre cette notion d'« union » :

# ls dir1

subdir

# ls dir1/subdir

file1

# ls dir2

subdir

# ls dir2/subdir

file2

# mkdir dir_union

# mount -t aufs -o dirs=dir1:dir2 no_device dir_union

# ls dir_union

subdir

# ls dir_union/subdir

file1 file2

#

Voilà pour le petit exemple amusant. Mais ces systèmes de fichiers en « union » peuvent faire bien plus que ça...

Commençons par remonter l'union en précisant que dir2 ne doit pas être modifié :

# umount dir_union

# mount -t aufs -o dirs=dir1=rw:dir2=ro no_device dir_union

Donc, logiquement, il n'est plus possible de supprimer file2, car il est dans l'arborescence de dir2, qui est en lecture seule...

# rm dir_union/subdir/file2

#

Pas d'erreur ??

Vérifions que la suppression a vraiment été effectuée :

# ls dir_union/subdir/

file1

#

Apparemment oui, donc l'union file2 n'est plus visible.

Est-ce que ce fichier aurait été supprimé dans l'arborescence de dir2 malgré l'option de montage dir2=ro ??

# ls dir2/subdir/

file2

#

Non, il est toujours là !

Au cas où, voyons ce qu'on a dans dir1, qui lui, pouvait être modifié...

# ls -a dir1/subdir/

. .. file1 .wh.file2

#

Voilà donc toute la subtilité... Comme il n'est pas autorisé à supprimer un fichier dans l'arborescence de dir2, aufs a créé ce fichier .wh.file26 dans la branche où il peut écrire, celle de dir1.

Au prochain démontage-remontage, aufs saura interpréter l'existence de ce fichier « spécial » et donc, file2 ne sera toujours pas visible dans l'arborescence du point de montage.

Revenons à nos moutons... Et imaginons maintenant les montages suivants :

# mount -o ro /dev/cdrom /mnt/cdrom

# mount -t tmpfs no_device /mnt/rw

# mount -t aufs -o dirs=/mnt/rw=rw:/mnt/cdrom=ro no_device /mnt/union

S'agissant d'une union, dans /mnt/union on voit, au départ, tous les fichiers du cdrom. Si ensuite des fichiers sont créés dans ce point de montage, ils seront écrits dans la branche en lecture-écriture, celle de tmpfs, donc en mémoire. Et si on supprime un fichier, cette suppression sera gérée via la création d'un fichier .wh.<file> dans l'arborescence de tmpfs.

On a donc, en quelque sorte, créé une couche (overlay) tmpfs, permettant de rendre l'arborescence de notre cdrom modifiable à souhait !

Il ne reste plus qu'à remplir le CD avec une arborescence d'OS et, si on fait booter l'OS de cette manière, on pourra tout modifier... Voilà donc la base des distributions live CD modernes...

Les filesystems en union aujourd'hui

On trouve en général un des filesystems-union suivants dans les distributions « live » d'aujourd'hui : aufs ou overlayfs. Cependant, malgré le fait qu'ils soient beaucoup utilisés dans ce contexte, aucun de ces filesystems n'a encore été intégré dans le noyau Linux. Les distributions doivent donc maintenir des patchs noyaux.

Il existe un autre moyen pour implémenter une fonctionnalité d'union : celle d'autoriser le montage de plusieurs filesystems sur le même point de montage. Pour cela, l'utilisateur préciserait une option --union lors du montage. Cette approche a également fait l'objet d'une implémentation préliminaire dans le noyau et, à l'époque, a remporté l'adhésion des décisionnaires du noyau Linux.

Malheureusement, des défauts se cachent dans ses diverses implémentations et les développeurs n'ont pas encore réussi à trouver des solutions qui fassent l'unanimité.

4.4 chroot : changeons de référentiel

Je vous raconte des trucs, mais peut-être que vous commencez à vous dire que j'ai tout imaginé depuis le début...

En effet, si je démarre une distribution live CD, disons une Ubuntu, je devrais donc m'attendre à ce que tous les chemins soient préfixés par le point de montage de l'union, non ?

Par exemple, pour lancer bash, il faudrait taper quelque chose comme /mnt/union/bin/bash... Et ce n'est pas le cas : je lance juste /bin/bash. Il y a donc une autre étape...

Cette autre étape, c'est d'indiquer à /mnt/union/bin/bash que la racine de son système de fichiers n'est pas / mais plutôt /mnt/union. C'est ce que fait la commande chroot7. En voici la preuve :

# cd union/

# pwd

/mnt/union

# chroot . pwd

/

#

Au 1er appel de la commande pwd, celle-ci nous indique qu'elle est lancée dans le répertoire /mnt/union. Au 2ème appel, chroot nous permet de lui indiquer que le répertoire courant (donc /mnt/union) est la racine de son système de fichiers ; elle nous renvoie donc /.8

Ce qu'il faut bien comprendre, c'est que, au final, la racine du système de fichiers n'est qu'un paramètre de chaque processus...

De plus, ce paramètre est transmis aux processus fils. Donc... il suffit de faire un chroot dans le 1er processus du système (/mnt/union/sbin/init a priori) pour que tous les processus considèrent que /mnt/union est leur racine !

Voilà donc pourquoi /mnt/union n'est pas visible une fois l'OS lancé.

4.5 squashfs : ajoutons de la compression

En dehors de la gestion du périphérique en lecture seule, les distributions basées sur un live CD doivent prendre en compte l'espace réduit disponible sur un CD.

Et là, vous commencez à deviner la solution : une autre couche de filesystem !

Certains filesystems proposent en effet de compresser les données (btrfs par exemple). Le plus simple à implémenter est de compresser le contenu de chaque fichier. Certains sont également capables de compresser les métadonnées (propriétaire, groupe, permissions, etc.).

Le filesystem squashfs9 est capable de compresser encore plus. En effet, plutôt que de compresser chaque fichier indépendamment des autres, on peut tirer partie des corrélations entre eux. Mais ce niveau de compression est à peu près ingérable dès qu'on autorise la modification des fichiers... C'est pourquoi squashfs est un système de fichiers en lecture seule. Mais, sur un live CD, où est le problème ? Le CD est déjà en lecture seule...

Voilà donc pourquoi, quand on installe une distribution à partir d'un live CD de 700Mo, on se retrouve avec un filesystem installé qui prend dans les 2 Go ou plus...

4.6 Mise en pratique : modification d'un live CD Ubuntu

Imaginons que vous vouliez déclarer un truc à une fille qui vous a demandé de réparer son PC. Déjà, vous avez prévu de lui installer un bon vieux système GNU/Linux, histoire de la libérer de l'emprise d'une multinationale. Et vous avez prévu le plan diabolique suivant :

- Vous allez modifier l'image ISO de l'OS à l'avance, pour ajouter un fichier secret.txt sur le bureau ;

- Vous partirez aux toilettes avant que le bureau ne s'affiche pour qu'elle ait envie d'ouvrir ce fichier.

Allez, c'est parti ! (J'ai une image d'Ubuntu 12.04 (« Precise ») sous la main, mais ce serait à peu près la même chose avec les versions qui suivent...).

Voyons ce qu'on a dans cette ISO :

# cd /tmp

# mkdir iso

# mount -o ro [...]/precise-desktop-i386.iso iso

# ls iso/

autorun.inf dists md5sum.txt preseed wubi.exe

boot install pics README.diskdefines

casper isolinux pool ubuntu

# ls iso/casper/

filesystem.manifest filesystem.squashfs

filesystem.manifest-remove initrd.lz

filesystem.size vmlinuz

#

Ce fichier filesystem.squashfs a un nom bien explicite : c'est une image de filesystem compressée par squashfs. Il nous faut un montage de plus...

# mkdir filesystem

# mount iso/casper/filesystem.squashfs filesystem

#

Voilà.

Pour notre modification, il y a une chose à savoir. En fait, quand on lance Ubuntu en mode live, l'utilisateur de la session live est créé dynamiquement (via une commande adduser ou un truc du genre). Si vous vous rappelez bien vos cours de système, quand on crée un utilisateur, le contenu de /etc/skel (pour skeleton) est copié dans le répertoire /home/<user> nouvellement créé. Cela permet de mettre en place un environnement minimal automatiquement pour chaque nouvel utilisateur créé. Donc, a priori, si on met notre fichier à l'emplacement /etc/skel/Desktop/secret.txt, il devrait se retrouver sur le bureau... Allons-y :

# mkdir filesystem/etc/skel/Desktop

mkdir: impossible de créer le répertoire «filesystem/etc/skel/Desktop»: Système de fichiers accessible en lecture seulement

#

Ah mince ! Évidemment, on est en lecture seule. A la fois l'ISO et l'image squashfs d'ailleurs...

Pour effectuer notre modification, il va falloir adopter une méthode différente. Voici celle qu'on trouve sur Internet :

1) extraire le contenu de l'ISO,

2) extraire le contenu de l'image squashfs,

3) modifier les fichiers comme on veut,

4) recompresser l'image squashfs,

5) reformater l'ISO,

6) supprimer les fichiers temporaires.

Vous voyez ce que je vois ?? On traite les images de filesystem comme de vulgaires archives !! Clairement, je ne le permettrai pas. Surtout dans cet article ! ;)

En fait, pour toucher du doigt la puissance des filesystems, on adoptera plutôt cette méthode-là :

1a) monter l'ISO,

1b) monter une union « union_iso » avec un overlay pour pouvoir modifier les fichiers de l'image ISO,

2a) monter l'image squashfs,

2b) monter une union « union_filesystem » avec un overlay pour pouvoir modifier les fichiers de l'image squashfs,

3) modifier les fichiers comme on veut,

4) recompresser l'image squashfs à l'emplacement adéquat de « union_iso »,

5) reformater l'ISO,

6) démonter.

En fait, ce n'est pas vraiment plus compliqué. D'ailleurs, on a toujours 6 étapes ;) Le principe, c'est qu'on a remplacé les 2 phases d'extraction par, à chaque fois, un montage et la mise en place d'une « union » pour gérer les modifications.

Outre le plaisir d'ajouter des niveaux d'abstraction, il y a clairement 2 avantages :

- Les extractions prennent du temps alors que les montages sont quasi-instantanés ;

- Ces deux phases d'extraction demandent beaucoup d'espace disque (temporaire), plusieurs gigas a priori (vu qu'on extrait tous les fichiers et que ceux-ci ne seront plus compressés). Alors que nos « unions » vont stocker uniquement ce qu'on modifie.

Pour être honnête, si on a pas mal d'espace disque, on aurait presque pu envisager la première méthode. Mais cela reste non optimal, donc désagréable pour l'esprit. :) Et si un jour vous envisagez d'industrialiser vos modifications d'image ISO, pour, par exemple, publier et maintenir une distribution maison (dérivée d'Ubuntu), alors il faudra appliquer ces modifs dans un script. Et là, il n'y a plus qu'un seul choix raisonnable, à mon avis.

Allons-y. En fait, plus haut, on a déjà fait les étapes 1a et 2a (montage ISO et image squashfs). On enchaîne donc avec les unions :

# mkdir union_iso union_filesystem rw_iso rw_filesystem

# mount -t tmpfs no_device rw_iso

# mount -t tmpfs no_device rw_filesystem

# mount -t aufs -o dirs=rw_iso=rw:iso=ro no_device union_iso

# mount -t aufs -o dirs=rw_filesystem=rw:filesystem=ro no_device union_filesystem

Le montage de rw_iso et rw_filesystem en mémoire (tmpfs) est optionnel. Il faut d'ailleurs se méfier, car si on a beaucoup de modifications à faire, on peut être limité par la quantité de mémoire sur la machine. Ici, on va en particulier mettre à jour le fichier casper/filesystem.squashfs dans union_iso et ce fichier fait quasiment 700 Mo. Sur une machine limitée en mémoire, il faudrait donc éviter ce montage.

On peut maintenant revenir à la modification que l'on voulait réaliser au départ :

# mkdir union_filesystem/etc/skel/Desktop

# cp [...]/secret.txt union_filesystem/etc/skel/Desktop/

Et voilà.

Notez qu'à cette étape on peut entreprendre des modifications de bien plus grande envergure, comme par exemple ajouter ou supprimer des paquets. Le principe est grosso-modo le suivant :

# [preparation de l'environnement]

# cd union_filesystem

# chroot . # par défaut le binaire exécuté est /bin/sh

-in_chroot-# apt-get [...]
-in_chroot-# [autres modifs...]
-in_chroot-# exit # sortie du chroot
# [nettoyage]

L'astuce est ainsi d'utiliser chroot pour interagir au sein de l'arborescence du filesystem.10

Une fois les modifications effectuées, on peut maintenant reformater l'image squashfs :

$ mksquashfs union_filesystem union_iso/casper/filesystem.squashfs

Et reformater l'ISO11:

$ cd union_iso

$ mkisofs [...tout un tas d'options...] -o [...]/custom.iso .

La dernière étape consiste à démonter les filesystems créés jusqu'ici, du dernier créé au premier créé (c'est trivial, je ne détaille pas).

Et c'est tout bon, il ne vous reste plus qu'à tenter votre chance avec cette fille et moi à me réjouir de participer à la lutte contre le célibat...

Et les live USB ?

Un live USB peut en théorie être géré beaucoup plus simplement qu'un live CD, car on n'a pas la problématique du périphérique en lecture seule. Et en réalité, contrairement à un CD, une clé USB a la même structure qu'un disque dur (on peut mettre plusieurs partitions, etc.). Le plus logique serait donc d'installer un OS sur une clé de la même façon qu'on l'installe sur un disque.

Mais, dans les faits, le fonctionnement des live USB est calqué sur celui des live CD. La différence étant que, pour permettre de sauvegarder de manière pérenne les modifications effectuées, le filesystem en overlay ne travaille pas en mémoire (comme tmpfs), mais plutôt dans un espace réservé de la clé USB (partition dédiée ou fichier dédié).

L'autre avantage du fonctionnement façon live CD vient de la couche de compression, qui est assez optimale, car basée sur un filesystem en lecture seule. Jusqu'à récemment, la taille des clés USB était un peu juste pour installer un OS comme sur un disque. Il est donc possible que le fonctionnement calqué sur les live CD se soit imposé parce qu'il permet une meilleure optimisation de l'espace disponible.

Cependant, l'optimisation d'espace n'est effective qu'au départ. Quand on met à jour des fichiers (essayez un apt-get upgrade sur votre live USB...), ces nouvelles versions de fichiers viennent remplir l'overlay. On se retrouve ainsi avec la nouvelle version du fichier, non compressée, dans l'overlay, mais aussi la version originale, qui est conservée ad vitam aeternam dans le filesystem compressé... Ce mécanisme de compression est donc très contre-productif sur le moyen terme.

A mon avis, il est donc possible que l'on revienne à un fonctionnement plus classique dans le futur, calqué sur celui des disques. On y trouvera sans doute toujours un filesystem compressé, mais un de ceux qui supportent les modifications (par exemple btrfs).

Scripter la création d'un live USB

Un live CD contient uniquement un filesystem, normalement de type ISO9660. Par contre, un live USB étant comme un disque, il contient des partitions, chacune formatée avec un filesystem (souvent il n'y a qu'une partition de type FAT, mais cela peut être différent). Un live CD est donc analogue à une partition de disque, alors qu'un live USB contient un schéma de partitionnement et une ou plusieurs partition(s).

Si vous copiez un CD-ROM avec la commande dd, vous obtiendrez donc une image ISO que vous pourrez monter directement avec la commande mount. Par contre, si vous faites de même avec une clé USB, vous obtiendrez une image disque. Vous pourrez ainsi observer son partitionnement avec une commande comme fdisk <image_cle_usb> (ou gdisk en cas de partitionnement de type GPT).

Scripter la création de systèmes live est donc (légèrement) plus compliqué dans le cas d'un live USB. Pour obtenir un accès aux partitions de l'image USB, je vous invite à consulter la page de manuel de la commande kpartx (installable par le paquet du même nom). Et pour que le noyau prenne en compte une modification du partitionnement, vous pourrez utiliser partprobe (du paquet parted).

5. Boot réseau et hébergement de filesystem

5.1 Contexte

Dans mon labo, on veut mettre en place un banc de test pour les réseaux de capteurs. Dans ce cadre, on a un ensemble de cartes Raspberry Pi qui bootent en réseau. Et sur certains points, cette problématique se rapproche... de celle des live CD12 !

Les cartes Raspberry Pi disposent d'un lecteur de carte SD. De ce fait, en général, on installe leur OS sur la carte SD. Dans notre cas, afin de faciliter l'administration du système, nous avons préféré réduire au strict minimum le contenu de cette carte (on n'y met que le bootloader, u-Boot), et les faire booter via le réseau, en utilisant un serveur central DHCP, TFTP & NFS. Et de ce fait, nous utilisons la carte SD uniquement en lecture. Nous pouvons donc en espérer une durée de vie bien supérieure.

5.2 Création du filesystem des Raspberry Pi

Sur le serveur, nous avons donc un répertoire /nfs/debian-fs qui contient l'arborescence de l'OS utilisé par les Raspberry Pi. Ce filesystem a été créé en utilisant la commande habituelle pour initialiser une arborescence de système Debian :

$ debootstrap --arch=armel [..autres options..] sid /nfs/debian-fs <url_ftp_debian>

Dans notre contexte, les clients NFS (les Raspberry Pi) n'ont pas la même architecture que le serveur, il faut donc bien préciser l'option --arch adéquate.

Après cette initialisation, nous devons bien évidemment adapter cette arborescence à notre problème, installer les outils nécessaires pour la gestion du banc de test, etc.

Dans ce genre de cas, vous devez commencer à connaître la technique :

# [preparation de l'environnement]

# cd /nfs/debian-fs

# chroot .

-in_chroot-# apt-get [...]
-in_chroot-# [autres modifs...]
-in_chroot-# exit
# [nettoyage]

Essayons :

# cd /nfs/debian-fs

# chroot .

chroot: impossible d'exécuter la commande « /bin/bash »: Exec format error

#

Ah oui, évidemment... L'architecture est différente... Notre serveur sera bien incapable d'exécuter les binaires de cette arborescence (tels que /nfs/debian-fs/bin/sh ou /nfs/debian-fs/usr/bin/apt-get), s'agissant de binaires pour architecture ARM !

Eh bien en fait si, il en sera capable. Juste après ça :

# apt-get install qemu-user-static

[...]

# cp /usr/bin/qemu-arm-static /nfs/debian-fs/usr/bin/qemu-arm-static

#

Et c'est tout ? Eh bien oui. La preuve :

# chroot .

-in_chroot-# echo 'je suis dedans !!!'

je suis dedans !!!

-in_chroot-# exit

#

Voilà qui mérite une petite explication...

Pour faire tourner un binaire d'une architecture différente, il existe bien une solution : utiliser un émulateur, tel que qemu. Il est donc logique d'installer qemu. Et comme on va faire des indirections, à coups de chroot, on a choisi la version compilée en statique (sinon c'est plus compliqué à cause des chemins des librairies partagées).

Le paquet nous a ainsi installé le binaire /usr/bin/qemu-arm-static. Voilà donc de quoi émuler des binaires ARM. On peut tester avec un binaire ARM téléchargé sur Internet :

# file ./hello_world-arm-static

./hello_world-arm-static: ELF 32-bit LSB executable, ARM, version 1, statically linked, for GNU/Linux 2.6.9, stripped

# qemu-arm-static ./hello_world-arm-static

Hello world!

#

Voilà pour la vérification. Mais vous allez me dire que, pour le chroot, je n'ai pas utilisé qemu-arm-static et ça marchait quand même.

Eh bien en fait, là non plus, je ne suis plus obligé de le préciser :

# ./hello_world-arm-static

Hello world!

#

Voilà qui vous en bouche un coin... On s'attendait plutôt à un « Exec format error » comme tout à l'heure, non ?

L'astuce, c'est que l'installation du paquet qemu-user-static a fait plus de choses que ce à quoi on s'attendait : elle a aussi enregistré tout un tas de nouvelles associations dans le système binfmt_misc du noyau :

# cd /proc/sys/fs/binfmt_misc/

# ls

jar python3.3 qemu-arm qemu-cris qemu-microblaze [...] register

#

Celle qui nous intéresse ici est bien évidemment qemu-arm :

# cat qemu-arm

enabled

interpreter /usr/bin/qemu-arm-static

flags: OC

offset 0

magic 7f454c4601010100000000000000000002002800

mask ffffffffffffff00fffffffffffffffffeffffff

#

Voilà donc la clé de l'énigme : grâce à cette association, quand on demande au noyau d'exécuter un fichier qui correspond aux champs magic et mask, il va lancer l'interpréteur indiqué (/usr/bin/qemu-arm-static ici) et cela de manière totalement transparente pour l'utilisateur !

Puissant ce système binfmt_misc, non ?

Plus d'info sur binfmt_misc

Le système binfmt_misc est utilisé dans bien d'autres cas. Par exemple, si j'installe wine pour pouvoir lancer un jeu Windows, je verrai apparaître l'association adéquate. Je peux même créer une association à la main en écrivant ce qu'il faut dans le fichier virtuel register.

J'ai dit « fichier virtuel » parce que, bien évidemment, l'interface de configuration de binfmt_misc est un système de fichiers :

# mount | grep binfmt_misc

binfmt_misc on /proc/sys/fs/binfmt_misc type binfmt_misc (rw,noexec,nosuid,nodev)

#

Revenons à nos moutons.

Nous avons donc tout ce qu'il faut pour customiser le système que vont booter nos Raspberry Pi :

# cd /nfs/debian-fs

# chroot .

-in_chroot-# ls

bin contiki etc lib mnt proc run srv tmp var

boot dev home media opt root sbin sys usr

-in_chroot-# apt-get update

0% [Working]qemu: Unsupported syscall: 374

Get:1 http://ftp.fr.debian.org unstable InRelease [198 kB]

Get:2 [...]

[...]

-in_chroot-# apt-get install [...]

[...]

-in_chroot-#

L'émulation est assez transparente pour l'utilisateur, sauf concernant 2 choses :

- c'est beaucoup plus lent, évidemment. Si vous voulez recompiler le noyau linux, alors cet environnement peut paraître pratique (pas de cross-compilation à configurer car le gcc de cet environnement va générer nativement des binaires ARM...) ; mais le temps de compilation va sans doute vous décourager !

- on peut rencontrer des erreurs si qemu ne supporte pas certaines fonctionnalités. La plupart du temps, on peut ignorer ces erreurs (comme celle reportée ici), mais il peut arriver que cela soit bloquant, pour certaines tâches bas-niveau.

Dans l'ensemble cela reste très pratique et fonctionnel.

5.3 Gestion du partage NFS en lecture seule

Bon, maintenant qu'on a préparé l'OS qui doit booter par le réseau, il reste un souci à gérer. Si on autorise chaque nœud Raspberry Pi à accéder en lecture-écriture à ce partage NFS, on va sans doute avoir des soucis, avec toutes les lectures & écritures concurrentes.

Il faut donc configurer le partage NFS en lecture seule. L'ennui, c'est que la distribution Debian que l'on a installée et configurée sur ce partage NFS n'est pas adaptée pour tourner sur un système de fichiers en lecture seule.

La solution, c'est que chaque Raspberry Pi suive le fonctionnement suivant :

1) Récupération (via TFTP) et boot du noyau Linux,

2) Montage du partage NFS (via l'option adéquate du noyau) en lecture seule,

3) Montage d'une union avec un overlay tmpfs pour stocker en mémoire les fichiers créés ou modifiés,

4) Lancement de l'OS sur l'union (via chroot).

Quand je vous disais que cette problématique se rapproche de celle des live CD !

Les étapes 1 et 2 sont l'affaire du bootloader et du noyau, je ne vais pas les détailler ici. Reste à imaginer comment on va insérer l'étape 3 avant le lancement de l'OS.

En fait, une fois le noyau lancé et le système de fichiers racine monté (ici le partage NFS), le noyau démarre l'OS en exécutant le premier processus, celui qui lance tous les autres. Sauf option noyau spécifique, ce premier processus est lancé en exécutant le fichier /sbin/init.

Voilà donc comment on peut insérer cette étape 3 :

$ cd /nfs/debian-fs

$ mv sbin/init sbin/init.orig

$ mv [...]/init-customise sbin/init

$

Et voilà. Le noyau va maintenant appeler notre init customisé à la place de l'ancien. Ce nouvel init devra ainsi créer l'union puis lancer /sbin/init.orig sur cette union.

Pour l'union, le script va avoir besoin de 2 points de montage. Le filesystem NFS sera en lecture seule, donc on ne pourra pas les créer dans le script ! Donc on le fait maintenant :

$ mkdir -p mnt/fs_rw mnt/fs_union

$

Et voilà ce script init customisé13 :

#!/bin/sh

mount_and_run_startup_init()

{

cd /mnt

# on cree l'union

# /: le montage nfs (lecture seule)

# /mnt/fs_rw: l'overlay pour stocker les modifs

# /mnt/fs_union: l'union

mount -t tmpfs no_device fs_rw

mount -t aufs -o dirs=fs_rw=rw:/=ro no_device fs_union

# on demarre le script d'init original

# en lui donnant l'union pour racine

cd fs_union

exec chroot . sbin/init.orig

}

if [ $$ = 1 ]

then

mount_and_run_startup_init

else

/sbin/init.orig $*

fi

La fonction mount_and_run_startup_init() ne doit pas trop vous surprendre...

Le if à la fin est moins évident. En fait, historiquement, les scripts init doivent être capables de fournir 2 fonctionnalités :

F1) Initialiser l'OS, donc faire office de 1er processus et lancer tous les autres ;

F2) Fournir une interface à ce processus d'initialisation.

Vous est-il déjà arrivé de taper la commande init 0 pour arrêter l'OS ou init 6 pour rebooter ? Cela correspond à F2. Dans ce cas, la commande init doit envoyer un message au 1er processus (celui qui a été créé par F1...), pour lui demander de passer dans le runlevel indiqué.

La méthode pour déterminer la fonctionnalité à remplir (F1 ou F2) consiste à vérifier si on est le 1er processus ou non. La variable spéciale $$ renvoie le PID du processus en cours d'exécution... Ce qui explique ce if à la fin du script. Si on est effectivement le 1er processus, on lance l'OS en appelant mount_and_run_startup_init(). Sinon, on délègue directement la requête à /sbin/init.orig, en lui passant le même argument que celui qu'on a reçu ; /sbin/init.orig n'aura pas non plus un PID égal à 1, donc il saura qu'il doit remplir la fonctionnalité F2.

Il y a quand même une subtilité dans mount_and_run_startup_init(). Avez-vous remarqué le exec avant le chroot ? Sans lui, la commande chroot . sbin/init.orig serait lancée dans un nouveau processus, donc avec un PID différent de 1. Donc init.orig croirait qu'il doit remplir la fonctionnalité F2 au lieu de F1 et c'est le kernel panic assuré. Au contraire, en préfixant avec exec, le processus en cours d'exécution est « remplacé » par la commande indiquée et de ce fait le PID (égal à 1 dans ce cas) est conservé.

Une fois ce script d'init mis en place, nos Raspberry Pi peuvent cohabiter sur le même partage NFS, en stockant les écritures en mémoire... Mission accomplie !

Pour conclure

J'espère vous avoir convaincu de la puissance des systèmes de fichiers. En particulier, le fait de pouvoir les empiler permet d'obtenir des fonctionnalités très évoluées tout en gardant des briques relativement simples. C'est donc un bon exemple de la philosophie UNIX...

Dans une seconde partie, je vais vous montrer que ces « briques » ne sont pas seulement faciles à utiliser, mais aussi faciles à concevoir...


Notes

1 La solution actuelle est de coder des programmes ad-hoc avec des interfaces TAP et des bridges, ce qui est relativement laborieux.

2 Sauf que si je me mets à faire ça et 3 ou 4 autres trucs du même genre, je ne vois plus ma femme...

3 En réalité, il y a bien du code noyau : il s'agit du module FUSE en lui-même. Mais le programmeur de charpentefs n'a pas eu à l'écrire...

4 En français : « Les gens qui pensent que les filesystems en espace utilisateur sont utiles pour autre chose que des jouets sont mal avisés. ».

5 Par exemple on peut mettre « stockage_long_terme » pour tromper l'ennemi ;).

6 Le préfixe « wh » vient de l'anglais « whiteout » (on « rend un fichier invisible »).

7 Les OS modernes font aussi usage de pivot_root, qui permet de garder trace de l'ancienne racine.

8 En fait, je simplifie un tout petit peu : pour être précis, ce n'est pas la même commande pwd qui est exécutée dans les 2 cas. La 1ère fois, c'est /bin/pwd et la 2ème fois, c'est /mnt/union/bin/pwd.

9 Et cloop (pour « compressed loop ») avant lui...

10 La phase de « préparation de l'environnement » consiste à pallier le fait que, dans cette arborescence, l'OS n'a pas été démarré. Par exemple, dans cette arborescence, /proc n'est pas monté, il faut donc le monter. On peut trouver ces étapes de préparation sur la page https://help.ubuntu.com/community/LiveCDCustomization. Et le nettoyage est le pendant de cette phase de préparation.

11 En réalité le formatage correct de l'ISO requiert un peu plus d'opérations (par exemple, il faut mettre à jour un fichier qui contient la taille de filesystem.squashfs) et j'ai omis les options à préciser à mkisofs (pour que l'image soit bootable, etc.), de façon à ne pas trop s'éloigner du thème de l'article. Le lecteur intéressé pourra obtenir ces détails en consultant l'URL donnée plus haut.

12 Avouez que vous ne vous y attendiez pas !

13 En réalité, pour être plus cohérent avec le reste de l'article, ce script est légèrement différent de celui que nous utilisons. Si un lecteur veut faire booter plusieurs Raspberry Pi sur un partage réseau, qu'il n'hésite pas à me contacter (http://tinyurl.com/eduble) pour que je lui transmette le vrai, avec 2 ou 3 infos utiles...




Article rédigé par

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous