Identificativi
Alcune chiamate di sistema:
System calls: punti d’entrata diretti attraverso cui un processo attivo può ottenere dei servizi dal kernel.
Un kernel di Unix ha tra 60 e 200 system calls.
La libreria standard del C dispone di un’interfaccia per ogni chiamata di sistema, tipica delle funzioni C.
Esempi di system call per la gestione dei processi:
fork(), exec(), wait(), exit()
kill()
ptrace(), nice(), sleep()
Le uniche entità attive in un sistema Unix.
L’unico modo in cui un processo può essere creato dal S.O. Unix è mediante la chiamata di sistema fork (eccezione processo init ).
Ogni processo ha un’unico identificatore di processo (Process Identifier: PID), intero tra 0 e 30000, assegnatogli dal kernel all’atto della sua creazione.
Un processo ottiene il suo pid attraverso la chiamata di sistema :
int getpid();
I processi in Unix sono organizzati gerarchicamente.
Ogni processo ha un processo padre (eccetto il processo init) con il relativo pid.
Un processo ottiene il pid del padre attraverso la chiamata di sistema :
int getppid();
// File: PidDemo.cpp
# include
# include
int main(void)
{
int pid,ppid;
pid = getpid();
cout << "\nsono il processo pid = " << pid << endl;
ppid = getppid();
cout << "\nIl mio processo genitore ha pid = " << ppid << endl;
return 0;
}
Processo scheduler : pid=0
Processo init : pid=1
Processo pagedaemon : pid=2
Sospende (transizione stato Blocked) un processo per un certo numero di secondi.
sleep(int sec)
La creazione di nuovi processi è gestita mediante la chiamata di sistema:
int fork(void)
che crea una copia esatta (figlio) del processo originale (padre).
Per la proprietà di rientranza del codice, padre e figlio condivideranno l’area testo.
Le aree dati globali, stack, heap e U-area sono copiate da padre e figlio, pertanto:
Siccome viene copiato il PC, padre e figlio riprendono l’esecuzione dal punto in cui è stata eseguita la fork().
E’ possibile comunque distinguere il padre dal figlio utilizzando il valore, intero, restituito dalla primitiva.
Implementazione della fork()
:
pid = fork();
if (pid = = -1) {
/* chiamata fallita */
}
else if (pid > 0) {
/* codice del padre */
wait();
}
else {
/* codice del figlio */
}
Consente al padre di raccogliere l’eventuale stato di terminazione dei figli:
int wait(int *stato)
Restituisce l’ID del processo figlio che è terminato. Se non esistono figli, restituisce –1:
int waitpid ( pid_t pid, int*stato, int options)
Se i figli non sono ancora terminati, il kernel sospende il processo padre finché uno dei figli non è terminato.
La variabile stato contiene il valore passato dal processo figlio alla system call exit.
Se un processo è terminato ma il suo genitore non ha ancora atteso (wait) la sua fine, il processo terminato viene definito processo zombie.
In tal caso il kernel rilascia tutte le risorse di tale processo tranne il suo stato di terminazione, per dargli la possibilità di ricongiungersi con il padre (wait).
Se il processo genitore termina prima dei suoi processi figli, attivi o zombie, tali processi sono detti orfani.
In tal caso Unix assegna all’ID del processo padre il valore 1, cioè diventano figli del processo init (non termina mai).
I figli non sono consapevoli della terminazione del processo padre.
Un processo termina con la chiamata di sistema exit.
Quando exit viene invocata, uno stato di uscita numerico intero viene passato dal processo al kernel. Tale valore è disponibile al processo padre attraverso la chiamata di sistema wait.
Un processo che termina normalmente restituisce uno stato di uscita 0.
Lo stato di terminazione è un intero a 16 bit.
Nel byte meno significativo contiene informazioni relative a come il figlio è terminato:
Nel caso in cui il figlio termini volontariamente, il byte più significativo contiene lo stato di terminazione (il valore del parametro attuale passato alla exit, 0 nell’esempio in figura).
L’unico modo in cui un programma può essere eseguito da Unix è che il processo esistente invii una chiamata di sistema exec (eccetto per il processo init).
Il nuovo programma viene eseguito nel contesto del processo chiamante, cioè il pid non cambia.
Fa ritorno al chiamante solo se si verifica un’errore, altrimenti il controllo passa al nuovo programma.
Esistono varie versioni della exec:
Il processo dopo l’exec:
if ((result=fork()) == 0) {
// codice figlio
...
if (execlp("program",...) perror("exec fallita");
exit(1);
}
} else if (result < 0){
perror("fork fallita");
}
// Il padre continua da questo
// punto in poi...
L’exec è anche nota come “sostituzione di codice”.
E’ lo stesso processo…
…ma esegue un programma differente !
Due possibili implementazioni:
Nel 99% dei casi, dopo una fork() viene eseguita una exec
vfork()
(BSD, Linux)
copy-on-write
(System V, Linux)
Disaccoppiare la fork e la exec dà la possibilità al programmatore di gestire il processo figlio solo a valle della sua creazione, in maniera completamente indipendente dal padre.
int pid = fork(); // crea il figlio
if(pid == 0) { // il figlio continua qui
// Op. qualsiasi (libera memoria, chiudi connessioni...)
execl("program", arg0, arg1, arg2, ...);
}
La capacità di creare nuovi processi è la chiave del funzionamento del timesharing in Unix:
Al terminale 0 non è presente nessun untente.
Al terminale 1, un utente ha appena terminato la fase di login.
Al terminale 2, un utente è già entrato nel sistema e sta eseguendo una copia di file.
Il programma login gestisce l’accesso degli utenti al sistema:
L’interprete dei comandi (shell):
while(1) {
Legge il nome del programma (arg0) da input
Legge gli argomenti associati al programma (arg1 ... argN)
int pid = fork(); // crea il figlio
if (pid == 0) { // il figlio continua qui
exec("programma", arg0, arg1, arg2, ...);
}
else { // il padre continua qui
...
}
1. Introduzione ai Sistemi Operativi
5. Scheduling nei sistemi mono-processore
6. Threads, SMP
8. Scheduling Multiprocessore e Real-Time
9. Gestione dei processi nei sistemi operativi Unix/Linux e Window...
10. Introduzione alla Programmazione Concorrente
11. Sincronizzazione nel modello ad ambiente globale
12. Problemi di cooperazione nel modello ad ambiente globale
14. Sincronizzazione nel modello ad ambiente locale
15. Deadlock
16. Programmazione Multithread
18. Memoria Virtuale
20. Il File System
21. Primitive di sincronizzazione nel kernel Linux
22. Esercitazione: System call per la gestione dei processi
23. Esercitazione: Inteprocess Communication e Shared Memory
24. Esercitazione: System Call per la gestione dei semafori in Linu...
25. Esercitazione: Problema dei Produttori e dei Consumatori
26. Posix Threads