Esercizio - Creazione di processi con fork, pid e processi orfani (laboratorio 2)
In questa pagina 4
Testo (laboratorio 2 del corso di Sistemi Operativi, Ing. Informatica UniPD). Su Linux:
- Task 1 (
newproc1.c): il padre crea un figlio confork; il figlio lancialsconexeclp; il padre aspetta conwait. Compilare, eseguire, spiegare l'output. - Task 2 (
newproc2.c): padre e figlio stampano il valore restituito daforke il proprio pid (getpid). Quale relazione c'è tra "[parent] res" e "[child] current pid"? - Task 3 (
newproc3.c): modificare il programma perché il figlio entri in un ciclo infinito e il padre termini senza aspettarlo. Il programma termina? Conps -eepstreecercare il processo orfano: chi è il suo nuovo padre? Rimuoverlo conkillokillall.
Teoria: Processi - stati, PCB, creazione con fork ed execUn processo è un programma in esecuzione (entità attiva) con codice (text), dati globali, heap, stack e lo stato dell'esecuzione; il kernel lo descrive con il PCB (Process Control Block: stato, program counter, registri, informazioni di scheduling, memoria, accounting, file aperti, in Linux la struct task_struct) e passa da un processo all'altro con il context switch (puro overhead); gli stati sono new, ready, running, waiting, terminated, con una ready queue e varie wait queue; in UNIX fork() crea un figlio copia del padre (restituisce 0 al figlio e il pid del figlio al padre), exec() sostituisce il programma del figlio, wait()/waitpid() raccoglie lo stato di uscita: senza di essa un figlio terminato è uno zombie, un figlio il cui padre è morto è un orfano adottato da init/systemd; i processi cooperanti comunicano con memoria condivisa (POSIX shm) o scambio di messaggi (diretto o indiretto, sincrono o asincrono, buffer a capacità zero, limitata, illimitata) e con pipe ordinarie (padre-figlio, unidirezionali) e named pipe (FIFO).Processi - stati, PCB, creazione con fork ed exec →; le chiamate di sistema sono quelle di System call POSIX, file descriptor e API delle socketLe system call sono le funzioni con cui un programma in user space chiede servizi al kernel (open, read, write, close, fork, pipe, dup2, socket, bind, listen, accept, connect); restituiscono -1 e impostano errno in caso di errore; un file descriptor e' un intero che indicizza la tabella dei file aperti del processo (0 stdin, 1 stdout, 2 stderr) e vale per file, pipe e socket; read e write possono trasferire MENO byte del richiesto (e sui socket TCP non c'e' nessun confine fra i messaggi), quindi servono cicli write_all e read_exact; l'API delle socket crea un punto finale di comunicazione (socket), lo lega a indirizzo e porta (bind), lo rende passivo (listen), accetta connessioni (accept) o si connette (connect); getaddrinfo traduce nomi e porte in indirizzi (IPv4 e IPv6); UDP usa sendto e recvfrom e conserva i confini dei datagrammi, TCP e' uno stream affidabile.System call POSIX, file descriptor e API delle socket →. Compilazione: gcc newproc1.c -o newproc1, esecuzione con ./newproc1.
Task 1: fork + exec + wait
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void)
{
pid_t pid = fork(); /* crea il figlio */
if (pid < 0) { /* errore */
fprintf(stderr, "fork failed\n");
return 1;
} else if (pid == 0) { /* FIGLIO */
printf("[child] fork returned %d\n", pid);
execlp("/bin/ls", "ls", (char *)NULL); /* sostituisce il programma con ls */
perror("execlp"); /* solo se exec fallisce */
return 127;
} else { /* PADRE */
usleep(10000); /* attende 10 ms */
printf("[parent] fork returned %d\n", pid);
wait(NULL); /* aspetta il figlio */
printf("[parent] child has finished\n");
}
return 0;
}Output atteso (su terminale):
[child] fork returned 0
<elenco dei file della directory, stampato da ls>
[parent] fork returned 1234
[parent] child has finished- Dopo la
forki processi sono due; nel figlioforkrestituisce 0, nel padre il pid del figlio (qui 1234, un valore qualunque). - Il padre dorme 10 ms con
usleepper rendere regolare l'ordine: il figlio stampa e lancialsper primo. Senza la pausa l'ordine delle prime righe non è garantito (processi concorrenti). execlpsostituisce il programma del figlio conls: dopo di essa non si torna al codice del figlio (ilprintfdopoexeclpnon verrebbe mai eseguito seexecriesce).- Il padre, con
wait(NULL), aspetta la fine dilse stampa l'ultima riga.
Attenzione al buffering. Se si reindirizza l'output (./newproc1 > out.txt o in una pipe), stdout è bufferizzato a blocchi e non a righe: la riga [child] ... è ancora nel buffer di stdio quando execlp sostituisce il processo, e il buffer viene perso: nel file mancherebbe la riga del figlio (e le righe del padre comparirebbero solo all'uscita). Si evita con fflush(stdout) prima di fork / di execlp.
Task 2: valore di fork e pid
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void)
{
pid_t res = fork();
if (res < 0) {
fprintf(stderr, "fork failed!\n");
return 1;
} else if (res == 0) { /* figlio */
printf("[child] res = %d\n", res); /* sempre 0 */
printf("[child] current pid = %d\n", getpid()); /* il pid del figlio */
} else { /* padre */
usleep(10000);
printf("[parent] res = %d\n", res); /* il pid del figlio */
printf("[parent] current pid = %d\n", getpid()); /* il pid del padre */
wait(NULL);
}
return 0;
}Esempio di output:
[child] res = 0
[child] current pid = 4321
[parent] res = 4321
[parent] current pid = 4320Risposta alla domanda. [parent] res e [child] current pid sono uguali (4321 nell'esempio): il valore restituito da fork al padre è proprio l'identificatore del figlio, che il figlio vede con getpid(). Invece [child] res vale 0 (non è un pid ma un segnale "sono il figlio") e [parent] current pid è il pid del padre. Per ottenere il pid del padre dal figlio esiste getppid().
Task 3: il processo orfano
La modifica richiesta: nel figlio while (1); dopo le stampe, e nel padre niente wait (wait(NULL) commentata).
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
pid_t res = fork();
if (res < 0) {
fprintf(stderr, "fork failed!\n");
return 1;
} else if (res == 0) { /* figlio */
printf("[child] res = %d\n", res);
printf("[child] current pid = %d\n", getpid());
fflush(stdout);
while (1)
; /* ciclo infinito */
} else { /* padre */
usleep(10000);
printf("[parent] res = %d\n", res);
printf("[parent] current pid = %d\n", getpid());
/* wait(NULL); <- non aspetta il figlio */
}
return 0;
}- Il programma (il padre) termina subito: nella shell compare di nuovo il prompt. Ma il figlio continua a girare in un ciclo infinito (consuma una CPU al 100%).
- In un secondo terminale,
ps -e(ops -e | grep newproc3) mostra il processonewproc3ancora vivo, con il suo pid (quello stampato come[child] current pid). È il figlio orfano: il padre è terminato prima di lui. pstree(opstree -p) mostra che il suo nuovo padre èsystemd(pid 1), cioè il processoinitdel sistema, che adotta gli orfani; in alcuni sistemi è ilsystemddell'utente (un subreaper di sessione).- Per eliminarlo:
kill PID(con il pid ottenuto daps -e) oppurekillall newproc3. Un nuovopsopstreeconferma che è sparito.
Questo mostra la differenza tra orfano (padre morto prima, adottato da init/systemd) e zombie (figlio morto non ancora raccolto dal padre, v. Processi - stati, PCB, creazione con fork ed execUn processo è un programma in esecuzione (entità attiva) con codice (text), dati globali, heap, stack e lo stato dell'esecuzione; il kernel lo descrive con il PCB (Process Control Block: stato, program counter, registri, informazioni di scheduling, memoria, accounting, file aperti, in Linux la struct task_struct) e passa da un processo all'altro con il context switch (puro overhead); gli stati sono new, ready, running, waiting, terminated, con una ready queue e varie wait queue; in UNIX fork() crea un figlio copia del padre (restituisce 0 al figlio e il pid del figlio al padre), exec() sostituisce il programma del figlio, wait()/waitpid() raccoglie lo stato di uscita: senza di essa un figlio terminato è uno zombie, un figlio il cui padre è morto è un orfano adottato da init/systemd; i processi cooperanti comunicano con memoria condivisa (POSIX shm) o scambio di messaggi (diretto o indiretto, sincrono o asincrono, buffer a capacità zero, limitata, illimitata) e con pipe ordinarie (padre-figlio, unidirezionali) e named pipe (FIFO).Processi - stati, PCB, creazione con fork ed exec →): se nel padre ci fosse wait, il padre aspetterebbe all'infinito il figlio che non termina mai.
I tre programmi sono stati compilati con zig cc -Wall -Wextra -Wpedantic per Linux senza avvisi (il corso usa l'equivalente gcc); non sono stati eseguiti qui.
Domande d'esame
Dopo una fork cosa restituisce nel padre e nel figlio? Come si distinguono i due processi?
Traccia. Nel figlio 0, nel padre il pid del figlio, -1 in caso di errore; si distinguono dal valore di ritorno.
Cos'è un processo orfano e chi lo adotta?
Traccia. Un figlio il cui padre è terminato prima di lui: viene adottato da init/systemd (pid 1), che ne raccoglie la fine.
Versione ripasso
- Task 1 (
fork+execlp("ls")+wait): output[child] fork returned 0, elenco dils,[parent] fork returned <pid>,[parent] child has finished.usleep(10000)nel padre rende l'ordine regolare.execsostituisce il programma. Teoria: Processi - stati, PCB, creazione con fork ed execUn processo è un programma in esecuzione (entità attiva) con codice (text), dati globali, heap, stack e lo stato dell'esecuzione; il kernel lo descrive con il PCB (Process Control Block: stato, program counter, registri, informazioni di scheduling, memoria, accounting, file aperti, in Linux la struct task_struct) e passa da un processo all'altro con il context switch (puro overhead); gli stati sono new, ready, running, waiting, terminated, con una ready queue e varie wait queue; in UNIX fork() crea un figlio copia del padre (restituisce 0 al figlio e il pid del figlio al padre), exec() sostituisce il programma del figlio, wait()/waitpid() raccoglie lo stato di uscita: senza di essa un figlio terminato è uno zombie, un figlio il cui padre è morto è un orfano adottato da init/systemd; i processi cooperanti comunicano con memoria condivisa (POSIX shm) o scambio di messaggi (diretto o indiretto, sincrono o asincrono, buffer a capacità zero, limitata, illimitata) e con pipe ordinarie (padre-figlio, unidirezionali) e named pipe (FIFO).Processi - stati, PCB, creazione con fork ed exec →. - Buffering: con
stdoutreindirizzato la riga del figlio può andare persa all'exec:fflush(stdout). - Task 2:
[parent] res[child] current pid(laforkrestituisce al padre il pid del figlio);[child] res = 0;getppid()dà il pid del padre. - Task 3: figlio
while(1);, padre senzawait: il padre termina, il figlio resta: orfano, adottato dasystemd(pid 1);ps -e,pstree,kill PID/killall newproc3. - Codice Task 1:
pid_t pid = fork(); if (pid<0) {fprintf(stderr,...); return 1;} else if (pid==0) {printf("[child] fork returned %d\n", pid); execlp("/bin/ls","ls",(char*)NULL);} else {usleep(10000); printf("[parent] fork returned %d\n", pid); wait(NULL); printf("[parent] child has finished\n");}. - Output atteso Task 2:
[child] res = 0,[child] current pid = 4321,[parent] res = 4321,[parent] current pid = 4320(i pid sono esempi): il valore diforknel padre è il pid del figlio. - Task 3, passi: figlio con
while(1);, padre conwaitcommentata; il padre termina subito; in un altro terminaleps -e(pid del figlio),pstree(padre =systemd),kill PIDokillall newproc3, verifica conps/pstree. - Orfano e zombie: orfano = padre morto, figlio vivo, adottato da
systemd(pid 1, o subreaper di sessione); zombie = figlio morto non raccolto. Conwaitnel padre il programma non terminerebbe mai (il figlio è infinito). - Compilazione ed esecuzione:
gcc newprocN.c -o newprocN,./newprocN; i tre programmi sono compilati conzig ccsenza avvisi, non eseguiti. - Errori tipici: pensare che il codice dopo
execgiri; scambiare pid del padre e del figlio; non vedere chewaitnel padre bloccherebbe per sempre.