NOME
perlfork - L'emulazione di fork() fornita dal Perl
SINOSSI
NOTA: Con la release 5.8.0, l'emulazione di fork() e` considerevolmente
maturata. Comunque, c'e ancora qualche bug conosciuto, e delle
differenze rispetto alla fork() reale, che potrebbero riguardarvi.
Consultate le sezioni "BUG" e "AVVERTIMENTI E LIMITAZIONI" piE<ugrave> avanti.
Il Perl fornisce una funzione fork() che corrisponde alla chiamata di sistema Unix con lo stesso nome. Sulla maggior parte dei sistemi tipo Unix su cui la chiamata di sistema fork() è disponibile, Perl altro non fa che chiamarla.
Su alcuni sistemi come Windows, dove la chiamata di sistema fork() non è disponibile, Perl può essere compilato per emulare fork() a livello di interprete. Benché l'emulazione sia progettata per essere, a livello del programma Perl, il più compatibile possibile con la vera fork(), ci sono alcune importanti differenze, che derivano dal fatto che tutti i "processi" pseudo-figli creati in questo modo, in realtà, per quanto concerne il sistema operativo, vivono nello stesso processo.
Questo documento offre una panoramica generale delle caratteristiche e delle limitazione dell'emulazione di fork(). Notate che le questioni discusse qui non sono applicabili alle piattaforme sulle quali è disponibile una vera fork() e Perl è stato configurato per usarla.
DESCRIZIONE
L'emulazione di fork() è implementata a livello di interprete Perl. Questo, in generale, significa che fork() di fatto clona l'interprete in esecuzione e tutto il suo stato, e manda in esecuzione l'interprete clonato in un thread separato, iniziandone l'esecuzione subito dopo il punto in cui fork() era stata chiamata nel processor genitore. D'ora in avanti chiameremo pseudoprocesso il thread che implementa questo "processo" figlio.
Tutto ciò è stato progettato in modo da essere trasparente al programma Perl che ha chiamato fork(). Il genitore ritorna dalla fork con un ID di pseudoprocesso, il quale può in seguito essere usato in qualsiasi funzione di manipolazione del processo; il figlio ritorna dalla fork() con un valore 0
per indicare che è lui lo pseudoprocesso figlio.
Comportamento di altre caratteristiche di Perl nello pseudoprocesso risultante dal fork
La maggior parte delle caratteristiche del Perl si comportano in maniera normale all'interno dello pseudoprocesso.
- $$ o $PROCESS_ID
-
Questa variabile speciale viene correttamente impostata all'ID dello pseudoprocesso. Può essere usata per identificare uno pseudoprocesso all'interno di una determinata sessione. Notate che questo valore è soggetto al riutilizzo se degli pseudoprocessi vengono lanciati dopo che è stata attesa la fine di altri con wait().
- %ENV
-
Ogni pseudoprocesso ha il suo ambiente virtuale. Le modifiche a %ENV influenzano l'ambiente virtuale, e sono visibili solamente all'interno di tale pseudoprocesso, ed in qualsiasi processo (o pseudoprocesso) lanciato da esso.
- chdir() e tutte le altre funzioni integrate che accettano nomi di file
-
Ogni pseudoprocesso ha la sua idea della directory corrente. Un eventuale cambiamento della directory corrente utilizzando chdir() è visibile solamente all'interno di tale pseudoprocesso, ed in qualsiasi processo (o pseudoprocesso) lanciato da esso. Tutti gli accessi a file e directory effettuati dallo pseudoprocesso effettueranno correttamente la mappatura della directory di lavoro virtuale alla reale directory di lavoro.
- wait() e waitpid()
-
A wait() e waitpid() può essere passato un ID di pseudoprocesso ritornato da fork(). Le chiamate a queste funzioni attenderanno, correttamente, il termine del pseudoprocesso e ritorneranno il suo stato.
- kill()
-
kill() può essere utilizzato per terminare uno pseudoprocesso. A questa funzione va passato l'ID ritornato da fork(). Essa non dovrebbe essere usata tranne che in circostanze estreme poiché, quando un thread viene terminato, il sistema operativo potrebbe non garantire l'integrità delle risorse del processo. Ricordate che l'uso di kill() su uno pseudoprocesso può causare dei memory leak [perdita di memoria, NdT], poiché il thread che implementa lo pseudoprocesso non ha avuto la possibilit<agrave> di effettuare la pulizia delle sue risorse.
- exec()
-
La chiamata ad exec() all'interno di uno pseudoprocesso in realtà lancia l'eseguibile richiesto in un processo separato ed attende che esso termini l'esecuzione prima di uscire con lo stesso valore di uscita del processo. Ciò significa che l'ID del processo indicato all'interno del programma in esecuzione sarà diverso da ciò che la funzione fork() di Perl può aver restituito in precedenza. Analogamente, ogni funzione di manipolazione del proceso applicata all'ID restituito da fork() influenzerà lo pseudoprocesso generato dalla chiamata ad exec(), non il vero processo che sta attendendo il ritorno di exec().
- exit()
-
exit() fa sempre uscire solamente lo pseudoprocesso in esecuzione, dopo aver automaticamente atteso il termine di qualsiasi pseudoprocesso figlio ancora in esecuzione. Notate che ciò significa che il processo principale non uscirà a meno che tutti i pseudoprocessi non abbiano terminato la loro esecuzione.
- Handle aperti verso file, directory e socket
-
Tutti gli handle aperti sono duplicati negli pseudoprocessi, dunque la chiusura di un handle in un processo non influisce sugli altri. Più avanti sono riportare alcune limitazioni.
Limiti di risorse
Agli occhi del sistema operativo, gli pseudoprocessi creati tramite l'emulazione di fork() sono semplicemente thread nello stesso processo. Ciò significa che qualsiasi limite a livello di processo imposto al sistema operativo vale per tutti gli pseudoprocessi messi assieme. Questo include qualsiasi limite imposto dal sistema operativo sul numero di file, directory e socket aperti, limiti sull'uso di spazio su disco, limiti sulle dimensioni in memoria, limiti sull'usco della CPU, ...
Uccidere il processo genitore
Se il processo genitore viene ucciso (usando la funzione kill() di Perl, o utilizzando qualche metodo esterno) anche tutti gli pseudoprocessi vengono uccisi, e l'intero processo esce.
Vita del processo genitore e degli pseudoprocessi
Durante il normale corso degli eventi, il processo genitore e tutti gli pseudoprocessi da esso generato attenderanno, prima di uscire, che i rispettivi pseudofigli terminino la loro esecuzione. Ciò significa che il genitore ed ogni suo pseudofiglio che è anche uno pseudogenitore usciranno solo dopo che i loro pseudofigli hanno terminato l'esecuzione.
Un modo per marcare gli pseudoprocessi cosicché siano eseguiti separatemente dal loro genitore (cosicché il genitore non debba attendere il loro termine se non lo desidera) verrà fornito in futuro.
AVVERTIMENTI E LIMITAZIONI
- blocchi BEGIN
-
L'emulazione di fork() non funziona in maniera del tutto corretta se è chiamata dall'interno di un blocco BEGIN. La copia ottenuta tramite fork() eseguirà i contenuti del blocco BEGIN, ma non continuerà il parsing del codice sorgente dopo tale blocco. Ad esempio, considerate il seguente codice:
BEGIN { fork and exit; # il padre crea un figlio ed esce print "interno\n"; } print "esterno\n";
Questo stamperà:
interno
anziché, come ci si aspetta:
interno esterno
Questa limitazione nasce da fondamentali difficoltà tecniche nel clonare e far ripartire gli stack usati dal parser Perl nel corso di un'operazione di parsing.
- Filehandle aperti
-
Qualsiasi filehandle aperto al momento del fork() viene duplicato. Dunque, i file possono essere chiusi indipendentemente nel padre e nel figlio, ma ricordate che gli handle duplicati continueranno a condividere il puntatore che indica la posizione all'interno del file. Cambiare tale posizione nel padre la cambierà nel figlio, e viceversa. Questo può essere evitato aprendo separatamente, nei processi figli, i file che necessitano di puntatori diversi.
- open() con pipe verso fork non ancora implementata
-
I costrutti
open(PIPPO, "|-")
eopen(PLUTO, "-|")
non sono ancora stati implementati. Questa limitazione può essere facilmente aggirata via codice, creando una pipe esplicitamente. Il seguente esempio mostra come creare un figlio derivante da una fork():# simulate open(PIPPO, "|-") sub pipe_verso_fork ($) { my $genitore = shift; pipe my $figlio, $genitore or die; my $pid = fork(); die "fork() fallita: $!" unless defined $pid; if ($pid) { close $figlio; } else { close $genitore; open(STDIN, "<&=" . fileno($figlio)) or die; } $pid; } if (pipe_verso_fork('FOO')) { # genitore print PIPPO "pipe_a_fork\n"; close PIPPO; } else { # figlio while (<STDIN>) { print; } close STDIN; exit(0); }
E questo legge dal figlio:
# simula open(PLUTO, "-|") sub pipe_da_fork ($) { my $genitore = shift; pipe $genitore, my $figlio or die; my $pid = fork(); die "fork() fallita: $!" unless defined $pid; if ($pid) { close $figlio; } else { close $genitore; open(STDOUT, ">&=" . fileno($figlio)) or die; } $pid; } if (pipe_da_fork('PLUTO')) { # genitore while (<PLUTO>) { print; } close PLUTO; } else { # figlio print "pipe_da_fork\n"; close STDOUT; exit(0); }
La funzione open() con pipe verso fork sarà supportata in futuro.
- Stato globale mantenuto dalle XSUB
-
Le subroutine esterne (XSUB) che mantengono il loro proprio stato globale, possono funzionare in maniera non corretta. Tali XSUB avranno il bisogno di conservare i lock per proteggere l'accesso simultaneo a dati globali da differenti pseudoprocessi, oppure dovranno conservare il loro stato sulla tabella dei simboli del Perl, che viene copiata di per s<eacute> quando fork() viene chiamata. Un meccanismo di callback che fornisce alle estensioni una via per clonare il loro stato sarà fornito in futuro.
- Interprete all'interno di un'applicazione più grande
-
L'emulazione di fork() può comportarsi in maniera imprevista quando viene eseguita in un'applicazione che contiene un interprete Perl e chiama delle API Perl che eseguono frammenti di codice. Ciò deriva dal fatto che l'emulazione conosce esclusivamente le strutture dati dell'interprete Perl, e non sa invece nulla sullo stato dell'applicazione che fa da contenitore. Per esempio, qualsiasi stato memorizzato nello stack dell'applicazione è fuori portata.
- Thread-safety delle estensioni [compatibilità delle estensioni con i thread, NdT]
-
Siccome l'emulazione di fork() esegue codice in thread multipli, le estensioni che effettuano chiamate a librerie che non sono thread-safe possono funzionare in maniera non affidabile qualora venga chiamata fork(). Man mano che il supporto ai thread del Perl si sta diffondendo anche sui sistemi in cui esiste una fork() nativa, tali estensioni saranno probabilmente corrette in modo da essere thread-safe.
BUG
Avere interi negativi come ID di pseudoprocesso causa problemi con il numero intero
-1
, poiché per le funzioni wait() e waitpid() si tratta di un caso speciale. L'assunzione tacita, nell'implementazione corrente, è che il sistema non alloca mai un ID di thread di1
per i thread utente. Una rappresentazione migliore per gli ID di pseudoprocesso sarà implementata in futuro.In alcuni casi, gli handle a livello di sistema operativo creati dagli operatori pipe(), socket() e accept() non sono duplicati correttamente negli pseudoprocessi. Ciò accade solo in alcune situazioni, ma quando accade, può portare a degli stalli tra l'estremo di lettura e quello di scrittura delle handle a pipe, o l'impossibilità di inviare o ricevere dati attraverso handle a socket.
Questo documento può essere incompleto in alcuni aspetti.
AUTORE
Il supporto per interpreti concorrenti per l'emulazione di fork() sono stati implementati da ActiveState, grazie a fondi forniti da Microsoft Corporation.
Questo documento è scritto e mantenuto da Gurusamy Sarathy <gsar@activestate.com>.
La traduzione in italiano è a cura di Michele Beltrame e del progetto pod2it ( http://pod2it.sourceforge.net ).