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:

  1. 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.
  2. 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.
  3. 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.
  4. 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 : 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.
  • Nell’ambiente del figlio, la fork() restituisce 0. Se necessario, il figlio può ottenere il proprio PID con la chiamata di sistema getpid() e il PID del processo padre con la chiamata di sistema getppid()

Esempio di distinzione tra i due processi

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

Fonti

  • Abraham Silberschatz, Peter Baer Galvin, Greg Gagne - Sistemi Operativi (10ᵃ Edizione) - Pearson, 2019 - ISBN: 9788891904560.
  • 🏫 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: