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
 
 
Il Corso Le lezioni del Corso La Cattedra
 
Materiali di approfondimento Risorse Web Il Podcast di questa lezione

Domenico Cotroneo » 22.Esercitazione: System call per la gestione dei processi


Roadmap

Identificativi

  • getpid()
  • getppid()

Alcune chiamate di sistema:

  • sleep()
  • fork()
  • exec()
  • wait()
  • exit()

System Calls per la gestione dei processi

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:

  • Creation and deletion: fork(), exec(), wait(), exit()
  • Process signaling: kill()
  • Process control: ptrace(), nice(), sleep()

I processi in Unix

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 ).

ID del processo (PID)

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();

Organizzazione Gerarchica

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();

getpid() e getppid() – Codice

// 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;

}

ID di processi speciali

Processo scheduler : pid=0

Processo init : pid=1

Processo pagedaemon : pid=2

Funzione: sleep

Sospende (transizione stato Blocked) un processo per un certo numero di secondi.

sleep(int sec)

System Call fork

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:

  • il figlio eredita gli stessi valori delle variabili, i descrittori agli stessi file aperti, e anche il Program Counter!
  • le modifiche apportate alle proprie variabili da uno dei due processi non sono visibili all’altro.

System Call fork

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():

  • allocazione memoria per il processo figlio;
  • copia della memoria e dei registri del processo padre su quello figlio;
  • Expensive!!

System Call fork

pid = fork();

if (pid = = -1) {

/* chiamata fallita */

}
else if (pid > 0) {

/* codice del padre */

wait();

}
else {

/* codice del figlio */

}


System Call wait

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)

System Call wait

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).

System Call 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.

System Call exit

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.

Wait e Exit

Lo stato di terminazione è un intero a 16 bit.

Nel byte meno significativo contiene informazioni relative a come il figlio è terminato:

  • =0: volontariamente;
  • _0: involontariamente (il valore esprime il segnale ricevuto).

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).


System Call exec

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:

  • int execlp (char *nomefile, char *arg0, .., char *argn, (char *) 0);
  • int execl (char *pathname, char *arg0, .., char *argn, (char *) 0);
  • etc.

System Call exec

Il processo dopo l’exec:

  • mantiene la stessa process structure;
  • ha codice, dati globali, stack e heap nuovi;
  • riferisce una nuova text structure;
  • mantiene user area (a parte PC e informazioni legate al codice) e stack del kernel.

Fork e 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...


Program Loading: exec()

L’exec è anche nota come “sostituzione di codice”.

E’ lo stesso processo…
…ma esegue un programma differente !

Due possibili implementazioni:

  • sovra-scrittura del segmento di memoria corrente con nuovi valori;
  • allocazione di nuovi segmenti di memoria, inizializzazione di questi con i valori del nuovo processo e deallocazione dei segmenti “vecchi”.

Una shell fa la “fork” ed esegue calc con “exec”


La sequenza fork/exec

Nel 99% dei casi, dopo una fork() viene eseguita una exec

  • L’operazione di copia della memoria tra padre e figlio è nella maggior parte dei casi sprecata.
  • L’overhead dunque è consistente.
  • Perché non combinare fork ed exec in un’unica system call (OS/2, Windows)?

vfork() (BSD, Linux)

  • Una system call che crea un processo senza copiare l’immagine dal padre al figlio.
  • Spesso chiamata lightweight fork().
  • Il proceso figlio dovrà subito invocare la exec?

copy-on-write (System V, Linux)

  • Inizialmente il figlio condivide la memoria del padre, configurata come read-only.
  • Al primo tentativo di modifica, il kernel provvede ad effettuare la copia.

Perchè due primitive diverse ?

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, ...);

}

Esempio: relazioni fra processi

La capacità di creare nuovi processi è la chiave del funzionamento del timesharing in Unix:

  • in seguito al bootstrap, il kernel crea un processo detto init;
  • init legge il file /etc/inittab, per determinare il numero di terminali del sistema da attivare;
  • per ogni terminale, init genera un figlio che esegue il programma /bin/login passando il nome di login dell’utente come argomento;
  • il programma login cerca il nome di login nel file /etc/password ed eventualmente visualizza un prompt per richiedere la password.

Relazioni fra processi – Schema concettuale

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.


Relazioni fra processi – login

Il programma login gestisce l’accesso degli utenti al sistema:

  • chiede username e password;
  • verifica che la password (crittata) coincida con quella contenuta nel file /etc/passwd;
  • in caso affermativo, esegue il programma shell, vale a dire l’interprete dei comandi.

Relazioni fra processi – shell

L’interprete dei comandi (shell):

  • legge la linea di comando;
  • estrae la prima parola, assumendo che sia il nome di un programma;
  • cerca il programma in questione e lo lancia in esecuzione (mediante fork ed exec), passandogli come argomenti le altre parole presenti sulla linea di comando;
  • attende che il figlio termini, prima di presentare nuovamente il prompt d’utente.

Esercizio: The Unix 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
...

}

  • 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