
Fabrizio Cardarello
Fabrizio Cardarello studia Ingegneria Informatica
e lavora a Roma come sistemista Solaris e Linux.
Ama il retro-computing e quando le cose si fanno ripetitive
cerca di perdere molto piu' tempo aiutandosi con il Perl.
|
Avete mai pensato di voler rendere indipendente il vostro vecchio
script per il collezionamento dati che gira ogni 10 minuti tramite
il crontab?
Oppure avete mai voluto aggiungere delle funzionalità in base a
determinati eventi o semplicemente voglia di scrivere un demone in Perl? Allora questo è proprio l'articolo che fa al caso vostro.
Prima di tutto mi preme precisare che questo articolo non deve essere
interpretato come una esauriente guida, ma vuole solamente riportare la
mia esperienza diretta riguardo ad alcuni dei problemi che giornalmente
ho affrontato e che grazie al Perl ho risolto. Un'altra avvertenza, infine:
è sottinteso che si sta parlando di un sistema tipo Unix!
Demoni
I Demoni sono processi in grado di vivere a lungo: sono spesso creati
partire durante la fase di boot-strap e terminati durante il processo di shutdown.
Essi non hanno un terminale di controllo in quanto la loro
caratteristica è di girare in background. I sistemi di tipo UNIX hanno
numerosi demoni che svolgono numerose attività giornaliere. Un classico
esempio è il crond, che permette la schedulazione a tempo dei vari job del sistema.
Prima di procedere diamo uno sguardo alle principali caratteristiche dei demoni tramite il comando ps -axj. Il flag -a rappresenta tutti i processi del sistema, -x mostra i processi
che non hanno un terminale di controllo ed infine il -j ci riporta le
relative informazioni: session ID, process group ID, terminale di
controllo ed in fine il terminal process group ID. Di seguito riporto un breve output di una linux-box.
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
0 1 0 0 ? -1 S 0 0:01 init [2]
1 2 0 0 ? -1 S 0 0:00 [migration/0]
1 3 0 0 ? -1 SN 0 0:00 [ksoftirqd/0]
1 4 0 0 ? -1 S 0 0:00 [watchdog/0]
1 4346 4346 4346 ? -1 Ss 0 0:00 /usr/sbin/cron
Tutti i processi con un PPID uguale a 0 riguardano i processi kernel, e non fanno parte della normale procedura di boot.
I processi kernel sono speciali in quanto, rimarranno in vita per tutto il tempo del sistema, gireranno con i privilegi del super utente e non
avranno mai nessun terminale di controllo.
Il processo con PID 1 è il demone init il quale è responsabile di far partire i servizi del sistema nei vari run levels. Il processo con il PID 4346, è il cron daemon di cui vi parlavo prima.
Caratteristiche comuni
Ci sono alcune regole di "buon comportamento" per la scrittura di un
demone che ci permettono di delineare delle linee guida da seguire. Nelle righe successive daremo uno sguardo a queste regole e come implementarle in Perl.
Se non volete implementare da zero il demone, è possibile utilizzare una valida alternativa. Su CPAN troverete una serie di moduli, Proc, che permettono una semplificazione della gestione dei processi
e in particolare Proc::Daemon per la creazione di un demone.
Sessioni
Una sessione è un insieme di uno o piu' gruppi di processi, per
chiarirci le idee possiamo dare uno sguardo al seguente output.
PPID PID PGID SID TTY COMMAND
1 4067 4067 4067 ? /usr/sbin/hald 4067 4068 4067 4067 ? \_ hald-runner
4068 4073 4067 4067 ? \_ /usr/lib/hal/hald-addon-acpi
4068 4127 4067 4067 ? \_ /usr/lib/hal/hald-addon-keyboard
4068 4137 4067 4067 ? \_ /usr/lib/hal/hald-addon-storage
4068 4138 4067 4067 ? \_ /usr/lib/hal/hald-addon-storage
Come vediamo il demone hald è leader della sessione 4067 e tutti i
sui gruppi di processi sottostanti hanno il suo stesso SID (session id).
Tramite la chiamata di sistema setsid
è possibile stabilire una nuova sessione, ovvero permettere al
processo che invoca setsid
di staccarsi dall'attuale sessione e creare una nuova sessione leader.
La funzione setsid
restituisce un errore se il processo che la sta invocando è già un leader di una
sessione. Per essere sicuri che questo non avvenga si genera un figlio
tramite la chiamata di sistema fork e si fa terminare il padre,
in questo modo il figlio non sarà mai il leader del gruppo in quanto il PGID
del padre è ereditato dal figlio che avrà un nuovo PID. Pertanto sarà impossibile per il figlio avere un PID uguale
al process group ID. Inoltre, se il demone parte da shell, far terminare il padre permette alla shell di credere che tutto sia andato a buon fine.
Detto questo possiamo passare alla parte pratica, prima di tutto nel
nostro script dobbiamo caricare il modulo POSIX in modo da avere a
disposizione la chiamata di sistema setsid:
use POSIX qw(setsid); # Nell'intestazione del programma
defined(my $pid = fork)
or die "Non posso eseguire la fork(): $!";
exit if $pid; ## termino il padre
setsid
or die "Non posso far partire la nuova sessione: $!";
POSIX è l'acronimo di Portable Operating System Interface, che è
uno standard per i sistemi operativi di tipo UNIX. Si occupa di delineare
le chiamate di sistema che un sistema operativo UNIX-like deve avere.
Directory di lavoro
La seconda cosa da fare è cambiare in root la
directory di lavoro del demone, tramite la funzione chdir. Infatti
se il demone girerà su un file system differente da root, quel
filesystem non potrà mai essere smontato e risulterà occupato dal
demone stesso.
chdir '/'
or die "Non posso eseguire il chdir /: $!";
Maschera di creazione dei file
La funzione umask che ci permetterà di impostare la maschera di creazione dei file. In questo modo, saremo sicuri che i file generati dal nostro
demone avranno i giusti permessi, che non si baseranno sull'umask
che l'utente ha normalmente quando utilizza la shell,
ma sui valori passati alla funzione Perl.
L'istruzione
umask 0;
permette la creazione dei file con i permessi di lettura e scrittura per l'utente, il gruppo e tutti gli altri. Il valore è espresso in ottale e si specificano i permessi che
si vogliono negare, di seguito riporto una tabella riassuntiva
per i soli permessi dell'utente.
| Mask |
permessi |
| 0400 |
lettura
utente |
| 0200 |
scrittura
utente |
| 0100 |
esecuzione
utente |
È anche possibile assegnare una maschera per il gruppo e per tutti gli altri utenti del sistema seguendo la stessa logica. Ecco un esempio:
umask 0077;
In questo caso, abbiamo fatto in modo che solo l'utente che sta eseguendo
il demone possa effettuare operazioni sul file, mentre stiamo inibendo
la possibilità ad altri utenti del gruppo o del sistema di fare qualsiasi
operazione sui file creati dal demone.
Rilascio del terminale
Caratteristica del demone è quella di girare in background ed
essere svincolato dal terminale. Per evitare che stampi a
video il suo output e bloccare ogni tipo di interferenza, i canali
standard di comunicazione (STDIN,STDOUT,STDERR) vengono chiusi o al più
rediretti verso un file come nel caso dello STDERR. Ogni file handler ereditato dal padre deve essere chiuso in modo da evitare che il demone lo tenga occupato
inutilmente.
close STDIN;
open STDERR,">>/tmp/daemon.err";
Per quanto riguarda STDOUT, possiamo chiuderlo subito
dopo aver stampato a video il PID del processo.
print "Stampa il pid: $$\n"; close STDOUT;
Attività indefinita
A questo punto il nostro demone è realizzato, quello che ci resta
da ultimare è il corpo del nostro script. Un demone è un processo la
cui attività ha una durata indefinita, per cui avremo bisogno di un ciclo
(potenzialmente) infinito, all'interno del quale inseriamo delle operazioni
di sleep per ridurre il carico sulla CPU.
while(1){
sleep 3;
...
}
SEGNALI
Ora che abbiamo costruito lo scheletro del nostro demone, dobbiamo lasciare
aperta qualche porta per poterci comunicare, soprattutto ricordando che
abbiamo rilasciato qualunque connessione con il terminale. A questo scopo ci
aiutano i segnali.
Cosa sono i segnali?
I segnali possono essere definiti come degli interrupts software,
ovvero messaggi che i programmi possono raccogliere e interpretare
a run-time, modificando il proprio comportamento sulla base dell'evento associato al messaggio.
Un esempio classico è la rilettura di un file di configurazione,
spesso può essere utile mandare un segnale ad un demone il cui evento
associato preveda la rilettura del file di configurazione.
Ogni segnale ha un nome che inizia con 3 caratteri rappresentativi SIG e ad ogni
segnale è anche associato un numero intero. Per vedere una lista
completa dei segnali che si possono gestire nel sistema
si può lanciare il comando
shell$ kill -l # si tratta della lettera L minuscola
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE
9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2
13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT
17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU
25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH
29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN
35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4
39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12
47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14
51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10
55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6
59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
Molti segnali rappresentano eventi predefiniti un classico esempio è
il SIGINT
che viene generato dalla shell quanto si preme a terminale Ctrl-c o il SIGSEGV
che rappresenta un'eccezione Hardware, esempio eseguire una divisione per 0 o un referenziamento errato della memoria. Quando un evento di questo tipo avviene il Kernel ne prende atto e genera il segnale nei confronti del processo. Altri segnali, invece, sono liberi e possono essere usati per diversi scopi come
il SIGUSR1
e SIGUSR2.
Il comportamento dei processi che non gestiscono i segnali
è solitamente quello di terminare il processo che li riceve e per alcuni, SIGSEGV ad esempio, generare un file core associato. Solamente due segnali non possono essere ignorati: SIGKILL
e SIGSTOP. Alla ricezione di un segnale, un processo può
essere impostato per comportarsi in tre modi differenti:
- Ignorare il Segnale, esclusi
SIGKILL
e SIGSTOP.
- Ricevere il segnale ed eseguire la funzione associata.
- Applicare la funzione di default, di solito il processo viene terminato.
I segnali in Perl
Il Perl permette la gestione dei segnali in un modo molto semplice,
bisogna infatti inserire nel corpo del nostro demone i vari segnali che
si vogliono usare e associarli alle funzioni da richiamare.
$SIG{'USR1'}= \&usr1;
$SIG{'USR2'}= \&usr2;
usr1 e usr2 saranno le nostre due funzioni
associate ai segnali SIGUSR1 e SIGUSR2.
sub usr1 {
if (my $pid=fork) {
open my $file,">>/tmp/test.txt";
or die "Non posso aprire il file: $!\n"
flock $file,LOCK_EX; ## lock
sleep 6;
print $file "pid padre: $$\n";
flock $file,LOCK_UN; ## unlock
close $file;
waitpid($pid,0);
} else {
sleep 3;
open my $file,">>/tmp/test.txt";
or die "Non posso aprire il file: $!\n";
flock $file,LOCK_EX;
print $file "pid figlio: $$\n";
flock $file,LOCK_UN;
close $file;
exit 0;
}
}
sub usr2 {
warn "SIGUSR2 ricevuto";
exit 0;
}
usr2 stamperà sullo STDERR un messaggio di ricezione del segnale.
Regolare l'accesso ai file
Avrete sicuramente notato che nella funzione usr1 abbiamo
utilizzato la funzione flock - vediamo a cosa può esserci
utile.
Se avete necessità di scrivere su file tra i vari
processi del demone
o tra i vari demoni, è sempre utile garantire un minimo di
organizzazione di scrittura. La funzione flock ci
permette di bloccare il file che stiamo scrivendo fino a che non
decidiamo di rilasciarlo. Il prototipo di utilizzo è il seguente:
flock FILEHANDLE, OPERATION
I valori che si possono passare a OPERATION
sono LOCK_SH,
LOCK_EX o
LOCK_UN che
rappresentano i valori 1,2, 8 che possono essere combinati a loro volta
con LOCK_BN il cui valore è 4.
I diversi hanno i seguenti significati:
LOCK_SH è un tipo di lock condiviso;
LOCK_EX richiede un lock exclusivo;
LOCK_UN rilascia definitivamente un lock.
LOCK_NB viene utilizzato in or con i valori
precedenti per creare un lock non
bloccante, ovvero flock uscirà immediatamente invece di aspettare il
lock.
Le costanti sopra citate fanno parte del modulo
Fcntl,
per ulteriori chiarimenti potete consultare la pagina di manuale.
Nel nostro demone ho utilizzato il segnale SIGUSR1 per creare
un figlio il cui compito è quello di scrivere su un file "test.txt"
ovviamente la scrittura è organizzata tramite la chiamata flock. Attraverso l'uso degli sleep si fa in modo che il padre blocchi il file
per primo, in modo che il figlio aspetti finchè il padre non lo
rilascerà.
sub usr1 {
if (my $pid=fork) {
open my $file,">>/tmp/test.txt";
or die "Non posso aprire il file: $!\n"
flock $file,LOCK_EX; ## lock
sleep 6;
print $file "pid padre: $$\n";
flock $file,LOCK_UN; ## unlock
close $file;
waitpid($pid,0);
} else {
sleep 3;
open my $file,">>/tmp/test.txt";
or die "Non posso aprire il file: $!\n";
flock $file,LOCK_EX;
print $file "pid figlio: $$\n";
flock $file,LOCK_UN;
close $file;
exit 0;
}
}
La funzione waitpid mi permetterà di aspettare la
corretta
terminazione del processo figlio, in modo da non generare processi
zombie.
Mettiamo tutto insieme
Questo che segue è lo script completo. Oltre a seguire i consigli
che vi ho dato nell'articolo, vi suggerisco anche di scrivere sempre
la documentazione POD per i vostri script... vi sarà utile quando cercherete
di capire cosa può fare questo demone dopo qualche mese che non vedete
più il codice!
#!/usr/bin/perl
## Author Fabrizio Cardarello
## email hunternet@tin.it
## location: Rome - Italy
##########################
#
use POSIX qw(setsid);
use strict;
use warnings;
use Fcntli qw( :flock );
sub usr1 {
if (my $pid=fork){
open my $file,">>/tmp/test.txt";
or die "Non posso aprire il file: $!\n";
flock $file,LOCK_EX; ## lock
sleep 6;
print $file "pid padre: $$\n";
flock $file,LOCK_UN; ## unlock
close $file;
waitpid($pid,0);
}else {
sleep 3;
open my $file,">>/tmp/test.txt"
or die "Non posso aprire il file: $!\n";
flock $file,LOCK_EX;
print $file "pid figlio: $$\n";
flock $file,LOCK_UN;
close $file;
exit 0;
}
}
sub usr2 {
warn "SIGUSR2 ricevuto";
exit 0;
}
chdir '/'
or die "Non posso eseguire il chdir /: $!\n";
umask 0;
close STDIN;
open STDERR,">>/tmp/daemon.err";
defined(my $pid = fork)
or die "Non posso eseguire la fork(): $!";
exit if $pid; ## termino il padre
setsid
or die "Non posso far partire la nuova sessione: $!";
print "Stampa il pid: $$\n";
close STDOUT;
while(1) {
sleep 3;
$SIG{'USR1'}= \&usr1;
$SIG{'USR2'}= \&usr2;
}
__END__
=head1 NAME
dattero.pl - Script per la creazione di un demone.
=head1 SYNOPSIS
./dattero.pl
=head1 DESCRIPTION
Questo script fa parte di un articolo esemplificativo
per la creazione di un demone in Perl, la gestione di
alcuni segnali SIGUSR1 SIGUSR2, e la scrittura controllata
di un file tra i vari processi.
=head1 AUTHOR
Fabrizio Cardarello
=head1 SEE ALSO
Proc::Daemon, perlipc
=cut
Concludendo...
Demoni, segnali, chiamate di sistema, programmazione concorrente, queste
sono alcune delle fondamenta di UNIX ed il Perl si dimostra uno strumento
eccellente per svelarci tutti i segreti di questo sistema operativo.
Per ulteriori approfondimenti, possono essere d'aiuto i seguenti link:
In questi siti potete trovare una visione alternativa delle problematiche affrontate in questo articolo. In particolare, i primi due affrontano la tematica della scrittura di demoni in ambienti UNIX, mentre l'ultimo offre una panoramica sulla progammazione concorrente.
Buon Perl a tutti!
Ti è piaciuto questo articolo? Iscriviti al feed!
|