Systèmes de fichiers d’union : Implémentations, partie I


Cet article vous est offert par les abonnés de LWN

Les abonnés de LWN.net ont rendu cet article – et tout ce qui l’entoure – possible. Si vous appréciez notre contenu, s’il vous plaît acheter un abonnement et rendre la prochaine série d’articles possible.

Le 25 mars 2009

Cet article a été contribué par Valerie Aurora (anciennement Henson)

Dans l’article de la semaine dernière, j’ai passé en revue les cas d’utilisation, les concepts de base, et les problèmes de conception communsdes systèmes de fichiers d’union. Cette semaine, je décrirai plusieursimplémentations de systèmes de fichiers d’union en détail technique. Les systèmes de fichiers d’union que je couvrirai dans cet article sont les répertoires d’union de Plan 9, les montages d’union BSD et les montages d’union Linux. Le prochain article couvrira unionfs, aufs, et peut-être un ou deux autres systèmes de fichiers d’union, et conclura la série.

Pour chaque système de fichiers, je décrirai son architecture de base, ses caractéristiques, et son implémentation. La discussion de l’implémentation se concentrera en particulier sur les blancs et la lecture des répertoires. Je terminerai par un regard sur les aspects d’ingénierie logicielle de chaque implémentation ; par exemple, la taille et la complexité du code, le caractère invasif et le fardeau pour les développeurs de systèmes de fichiers.

Avant de lire cet article, vous pourriez vouloir consulter l’article d’Andreas Gruenbacher qui vient d’être publié sur l’atelier union mount tenu en novembre dernier. C’est un bon résumé des caractéristiques des systèmes de fichiers d’union qui sont les plus pressantes pour les développeurs de distribution. Extrait de l’introduction : « Tous les cas d’utilisation qui nous intéressent se résument fondamentalement à la même chose : avoir une image ou un système de fichiers qui est utilisé en lecture seule (soit parce qu’il n’est pas inscriptible, soit parce que l’écriture sur l’image n’est pas souhaitée), et prétendre que cette image ou ce système de fichiers est inscriptible, en stockant les modifications ailleurs. »

Répertoires d’union Plan 9

Le système d’exploitation Plan 9 (parcourir le code source ici) met en œuvre l’union de sa propre manière spéciale Plan 9. Dans les répertoires d’union Plan 9, seul l’espace de nom de répertoire de premier niveau est fusionné, pas les sous-répertoires. Sans contrainte par les normes UNIX, les répertoires d’union de Plan 9 n’implémentent pas les blancs et ne filtrent même pas les entrées dupliquées – si le même nom de fichier apparaît dans deux systèmes de fichiers, il est simplement retourné deux fois dans les directorylistings.

Un répertoire d’union Plan 9 est créé comme suit :

 bind -a /home/val/bin/ /bin

Ceci entraînerait le montage en union du répertoire/home/val/bin« après » (l’option-a)/bin; les autres options sont de placer le nouveau répertoire avant le répertoire existant,ou de remplacer entièrement le répertoire existant. (Cela me semble un ordre étrange, puisque j’aime que les commandes de monbin/personnel soient prioritaires sur les commandes de l’ensemble du système, mais c’est l’exemple de la documentation de Plan 9). Brian Kernighanexplique l’une des utilisations des répertoires collectifs : « Ce mécanisme de répertoires collectifs remplace le chemin de recherche des shells UNIX conventionnels. En ce qui vous concerne, tous les programmes exécutables se trouvent dans /bin. » Les répertoires d’union peuvent théoriquement remplacer de nombreuses utilisations des blocs de construction UNIX fondamentaux que sont les liens symboliques et les chemins de recherche.

Sans les blancs ou l’élimination des doublons, readdir()les répertoires d’union sont triviaux à mettre en œuvre. Les offsets d’entrée de répertoire du système de fichiers sous-jacent correspondent directement au décalage en octets de l’entrée de répertoire à partir du début du répertoire. Un répertoireunion est traité comme si le contenu des répertoires sous-jacents était concaténé ensemble.

Le plan 9 met en œuvre une alternative à readdir() qui mérite d’être signalée, dirread().dirread() renvoie des structures de type Dir,décrites dans la page de manuel stat(). La partie importante de la Dir est le membre Qid. Un Qid est:

…une structurecontenant les champs path et vers : path estgaranti pour être unique parmi tous les noms de chemin actuellement sur le serveur de fichiers, et vers change chaque fois que le fichier est modifié. Le chemin est un long long (64 bits, vlong)et le vers est un long non signé (32 bits, ulong).

Alors pourquoi est-ce intéressant ? L’une des raisons pour lesquelles readdir() est si pénible à mettre en œuvre est qu’ilrenvoie le membre d_off de struct dirent, sous la forme d’un seul off_t (32 bits, sauf si l’application est compilée avec le support des grands fichiers), pour marquer l’entrée du répertoire où une application doit continuer à lire lors du prochain appel readdir(). Cela fonctionne bien tant que d_off est un simple décalage d’octets dans un fichier plat de moins de 232 octets et que les entrées de répertoire existantes ne sont jamais déplacées – ce qui n’est pas le cas pour de nombreux systèmes de fichiers modernes (XFS, btrfs, ext3 avec index htree). Le Qid de 96 bits est un marqueur de place beaucoup plus utile que le off_t de 32 ou 64 bits. Pour un bon résumé des problèmes liés à la mise en œuvre de readdir(), lisez l’excellent billet de TheodoreY. Ts’o’s excellent post sur le sujet à la liste de diffusion btrfs.

D’un point de vue de génie logiciel, les répertoires d’union du Plan 9 sont le paradis. Sans les blancs, l’élimination des entrées en double, les décalages de répertoire compliqués, ou la fusion des espaces de noms au-delà du top-leveldirectory, l’implémentation est simple et facile à maintenir.Cependant, toute implémentation pratique de systèmes de fichiers d’union pourLinux (ou tout autre UNIX) devrait résoudre ces problèmes. Pour nospurposes, les répertoires d’union de Plan 9 servent principalement d’inspiration.

Montages d’union deBSD

BSD implémente deux formes d’union : l’option"-o union"à la commandemount, qui produit un répertoire d’union similaire à celui de Plan 9, et la commandemount_unionfs, qui implémente un système de fichiers d’union plus complet avec des mises en blanc et la fusion de l’espace de noms entier. Nous nous concentrerons sur cette dernière.

Pour cet article, nous utilisons deux sources pour les détails d’implémentation spécifiques : l’implémentation originale de BSD union mount telle que décrite dans le document USENIX de 1995Unionmounts in 4.4BSD-Lite , et la page de manuel et le code source de FreeBSD7.1 mount_unionfs. D’autresBSDs peuvent varier.

Un répertoire peut être monté en union soit « au-dessous », soit « au-dessus » d’un répertoire existant ou d’un montage en union, tant que la branche supérieure d’une union inscriptible est inscriptible. Deux modes de whiteouts sont supportés : soit unwhiteout est toujours créé lorsqu’un répertoire est supprimé, soit il n’est créé que si une autre entrée de répertoire avec ce nom existe actuellement dans une branche située sous la branche inscriptible. Trois modes pour définir la propriété et le mode des fichiers copiés sont supportés. Le plus simple esttransparent, dans lequel le nouveau fichier conserve le même propriétaire et le même mode que l’original. Le mode masquerade fait des fichiers copiés vers le haut la propriété d’un utilisateur particulier et supporte un ensemble d’options de montage pour déterminer le mode du nouveau fichier.Le mode traditional définit le propriétaire à l’utilisateur qui rant la commande union mount, et définit le mode selon l’umask au moment de l’union mount.

Chaque fois qu’un répertoire est ouvert, un répertoire du même nom est créé sur la couche supérieure inscriptible s’il n’existe pas déjà. Extrait de l’article:

En créant des répertoires fantômes de manière agressive pendant la recherche, le système de fichiers union évite d’avoir à vérifier et éventuellement à créer la chaîne de répertoires depuis la racine du montage jusqu’au point d’une copie.Puisque l’espace disque consommé par un répertoire est négligeable, créer des répertoires lorsqu’ils sont traversés pour la première fois semblait être une meilleure alternative.

En conséquence, un "find /union" aura pour résultat de copier chaque répertoire (mais pas les entrées de répertoire pointant vers des non-répertoires) vers la couche inscriptible. Pour la plupart des images de systèmes de fichiers, cela utilisera une quantité d’espace négligeable (moins que, par exemple, l’espace réservé à l’utilisateur root, ou celui occupé par les inodes inutilisés dans un système de fichiers de style FFS).

Un fichier est copié vers la couche supérieure lorsqu’il est ouvert avec une permission d’écriture ou que les attributs du fichier sont modifiés. (Comme les répertoires sont copiés lorsqu’ils sont ouverts, il est garanti que le répertoire contenant le fichier existe déjà sur la couche accessible en écriture). Si le fichier à copier a plusieurs liens durs, les autres liens sont ignorés et le nouveau fichier a un nombre de liens de un. Cela peut casser les applications qui utilisent des liens durs et s’attendent à ce que les modifications par un nom de lien apparaissent lorsqu’elles sont référencées par un lien dur différent. De telles applications sont relativement rares, mais personne n’a fait une étude systématique pour voir quelles applications échoueront dans cette situation.

Les whiteouts sont implémentés avec un type d’entrée de répertoire spécial, DH_WHT. Les entrées de répertoire whiteout ne se réfèrent à aucun inode réel, mais pour faciliter la compatibilité avec les utilitaires de système de fichiers existants tels que fsck, chaque entrée de répertoire whiteout comprend un faux numéro d’inode, le numéro d’inode whiteout réservé WINO. Le système de fichiers sous-jacent doit être modifié pour prendre en charge le type d’entrée de répertoire whiteout. Les nouveaux répertoires qui remplacent une entrée whiteout sont marqués comme opaques via un nouvel attribut d’inode « opaque » pour que les recherches ne les traversent pas (nécessitant à nouveau un support minimal du système de fichiers sous-jacent).

Les entrées de répertoire en double et les whiteouts sont gérés dans l’implémentation userspacereaddir(). Au opendir()temps, la bibliothèque C lit le répertoire en une seule fois, supprime les doublons, applique les blancs et met en cache les résultats.

Les montages d’union BSD ne tentent pas de traiter les modifications des branches situées sous la branche supérieure inscriptible (bien qu’elles soient autorisées). La façon dont rename() est géré n’est pas décrite.

Un exemple de la page de manuel mount_unionfs:

 The commands mount -t cd9660 -o ro /dev/cd0 /usr/src mount -t unionfs -o noatime /var/obj /usr/src mount the CD-ROM drive /dev/cd0 on /usr/src and then attaches /var/obj on top. For most purposes the effect of this is to make the source tree appear writable even though it is stored on a CD-ROM. The -o noatime option is useful to avoid unnecessary copying from the lower to the upper layer.

Un autre exemple (notant que je crois que le contrôle de la source est mieux implémenté en dehors du système de fichiers) :

 The command mount -t unionfs -o noatime -o below /sys $HOME/sys attaches the system source tree below the sys directory in the user's home directory. This allows individual users to make private changes to the source, and build new kernels, without those changes becoming visible to other users.

Linux union mounts

Comme les union mounts de BSD, les union mounts de Linux implémentent l’union du système de fichiers dans la couche VFS, avec un support mineur des systèmes de fichiers sous-jacents pour les blancs et les balises de répertoire opaques. Plusieursversions de ces correctifs existent, écrites et modifiées par Jan Blunck,Bharata B. Rao, et Miklos Szeredi, entre autres.

Une version de ce code est fusionne les répertoires de premier niveau seulement,similaire aux répertoires d’union de Plan 9 et à l’option BSD -o unionmount. Cette version des montages d’union, que je désigne sous le nom de répertoires d’union, est décrite de manière assez détaillée dans un article récent de la LWN parGoldwyn Rodrigues et dans le récentpost de Miklos Szeredi d’un ensemble de correctifs mis à jour. Pour le reste de cet article,nous nous concentrerons sur les versions de union mount qui fusionnent le fullnamespace.

Les union mounts linux sont actuellement en développement actif. Cet article décrit la version publiée par Jan Blunck contre Linux2.6.25-mm1, util-linux 2.13, et e2fsprogs 1.40.2. Les ensembles de correctifs, comme la série Quilt, peuvent être téléchargés sur le site ftp de Jan:

Patches du noyau : ftp://ftp.suse.com/pub/people/jblunck/patches/

Utilitaires : ftp://ftp.suse.com/pub/people/jblunck/union-mount/

J’ai créé une page web avec des liens vers les versions git des patchs ci-dessus et une documentation de style HOWTOà http://valerieaurora.org/union.

Une union est créée en montant un système de fichiers avec le jeu de drapeaux MS_UNION. (Les MS_BEFORE, MS_AFTER,et MS_REPLACE sont définis dans la base de code mount mais ne sont pas utilisés actuellement.) Si l’indicateur MS_UNION est spécifié, alors le système de fichiers monté doit être soit en lecture seule, soit supporter les blancs. Dans cette version des montages d’union, le drapeau de montage d’union est spécifié par l’option « -o union » de mount. Par exemple, pour créer une union de deux systèmes de fichiers de périphérique de bouclage, /img/ro et /img/rw, vous devez exécuter :

 # mount -o loop,ro,union /img/ro /mnt/union/ # mount -o loop,union /img/rw /mnt/union/

Chaque montage d’union crée unstruct union_mount:

 struct union_mount {atomic_t u_count;/* reference count */struct mutex u_mutex;struct list_head u_unions;/* list head for d_unions */struct hlist_node u_hash;/* list head for searching */struct hlist_node u_rhash;/* list head for reverse searching */struct path u_this;/* this is me */struct path u_next;/* this is what I overlay */ };

Comme décrit dansDocumentation/filesystems/union-mounts.txt, « Toutes les structures de montage d’union sont mises en cache dans deux tables de hachage, une pour la consultation de la couche inférieure suivante de la pile d’union et une pour la consultation inverse de la couche supérieure suivante de la pile d’union. »

Les mises en cache et les répertoires opaques sont implémentés à peu près de la même manière que dans BSD. Le système de fichiers sous-jacent doit explicitement supporter les whiteouts en définissant l’opération d’inode .whiteout pour les répertoires (actuellement, les whiteouts ne sont implémentés que pour ext2, ext3 et tmpfs).Les implémentations ext2 et ext3 utilisent le type d’entrée de répertoire whiteout, DT_WHT, qui a été défini dans include/linux/fs.h pendant des années mais pas utilisé en dehors du système de fichiers Coda jusqu’à présent. Un numéro d’inode réservé au whiteout, EXT3_WHT_INO, est défini mais pas encore utilisé ; les entrées whiteout allouent actuellement un inode normal. Un nouvel inodeflag, S_OPAQUE, est défini pour marquer les répertoires comme opaques.Comme dans BSD, les répertoires ne sont marqués opaques que lorsqu’ils remplacent une entrée whiteout.

Les fichiers sont copiés vers le haut lorsque le fichier est ouvert pour l’écriture. Si nécessaire, chaque répertoire du chemin d’accès au fichier est copié vers le haut (copie à la demande des répertoires). Actuellement, la copie vers le haut n’est supportée que pour les fichiers et répertoires réguliers.

readdir() est l’un des points les plus faibles de l’implémentation actuelle. Il est implémenté de la même manière que BSD union mountreaddir(), mais dans le noyau. Le champ d_off est défini comme le décalage dans le répertoire sous-jacent actuel, moins les tailles des répertoires précédents. Les entrées de répertoires provenant des répertoires situés sous la couche supérieure doivent être vérifiées par rapport aux entrées précédentes pour détecter les doublons ou les blancs. Tel qu’il est actuellement implémenté, chaque appel système readdir() (techniquement, getdents()) lit toutes les entrées de répertoire précédentes dans un cache interne au noyau, puis compare chaque entrée à renvoyer avec celles qui sont déjà dans le cache avant de la copier dans le tampon utilisateur. Le résultat final est que readdir() est complexe, lent, et alloue potentiellement une grande quantité de mémoire noyau.

Une solution est de prendre l’approche BSD et de faire la mise en cache, la mise en blanc, et le traitement dupliqué dans l’espace utilisateur. Bharata B. Raois conçoit un support pour union mount readdir() dans la glibc. (Le standard POSIX permet d’implémenter readdir() au niveau de la libc si l’appel système du noyau nu ne remplit pas toutes les exigences). Cela permettrait de déplacer l’utilisation de la mémoire dans l’application et de rendre le cache persistant. Une autre solution serait de rendre le cache dans le noyau persistant d’une manière ou d’une autre.

Ma suggestion est de prendre une technique de BSD union mounts et de l’étendre : copier proactivement non seulement les entrées de répertoire pour les répertoires, mais toutes les entrées de répertoire des systèmes de fichiers inférieurs, traiter les doublons et les blancs, rendre le répertoire opaque, et l’écrire sur le disque. En fait, vous traitez les entrées de répertoire pour les blancs et les doublons lors de la première ouverture du répertoire, puis vous écrivez le « cache » résultant des entrées de répertoire sur le disque. Les entrées de répertoire pointant vers des fichiers sur les systèmes de fichiers sous-jacents doivent indiquer d’une manière ou d’une autre qu’il s’agit d’entrées « fall-through » (le contraire d’un whiteout – il demande explicitement de rechercher un objet dans un système de fichiers inférieur). Un effet secondaire de cette approche est que les blancs ne sont plus du tout nécessaires.

Un problème qui doit être résolu avec cette approche est comment représenter les entrées de répertoire pointant vers des systèmes de fichiers inférieurs. Un certain nombre de solutions se présentent : l’entrée pourrait pointer vers un numéro d’inode réservé, le système de fichiers pourrait allouer un inode pour chaque entréemais le marquer avec un nouvel attribut d’inode S_LOOKOVERTHERE,il pourrait créer un lien symbolique vers une cible réservée, etc. Cette approche utiliserait plus d’espace sur le système de fichiers superposé, mais toutes les autres approches nécessitent d’allouer le même espace en mémoire, et généralement la mémoire est plus chère que le disque.

Un problème moins pressant avec l’implémentation actuelle est que les nombres d’inodes ne sont pas stables à travers le démarrage (voir l’article précédent sur les systèmes de fichiers d’union pour les détails sur la raison pour laquelle c’est un problème).Si les répertoires « fall-through » sont implémentés en allouant un inode pour chaque entrée de répertoire sur les systèmes de fichiers sous-jacents, alors les nombres d’inodes stables seront un effet secondaire naturel. Une autre option est de stocker une carte d’inodes persistante quelque part – dans un fichier dans le répertoire de premier niveau, ou dans un système de fichiers externe, peut-être.

Les liens durs sont gérés – ou, plus précisément, ne sont pas gérés – de la même manière que les montages d’union BSD. Encore une fois, il n’est pas clair combien d’applications dépendent de la modification d’un fichier via un chemin en lien dur et de la visualisation des changements via un autre chemin en lien dur (par opposition au lien symbolique). La seule méthode que je peux trouver pour gérer cela correctement est de garder un cache persistant quelque part sur le disque des inodes que nous avonsencounter avec des liens durs multiples.

Voici un exemple de comment cela fonctionnerait : Disons que nous démarrons une copie pour l’inode 42 et trouvons qu’il a un nombre de liens de trois. Nous créerions une entrée pour la base de données des liens durs qui comprendrait l’identifiant du système de fichiers, le numéro de nœud, le nombre de liens et le numéro d’inode de la nouvelle copie sur le système de fichiers de niveau supérieur. Cela pourrait être stocké dans un fichier au format CSV, ou comme un lien symbolique dans un répertoire réservé dans le répertoire racine (par exemple, « /.hardlink_hack/<fs_id>/42« , qui est un lien vers « <new_inode_num> 3« ), ou dans une vraie base de données. Chaque fois que nous ouvrons un inode sur un système de fichiers sous-jacent, nous le recherchons dans notre base de données de liens durs ; si une entrée existe, nous décrémentons le nombre de liens et créons un lien dur vers l’inode correct sur le nouveau système de fichiers. Lorsque tous les chemins sont trouvés, le nombre de liens tombe à un et l’entrée peut être supprimée de la base de données. Ce qui est bien avec cette approche, c’est que la quantité d’overhead est limitée et disparaîtra entièrement lorsque tous les chemins vers les inodes pertinents auront été trouvés. Cependant, cela introduit encore une quantité significative de complexité possiblement inutile ; l’implémentation BSD montre que de nombreuses applications fonctionneront volontiers avec un comportement de lien dur pas tout à fait POSIXLY-correct.

En ce moment, rename() de répertoires à travers les branches renvoie EXDEV, l’erreur pour essayer de renommer un fichier à travers différents systèmes de fichiers. L’espace utilisateur gère généralement cela de manière transparente (puisqu’il doit déjà gérer ce cas pour les répertoires provenant de différents systèmes de fichiers) et se rabat sur la copie des contenus du répertoire un par un. L’implémentation du rename()récursif des répertoires à travers les branches dans le noyau n’est pas une idée brillante pour les mêmes raisons que le renommage à travers les systèmes de fichiers réguliers ; probablement le retour du EXDEV est la meilleure solution.

D’un point de vue d’ingénierie logicielle, les montages d’union semblent être un compromis raisonnable entre les fonctionnalités et la facilité de maintenance. La plupart des modifications du VFS sont isolées dans fs/union.c, un fichier d’environ 1000 lignes. Environ 1/3 de ce fichier est l’implémentation readdir() dans le noyau, qui sera presque certainement remplacée par quelque chose d’autre avant toute fusion possible.Les changements aux systèmes de fichiers sous-jacents sont assez minimes et seulement nécessaires pour les systèmes de fichiers montés comme branches inscriptibles. Le principal obstacle à la fusion de ce code est l’implémentation readdir(). Sinon, les mainteneurs de systèmes de fichiers ont été sensiblement plus positifs sur les montages d’union que toute autre implémentation d’union.

Un bon résumé des montages d’union peut être trouvé dans les diapositives de Bharata B. Rao sur les montages d’union pour FOSS.IN .

À venir

Dans le prochain article, nous passerons en revue unionfs et aufs, et nous comparerons les différentes implémentations des systèmes de fichiers d’union pour Linux. Restez à l’écoute !

Index des entrées pour cet article
Kernel Systèmes de fichiers/Union
Kernel Montages d’union
Articles invités Aurora (Henson), Valerie

.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.