Le système de fichier est une partie importante du système d'exploitation, puisqu'il contient toutes les informations non volatiles, c'est à dire les informations qui doivent être conservées lorsqu'on éteint la machine entre deus sessions de travail, et sont donc enregistrées sur un périphériques de stockage externe (disque magnétique, clé USB, etc).
En règle générale, le système de fichiers est (beaucoup) plus gros que la capacité totale de la mémoire vive. Au cours d'une session de travail particulière, seule une (toute petite) partie des informations stockées dans le système de fichiers ont effectivement besoin d'être amenées en mémoire. Tous les systèmes d'exploitation définissent donc un cache des fichiers, qui est une représentation en mémoire, d'une partie du système de fichiers stocké sur disque.
Ce cache des fichiers est un bon exemple de structure de donnée du système d'exploitation susceptible de créer un goulot d'étranglement. Cette structure est partagée, puisqu'elle peut être accédée par n'importe quel thread de n'importe quel process souhaitant accéder à un fichier, et elle est potentiellement trop volumineuse pour être stockée dans un seul cluster. Elle doit donc être distribuée sur plusieurs clusters.
Dans ce TP, on présente les techniques de distribution utilisées dans Almos-mkh pour minimiser la contention lors des accès concurrents au système de fichier.
Le périphérique de stockage externe contenant le système de fichier est toujours organisé comme un tableau de secteurs physiques. Chaque secteur peut contenir un bloc de 512 octets, et est identifié par un numéro de secteur, appelé LBA (Logic Block Address). Bien que le plus petit élément de stockage adressable soit le secteur, presque tous les systèmes de fichiers définissent une unité d'allocation plus grosse, appelée cluster. Généralement, le cluster a une capacité égale à une page, c'est à dire 4 Koctets (8 secteurs consécutifs). Le système de fichier utilise donc le périphériques comme un tableau de clusters. Un fichier occupe toujours un nombre entier de clusters, et un même cluster ne peut pas être partagé par deux fichiers.
Dans les systèmes UNIX ou Microsoft, le système de fichier possède une structure hiérarchique arborescente, puisque certains fichiers sont des répertoires, qui contiennent eux-mêmes des liens vers d'autres fichiers ou répertoires. N'importe quel fichier (ou répertoire) dans le système de fichiers peut être désigné sans ambiguité par un cheminom, décrivant le chemin allant du répertoire racine du système de fichier à un fichier particulier.
Tout système de fichier doit donc stocker trois types d'information
Il existe évidemment de nombreux types de système de fichier, qui sont différentes implémentations de la structure arborescente générique définie ci-dessus. C'est le format utilisé pour représenter les répertoires et la FAT qui différencient les différents types de systèmes de fichier. Pour pouvoir s'adapter facilement à plusieurs types de systèmes de fichiers, les systèmes UNIX définissent généralement une API indépendante du type de système de fichier appelée VFS (Virtual File Système).
A ce jour, l'API VFS de Almos-mkh a été portée sur trois systèmes de fichiers : FATFS, DEVFS, RAMFS. Dans ce TP, on utilisera un disque contenant un système de fichier FATFS, respectant le format Microsoft FAT32.
Le cache des fichiers est le nom générique de la représentation interne - en mémoire - du système de fichiers. Almos-mkh utilise deux techniques pour réduire l'encombrement du cache de fichiers en mémoire par rapport à encombrement du systèmes de fichiers sur le disque:
La structure générale du système de fichier n'est pas un arbre, mais un DAG (Graphe orienté sans cycles), puisqu'il peut exister plusieurs cheminoms permettant d'atteindre un même fichier par plusieurs chemins partant de la racine. Malgré cela, on utilise le nom arbre des inodes pour désigner la structure représentant le sous ensemble des fichiers et répertoires présents à un instant donné dans le cache des fichiers. Cette structure est implémentée par un graphe bi-parti contenant deux types de noeuds:
L'arbre des inodes est une structure distribuée: Les structures vfs_inode_t représentant les fichiers sont distribuées le plus uniformément possible sur les différents clusters de l'architecture. Dans le cas cas d'un inode père représentant un répertoire, toutes les structures vfs_dentry_t décrivant les liens vers les inodes fils sont toujours allouées dans le cluster contenant l'inode père. Mais l'inode père et l'inode fils sont généralement allouées dans deux clusters différents. Deux inodes fils d'un même inode père sont également rangés dans des clusters différents. Par conséquent, l'arbre des inodes utilise principalement des pointeurs étendus.
A chaque noeud de type inode de l'arbre des inodes est attaché une structure annexe, appelée mapper contenant une copie partielle, en mémoire, du contenu du fichier représenté par cet inode. Cette structure mapper_t est implémentée comme un radix tree de profondeur 3. Ce radix tree est indexé par le numéro de la page dans le fichier, vu comme un tableau de pages de 4 octets, et chaque entrée du radix tree contient un pointeur sur une page de 4Koctets en mémoire.
La structure mapper_t est entièrement localisée dans le cluster contenant la structure vfs_inode_t à laquelle il est attaché.
Chaque fois qu'un processus ouvre un fichier avec un appel système open(), le noyau crée un descripteur de fichier ouvert, et renvoie au processus client un index identifiant ce descripteur de fichier. Ce descripteur de fichier doit être utilisé par le processus pour accéder au fichier, car de descripteur contient en particulier un offset (pointeur) définissant quelle zone du fichier doit être accédée.
Ce descripteur de fichier ouvert est implémenté par le structure vifs_file_t.
Il peut exister à un instant donné plusieurs descripteurs de fichiers ouverts pour un même fichier X, mais toutes les structures vfs_file_t concernant le fichier X sont allouées dans le cluster contenant l'inode représentant le fichier X.
Pour minimiser la contention, les structures vfs_inode_t sont distribuées le plus uniformément possible sur tous les clusters. Et les structures annexes attachées à un fichier X particulier: la structure mapper_t contenant les données du fichier, les structures vfs_dentry_t attachées à un fichier répertoire, et les structure vfs_file_t permettant l'accès au fichier, sont toutes allouées dans le cluster contenant la structure vfs_inode_t représentant le fichier X. Il s'agit donc d'une politique de distribution possédant la granularité fichier, qui minimise la contention dans le cas ou un grand nombre de threads accèdent parallèle à un grand nombre de fichiers différents, mais qui ne sera pas efficace dans le cas où un grand nombre de threads accèdent en parallèle à un même fichier.
Pour répondre aux questions ci-dessous, il faut plonger dans le code du VFS dans les fichiers almos-mkh/kernel/fs/vfs.c et almos-mkh/kernel/fs/vfs.h.
Vous pouvez également consulter le code du mapper dans les fichiers almos-mkh/kernel/mm/mapper.c et almos-mkh/kernel/mm/mapper.h.
Le code du radix_tree générique est défini dans les fichiers almos-mkh/kernel/libk/grdxt.c et almos-mkh/kernel/libk.grdxt.h.
Les périphériques sont des ressources partagées par tous les threads en cours d'exécution : n'importe quel thread, de n'importe quel processus, s'exécutant dans n'importe quel cluster, peut effectuer un appel système nécessitant un accès au disque.
Dans une architecture manycore capable d'exécuter plusieurs centaines de threads en parallèle, le sous-système d'entrées/sorties, et plus précisément les structures de données du noyau permettant l'accès aux périphériques peuvent facilement devenir - après l'accès à la mémoire partagée - le principal goulot d'étranglement de la machine.
Le but de ce TP est de présenter les techniques de distribution utilisées par Almos-mkh pour minimiser les risques de contention lors de l'accès au sous-système d'entrées/sorties.
Pour chaque type de périphérique (contrôleur de disque, contrôleur réseau, contrôleur graphique, terminal alpha-numérique, etc.), il existe un - très - grand nombre de modèles de périphériques fournissant globalement le même service, mais ayant chacun leur propres commandes, leurs propre jeu de registres adressables, et leur propre mécanisme de signalisation.
Pour faciliter l'intégration des périphériques, et pour éviter de devoir modifier et/ou recompiler le code du noyau lors de l'ajout d'un nouveau périphérique, tous les systèmes d'exploitation définissent une représentation abstraite de chaque type de périphérique, permettant de masquer les différences entre différentes implémentations matérielles dans un pilote logiciel (appelé driver), généralement fourni par le fabricant.
Par ailleurs, pour améliorer la bande passante (c'est à dire le nombre d'opérations d'entrées sortie par seconde), les contrôleurs de périphériques sont souvent multi-canaux : un même contrôleur contient plusieurs jeux de registres indépendants, permettant à l'OS d'exécuter plusieurs opérations d'entrée/sortie en parallèle pour le compte de plusieurs clients. C'est presque toujours le cas des contrôleur de terminaux, c'est le cas des contrôleurs de disque respectant la norme AHCI, et c'est de plus en plus souvent le cas des contrôleurs réseau.
La principale abstraction définie par almos-mkh est la structure chdev, qui représente un canal d'un périphérique. Pour les terminaux texte, et pour les contrôleurs réseau, Almosmkh définit même un chdev par canal et par direction (direction RX pour la réception de donnée depuis l'extérieur / direction TX pour la transmission de données vers l'extérieur).
La structure chdev_t et les fonctions d'accès sont définis dans les fichiers chdev.h et chdev.c.
Almost-mkh identifie un périphérique matériel par un double index (func,impl). L'index func défnit la fonctionnalité. Pour une fonctionnalité donnée, l'index impl définit une réalisation matérielle particulière.
Ll'interface entre les pilotes et le code noyau qui implémente - pour chaque type fonctionnel - l'API noyau, est standardisée et contient trois fonctions:
Par définition un canal de périphérique ne peut traiter qu'une seule opération d'entrée/sortie à la fois. Il faut donc séquencialiser les requêtes concurrentes visant un même chdev. Almos-mkh attache donc à chaque chdev une file d'attente permettant à chaque thread client d'enregistrer sa requête puis de se bloquer, et de relâcher le processeur, en attendant que sa requête soit traitée. Ce n'est généralement pas le thread client qui exécute l'opération d'entrée/sortie. C'est un autre thread server, de type DEV, attaché au chdev, qui est chargé d'exécuter séquenciellement les requêtes d'entrée/sortie enregistrées dans la file d'attente, et de réveiller chaque thread client, quand le transfert de données demandé par le client est terminé.
En pratique, la file d'attente est implémentée comme une XLIST trans-clusters, enracinée dans la structure chdev_t, et permettant de chaîner entre elles les threads clients. Les paramètres de la requête (type de la requête, adresse du tampon mémoire source ou destination, nombre d'octets ou de blocs à transférer, etc) sont enregistrés dans le descripteur du thread client.
Lorsque la file d'attente est vide, le thread server se bloque et relâche le processeur. La politique d'ordonnancement des threads prévoit qu'un thread de type DEV a une priorité plus élevée qu'un thread de type USR, mais elle prévoit également qu'un thread DEV n'est pas exécutable lorsque la file d'attente est vide.
Les périphériques externes sont généralement connectés sur un bus externe (type PCI-express ou Hypertransport) qui est connecté au bus sytème (ou au micro-réseau dans le cas d'une architecture manycore), par un bridge. Il faut donc nécessairement passer par ce bridge, localisé dans un cluster particulier (appelé cluster_io) pour accéder aux registres adressables de tous les périphériques.
Pour minimiser les risques de contention, Almos-mkh ne place pas les descripteurs de chdev (et les threads server associés) dans le cluster_io, mais il les distribue, le plus uniformément possible, sur tous les clusters de l'architecture.
Par conséquent, dans le cas général, une opération d'entrée/sortie met en jeu trois clusters:
La structure chdev_t contient (entre autres) les informations suivantes:
Pour répondre aux questions ci-dessous, vous devez commencer par lire la documentation décrivant le sous-système d'entrées/sorties, que vous trouverez ici.
L'API du contrôleur de disque générique (IOC pour Input/Outptut Controler) est définie ici
L'API du terminal texte générique (TXT pour Terminal teXTe) est définie ici ici
Trois contrôleurs de disque (et donc trois pilotes) ont été intégrés dans Almos-mkh : IOC_BDV, IOC_HBA, IOC_RDK. Le code du pilote IOC_BDV utilisé dans TSAR est défini dans les fichiers soclib_bdv.h et soclib_bdv.c.
Deux contrôleurs de terminaux texte (et donc deux pilotes) ont été intégrés dans Almos-mkh : TXT_TTY, TXT_MTY, Le code du pilote TXT_TTY utilisé dans TSAR est défini dans les fichiers soclib_tty.h et soclib_tty.c.