Aller au contenu principal

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 avec F_UNLCK) non bloquante ; en cas de conflit, l'appel échoue avec EACCES ou EAGAIN ;
  • F_SETLKW : pose bloquante (Wait) ; l'appel attend la libération des verrous incompatibles et peut échouer avec EDEADLK si 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_type est 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).