Il meccanismo dnotify

di Alessandro Rubini

Riprodotto con il permesso di Linux Magazine, Edizioni Master.

Quando un'applicazione risponde agli eventi, normalmente riceve notifica di quello che avviene attraverso un descrittore di file. Questo succede per esempio per l'arrivo di pacchetti di rete o di dati su una pipe o una FIFO, come pure per gli eventi di interazione utente che comunicati da un dispositivo (mouse, tastiera) o da un altro applicativo (come X11) tramite un socket di rete. Le chiamate di sistema select e poll, unitamente alla notifica asincrona offerta da fcntl(F_SETOWN), rappresentano un buon meccanismo per la notifica di eventi di questo tipo.

I meccanismi classici di Unix, però, non consentono alle applicazioni di ricevere informazioni su eventi di altro tipo, come l'accrescersi di un file regolare, i cambiamenti nel nome di un file o dei suoi pemessi di accesso, la creazione o la rimozione di file nel sistema.

Questa lacuna viene colmata da dnotify, un meccanismo di notifica per gli eventi relativi alle directory (da cui il nome), che affianca le notifiche di trasferimento dati offerte da poll/select/F_SETOWN.

Il codice presentato è stato verificato con Linux-2.6.10, anche se dnotify è già presente in Linux-2.4.0, senza modifiche nell'interfaccia verso lo spazio utente. I programmi di esempio relativi a questo articolo sono diponibili come http://www.linux.it/kerneldocs/dnotify/src.tar.gz.

Principi di dnotify

Il problema affrontato da dnotify è quello della notifica delle modifiche nella struttura del filesystem, ad uso di programmi come i file manager, che dovrebbero aggiornare la presentazione grafica non appena la situazione del sistema viene modificata dall'esterno, senza con questo appesantire il sistema con continue richieste di stato.

Il metodo classico per affrontare questo problema è il più brutale: risvegliare periodicamente il processo e richiedere al sistema una nuova istanza di tutte le informazioni che si tengono sott controllo. In questo modo, il compito viene svolto nel peggior modo possibile: non si garantisce né la velocità di risposta né la leggerezza computazionale; ogni miglioramento in uno dei due parametri porta invariabilmente ad un peggioramento dell'altro.

L'idea base di dnotify è quella di implementare una notifica attiva associata alle directory: ogni evento relativo lle caratteristiche di un file viene riportato ai processi che hanno dichiarato il proprio interesse a quel tipo di evento, relativamente alla directory che contiene il file. Tali eventi includono la creazione e la rimozione di file, la lettura, la modifica dei contenuti o dei permessi di accesso.

La dichiarazione degli eventi cui si è interessati avviene tramite un comando di fcntl (F_NOTIFY), eseguito sul descrittore di file relativo alla directory che si vuole osservare.

Gli eventi cui si è interessati sono specificati tramite una maschera di bit, passata come terzo argomento di fcntl. Il Riquadro 1 elenca tali bit, estratti da <linux/fcntl.h>; di questi, DN_MULTISHOT indica che l'applicazione è interessata anche a più di una notifica, mentre in sua assenza il kernel riporterà all'applicazione un solo evento, e fcntl andrà reinvocata prima di ricevere ulteriori notifiche.

Riquadro 1 - I bit di DN_NOTIFY

    /*
     * Types of directory notifications that may be requested.
     */
    #define DN_ACCESS       0x00000001      /* File accessed */
    #define DN_MODIFY       0x00000002      /* File modified */
    #define DN_CREATE       0x00000004      /* File created */
    #define DN_DELETE       0x00000008      /* File removed */
    #define DN_RENAME       0x00000010      /* File renamed */
    #define DN_ATTRIB       0x00000020      /* File changed attibutes */
    #define DN_MULTISHOT    0x80000000      /* Don't remove notifier */

Uso nelle applicazioni

Un programma applicativo che voglia usare dnotify deve definire la macro _GNU_SOURCE prima di includere <fcntl.h> (si veda il riquadro 2). È anche possibile includere sia <fcntl.h> sia <linux/fcntl.h> (senza definire _GNU_SOURCE), ma in questo modo il compilatore segnalerà uno o due warning, a seconda della versione di glibc in uso.

La notifica di un evento sulla directory avviene tramite l'invio di un segnale. In mancanza di indicazioni diverse da parte dell'applicazione verrà inviato SIGIO (che, se non gestito, provoca l'uscita del programma). Qualora l'applicazione volesse informazioni più precise su cosa è successo nella directory osservata, dovrà usare readdir(3) e stat(2), per confrontare la nuova situazione con la sua conoscenza pregressa.

Il programma minidn, riportato nel riquadro 3, è un'utilizzo minimale di dnotify. Il programma va invocato redireigneto stdin su una directory; il programma a quel punto attende la notifica di creazione di un file. Per esempio, invocando minidn come segue e poi creando un file in /tmp si ottiene:

    ostro$ ./minidn < /tmp
    I/O possible

Naturalmente, un'applicazione più lunga di 4 righe installerà un gestore di segnale per poter fare uso della notifica. Inoltre, invocando il comando F_SETSIG di fcntl, un programma può richiedere l'invio di un segnale diverso da SIGIO. In questo caso il kernel fornisce informazioni aggiuntive al gestore di segnale, tramite la struttura struct siginfo; in particolare, il campo si_code viene posto a POLL_MSG e si_fd indica il descrittore di file che ha scatenato l'invio del segnale.

Riquadro 2 - _GNU_SOURCE

Gli header della libreria C del progetto GNU (glibc) permettono di attivare o disattivare alcune funzionalità in base ad alcune macro definite in compilazione. È così possibile preferire la compatibilità con BSD ("#define _BSD_SOURCE") piuttosto che con SystemV ("#define _SVID_SOURCE"); è possibile rendere disponibili alcune funzionalità caratteristiche dei sistemi liberi, definendo il simbolo _GNU_SOURCE. Oltre alle macro di dnotify, un esempio di funzione non disponibile senza _GNU_SOURCE è strsignal, in <string.h>, che converte un codice di segnale come SIGIO in una stringa ("SIGIO"), analogamente a quanto fa strerror per i codici di errore.

L'implementazione di _GNU_SOURCE, unitamente alla documentazione di questo simbolo e degli altri con lo stesso ruolo, sta in <features.h>, che viene incluso da ogni header di glibc.

Riquadro 3 - minidn.c
    #define _GNU_SOURCE /* activate extensions */
    #include <unistd.h>
    #include <fcntl.h>
    
    int main(int argc, char **argv)
    {
	if (argc != 1) exit(1);
	if (fcntl(0 /* stdin */, F_NOTIFY, DN_CREATE)) exit(2);
	pause();
        return 0;
    }

Un esempio di osservazione di più directory.

Il programma watch2.c, parte dei sorgenti associati a questo articolo, è un esempio di uso di F_SETSIG e struct siginfo per ricevere notifica di eventi su due directory. Il numero 2 è stato scelto per semplicità ed è facile modificare il programma per lavorare su un numero a piacere di directory.

Il programma, su ogni directory osservata, apre un descrittore di file per ognuno degli eventi di dnotify, in modo da riportare su stdout gli eventi ricevuti, riconoscibili dal campo si_fd nella struttura siginfo.

Per esempio:

    ostro$ ./watch2 /tmp $HOME
    in /home/rubini: file accessed
    in /tmp: file created
    in /tmp: file modified
    in /home/rubini: file modified
    in /home/rubini: file accessed

Il riquadro 4 include una versione ridotta, per ragioni di spazio, del sorgente di watch2.

Riquadro 4 - watch2.c
    struct events {
	int flag;  char *name;
    } events[] = {
	{ DN_ACCESS, "accessed"},
	/* ... */
    };
    
    struct direvents {
	int fd[NEVENTS]; int count[NEVENTS];
    } direvents[NDIRS];
    
    int sigcount;
    
    void handler(int signo, siginfo_t *info, void *v)
    {
	/* ... */
	for (i = 0; i < NDIRS; i++) {
	    for (j = 0; j < NEVENTS; j++) {
		if (info->si_fd == direvents[i].fd[j])
		    direvents[i].count[j]++;
	    }
	}
	sigcount++;
    }
    
    int main(int argc, char **argv)
    {
	/* ... */
	for (i = 0; i < NDIRS; i++) {
	    for (j = 0; j < NEVENTS; j++) {
		int fd = open(dirs[i], O_RDONLY);
		/* ... */
		fcntl(fd, F_SETSIG, SIGUSR1);
		fcntl(fd, F_NOTIFY, events[j].flag | DN_MULTISHOT);
	    }
	}
    
	while (1) {
	    if (!sigcount) select(0, NULL, NULL, NULL, NULL);
	    sigcount = 0;
	    for (i = 0; i < NDIRS; i++)
		for (j = 0; j < NEVENTS; j++)
		    /* ... */
	}
    }

Un esempio di "tail -f"

Una tipica applicazione in cui si nota la mancanza di notifica è tail -f, usata per osservare i messaggi di log. Il programma tail distribuito nelle coreutils del progetto GNU una una procedura di poll sul file osservato: ogni secondo viene invocata la chiamata di sistema stat per verificare se la dimensione del file è cambiata.

Il programma follow, anch'esso nell'archivio dei sorgenti, effettua lo stesso lavoro appoggiandosi su dnotify. A differenza di tail, non è in grado di stampare le ultime dieci righe del file osservato, limitandosi a visualizzare gli accrescimenti successivi, ma senza la granularità temporale e le latenze del programma tail.

Il riquadro 5 mostra le righe più significativa del programma follow, depurate della gestione degli errori (presente invece nel sorgente completo).

Riquadro 5 - follow.c
    void handler(int signo, siginfo_t *info, void *v)
    { /* nothing to do */ }
    
    int main(int argc, char **argv)
    {
	/* ... */
	dd = open(dirname, O_RDONLY);
	sigaction(SIGUSR1, &sa, NULL);
	fcntl(dd, F_SETSIG, SIGUSR1);
	fcntl(dd, F_NOTIFY, DN_MODIFY | DN_MULTISHOT);
    
	/* seek to end of file and loop waiting for EINTR, then read */
	lseek(fd, 0, SEEK_END);
	while (1) {
	    select(0, NULL, NULL, NULL, NULL); /* forever */
	    if (errno != EINTR) abort(); /* can't happen */
	    while ((i = read(fd, buf, BSIZE)) > 0) {
		write(fileno(stdout), buf, i);
	    }
	}
    }

L'implementazione del kernel

Nel kernel, tutto il codice relativo a dnotify si trova in fs/dnotify.c, un file di sole 180 righe. Qui si trovano tre funzioni principali: fcntl_dnotify, che implementa lo specifico comando di fcntl, e la coppia __inode_dir_notify/dnotify_parent (la prima delle quali viene normalmente chiamata attraverso inode_dir_notify, funzione inline che si trova in <linux/fcntl.h>).

Queste ultime due funzioni vengono chiamate dalle implementazioni delle chiamate di sistema che si trovano negli altri file della directory fs. Per esempio, fs/read_write.c notifica le scritture e le letture dai file invocando dnotify_parent; la funzione viene chiamata dopo ogni chiamata di sistema read o write eseguita con successo.

L'invio del segnale è delegato alla funzione send_sigio, in fs/fcntl.c. Qui viene compilata la struttura siginfo, passata poi a send_sig_info (kernel/signal.c) che si occupa dell'effettiva consegna del segnale al processo.

Come strutture dati, il meccanismo dnotify risulta abbastanza leggero. Quando tramite fcntl viene attivata o disattivata una richiesta di notifica su una cartella, il sistema accoda una struttura dnotify_struct alla lista inode->i_dnotify e aggiorna la maschera di bit inode->i_dnotify_mask; entrambi i campi fanno parte dell'inode relativo alla directory osservata. Ogno struttura nella lista registra la maschera di eventi, la struct file, il descrittore di file nel processo chiamante e il proprietario della richiesta di notifica.

Nel momento in cui avviene un evento notificabile, la lista i_dnotify viene scandita solo se l'evento è attivo nella i_dnotify_mask della directory corrispondente, per evitare di scandire inutilmente la lista delle notifiche in risposta ad eventi cui nessuno ha dichiarato interesse.

Per esempio, nel caso di watch2, la maschera dei bit associata all'inode indicherà che tutti gli eventi sono sotto osservazione e la scansione della lista troverà una corrispondenza. Nel caso di follow, assumendo che non ci siano altri processi che osservano la stessa directory, solo gli eventi DN_MODIFY porteranno alla scansione della lista, composta in questo caso da un'unica dnotify_struct.

Il demone "fam"

Una proposta di soluzione al problema delle notifiche relative alle operazioni sui file è il demone fam (o famd), il "file alteration monitor", un pacchetto sviluppato alla Silicon Graphics nel 1989 e ora rilasciato con licenza GPL/LGPL. Fam implementa un servizio di rete e una API, accessibile tramite una libreria inclusa nel pacchetto, per inviare richieste al demone. Il vantaggio del servizio fam rispetto all'approccio "fai da te" è l'universalità dell'interfaccia, che resta indipendente dal meccanismo utilizzato dal kernel per inviare le notifiche.

Nella versione distribuita da SGI, il demone fam può ricevere le notifiche tramite imon, il meccanismo di notifica usato in IRIX. In assenza di tale meccanismo, fam lavora in poll, come tail -f. È disponibile, comunque, una patch per far sì che fam si appoggi su dnotify, come pure un'altra per aggiungere il meccanismo imon al kernel Linux. Mentre la patch al kernel non è stata integrata e probabilmente non lo sarà nemmeno in futuro, le distribuzioni GNU/Linux che distribuiscono fam includono al suo interno il supporto per dnotify.

Nonostante fam sia teoricamente una buona centralizzazione del problema delle notifiche, utile anche per poter migliorare l'efficienza dei meccanismi sottostanti senza ricompilare le applicazioni, in pratica si tratta di un servizio usato abbastanza raramente. Di conseguenza, le applicazioni in ambiente GNU/Linux normalmente non sono preconfigurate per usufruire del servizio, non potendo fare affidamento sulla sua presenza nel sistema ospite.

I problemi di dnotify

Il meccanismo dnotify, pur essendo una funzionalità consolidata del kernel Linux che risolve in maniera abbastanza elegante il problema delle notifiche, soffre comunque di alcuni problemi non trascurabili, che hanno portato alla scrittura di un'implementazione alternativa, chiamata inotify (da inode).

I punti "fastidiosi" di dnotify sono in qualche modo tutti legati alla scelta di appoggiarsi sulle directory, le stesse che sono usate durante il normale accesso al filesystem.

Innanzitutto, poiché la directory sotto osservazione deve essere aperta, il filesystem che la ospita non può essere smontato, perché "in uso". Questo impedisce in pratica l'uso di dnotify su filesystem rimuovibili come dischetti, CD, chiavette USB.

Ogni directory osservata va poi aperta; l'osservazione di un intero albero di directory richiede quindi l'apertura di un elevato numero di file, in certi casi inaccettabile.

La notifica tramite invio di segnale dà luogo a corse critiche rispetto ad altri tipi di notifica, quali poll e select, per cui occorre scrivere codice abbastanza complesso per evitare potenziali malfunzionamenti. Per esempio, il codice di watch2 nel riquadro 4 ha una breve finestra temporale in cui un evento può andare perduto, tra il controllo "if (!sigcount)" e la chiamata a select che appare sulla stessa riga.

Il segnale, anche quando siginfo specifichi il descrittore di file che ha scatenato l'evento, non fornisce tutta l'informazione che è invece a disposizione del kernel nel momento esso in cui notifica l'evento (per esempio, l'informazione su quale sia il file che ha scatenato evento); l'applicativo deve riesaminare l'intera directory per sapere cosa è successo. Alcuni eventi (come la lettura o la scrittura da un dispositivo) non risultano neppure ricnoscibili a posteriori, in quanto non lasciano traccia nel filesystem.

Tutti questi problemi vengono risolti da inotify che al momento (Gennaio 2005) non è ancora parte del kernel ufficiale, anche se probabilmente lo sarà a breve. Il meccanismo proposto è necessariamente incompatibile con dnotify nella sua interfaccia verso le applicazioni, in quanto le notifiche vengono consegnate tramite pacchetti informativi su un file speciale, senza che i programmi debbano aprire la directory che stanno osservando. Le due infrastrutture di notifica possono comunque essere attive contemporaneamente nello stesso kernel.

Riquadro 6 - Approfondimenti

Il meccanismo dnotify è descritto brevemente in Documentation/dnotify.txt.

La pagina principale di fam è http://oss.sgi.com/projects/fam/.

Il codice di inotify (patch per il kernel e strumenti in spazio utente) si può trovare presso ftp://ftp.it.kernel.org/pub/linux/kernel/people/rml/inotify/