Ogni nuovo processo viene creato a partire da un processo già esistente: il processo creante viene detto processo padre (in inglese parent process), mentre il nuovo processo è chiamato processo figlio (in inglese child process). Il sistema operativo assegna al processo appena creato risorse (come spazio di memoria e tempo CPU) e lo aggiunge alla coda dei processi.
La creazione di processi può essere uno strumento utile per suddividere un compito: per esempio, un server di rete può ascoltare le richieste da parte dei client e creare un nuovo processo figlio per gestire ciascuna richiesta, e nel frattempo continuare a restare in ascolto di ulteriori contatti da parte di altri client.
1 - Fasi principali della creazione di un processo
Le fasi principali della creazione di un processo sono:
Assegnazione di un PID: a ogni processo viene assegnato un identificatore univoco, il PID, che può essere usato come indice per accedere a vari attributi di un processo all’interno del kernel.
Allocazione delle risorse: vengono allocate le risorse necessarie per il nuovo processo, come lo spazio di memoria, i descrittori dei file aperti dal processo padre che possono essere ereditati o copiati da lui, il tempo di CPU e così via.
Inizializzazione del contesto: il sistema operativo crea un PCB per il processo figlio contenente tutte le informazioni di controllo necessarie, come lo stato del processo, i registri, i puntatori alla memoria, i file aperti e altro.
Inserimento nelle code di schedulazione: il processo viene aggiunto alla coda dei processi, cioè alla coda di schedulazione contenente tutti i processi nel sistema.
2 - Creazione di un processo tramite fork() in UNIX
In UNIX, la funzione fork() è una chiamata di sistema utilizzata da un processo padre per creare un processo figlio che sarà una copia (quasi) esatta del padre: ottiene copie di stack, dati, heap, e text segments del padre.
Il termine fork (in italiano biforcazione) deriva dal fatto che si può raffigurare il processo padre come un processo che si suddivide in due.
Il suo prototipo è il seguente:
#include <unistd.h>pid_t fork();
dove:
pid_t restituito: può assumere i seguenti valori:
0: la funzione ha eseguito correttamente l’operazione e il processo che riceve questo valore è il nuovo processo figlio.
pid_t≥0: la funzione ha eseguito correttamente l’operazione e il processo che riceve questo valore è il processo padre.
-1: c’è stato un errore durante l’esecuzione della funzione.
Dopo l’esecuzione della fork(), esisteranno due processi (il processo padre iniziale e il nuovo processo figlio) e in ciascuno dei due l’esecuzione riprende dal punto in cui la fork() restituisce il valore.
Nello spazio di indirizzamento del processo figlio viene creata una copia delle variabili del padre, col valore loro assegnato al momento della fork.
I due processi eseguono lo stesso testo, ma mantenendo copie distinte di stack, data, heap, buffer I/O. Stack, dati, heap, buffer I/O del figlio sono inizialmente esatti duplicati delle corrispondenti parti della memoria del padre. Dopo la fork(), ogni processo può modificare le variabili in tali segmenti senza influenzare l’altro processo.
2.1 - Come distinguere i due processi
All’interno del codice di un programma possiamo distinguere i due processi per mezzo del valore restituito dalla fork():
Nell’ambiente del padre, fork() restituisce il PID del figlio appena creato e proprio grazie al PID può tenere traccia del figlio. Per attenderne la terminazione può usare la wait() o altre syscall della stessa famiglia.
Un esempio di codice che si può usare per distinguere i due processi è il seguente:
#include <unistd.h>pid_t f = fork();switch (f) { case -1: // Errore nella fork // Gestione dell'errore case 0: // Processo figlio // Codice da far eseguire al processo figlio default: // Processo padre // Codice da far eseguire al processo padre}
2.2 - Quale processo sarà eseguito per primo dopo una fork()?
Dopo una fork(), è indeterminato quale dei due processi sarà scelto per ottenere la CPU.
In programmi scritti male, questa indeterminatezza può causare errori. Se invece abbiamo bisogno di garantire un particolare ordine di esecuzione, è necessario utilizzare una qualche tecnica di sincronizzazione (es. i semafori), oppure uno dei due processi può aspettare qualche secondo in modo che l’altro nel frattempo compia per primo le proprie azioni.
Esempio di processo padre che temporeggia
Un esempio di codice che si può usare per forzare l’esecuzione di un processo prima dell’altro è il seguente, in cui il processo padre aspetta 3 secondi prima di procedere, per far sì che il figlio nel frattempo compia per primo le proprie operazioni.
#include <unistd.h>pid_t f = fork();switch (f) { case -1: // Errore nella fork // Gestione dell'errore case 0: // Processo figlio // Codice da far eseguire al processo figlio default: // Processo padre sleep(3); // Il padre aspetta 3 secondi // Codice da far eseguire al processo padre}
2.3 - File sharing tra padre e figlio
All’esecuzione della fork(), il figlio riceve duplicati di tutti i descrittori di file del padre. Quindi, gli attributi di un file aperto sono condivisi fra padre e figlio. Per esempio, se il figlio aggiorna l’offset del file, tale modifica è visibile attraverso il corrispondente descrittore nel padre.
3 - Creazione di un processo tramite CreateProcess() in Windows
In Windows, la funzione CreateProcess() è una API utilizzata da un processo padre per creare un processo figlio e contemporaneamente un thread principale all’interno del processo.
A differenza della fork() in UNIX che duplica il processo corrente, CreateProcess() consente di specificare il nome dell’eseguibile che il processo figlio deve eseguire. Questo significa che, in Windows, il processo figlio inizia immediatamente con l’esecuzione di un programma distinto.
Esempio di uso di CreateProcess()
Un esempio di codice in linguaggio C che utilizza CreateProcess():
#include <windows.h>#include <stdio.h>int main() { STARTUPINFO si; PROCESS_INFORMATION pi; // Zero-fill the structures ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); ZeroMemory(&pi, sizeof(pi)); // Create a new process if (CreateProcess(NULL, // No module name (use command line) "C:\\Program.exe", // Command line NULL, // Process handle not inheritable NULL, // Thread handle not inheritable FALSE, // Set handle inheritance to FALSE 0, // No creation flags NULL, // Use parent's environment block NULL, // Use parent's starting directory &si, // Pointer to STARTUPINFO structure &pi) // Pointer to PROCESS_INFORMATION structure ) { printf("Processo creato con successo!\n"); printf("PID del processo figlio: %d\n", pi.dwProcessId); // Wait until child process exits WaitForSingleObject(pi.hProcess, INFINITE); // Close process and thread handles CloseHandle(pi.hProcess); CloseHandle(pi.hThread); } else { printf("Errore nella creazione del processo.\n"); } return 0;}
🏫 Lezioni e slide del Prof. Aldinucci Marco del corso di Sistemi Operativi (canale B), Corso di Laurea in Informatica presso l’Università di Torino, A.A. 2024-25:
🏫 Lezioni e slide del Prof. Schifanella Claudio del corso di Laboratorio di Sistemi Operativi (canale B, turno T4), Corso di Laurea in Informatica presso l’Università di Torino, A.A. 2024-25: