Vai alla Home Page About me Courseware Federica Living Library Federica Federica Podstudio Virtual Campus 3D Le Miniguide all'orientamento Gli eBook di Federica La Corte in Rete
 
I corsi di Scienze Matematiche Fisiche e Naturali
 
Il Corso Le lezioni del Corso La Cattedra
 
Materiali di approfondimento Risorse Web Il Podcast di questa lezione

Clemente Galdi » 16.La programmazione multi-processo


Il multi-tasking

Nei moderni sistemi operativi, un processo, è una istanza di un programma in esecuzione.
Il processo è, quindi, un’entità dinamica che include, oltre al codice del programma, anche lo stato dell’esecuzione, identificato dal contenuto della memoria del processo.
Esistono sistemi operativi “moderni” che non supportano alcune (o nessuna) delle proprietà appena descritte. Esempi classici sono i sistemi operativi “embedded” che gestiscono device con bassa capacità di calcolo, e.g., lettori di smart card.

Il multi-tasking (segue)

La maggior parte dei sistemi operativi “general purpose” moderni sono:

  • Multi-tasking: consentono la possibilità di eseguire più processi “contemporaneamente” su un unico processore. Inizialmente sviluppato per ottimizzare l’uso del(l’unico) processore disponibile. Chiaramente, avendo un’unica unità di calcolo, la CPU diventa una risorsa contesa e condivisa tra tutti i processi in esecuzione. Elemento centrale del multi-tasking è la prelazione della CPU che impedisce che un processo monopolizzi questa risorsa a danno degli altri processi.
    • In alcuni testi si definisce il multi-programming come multi-tasking senza la possibilità di prelazionare la CPU.
  • Multi-processing: consentono la possibilità di coordinare l’esecuzione di più processi utilizzando più processori. Nelle moderne architetture multi-core, i singoli core vengono visti come “processori”. Se il sistema operativo supporta il multi-processing, è allora possibile bilanciare il carico dei processori facendo migrare processi da un processore ad un altro meno carico.
  • Multi-threading: consentono la possibilità che uno stesso processo abbia più “path” di esecuzione. Ad es., un server web multi-threaded può gestire diverse richieste (da parte anche di diversi browser) contemporaneamente, avendo la necessità di duplicare solo le aree di memoria sufficienti a gestire le diverse richieste (e senza dover creare una copia, ad esempio, del codice da eseguire).
  • Multi-user: consentono la possibilità che task di utenti diversi vengano eseguiti sulla stessa macchina. Il S.O. fornisce strumenti per il protezione dell’accesso a dati ed aree di memoria da parte di utenti diversi.

Il Process Identifier o PID

Dal punto di vista dell’utente, quindi, l’elemento centrale dei moderni sistemi operativi è il concetto di task.

Un task è implementato da uno o più processi cooperanti. Ci occuperemo della programmazione multi-thread dalla lezione n° 22.

Nei sistemi Unix ad ogni processo è assegnato un identificativo univoco denominato Process Identifier o PID.
Il PID è univoco, indipendentemente dal numero di CPU presenti.

Il PID non è altro che un intero, compreso tra 0 e 32767.
Nei sistemi Linux il valore massimo del PID è pari al numero di bit in una pagina fisica (page frame) ed è configurabile all’atto della compilazione del kernel.

I PID 0 ed 1 sono riservati al sistema operativo e sono associati, nei sistemi Linux, ai processi denominati “swapper” (o “cpu_idle”) ed “init”.

All’atto della creazione di un processo, il sistema operativo assegna al nuovo processo un PID non utilizzato. Il processo mantiene il proprio PID fino alla sua terminazione.

Attenzione: Il sistema operativo assegna al nuovo processo il valore dell’ultimo PID assegnato +1. Quando questo valore eccede il massimo, il sistema operativo riutilizza i PID dei processi terminati.

Come già accennato in precedenza, ad ogni utente il sistema operativo assegna un identificativo univoco detto User Identifier (o UID).

È possibile definire gruppi di utenti, ad ognuno dei quali viene assegnato un identificativo univoco detto Group Identifier (o GID).

System call per il reperimento di PID

I sistemi Unix forniscono una serie di system call per ottenere informazioni sull’identità dl processo.
Sebbene PID, UID e GID siano interi, per questioni di portabilità il sistema operativo fornisce tipi specifici per ognuno di questi oggetti. Rispettivamente, un PID, UID e GID sono memorizzati in una variabile di tipo pid_t, uid_t e gid_t.
Lo standard POSIX definisce le seguenti system call:

  • pid_t getpid(void): Ritorna il PID del processo corrente.
  • pid_t getppid(void): Ritorna il PID del processo “padre”, I.e., il processo che ha creato il processo corrente.
  • uid_t getuid(void): Ritorna il “real user ID” del processo che corrisponde allo UID dell’utente che ha iniziato l’esecuzione del processo.
  • uid_t geteuid(void): Ritorna “l’effective user ID” del processo che corrisponde allo UID che sta eseguendo il processo. Si ricorda che lo UID associato ad un processo può cambiare durante la sua esecuzione.
  • gid_t getgid(void): Ritorna il “real group ID” del processo che corrisponde al GID del gruppo cui l’utente che ha iniziato l’esecuzione del processo appartiene.
  • gid_t getegid(void): Ritorna “l’effective group ID” del processo che corrisponde al GID corrente del processo. Come per lo UID, anche il GID di un processo può cambiare durante la sua esecuzione.

Si noti che nessuna di queste funzioni ritorna mai un errore.

La system call fork

L’unico modo per un processo utente di creare nuovi processi è attraverso la system call fork() o sue varianti.

pid_t fork(void)

Il nuovo processo creato è detto processo “figlio” mentre il processo che ha invocato la fork è detto processo “padre”.
La funzione è invocata una volta ma ritorna due volte:

  • Una volta nel padre ritornando il PID del figlio
    • Ogni processo può avere più figli. Ritornare il PID del figlio appena creato consente al padre di memorizzare queste informazioni. Non esistono system call consentono di elencare i figli di un dato processo.
  • Una volta nel figlio ritornando 0 (zero)
    • Ogni processo ha sempre un unico padre di cui è possibile ottenerne il PID invocando la system call getppid().

In entrambi i casi, viene eseguita l’istruzione successiva alla fork.
La fork crea un nuovo processo “copiando” l’intera memoria (testo, dati statici, dati dinamici e stack) utilizzata dal processo padre. In realtà, per questioni di efficienza, il S.O. NON esegue una copia immediatamente ma solo quando effettivamente necessario (copy on write).
Dal momento della sua creazione, però, I processi padre e figlio sono:
Identici (a meno del valore ritornato da fork)
Indipendenti: NON condividono aree di memoria

Fork

Esempio di utilizzo della fork: Mostra codice

Il programma:

  • Dichiara una variabile globale ed alcune locali;
  • Esegue I/O bufferizzato e non;
  • Esegue la fork che ritorna un valore 0 nel figlio e pari al pid del figlio nel padre. Il figlio esegue il codice nel blocco "then" ED eventuale codice successivo alla fine dell'else (la sprintf e write);
  • Il padre esegue il codice nel blocco "else" ED eventuale codice successivo alla fine dell'else (la sprintf e write).

Da questo momento in poi I due processi, padre e figlio, sono indipendenti. Difatti il figlio modifica le variabili "globale" e "locale" e poi visualizza il loro valore attraverso la write.
Il padre attende due secondi ed esegue la stessa printf del figlio, ovviamente su dati diversi.
L'output del programma: Mostra codice
Di nuovo, sebbene il figlio modifichi la sua copia delle variabili, le variabili del padre NON vengono modificate.
NON ha senso utilizzare variabili globali e "sperare" che la loro modifica possa "trasferirsi" tra padre e figlio.

Fork (segue)

Se redirigiamo l’output dello stesso programma su un file e ne visualizziamo il contenuto, otteniamo un comportamento difforme dal precedente. Compare nel file: Mostra codice

Difatti la stringa "prima della fork" visualizzata attraverso l'I/O bufferizzato, compare nel file DUE volte, una volta per il padre ed una volta per il figlio ed, in entrambi I casi, successivamente alla stringa visualizzata dalla write a fine file. In figura:

  • I processi padre e figlio condividono gli stessi elementi nella file table, ma hanno tabelle dei file descriptor separate.
  • Se uno dei due processi scrive su un file, il sistema operativo aggiorna l'offset corrente, comune ad entrambi i processi. Per questa ragione l'output delle "write" in padre e figlio non si sovrascrivono.
  • Come abbiamo già visto, la redirezione della printf, trasforma il suo buffering da "a linea" a "completo". Quindi la printf scrive la stringa "Prima della fork" in un buffer in memoria che viene duplicato dalla fork. A differenza dell'I/O non bufferizzato che viene eseguito "immediatamente", questo buffer (in questo esempio in cui le stringhe sono brevi) viene svuotato solo alla terminazione dei processi. L'effetto, quindi, è la visualizzazione degli output in un ordine che non rispecchia quello indicato dal codice.
Una rappresentazione dei puntatori alla file table dopo un fork.

Una rappresentazione dei puntatori alla file table dopo un fork.


Proprietà ereditate dal figlio

Dopo l’esecuzione della system call fork, il processo figlio eredita dal padre le seguenti proprietà:

  • Tabella dei file descriptor. Tutti i file aperti dal padre (o da questi ereditati) sono accessibili al figlio con le stesse modalità di accesso.
  • Real/Effective user/group ID.
  • Supplementary group id.
  • Process group Id/Session id.
  • Terminale (/dev/tty…). Processo padre e figlio utilizzano lo stesso terminale, fino a quando uno dei due processi non modifica standard input/output/errror.
  • I flag set-user-id e set-group-id.
  • Directory root e corrente. Padre e figlio operare nella stessa directory (se uno dei due non modifica esplicitmente la directory di lavoro).

Proprietà ereditate dal figlio (segue)

Proprietà ereditata dal processo padre dopo l’esecuzione della system call fork:

  • File mode creation mask.
  • Maschera dei segnali. Il processo continua a bloccare I segnali bloccati dal padre.
  • close-on-exec flag. Questo flag indica al sistema operativo di chiudere I file aperti (ad eccezione degli standard input, output ed error) prima di eseguire una exec.
  • Segmenti di memoria condivisa. Se il padre condivide memoria con atri processi, le aree di memoria condivise sono accessibili anche al figlio.
  • Limiti associati alle risorse. Il sistema operativo consente di limitare le risorse che possono essere assegnate ad ogni singolo processo ed ad ogni utente. Esempio di limitazione è la dimensione massima di memoria allocabile da un processo. Il processo figlio eredita tutte le limitazioni del processo padre.

Differenze tra padre e figlio

La system call fork ritorna due volte, una volta nel padre ed una volta nel figlio.
I processi padre e figlio sono due processi:

  • Indipendenti. In linea di principio NON condividono nessuna variabile.
    • Errore estremamente comune è considerare due variabili (con lo stesso nome) nel padre e nel figlio come se fossero un’unica variabile.
  • Identici, a meno dei seguenti valori:
    • Il valore di ritorno della fork. È possibile utilizzare questo valore per discriminare il comportamento tra padre e figlio;
    • Il valore del PID;
    • Il valore del PPID (parent PID);
    • I file lock del padre NON sono ereditati al figlio;
    • “Alarms” e segnali in attesa per il padre non vengono ereditati dal figlio.

La fork può fallire (valore di ritorno -1) se:

  • Ci sono troppi processi nel sistema;
  • Il numero di processi per il real user supera i limiti di sistema (CHILD_MAX).

Verificare sempre il valore di ritorno della system call.

Fork: creazione di più figli

In molti casi è necessario che un processo crei più processi figli.
Di seguito un programma in cui il processo “padre” crea 5 processi figli e termina: Mostra codice
Ogni processo figlio esegue la funzione fun_figlio che:

  • Riceve come unico parametro un intero che indica il numero progressivo del figlio.
  • Visualizza il pid del padre, il proprio pid, il progressivo ed il valore di una variabile globale j.
  • Modifica j.

Si noti che l'esecuzione delle istruzioni all'interno dell'if vengono eseguite SOLO dal processo figlio. Inoltre, la presenza dell'exit provoca la terminazione del processo figli dopo l'esecuzione della funzione.
Come già evidenziato nell'esempio precedente, la modifica di una variabile (locale o globale) nel processo padre NON influenza il valore della variabile con lo stesso nome del processo padre o viceversa.
Di seguito un possibile output del programma: Mostra codice. Sono da rimarcare:

  1. L'ordine in cui vengono eseguite le istruzioni dei diversi processi NON è predicibile (a meno che non sia esplicitamente imposto). Difatti, i valori delle variabili "i" visualizzati dai diversi processi NON sono in sequenza, come ci si aspetterebbe.
  2. Il processo padre può terminare prima dei processi figli. Come vedremo, in questo caso i processi orfani diventano figli di init. Questa è la ragione per cui, l'ultima riga indica che il padre del figlio 3 con pid 734 ha pid 1.

Fork: creazione di più figli (segue)

È necessario prestare la massima attenzione quando la system call fork viene inserita all’interno di cicli. Ecco l’output di una variante (SBAGLIATA) del programma precedente in cui è stata eliminata l’invocazione della system call exit e l’indice i assume i valori 0,1,2 (invece di 0,1,2,3,4): Mostra codice. L'output può essere compreso osservando che, a causa dell'eliminazione della exit, in realtà il programma crea l'albero dei processi (vedi figura). Infatti, per I=0, il processo padre (la radice dell'albero), crea il primo processo figlio (il nodo rosso), il quale visualizza una riga dell'output. Il processo figlio possiede una copia locale della variabile I con valore pari a 0. Non essendo più presente la exit, nessuno dei due termina, entrambi continuano l'esecuzione del ciclo e, quindi:
- entrambi incrementeranno la (copia locale della) variabile I al valore 1;
- entrambi eseguono la system call fork, creando DUE processi (I nodi in giallo), ognuno dei quali visualizza una riga dell'output.
Ora tutti I processi attivi hanno una copia locale della variabile I pari ad 1. Ne consegue che, di nuovo, che ognuno di essi:
- incrementa la (copia locale della) variabile I al valore 2;
- Esegue la system call fork creando, in totale quattro nuovi processi (i nodi in blu).
A questo punto, tutti gli otto processi attivi, incrementano la (copia locale della) variabile I al valore tre ed terminando il ciclo.

L’albero dei processi creati.

L'albero dei processi creati.


Perché creare processi figli

La creazione di un processo figlio può avere, sostanzialmente, due motivazioni.

  • Semplificazione della progettazione di una applicazione.
    • La possibilità di creare processi, identici a quello corrente, consente di progettare applicazioni suddividendo il lavoro in diversi task ed assegnando ogni task ad un processo diverso. Ad esempio:
      • Un processo attende la richiesta di servizio. Quando riceve la richiesta:
        • il figlio serve la richiesta
        • Il padre ritorna in attesa
    • L’utilizzo di questa semplice strategia consente di rendere i processi più:
      • Semplici. Il “figlio” deve essere progettato per gestire una sola richieste di servizio.
      • Performanti. Il padre può gestire più richieste contemporaneamente.
  • Esecuzione di una applicazione scritta da terzi
    • In moltissimi casi, un processo si trova a dover eseguire un programma scritto da altri. Si pensi, ad esempio, al lavoro fatto dalla shell ogni volta che viene richiesta l’esecuzione di un comando NON built-in.
    • La fork, in combinazione con le system call della famiglia exec, rende estremamente semplice questo task.

Si tenga conto che l’operazione di start-up di un sistema operativo parte da un unico processo che utilizza la fork per creare tutti gli altri processi del sistema. Quindi TUTTI I processi, sono, in realtà, “discendenti” del processo init.

Creazione del processo e vfork

Quando un processo esegue una fork, il sistema operativo crea idealmente una “copia” (testo, dati statici, dati dinamici e stack) del padre prima di eseguire il figlio.
In realtà NON è esattamente così.
I moderni sistemi NON eseguono una copia del contenuto della memoria fisica se non è strettamente necessario.
Viene creata una copia dello spazio di indirizzamento del processo padre, in modo che il figlio abbia la stessa “visione della memori fisica del padre”, (i.e., abbia accesso alle “stesse variabili”, allo “stesso codice”, allo “stesso stack”) pur non avendone una copia propria.

Se il processo figlio deve eseguire una applicazione scritta da terzi, la copia dello spazio di indirizzamento del processo padre è inutile.
La exec “sovrascrive” l’intera area di memoria del processo che la esegue.

In questo caso è possibile usare la vfork().
vfork() ritorna gli stessi valori della fork().
il padre attende che il figlio esegua la exec() o exit().
NON esegue la “copia” del processo padre. Il figlio usa lo stesso address space del padre.
Se il processo figlio (per errore del programmatore) modifica la memoria, la vfork si comporta come una fork , duplicando sia lo spazio di indirizzamento del padre che l’area di memoria fisica da modificare.

wait e waitpid

Il sistema operativo informa il processo padre quando uno dei suoi processi figli termina.
L’informazione che sopravvive alla terminazione di un processo è il suo exit status. Quando un processo (figlio) termina, il sistema operativo:

  • Rilascia tutte le risorse associate al processo;
  • Mantiene il descrittore di processo (DP), contenente il valore di uscita ed alcune informazioni ad esso relate, e tenta di comunicare queste informazioni al processo padre. L’exit status di un processo può essere:
    • Definito dall’utente attraverso la system call exit.
    • Assegnato dal sistema operativo se l’utente non lo fa esplicitamente od in caso di terminazione con errore.

wait e waitpid (segue)

Nei sistemi Linux, quando un processo termina:

  • Lo stato dl processo diventa exit_zombie fino a quando il padre non ha avuto la possibilità di leggere l’exit status del figlio terminato.
  • I descrittori di processo vengono definitivamente deallocati solo dopo che le informazioni in essi contenute sono state lette (od ignorate) dal padre.
    • Si noti che il PID del processo rimane allocato (ad un processo “zombie”) fino a quando il descrittore non viene deallocato.
  • Il sistema operativo informa il processo padre della terminazione di un figlio attraverso l’invio di un segnale.
    • Il segnale di default per questo messaggio è SIGCHLD.
    • La notifica dei messaggi è asincrona (il padre può ricevere il segnale in qualsiasi momento).

Se un processo termina prima dei suoi figli, I processi orfani diventano figli del processo init che si occupa di leggere il loro exit status per consentire il rilascio dei descrittori.

wait e waitpid (segue)

Il processo terminato viene definitivamente eliminato dalla memoria quando il padre:

  • Ha ignorato (implicitamente o esplicitamente) il segnale SIGCHLD.
    • Come vedremo, questa è l’azione di default associata a questo segnale. Quindi è “sufficiente” che il processo padre venga schedulato affinché il sistema operativo consideri “ignorato” il segnale.
  • Gestisce esplicitamente il segnale attraverso un signal handler.

La consapevolezza che la terminazione di un figlio implica la ricezione di un segnale SIGCHLD consente al processo padre di continuare le proprie attività indipendentemente dall’operato dei figli.
Come vedremo nella Lezione 18, è possibile progettare “indipendentemente” l’applicazione e le funzioni che gestiscono i segnali.

In alternativa il padre può attendere esplicitamente la terminazione di un figlio tramite l’invocazione di una system call wait o waitpid ed ottenere l’exit status del figlio.

wait e waitpid (segue)

Le system call wait ha a seguente firma:

pid_t wait(int *status);

Riceve come parametro un puntatore ad un intero, allocato dal processo che la invoca, o NULL;
Il comportamento del processo è il seguente:

  • Se nessuno dei figli è nello stato exit_zombie: Blocca il processo padre ed attende la terminazione di un figlio.
  • Se esistono figli nello stato exit_zombie, ritorna immediatamente. Il valore di ritorno della wait è il pid di un processo terminato il cui exit status è puntato da *status. Se il parametro è NULL, l’exit statu del figlio non viene ricevuto dal padre.
  • Se il processo corrente non ha figli o se la system call viene interrotta: Ritorna -1 e viene assegnata la variabile errno opportunamente.

Se la system call wait viene eseguita una volta all’interno del’handler di SIGCHLD, non risulta bloccante visto che vi sarà almeno un processo terminato.

wait e waitpid (segue)

Le system call waitpid ha la seguente firma:

pid_t waitpid(pid_t pid, int *status, int options);

waitpid consente di attendere la terminazione di un processo (figlio) specifico. Il parametro pid è interpretato come segue:

  • pid=-1: aspetta la terminazione di un processo figlio qualsiasi, è in questo caso equivalente alla wait;
  • pid>0: Aspetta la terminazione con PID=pid;
  • pid=0 e pid<0: Aspetta la terminazione di un processo figlio appartenente ad un process group specifico.

waitpid ritorna un errore se il pid specificato non identifica nessun processo o process group (e se la system call è interrotta).

Come la wait, riceve come parametro un puntatore ad un intero, allocato dal processo che la invoca o NULL;
Il parametro options è zero o composto come OR delle seguenti costanti:

  • WHOHANG: Non blocca il processo padre se il figlio specificato da parametro pid non è disponibile. In questo caso ritorna 0 (zero)
  • WUNTRACED: Oltre alla terminazione dei processi figli, riporta al padre anche I casi in cui i processi figli identificati dal parametro pid vengono interrotti a seguito della ricezione di alcuni segnali.
  • WCONTINUED: Ritorna lo stato del processo riattivato dopo esser stato bloccato.

Wait: un esempio

Riproponiamo l’esempio visto precedentemente in cui il processo padre, questa volta, attende la terminazione dei figli: Mostra codice

Si noti che il padre esegue una invocazione della system call per ogni figlio creato.
A differenza dell'esecuzione precedente, il pid del processo padre visualizzato su ogni riga dell'output del programma è sempre lo stesso.
Difatti, la presenza della wait blocca il processo fino alla terminazione del figlio.

Quando un processo figlio invoca la system call getppid(), quindi, il padre è ancora attivo ed il suo pid viene ritornato al figlio che lo visualizza.

Output della funzione: Mostra codice

wait e waitpid

L’analisi dell’exit status deve essere effettuato tramite l’utilizzo di macro specifiche:

  • WIFEXITED(status): Vero se il figlio è terminato “normalmente”. In questo caso:
    • WEXITSTATUS(status) ritorna gli 8 bit meno significativi dell’argomento delle system call exit(), _exit() o _Exit() con cui il processo è terminato.
    • Si rammenta che, in mancanza di una exit explicita inserita nel codice, il sistema operativo assume come valore di ritorno zero (“successo”).
  • WIFSIGNALED(status): Vero se il figlio è terminato in modo anormale ricevendo un segnale non gestito: Es. “Segmentation fault”
    • WTERMSIG(status) ritorna il numero del segnale non gestito;
    • WCOREDUMP(status) è vero se è stato generato un file “core”. Solo In alcune implementazioni (Linux incluso).
  • WIFSTOPPED(status): Vero se il figlio è temporaneamente bloccato
    • WSTOPSIG(status) ritorna il numero del segnale che ha bloccato il processo figlio.
  • WIFCONTIUNED(status): Vero se il figlio è stato riattivato dopo essere stato bloccato.

Wait: un esempio

L’esempio: Mostra codice, estende ancora l'esempio precedente analizzando il valore dell'exit status.
Il processo padre, dopo aver creato i tre processi figli esegue la system call wait passando come parametro un puntatore ad un intero.
La system call ritorna il pid del processo figlio terminato.
L'analisi del valore di status consente di ottenere l'exit value del processo figlio.
Nell'esempio, tutti I processi terminano normalmente eseguendo la system call exit a cui passano come parametro il valore corrente della variabile I.
È da notare che:

  • NON è possibile stabilire a priori quale sarà lo scheduling di esecuzione dei processi (padre e figli). Nell'esempio, lo scheduler esegue il figlio 1, esegue quindi la prima wait del padre. Viene quindi visualizzato l'output dei figli 1 e 2. Alla fine viene visualizzato l'output del padre per I figli 2 ed 1 (in questo ordine)
  • È solo possibile solo asserire l'ovvio: "La system call wait per il processo I ritorna solo dopo che il processi I è terminato".

Output della programma: Mostra codice

Waitpid: un esempio (segue)

Esempio di programma che utilizza la system call waitpid: Mostra codice
A differenza dell'esempio appena visto, il pid ritornato dalla fork viene memorizzato in un array di interi, in modo da poter essere referenziato successivamente.

Ad ogni iterazione del secondo ciclo, il programma invoca la waitpid passando, alla iterazione I-esima il valore del pid dell'I-esimo processo creato.
L'effetto di questa variazione al codice è: Mostra codice
Difatti, indipendentemente dall'ordine di esecuzione dei processi padre e figli, il processo padre visualizza sempre (in quest'ordine) l'exit status del primo processo figlio, seguito dall'exit status del secondo processo figlio, seguito dall'exit status del terzo processo figlio.

Le system call exec

La famiglia delle system call exec è utilizzata per sostituire il processo corrente con un nuovo “programma”.

Il codice del programma da eseguire viene letto dal disco l’intera memoria associata al processo viene riscritta (testo, dati, heap e stack). In particolare:

  • L’area codice è sostituita con il codice letto dal disco;
  • Dati statici, heap e stack vengono reinizializzati;
  • Il nuovo processo NON può accedere alle variabili del processo che ha invocato la exec;
  • Il nuovo programma inizia la sua esecuzione “come se fosse eseguito da shell”.

Chiaramente, affinché sia possibile eseguire il codice, è necessario che:

  • Il file indicato dalla system call sia un “eseguibile”. In particolare:
    • un file eseguibile (ovviamente)
    • uno script shell che indica al suo interno l’interprete (e.g., #!/bin/bash). Nota, in alternativa è possibile eseguire l’eseguibile “shell” passando come parametro il nome dello script da eseguire all’interno della shell.
  • l’effective user del processo che invoca la exec abbia I permessi di esecuzione sul file indicato dalla system call.

Le system call exec (segue)

Le system call della famiglia exec sono le seguenti:

int execl(const char *path, const char *arg, ...);
int execv(const char *path, char *const argv[]);
int execle(const char *path, const char *arg , ..., char * const envp[]);
int execve(const char *file, char *const argv [], char *const envp[]);
int execlp(const char *file, const char *arg, ...);
int execvp(const char *file, char *const argv[]);

Tutte le system call sono front-end della system call execve.
Il parametro path indica il pathname dell’eseguibile da caricare (e.g., /bin/ls)
Il parametro file:

  • Se contiene uno “/” è interpretato come pathname
  • Altrimenti, viene interpretato come il nome del file da eseguire ed è ricercato nelle directory specificate dalla variabile d’ambiente PATH.

Le system call exec (segue)

Le system call execl, execle, execlp prendono come parametri la lista di stringhe (array di caratteri terminati dal carattere nullo) che assumeranno il valore di “parametri su riga di comando” per il programma da eseguire;
L’ultimo elemento della lista DEVE essere NULL.

Le system call execv, execve, execvp prendono come parametro il vettore di puntatori agli argomenti da passare all’eseguibile.
L’ultimo elemento dell’array DEVE essere NULL.

Le system call execle, execve prendono come ulteriore parametro un descrittore (lista o array) del “environment”
PATH, SHELL, HOME, PWD,….
Utile per specificare un ambiente specifico per un figlio.

La altre system call utilizzano la variabile environ del processo padre.

Le system call exec (segue)

Le system call exec ritornano SOLO in caso di errore. In tal caso, il valore di ritorno è -1 e la variabile globale errno contiene il codice d’errore. Nel caso in cui la exec venga eseguita con successo, la system call NON ritorna. Il processo originario viene sostituito dal programma richiesto ed il “nuovo processo” termina quando questi termina.

L’esecuzione di una system call exec sostituisce il codice del processo corrente con un altro ma eredita dal processo che originario:

  • PID e PPID
  • Real user/group ID/ Supplementary group id
  • Process group Id/Session id
  • Terminale (/dev/tty…)
  • Directory root e directory corrente
  • File mode creation mask
  • Maschera dei segnali
  • Segnali in attesa
  • Limiti associati alle risorse.

Il processo, NON eredita dal processo originario:

  • L’effective group id
  • I segmenti di memoria condivisi

exec: un esempio

Di seguito un programma in cui il processo padre, crea un figlio ed attende la sua terminazione: Mostra codice

Il processo figlio esegue lo script test.sh, riportato nella figura in basso.

La execl prende come parametro il pathname del file "test.sh". In questo caso, mancando "/" nel primo parametro, il file viene ricercato nelle directory indicate dalla variabile PATH.

I successivi tre parametri identificano i "parametri su riga" di comando visibili al uovo programma in esecuzione, arg0, arg1.... Per convenzione, arg0 punta al nome del file.

La lista di parametri della execl DEVE terminare con un puntatore NULL.

Si noti che l'istruzione "perror" NON viene eseguita.

L'output del programma: Mostra codice. Il programma in C esegue lo script che visualizza il "nome dello script" $0, l'elenco dei suoi parametri "par 1 par 2", il primo parametro "par 1", e l'elenco dei file nella directory corrente.

exec: un esempio (segue)

È da rimarcare che è (ovviamente):

  • possibile modificare lo script ed eseguire il programma SENZA dover ricompilare il sorgente in C.
  • NECESSARIO ricompilare il codice nel momento in cui è necessario modificare il nome dello script da eseguire od I suoi parametri, nel momento in cui questi sono stringhe costanti definite nel codice.

Si noti, inoltre, che non indicando l’interprete all’interno dello script, la exec ritorna con errore.

Nel seguente codice: Mostra codice, dallo script originario è stata eliminata la prima riga. L'esecuzione del codice porta alla esecuzione della funzione perror.

Output della programma: Mostra codice

exec: un esempio (segue)

Riportiamo un esempio di utilizzo della system call execvp: Mostra codice
Questa system call prende come parametri il nome del file da eseguire ed un array di puntatori a stringhe, in cui l'ultimo elemento è un puntatore nullo.
L'array contiene, come nel caso della lista, I parametri per il programma da eseguire. Il primo elemento dell'array contiene, per convenzione, il nome del file da eseguire.
Il programma, mostrato nell'esempio, crea un processo figlio che esegue il comando "ls" passando l'opzione "-l".
Il processo padre attende la terminazione del figlio, (I.e., del comando ls), visualizza il messaggio "Fine ls" e poi termina.

I materiali di supporto della lezione

W.R. Stevens, S.A. Rago - Advanced Programming in the Unix Environment - Capitolo 8 (8.1-8.7/8.10)

  • Contenuti protetti da Creative Commons
  • Feed RSS
  • Condividi su FriendFeed
  • Condividi su Facebook
  • Segnala su Twitter
  • Condividi su LinkedIn
Progetto "Campus Virtuale" dell'Università degli Studi di Napoli Federico II, realizzato con il cofinanziamento dell'Unione europea. Asse V - Società dell'informazione - Obiettivo Operativo 5.1 e-Government ed e-Inclusion