Salta al contenuto
Note per Studenti Esercizio - Creazione di processi con fork, pid e processi orfani (laboratorio 2)

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 con fork; il figlio lancia ls con execlp; il padre aspetta con wait. Compilare, eseguire, spiegare l'output.
  • Task 2 (newproc2.c): padre e figlio stampano il valore restituito da fork e 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? Con ps -e e pstree cercare il processo orfano: chi è il suo nuovo padre? Rimuoverlo con kill o killall.

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

newproc1.cc
#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 fork i processi sono due; nel figlio fork restituisce 0, nel padre il pid del figlio (qui 1234, un valore qualunque).
  • Il padre dorme 10 ms con usleep per rendere regolare l'ordine: il figlio stampa e lancia ls per primo. Senza la pausa l'ordine delle prime righe non è garantito (processi concorrenti).
  • execlp sostituisce il programma del figlio con ls: dopo di essa non si torna al codice del figlio (il printf dopo execlp non verrebbe mai eseguito se exec riesce).
  • Il padre, con wait(NULL), aspetta la fine di ls e 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

newproc2.cc
#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 = 4320

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

newproc3.cc
#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 (o ps -e | grep newproc3) mostra il processo newproc3 ancora vivo, con il suo pid (quello stampato come [child] current pid). È il figlio orfano: il padre è terminato prima di lui.
  • pstree (o pstree -p) mostra che il suo nuovo padre è systemd (pid 1), cioè il processo init del sistema, che adotta gli orfani; in alcuni sistemi è il systemd dell'utente (un subreaper di sessione).
  • Per eliminarlo: kill PID (con il pid ottenuto da ps -e) oppure killall newproc3. Un nuovo ps o pstree conferma 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

Teoria collegata