Le système virtuel VFS
VFS suppose de tout F.S. :
-
qu'il est organisé sous forme d'une arborescence (ce qui ne pose pas de problème),
-
qu'il possède un superbloc, contenant des informations caractéristiques
du F.S.,
-
qu'il supporte la notion de i-nœud (i-node).
A chaque fichier correspond
un i-nœud, structure qui contient la description du fichier.
Pour des raisons de clarté, le superbloc et les i-nœuds de VFS seront appelés "superbloc VFS " et " i-nœud VFS " pour ne pas les confondre avec les superbloc et i-nœud du F.S. lui-même, ext2 par exemple.
Il n'est pas surprenant de retrouver dans le VFS les caractéristiques d'ext2fs, le but du premier étant de généraliser les concepts du second à tous les autres F.S.
Les caches de VFS
Cache de données
Comme tous les S.G.F., Linux utilise un cache de données (en fait un ensemble de caches, appelés tampons caches (cache buffers) ou simplements tampons, qui sont une image mémoire des données enregistrées sur disque.
Tous les périphériques à structure de blocs (blocks devices) sont vus par Linux à travers une interface uniforme, généralement asynchrone (y compris les périphériques SCSI).
Les transferts entre les tampons et le périphérique physique sont totalement transparents à l'utilisateur et se décomposent en plusieurs opérations :
-
lorsqu'un système de fichiers réel (ext2 par exemple) veut lire les données sur un disque physique, il dépose une requête de lecture d'un bloc physique auprès du pilote de périphériques qui le transmet au contrôleur du disque.
Un tampon cache est intégré dans cette interface pilote bloc.
-
après que des blocs physiques ont été lus par le F.S. ils sont sauvegardés dans les tampons globaux, partagés par tous les F.S. et le noyau Linux (VFS).
Les différents tampons globaux sont identifiés par leur numéro de bloc et un identificateur unique pour le périphérique sur lequel il a été lu.
Ainsi, si la même donnée est souvent nécessaire, elle sera retrouvée à partir des tampons caches plutôt qu'à partir d'une lecture physique sur disque, qui serait beaucoup plus longue.
Certains périphériques permettent une lecture anticipée (ahead) de blocs de données qui sont supposés devenir ultérieurement nécessaires.
Toutes les requêtes de lecture/écriture sont transmises aux pilotes de périphériques sous la forme d'une structure de données buffer_head par l'intermédiaire de fonctions standard du noyau.
Elles contiennent toutes les informations dont le pilote de périphérique blocs a besoin.
Tous les périphériques blocs sont vus comme des collections linéaires de blocs de même taille.
Les tampons cache appartiennent logiquement à deux structures :
-
une des 5 listes chaînées des tampons libres de même taille (512, 1024, 2048, 4096, 8192 octets).
C'est à la fin de ces listes que sont rendus les blocs lorsqu'ils sont libérés ou créés,
-
une table de hash-code dont les éléments sont des pointeurs vers les tampons qui ont la même valeur de fonction de hachage, calculée à partir du numéro de périphérique et du numéro de bloc.
Les tampons qui sont dans la table sont aussi dans des listes LRU (voir plus haut).
Il y a une liste LRU pour chaque taille de bloc.
Cache d'i-nœuds
Au fur et à mesure que les processus du système accèdent aux répertoires et aux fichiers, différentes fonctions sont appelées, qui traversent les i-nœuds VFS.
Certains sont accédés très souvent.
C'est pourquoi ils sont conservés dans un cache d'i-nœuds directement géré par VFS afin d'être retrouvés plus rapidement.
On remarque par exemple que, lorsqu'on répète deux fois de suite la commande ls sur un répertoire qui n'a pas été accédé depuis longtemps, la seconde s'exécute beaucoup plus rapide que la première.
Les i-nœuds VFS les moins souvent accédés finissent par être supprimés du cache.
Le cache d'i-nœuds est organisé selon une table de hash-code classique, dont les entrées sont des pointeurs de listes de nœuds VFS ayant la même valeur de hachage.
Celle-ci est calculée à partir de la valeur de l'i-nœud et de l'identificateur du périphérique.
Si VFS trouve l'i-nœud dans la table, le compteur de ce dernier est incrémenté pour signifier qu'il est accédé par un nouvel utilisateur, puis le traitement continue.
Si VFS ne trouve pas le couple {i-nœud, périphérique} cherché, il doit trouver un i-nœud libre.
Pour ce faire, il dispose de plusieurs possibilités :
-
s'il peut le faire, le système alloue un nouvel espace au noyau, l'organise en un ensemble de nouveaux i-nœuds libres et les ajoute à la liste des i-nœuds libres, puis en prélève un,
-
si l'espace maximal prévu pour les i-nœuds VFS a déjà été alloué, il recherche un i-nœud dont le nombre d'utilisateurs est égal à 0 ce qui indique que VFS ne les utilise plus (les i-nœuds verrouillés (locked) ne peuvent pas être réutilisés avant d'être déverrouillés).
Avant d'en écraser la valeur il doit le réécrire physiquement sur disque s'il est "sale" (dirty : il a été modifié depuis sa création ou sa dernière sauvegarde sur fichier).
Pour cela, il fait appel à la fonction write_inode() spécifique du F.S. Lorsqu'un espace mémoire a été trouvé pour y ranger le nouvel i-nœud, il doit encore être chargé à partir du disque (ou par simulation) par une autre fonction spécifique du F.S. : read_inode().
Pendant tout le temps de ces opérations d'écriture/lecture, le compteur de l'i-nœud est positionné à 1 (donc il n'est pas récupérable) mais il est verrouillé (donc pas immédiatement utilisable).
Cache de répertoires VFS
Enfin, VFS gère aussi un cache de répertoires (directory entry cache, ou en abrégé dentry cache ou dcache) parcourus, afin de retrouver rapidement les i-nœuds des répertoires les plus fréquemment utilisés.
Seules les entrées de répertoires courtes (moins de 16 caractères) sont stockées, ce qui correspond en principe aux répertoires les plus fréquemment parcourus.
Le cache des répertoires est lui aussi organisé en une table de hash-code, la fonction de calcul de la valeur de hachage utilisant le numéro de périphérique et le nom du répertoire.
Afin de maintenir le cache valide et à jour, VFS gère deux listes LRU (Last Recently Used) pour chaque entrée de la table de hash-code : la première fois qu'un répertoire est entré dans la table, il est placé en queue de la première liste.
Si les deux listes sont pleines, cela a pour effet de prélever l'élément de tête de la première liste et de le placer en queue.
Si le répertoire est de nouveau accédé, il est placé à la queue de la seconde liste.
Dans les deux cas, l'effet est de faire remonter les répertoires les moins utilisés en tête de liste.
Lorsqu'un élément doit être prélevé pour ajouter un répertoire, c'est le répertoire le plus ancien qui est supprimé.
De plus, les éléments de la seconde liste sont conservés plus longtemps que ceux de la première liste, ce qui est logique puisqu'ils ont été accédée au moins deux fois .
Les démons de VFS
Le noyau de Linux possède deux processus démons qui participent à la gestion des buffers :
- bdflush est un thread du noyau lancé au moment du démarrage du système, qui passe la majorité de son temps à dormir.
Chaque fois qu'un tampon est alloué ou libéré, le nombre de tampon "sales" est incrémenté.
Lorsque le pourcentage atteint une limite fixée (60 % par exemple) mais modifiable, bdflush est réveillé et appelle lui-même le processus kflush.
- update est un démon qui écrit périodiquement les tampons "sales" sur disque.
Son action est à peu près identique à celle de bdflush.
Structures de VFS
Le rôle du module correspondant au F.S. (figure 3) est de simuler ces propriétés s'il ne les possède pas réellement : c'est le cas de msdos par exemple.
Le VFS gère une table des F.S. installés, qu'il reconnaît.
Chaque élément de la table est de type file_system_type, défini ainsi (figure 12) :
struct file_system_type {
const char * name;
int fs_flags;
struct super_block * (*read_super) (struct super_block
*, void *, int);
struct file_system_type * next;
}; // file_system_type |
figure 12
name : nom sous lequel a été enregistré le F.S. (ext2, ntfs, etc...)
fs_flags : différents indicateurs (par exemple F.S. en lecture seulement, etc...
read_super : pointeur vers la fonction que doit appeler VFS pour charger le super-bloc d'une arborescence de fichiers au moment où celle-ci est montée sous Linux,
next : pointeur vers le suivant dans la liste chaînée (l'initialiser à NULL)
Il est possible d'ajouter à Linux un F.S. personnel.
Il suffit de remplir une structure de type file_system_type et de l'enregistrer auprès de VFS, soit, schématiquement (figure 13) :
struct super_block * MonReadSuperBloc (struct super_block
*, void *, int)
{
// ...
} // MonReadSuperBloc()
//
name fs_flags read_super
next
file_system_type MonFs = {"MonFileSystem", ..., MonReadSuperBloc,
NULL};
...
register_filesystem (MonFs); |
figure 13
La fonction système register_filesystem() parcourt la liste des F.S. déjà installés, vérifie par son nom qu'il est nouveau, et l'ajoute éventuellement à la liste en modifiant le pointeur next.
L'ajout d'une sous-arborescence dans l'arborescence des fichiers est effectuée par la commande mount() à laquelle est indiqué le nœud sur laquelle elle doit être installée, et le nom du F.S. selon lequel elle est organisée (par défaut iso9660).
VFS parcourt la liste des F.S. reconnus et, s'il le trouve, appelle la fonction read_super().
C'est donc à partir de la structure file_system_type que VFS apprend à lire le superbloc de l'arborescence installée ou à en simuler la lecture.
Le superbloc de VFS est défini par la structure super_block suivante (figure 14) :
struct super_block {
struct list_head s_list; /* Keep this first */
kdev_t s_dev;
unsigned long s_blocksize;
unsigned char s_blocksize_bits;
unsigned char s_lock;
unsigned char s_rd_only;
unsigned char s_dirt;
struct file_system_type *s_type;
struct super_operations *s_op;
struct dquot_operations *dq_op;
unsigned long s_flags;
unsigned long s_magic;
unsigned long s_time;
struct dentry *s_root;
struct wait_queue *s_wait;
struct inode *s_ibasket;
short int s_ibasket_count;
short int s_ibasket_max;
struct list_head s_dirty;
/* dirty inodes */
union {
struct minix_sb_info minix_sb;
struct ext2_sb_info ext2_sb;
// ... : tous les autres F.S. reconnaissables par
VFS
} u;
/*
* The next field is for VFS *only*. No filesystems have
any business
* even looking at it. You had been warned.
*/
struct semaphore s_vfs_rename_sem; /* Kludge */
}; // super_block |
figure 14
C'est par l'intermédiaire de ce superbloc qu'il apprend à manipuler les i-nœuds de VFS : le champ s_op est un pointeur sur une structure de type super_operations (figure 15) permettant d'accéder à toutes les fonctions de manipulation des i-nœud de l'arborescence montée :
struct super_operations {
void (*read_inode) (struct inode *);
void (*write_inode) (struct inode *);
void (*put_inode) (struct inode *);
void (*delete_inode) (struct inode *);
int (*notify_change)(struct dentry *, struct iattr
*);
void (*put_super) (struct super_block *);
void (*write_super) (struct super_block *);
int (*statfs) (struct
super_block *, struct statfs *, int);
int (*remount_fs) (struct super_block *,
int *, char *);
void (*clear_inode) (struct inode *);
void (*umount_begin) (struct super_block *);
}; // super_operations |
figure 15
read_inode, write_inode : lecture/écriture de l'i-nœud sur disque,
put_inode : suppression d'un i-nœud du cache des i-nœuds
delete_inode : suppression d'un i-nœud du fichier
notify_change : appelé lorsque les attributs d'un i-nœud sont modifiés
put_super : libération du superbloc (appelée lors de l'opération unmount()),
write_super : appelée lorsque le superbloc doit être écrit sur disque
statfs : appelée pour obtenir des statistiques du F.S.
remount_fs : appelée pour remonter le F.S.
clear_inode : appelée pour purger l'i-nœud
Un i-nœud VFS a la structure suivante (figure 16) :
struct inode {
struct list_head i_hash;
struct list_head i_list;
struct list_head i_dentry;
unsigned long i_ino;
unsigned int i_count;
kdev_t i_dev;
umode_t i_mode;
nlink_t i_nlink;
uid_t i_uid;
gid_t i_gid;
kdev_t i_rdev;
off_t i_size;
time_t i_atime;
time_t i_mtime;
time_t i_ctime;
unsigned long i_blksize;
unsigned long i_blocks;
unsigned long i_version;
unsigned long i_nrpages;
struct semaphore i_sem;
struct semaphore i_atomic_write;
struct inode_operations *i_op;
struct super_block *i_sb;
struct wait_queue *i_wait;
struct file_lock *i_flock;
struct vm_area_struct *i_mmap;
struct page *i_pages;
struct dquot *i_dquot[MAXQUOTAS];
unsigned long i_state;
unsigned int i_flags;
unsigned char i_pipe;
unsigned char i_sock;
int i_writecount;
unsigned int i_attr_flags;
__u32 i_generation;
union {
struct pipe_inode_info pipe_i;
struct minix_inode_info minix_i;
struct ext2_inode_info ext2_i;
// ... : tous les autres F.S. reconnaissables par
VFS
void *generic_ip;
} u;
}; // inode |
On y reconnaît un certain nombre de champs qui ont été évoqués précédemment.
Le champ i_op, de type inode_operations (figure 17), décrit comment le VFS peut manipuler les i-nœuds VFS :
struct inode_operations {
struct file_operations * default_file_ops;
int (*create) (struct inode *, struct
dentry *,int);
struct dentry * (*lookup) (struct inode *, struct
dentry *);
int (*link) (struct dentry
*,struct inode *, struct dentry *);
int (*unlink) (struct inode *, struct
dentry *);
int (*symlink) (struct inode *,struct dentry
*, const char *);
int (*mkdir) (struct inode *, struct
dentry *, int);
int (*rmdir) (struct inode *, struct
dentry *);
int (*mknod) (struct inode *, struct
dentry *, int, int);
int (*rename) (struct inode *, struct
dentry *,
struct inode *, struct dentry *);
int (*readlink)(struct dentry *, char *,int);
struct dentry *(*follow_link) (struct dentry
*, struct dentry *,
unsigned int);
int (*readpage) (struct file *, struct
page *);
int (*writepage) (struct file *, struct page
*);
int (*bmap) (struct
inode *,int);
void (*truncate) (struct inode *);
int (*permission)(struct inode *, int);
int (*smap) (struct
inode *,int);
int (*updatepage)(struct file *, struct page
*, unsigned long,
unsigned int, int);
int (*revalidate)(struct dentry *);
}; // inode_operations |
figure 17
default_file_ops : pointeur sur une structure qui décrit comment manipuler les fichiers ouverts (voir figure 18),
create : appelée par les fonctions système open() et creat(),
lookup : appelée lorsque VFS doit rechercher un i-nœud dans un répertoire parent.
link, unlink, symlink, mkdir, rmdir, mknod, rename, readlink : appelées par les fonctions système équivalentes
follow_link : appelée par VFS pour suivre un lien symbolique jusqu'à l'i-nœud qu'il désigne.
readpage :
writepage :
bmap :
truncate :
permission :
smap :
updatepage :
revalidate :
Les opérations sur les fichiers ouverts sont décrites dans la structure suivante (figure 18) :
struct file_operations {
loff_t (*llseek) (struct file *,
loff_t, int);
ssize_t (*read) (struct file
*, char *, size_t, loff_t *);
ssize_t (*write) (struct file *,
const char *, size_t, loff_t *);
int (*readdir) (struct
file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct
poll_table_struct *);
int (*ioctl) (struct inode *, struct
file *, unsigned int,
unsigned long);
int (*mmap) (struct file *,
struct vm_area_struct *);
int (*open) (struct inode
*, struct file *);
int (*flush) (struct file *);
int (*release) (struct inode *, struct file
*);
int (*fsync) (struct file *, struct
dentry *);
int (*fasync) (int, struct file *, int);
int (*check_media_change) (kdev_t dev);
int (*revalidate)
(kdev_t dev);
int (*lock) (struct file *,
int, struct file_lock *);
}; // file_operations |
figure 18
On y reconnaît les équivalents des fonctions système de même nom.
Dernière mise à jour : 12/07/2001