Inotify : monitorer un fichier ou un répertoire
Date de publication : 11/03/2007
Par
julp (Autres articles)
Inotify, fonction intégrée au noyau depuis la version 2.6.13, fournit un
moyen simple et efficace pour être notifié de diverses actions effectuées
sur un fichier ou un répertoire. Elles vont de la simple ouverture à son
déplacement.
Je vous propose donc de découvrir cette fonctionnalité et de la mettre en
oeuvre à l'aide de l'API C, côté utilisateur, fournit par glibc.
1. Introduction
1.1. Présentation
1.2. Pré-requis
2. Utilisation en C
2.1. Description des fonctions
2.2. Les évènements que l'on peut intercepter
2.3. Mise en oeuvre par l'exemple
2.3.1. Découverte
2.3.2. Cas concret : intercepter les modifications du fichier de configuration d'Apache et les lui notifier
3. Exemple en Perl
4. Conclusion
1. Introduction
1.1. Présentation
A la manière des auditeurs que l'on rencontre dans les langages comme
Java, Inotify est une fonction standard intégrée au noyau Linux qui
permet de monitorer un fichier en vue de certains évènements :
déplacement, suppression, accès, etc dont il informera l'utilisateur.
Le terme fichier est ici utilisé au sens général car ce procédé n'est
pas limité aux seuls fichiers réguliers mais permet de placer un
"écouteur" sur un répertoire, un lien symbolique, un fichier spécial,
...
Fort de cette incorporation au système Linux, l'exploitation de cette
fonctionnalité par les développeurs se résument généralement à
quelques instructions. En effet, les traitements à effectuer à la
réception d'une notification sont bien plus longs à coder (aussi bien
en terme de temps qu'en nombre de lignes) que la mise sous
surveillance elle-même des fichiers. De plus, cette "interface" n'est
pas seulement disponible aux programmeurs C mais existe dans des
langages scripts comme Perl, Ruby, Python, etc.
1.2. Pré-requis
-
Un noyau de version supérieure ou égale à 2.6.13 avec les
options relatives à Inotify activées. Pour les activer
rendez-vous dans la catégorie appelée File systems
lors du make menuconfig, cochez ensuite les options
Inotify file change notification support et Inotify
support for userspace puis recompilez et installez ce
noyau.
-
GNU C Library en version 2.4 ou supérieure requise, les
versions antérieures ne disposant pas des fonctions relatives
à inotify que nous verrons ultérieurement.
2. Utilisation en C
2.1. Description des fonctions
Comment mettre en place la surveillance sur un fichier particulier ?
Elle se résume à quelques appels de fonctions dont l'ordre vous est
présenté ci-dessous :
Les prototypes de ces fonctions sont :
-
int inotify_init(void) :
Initialise une nouvelle instance inotify et renvoie un
descripteur de fichier associée à celle-ci. En cas d'échec
la valeur renvoyée est -1.
-
int inotify_add_watch(int fd, const char *path, uint32_t mask) :
Place un fichier (ou répertoire) sous surveillance et retourne
un identifiant (watch descriptor) qui sera renvoyé par read
afin de permettre l'identification du fichier source de
l'évènement parmi ceux qui sont inspectés. En cas d'échec,
cette fonction renvoie également la valeur -1.
- fd : descripteur de fichier désignant une instance de inotify précédemment créée par la fonction inotify_init
- path : chemin du fichier ou répertoire à monitorer
- mask : masque des évènements à intercepter (vus dans la partie suivante)
-
int inotify_rm_watch(int fd, uint32_t wd) :
Stoppe une observation particulière. La valeur retournée est
nulle en cas de succès sinon -1.
- fd : descripteur de fichier faisant référence à une instance valide inotify, créée à l'aide de la fonction inotify_init
- wd : un descripteur d'observation renvoyé tantôt par la fonction inotify_add_watch
-
ssize_t read(int fd, void *buf, size_t count) :
Permet de récupérer les informations envoyées par le noyau
lorsqu'un évènement se produit. Elle renvoie le nombre d'octets
lus ou -1 en cas d'erreur.
- fd : le descripteur de fichier associé à une instance valide de inotify (créée par la fonction inotify_init)
- buf : le buffer accueillant les données lues
- count : nombre d'octets à lire au maximum
Si seuls des fichiers sont mis en écoute alors buf pourra
être directement de type struct inotify_event (exemple n°2).
En revanche, en cas de présence d'un répertoire il faudra un
buffer beaucoup plus important car une chaîne décrivant
l'objet source sera disponible (exemple n°1).
La structure inotify_event est définie comme telle :
struct inotify_event {
int wd;
uint32_t mask;
uint32_t cookie;
uint32_t len;
char name[];
};
|
L'attribut mask vous permettra d'identifier précisément
l'évènement reçu parmi ceux que vous aurez spécifié lors de
la mise sous surveillance (fonction inotify_add_watch). Le
champ name n'est utilisé uniquement si c'est un
répertoire qui est monitoré, fournissant ainsi le nom de
l'objet source de l'évènement de ce répertoire (pouvant être
le dossier lui-même).
-
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout) :
Permet de tester (entre autres) si des données sont
disponibles en lecture sur un ou plusieurs descripteurs de
fichiers.
- nfds : le plus grand descripteur plus un
- readfds : ensemble de descripteurs soumis à surveillance pour vérifier si des données deviennent disponibles en lecture
- writefds : identique au paramètre readfds sauf que la surveillance concerne l'aspect non bloquant de l'écriture
- exceptfds : descripteurs surveillés pour l'occurrence de conditions exceptionnelles
- timeout : fixe le temps d'attente de la fonction select avant que celle-ci ne rende la main (la valeur particulière zéro permet de la rendre immédiatement)
La fonction poll peut éventuellement remplacer select.
-
int close(int fd) :
Ferme un descripteur de fichier (indiqué par son seul
paramètre). Elle retourne -1 en cas d'erreur et
0 dans le cas contraire.
2.2. Les évènements que l'on peut intercepter
La liste exhaustive des évènements dont vous pouvez être notifié est
la suivante :
- IN_ACCESS : accès au fichier
- IN_MODIFY : le fichier a été modifié
- IN_ATTRIB : modification des attributs du fichier, c'est à dire l'une des informations fournie par la fonction stat (permissions, propriétaire, ...)
- IN_CLOSE_WRITE : le fichier a été fermé après qu'une modification y est été apportée
- IN_CLOSE_NOWRITE : le fichier a été fermé mais n'a pas subi de modification
- IN_OPEN : le fichier a été ouvert
- IN_MOVED_FROM : élément déplacé du répertoire surveillé
- IN_MOVED_TO : élément déplacé vers le répertoire surveillé
- IN_CREATE : un élément a été créé dans le répertoire surveillé
- IN_DELETE : un élément a été supprimé dans le répertoire surveillé
- IN_DELETE_SELF : l'objet s'est supprimé
- IN_MOVE_SELF : l'objet s'est lui-même déplacé
- IN_ALL_EVENTS : l'ensemble des évènements cités ci-dessus
- IN_CLOSE : le fichier a été fermé (modifié ou non : défini comme IN_CLOSE_WRITE|IN_CLOSE_NOWRITE)
- IN_MOVE : déplacement quelque soit la direction de celui-ci (défini comme IN_MOVE_FROM|IN_MOVE_TO)
Dans le cas d'une mise en écoute sur un répertoire, tout évènement, à
l'exception de IN_MOVE_SELF et IN_DELETE_SELF, concernant l'un de ses
éléments sera rapporté par la notification correspondante. Par contre,
ceci ne concerne pas toute sa hiérarchie où il vous faudra mettre en
place, dans le cas présent, plusieurs observateurs.
Certaines options ne sont disponibles que lors de la mise sous monitor
à l'aide de la fonction inotify_add_watch, à savoir :
- IN_ONLYDIR : ne s'intéresse pas aux fichiers qui composent le répertoire mais à lui seul
- IN_DONT_FOLLOW : ne suit pas le lien symbolique, concerne alors le lien lui-même
- IN_MASK_ADD : si le fichier spécifié fait déjà l'objet d'une surveillance, ce drapeau permet d'ajouter les options spécifiées aux masques au lieu de les remplacer
- IN_ONESHOT : l'évènement ne sera notifié qu'une seule fois (réellement effectif qu'à partir de la version 2.6.16 du noyau)
Vous pouvez être avertis, quelque soit les options choisies, de
certains évènements particuliers comme :
- IN_UNMOUNT : le fichier espionné n'est plus accessible suite au démontage d'une partie du système de fichier
- IN_Q_OVERFLOW : la queue des évènements est saturée
- IN_IGNORED : notification de suppression explicite (appel à inotify_rm_watch) ou implicite (fichier supprimé ou indisponible suite à un démontage de partition) d'une surveillance
- IN_ISDIR : indique que la source de l'évènement est un répertoire
2.3. Mise en oeuvre par l'exemple
2.3.1. Découverte
Voici un exemple qui vous permettra de tester les possibilités de
inotify et qui pourra également vous resservir en tant que
squelette. Je vous recommande de faire un test sur un fichier
puis sur un répertoire, ce dernier cas étant plus intéressant. Le
nom du fichier à monitorer est à passer en paramètre lors de
l'exécution du programme :
#include <sys/select.h>
#include <sys/inotify.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
int fd, wd;
void sigint_handler(int signum)
{
(void) signum;
if (inotify_rm_watch(fd, wd)) {
perror("inotify_rm_watch");
exit(EXIT_FAILURE);
}
if (close(fd) < 0) {
perror("close");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
void afficher_masque(int mask)
{
printf("Evénement : ");
if (mask & IN_ACCESS)
printf("Accès");
if (mask & IN_MODIFY)
printf("Modification");
if (mask & IN_ATTRIB)
printf("Attributs");
if (mask & IN_CLOSE)
printf("Fermeture");
if (mask & IN_OPEN)
printf("Ouverture");
if (mask & IN_MOVED_FROM)
printf("Déplacé de");
if (mask & IN_MOVED_TO)
printf("Déplacé vers");
if (mask & IN_MOVE_SELF)
printf("Lui-même déplacé");
if (mask & IN_DELETE)
printf("Suppression");
if (mask & IN_CREATE)
printf("Création");
if (mask & IN_DELETE_SELF)
printf("Lui-même supprimé");
if (mask & IN_UNMOUNT)
printf("N'est pas monté");
if (mask & IN_Q_OVERFLOW)
printf("Queue saturée");
if (mask & IN_IGNORED)
printf("Ignoré");
if (mask & IN_ISDIR)
printf("(répertoire)");
else
printf("(fichier)");
}
int main(int argc, char *argv[])
{
size_t r;
fd_set fds;
char buffer[8192];
struct inotify_event *event;
if (argc != 2) {
fprintf(stderr, "usage : %s fichier_ou_répertoire_à_monitorer\n", argv[0]);
return EXIT_FAILURE;
}
fd = inotify_init();
if (fd < 0) {
perror("inotify_init");
return EXIT_FAILURE;
}
wd = inotify_add_watch(fd, argv[1], IN_ALL_EVENTS);
if (wd < 0) {
perror("inotify_add_watch");
return EXIT_FAILURE;
}
printf("Monitoring : '%s' (numéro = %d)\n", argv[1], wd);
signal(SIGINT, sigint_handler);
while (1) {
FD_ZERO(&fds);
FD_SET(fd, &fds);
if (select(fd + 1, &fds, NULL, NULL, 0) <= 0) {
continue;
}
r = read(fd, buffer, sizeof(buffer));
if (r <= 0) {
perror("read");
return EXIT_FAILURE;
}
event = (struct inotify_event *) buffer;
printf("Fichier surveillé n°%d\t", event->wd);
afficher_masque(event->mask);
if (event->len) {
printf("\tObjet : %s", event->name);
}
printf("\n");
}
return EXIT_FAILURE;
}
|
2.3.2. Cas concret : intercepter les modifications du fichier de configuration d'Apache et les lui notifier
Une application possible consisterait à détecter l'apport de
modifications sur les fichiers de configuration de nos différents
services et de les redémarrer le cas échéant (sur un environnement
de développement ou de test).
Nous allons donc illustrer ce cas au travers du serveur Apache et
mettre au point un petit programme capable de redémarrer le
serveur Apache si son fichier de configuration vient à être
modifié.
Pour la rédaction de cette source, j'ai tenté d'allier code le
plus court possible et praticité.
#include <sys/types.h>
#include <sys/select.h>
#include <sys/inotify.h>
#include <unistd.h>
#include <signal.h>
#include <syscall.h>
#include <stdlib.h>
#include <stdio.h>
#include <syslog.h>
#include <string.h>
#include <errno.h>
#define my_perror(msg) \
syslog(LOG_ERR, msg ": %s", strerror(errno))
int fd, wd;
void sigterm_handler(int signum)
{
(void) signum;
exit(EXIT_SUCCESS);
}
void fin_programme() {
if (wd > 0) {
if (inotify_rm_watch(fd, wd)) {
my_perror("inotify_rm_watch");
}
}
if (fd > 0) {
if (close(fd) < 0) {
my_perror("close");
}
}
closelog();
}
int get_pid() {
int pid;
FILE *fp;
fp = fopen(PID_FILE, "r");
if (fp == NULL) {
return -1;
}
fscanf(fp, "%d", &pid);
fclose(fp);
return pid;
}
int main()
{
int pid;
size_t r;
fd_set fds;
struct inotify_event event;
extern char *__progname;
if (daemon(0, 0)) {
perror("daemon");
return EXIT_FAILURE;
}
openlog(__progname, LOG_PID, LOG_DAEMON);
fd = wd = -1;
fd = inotify_init();
if (fd < 0) {
my_perror("inotify_init");
return EXIT_FAILURE;
}
if (atexit(fin_programme)) {
syslog(LOG_ERR, "atexit");
return EXIT_FAILURE;
}
wd = inotify_add_watch(fd, HTTPD_CONF, IN_MODIFY);
if (wd < 0) {
my_perror("inotify_add_watch");
return EXIT_FAILURE;
}
signal(SIGTERM, sigterm_handler);
while (1) {
FD_ZERO(&fds);
FD_SET(fd, &fds);
if (select(fd + 1, &fds, NULL, NULL, 0) <= 0) {
continue;
}
r = read(fd, &event, sizeof(event));
if (r <= 0) {
my_perror("read");
return EXIT_FAILURE;
}
if ((event.mask & IN_MODIFY) || (event.mask & IN_IGNORED)) {
if (event.mask & IN_IGNORED) {
wd = inotify_add_watch(fd, HTTPD_CONF, IN_MODIFY);
if (wd < 0) {
my_perror("inotify_add_watch");
return EXIT_FAILURE;
}
}
pid = get_pid();
if (pid == -1) {
syslog(LOG_ERR, "Impossible de récupérer le PID du processus principal d'Apache !");
return EXIT_FAILURE;
}
if (kill(pid, SIGHUP)) {
my_perror("kill");
return EXIT_FAILURE;
}
} else {
syslog(LOG_ERR, "Evènement inattendu : %08X !", event.mask);
exit(EXIT_FAILURE);
}
}
return EXIT_FAILURE;
}
|
3. Exemple en Perl
Ci-dessous un exemple commenté, écrit en Perl, tendant à prouver qu'il
est possible de concevoir des programmes ayant la même fonctionnalité
qu'en C. Vous pourrez remarquer que l'on retrouve le même cheminement
dans les appels de fonction à la différence prêt que Perl utilise
des objets de trois types correspondant aux instances de
inotify, aux auditeurs que l'on place sur les fichiers et enfin aux
évènements.
L'exemple s'articule autour de Named/Bind, démon assurant le service
DNS qui sera redémarré si l'un de ses fichiers de configuration est
modifié. On suppose que ces fichiers sont regroupés dans un
répertoire à part pour plus de praticité mais aussi pour apporter
une nouveauté par rapport aux exemples précédents.
use strict;
use Linux::Inotify2;
use Proc::Daemon;
use Unix::Syslog qw(:subs :macros);
use constant PID_FILE => '/var/run/named/named.pid';
use constant NAMED_CONFDIR => '/etc/bind';
Proc::Daemon::Init;
openlog $0 =~ s|.*/||, LOG_PID, LOG_DAEMON;
my $inotify = new Linux::Inotify2 or &error("Impossible de créer une nouvelle instance inotify : %m");
my $watch = $inotify->watch(NAMED_CONFDIR, IN_CREATE|IN_MODIFY|IN_MOVE|IN_DELETE)
or &error("La création de l'observateur a échoué : %m");
sub fin {
$watch->cancel;
closelog;
exit 0;
}
sub error {
syslog LOG_ERR, @_[0];
&fin;
}
sub get_pid {
open(IN, "< " . PID_FILE) or &error("Impossible d'ouvrir " . PID_FILE . " en lecture : %m");
(my $pid = <IN>) =~ s/([0-9]+)/$1/;
close IN;
return $pid;
}
$SIG{'TERM'} = \&fin;
while (1) {
my @events = $inotify->read;
unless (@events > 0) {
syslog LOG_WARNING, "erreur sur read: %m";
last;
}
foreach (@events) {
my $pid = &get_pid;
kill(1, $pid) or &error("Impossible d'envoyer le signal SIGHUP à $pid : %m")
if $_->fullname =~ /\.(conf|zone)$/;
}
}
|
4. Conclusion
Inotify se trouve être une solution simple et pratique à intégrer au sein
de vos développements pour l'ensemble des évènements qu'il est capable
de gérer. Ne perdons toutefois pas de vue que cet outil pourrait être
retourné contre vous, c'est pourquoi il est fortement déconseillé
d'activer cette fonctionnalité sur un serveur en production.
Ajoutons que les exemples présentés plus haut n'ont qu'un but éducatif,
il manque notamment une gestion du temps pour éviter les appels
intempestifs à nos sous-routines de redémarrage dus, entre autre, aux
recours multiples d'appels systèmes lors d'une action (come l'édition par
exemple).
Note : tous les tests, codes sources et autres ont été réalisés sur une
Gentoo 2006.1 à jour (noyau 2.6.19, glibc 2.5).
Quelques pages de manuel :
Utilisation de inotify dans d'autres langages :


Copyright © 2007 julp. Aucune reproduction, même partielle, ne peut être faite
de ce site et de l'ensemble de son contenu : textes, documents, images, etc
sans l'autorisation expresse de l'auteur.
Sinon vous encourez selon la loi jusqu'à 3 ans de prison et jusqu'à 300 000 E
de dommages et intérêts.
Cette page est déposée à la
SACD.