di Alessandro Rubini
Riprodotto con il permesso di Linux Magazine, Edizioni Master.
Come funziona il sistema di notifica degli eventi sui file.
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
.
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.
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 */
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.
_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.
#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;
}
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
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++)
/* ... */
}
}
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).
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);
}
}
}
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
.
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.
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.
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/