Verrous de fichiers
Mécanismes de contrôle des accès concurrents à un fichier, les verrous sont d'une
grande utilité dans les applications de gestion et dans l'élaboration de bases
de données partagées. Les verrous sont rattachés aux inodes : ainsi, toutes
les ouvertures d'un même fichier, et a fortiori tous les descripteurs sur ces
ouvertures, voient le verrou. La protection réalisée par le verrou a donc lieu
sur le fichier physique. Un verrou (au sens de fcntl) est la propriété d'un
seul processus, et seul le processus propriétaire du verrou peut le modifier
ou l'enlever. Attention : le verrou ne protège pas contre les accès du processus
propriétaire.
Deux conséquences de ce rattachement au couple (processus, inode) sont souvent source d'erreurs :
- les verrous ne sont pas hérités par le fils lors d'un
fork; - la fermeture par le processus de n'importe quel descripteur sur le fichier libère tous les verrous qu'il détient sur ce fichier.
Caractéristiques d'un verrou
Les verrous sont définis par deux caractéristiques :
- la portée : ensemble de positions du fichier auxquelles le verrou
s'applique. Cet ensemble est un intervalle, soit une portion de fichier
[position1, position2], soit jusqu'à la fin du fichier[position1, fin de fichier[; dans ce dernier cas, si le fichier augmente, le verrou protège les nouvelles positions ; - le type, qui décrit les possibilités de cohabitation des différents verrous : plusieurs verrous partagés (lecture) peuvent coexister sur une même zone, alors qu'un verrou exclusif (écriture) ne peut coexister avec aucun autre verrou.
Le mode opératoire des verrous
Le mode opératoire joue sur le comportement des primitives read et write.
Les verrous d'un fichier sont soit consultatifs (advisory), soit impératifs
(mandatory). Dans le premier mode, la présence d'un verrou n'est testée qu'à la
pose d'un verrou : la pose sera refusée s'il existe un verrou de portée non
disjointe et que l'un des deux verrous est exclusif. Dans le second mode, la
présence de verrous est testée pour la pose, mais aussi lors des appels système
read et write. Dans le mode consultatif, les verrous n'ont d'effet que sur
les processus jouant effectivement le jeu, c'est-à-dire posant des verrous sur
les zones du fichier sur lesquelles ils veulent réaliser une lecture (verrou
partagé) ou une écriture (verrou exclusif).
Dans le mode impératif, les verrous ont un impact sur les lectures / écritures de tous les processus :
- sur les zones couvertes par un verrou de type partagé, toute tentative d'écriture par un autre processus est bloquée ;
- sur les zones couvertes par un verrou de type exclusif, toute tentative de lecture ou d'écriture par un autre processus est bloquée.
(Si le descripteur est en mode O_NONBLOCK, l'appel échoue avec EAGAIN au
lieu de bloquer.)
Pour rendre l'utilisation impérative sous Linux, il fallait monter le système de
fichiers avec l'option -o mand, puis utiliser la commande chmod pour
positionner le bit set-group-ID du fichier tout en retirant le droit
d'exécution pour le groupe (par exemple chmod g+s,g-x fichier). Le verrouillage
impératif, peu fiable, a été retiré du noyau Linux à partir de la version 5.15 :
seuls les verrous consultatifs y sont désormais disponibles.
Manipulation des verrous
La structure de verrou flock :
struct flock {
short l_type; /* F_RDLCK (verrou partagé), F_WRLCK (verrou exclusif), F_UNLCK (déverrouillage) */
short l_whence; /* SEEK_SET, SEEK_CUR, SEEK_END */
off_t l_start; /* position relative à l_whence */
off_t l_len; /* longueur de l'intervalle (0 : jusqu'à la fin du fichier) */
pid_t l_pid; /* PID du processus propriétaire (rempli par F_GETLK) */
};
Les manipulations de verrous se font avec la primitive fcntl, c'est-à-dire par
le biais d'un descripteur. Pour poser un verrou partagé, ce descripteur doit
pointer sur une ouverture en lecture. De même, il faut un descripteur sur une
ouverture en écriture pour un verrou de type exclusif.
Pour décrire la portée du verrou que l'on veut poser, on utilise la même syntaxe
que pour la primitive lseek : le début de l'intervalle est l_whence + l_start.
La longueur du verrou est définie par le champ l_len. Si cette valeur est
nulle, le verrou va jusqu'à la fin du fichier, quelle que soit sa taille future.
Le champ l_pid contient le PID du processus propriétaire du verrou ; ce champ
est rempli par fcntl dans le cas d'un appel d'interrogation (F_GETLK).
Utilisation de fcntl pour manipuler les verrous
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* struct flock *verrou */);
Trois commandes sont possibles :
F_SETLK: pose (ou retrait avecF_UNLCK) non bloquante ; en cas de conflit, l'appel échoue avecEACCESouEAGAIN;F_SETLKW: pose bloquante (Wait) ; l'appel attend la libération des verrous incompatibles et peut échouer avecEDEADLKsi le noyau détecte un interblocage ;F_GETLK: test d'existence d'un verrou incompatible avec le verrou passé en paramètre ; s'il n'y en a pas,l_typeest positionné àF_UNLCK, sinon la structure est remplie avec la description de l'un des verrous gênants.
Exemple : pose bloquante d'un verrou exclusif sur tout le fichier.
struct flock v = {
.l_type = F_WRLCK,
.l_whence = SEEK_SET,
.l_start = 0,
.l_len = 0, /* jusqu'à la fin du fichier */
};
if (fcntl(fd, F_SETLKW, &v) == -1)
perror("fcntl");
Il existe d'autres interfaces de verrouillage : lockf (POSIX, construite sur
fcntl sous Linux) et flock (BSD, verrou sur le fichier entier, attaché à
l'ouverture et non au processus).