Accueil
Rechercher:
sur developpez.com sur les forums
Forums | Tutoriels | F.A.Q's | Participez | Hébergement | Contacts
Club Emploi Blogs   TV   Dév. Web PHP XML Python Autres 2D-3D-Jeux Sécurité Windows Linux PC Mac
Accueil Conception Java DotNET Visual Basic  C  C++ Delphi MS-Office SQL & SGBD Oracle  4D  Business Intelligence
FORUMS C FAQs C TUTORIELS C LIVRES C COMPILATEURS C SOURCES GTK+

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 :

Pile des appels nécessaires pour recourir à inotify
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;        /* Identifiant de l'auditeur */
        uint32_t mask;      /* Masque de l'évènement */
        uint32_t cookie;    /* Identifiant permettant de lier des évènements qui sont liés - n'est utilisé que par l'appel système rename */
        uint32_t len;       /* Taille du champ name*/
        char     name[];    /* Nom du fichier concerné (optionnel et terminé par un caractère nul) */
    };
    
    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>

/**
 * Compilation : gcc inotify-exemple.c -o inotify-exemple -O2 -Wall -W -Werror -ansi -pedantic
 **/

int fd, wd;

void sigint_handler(int signum)
{
    (void) signum;

    /* Nous ne surveillons plus ce fichier/répertoire */
    if (inotify_rm_watch(fd, wd)) {
        perror("inotify_rm_watch");
        exit(EXIT_FAILURE);
    }

    /* Fermeture du descripteur de fichier obtenu lors de l'initialisation d'inotify */
    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;
    }

    /* Initialisation d'inotify */
    fd = inotify_init();
    if (fd < 0) {
        perror("inotify_init");
        return EXIT_FAILURE;
    }

    /* Surveillance du fichier/répertoire passé en paramètre
     * On accepte tous les évènements possibles */
    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);

    /* Capture de SIGINT (Ctrl + C) */
    signal(SIGINT, sigint_handler);

    while (1) {
        FD_ZERO(&fds);
        FD_SET(fd, &fds);
        if (select(fd + 1, &fds, NULL, NULL, 0) <= 0) {
            continue;
        }

        /* Obtention des informations sur l'évènement qui vient de se produire */
        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>

/**
 * Compilation (adapter les chemins relatifs à Apache à votre propre configuration) :
 *     gcc inotify-apache.c -o inotify-apache -O2 -Wall -W -Werror -pedantic -DHTTPD_CONF='"/etc/httpd.conf"' -DPID_FILE='"/var/run/httpd.pid"'
 **/

#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) {
        /* Nous ne surveillons plus ce fichier/répertoire */
        if (inotify_rm_watch(fd, wd)) { /* Génère parfois une erreur ici, bug ? */
            my_perror("inotify_rm_watch");
        }
    }
    if (fd > 0) {
        /* Fermeture du descripteur de fichier obtenu lors de l'initialisation d'inotify */
        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;

    /* Initialisation d'inotify */
    fd = inotify_init();
    if (fd < 0) {
        my_perror("inotify_init");
        return EXIT_FAILURE;
    }

    if (atexit(fin_programme)) {
        syslog(LOG_ERR, "atexit");
        return EXIT_FAILURE;
    }

    /* Surveillance du fichier de configuration d'Apache
     * Seuls les évènements liés à l'écriture nous intéressent */
    wd = inotify_add_watch(fd, HTTPD_CONF, IN_MODIFY);
    if (wd < 0) {
        my_perror("inotify_add_watch");
        return EXIT_FAILURE;
    }

    /* Capture de SIGTERM */
    signal(SIGTERM, sigterm_handler);

    while (1) {
        FD_ZERO(&fds);
        FD_SET(fd, &fds);
        if (select(fd + 1, &fds, NULL, NULL, 0) <= 0) {
            continue;
        }

        /* Obtention des informations sur l'évènement qui vient de se produire */
        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;
                }
            }
            /* Le "redémarrage" d'apache se fait ici (un signal HUP suffira) */
            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.
#!/usr/bin/perl

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';

# On rend le programme indépendant et l'exécute en tâche de fond
Proc::Daemon::Init;
# Tout message sera logué via syslog
openlog $0 =~ s|.*/||, LOG_PID, LOG_DAEMON;

# Création d'une instance inotify
my $inotify = new Linux::Inotify2 or &error("Impossible de créer une nouvelle instance inotify : %m");
# Mise sous surveillance du répertoire
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");

# Sera appelé à la fin du programme
sub fin {
    $watch->cancel;
    closelog;
    exit 0;
}

# Logue les erreurs graves et met fin au programme
sub error {
    syslog LOG_ERR, @_[0];
    &fin;
}

# Récupèration du PID inscrit par bind/named
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;
}

# Les signaux pour intercepter la fin du programme et la rendre effective proprement
$SIG{'TERM'} = \&fin;

# Boucle infinie ayant pour but de récupérer les évènements
while (1) {
    my @events = $inotify->read;
    unless (@events > 0) {
        syslog LOG_WARNING, "erreur sur read: %m";
        last;
    }
    foreach (@events) {
        my $pid = &get_pid;
        #syslog LOG_DEBUG, "Fichier : %s %s [%08X]\n", ($_->fullname, $_->name, $_->mask);
        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 :


Valid XHTML 1.1!Valid CSS!

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.

Responsable bénévole de la rubrique C : Arnaud Feltz (buchs) - Contacter par EMail :
Vos questions techniques : forum d'entraide C - Publiez vos articles, tutoriels et cours
et rejoignez-nous dans l'équipe de rédaction du club d'entraide des développeurs francophones
Nous contacter - Copyright © 2000-2008 www.developpez.com - Legal informations.