Processi: un intero mondo sotto il cofano di macOS!

Nel precedente articolo abbiamo terminato il discorso sandbox. Oggi iniziamo un nuovo argomento: ogni istanza di un’applicazione in esecuzione costituisce un processo: discuteremo quindi la prospettiva dei processi dal punto di vista dell’utente, iniziando dal loro formato “eseguibile” proseguendo attraverso il processo di caricamento in memoria.

processi

Preambolo

Proprio come qualsiasi altro sistema multitasking, Unix è stato costruito attorno al concetto
di processo come istanza di un programma in esecuzione. Tale istanza è definita in modo univoco da un ID di processo (che definiremo come PID). Anche avviando uno stesso eseguibile più volte, contemporaneamente, ogni istanza del programma avrà un PID diverso.  Un processo, può a sua volta generare un altro processo come fosse una sorta di parentela padre/figlio mantenendo per tutta la durata della sua vita la parentela con il processo genitore – attraverso un altro ID univoco, cioè il PID del processo padre: il PPID.

I sistemi operativi, attualmente, non trattano più i processi come delle singole unità operative di base (atomiche), ma funzionano attraverso i thread. A cosa servono i thread?

Se, in fase di creazione di un’app si ha la necessità di eseguire due azioni contemporaneamente, risulta impossibile data la natura sequenziale dei processi. Si dovrebbero creare due processi (che per definizione hanno memoria e codice separati) che dovrebbero continuamente scambiarsi dati, introducendo una forte complessità.  E’ qui che vengono in aiuto i thread: sono dei sottoprocessi chiamati anche “flussi di controllo”, che eseguono azioni per uno stesso processo in modo contemporaneo.

Il ciclo di vita di un processo

L’intero ciclo di vita di un processo in macOS, può essere illustrato grazie alla figura sottostante.

Un processo inizia la sua vita nello stato SIDL, che rappresenta lo stato momentaneamente inattivo, appena nato da un processo padre. In questo stato, il processo viene ancora definito come in fase di inizializzazione e non risponde a nessun segnale né esegue alcuna azione mentre dalla memoria e vengono caricate le dipendenze richieste per l’esecuzione. Una volta pronto, il processo può iniziare l’esecuzione scappando per sempre dallo SIDL. Un processo in SIDL è sempre a thread singolo, poiché i thread possono essere generati solo in seguito.

Quando un processo è in esecuzione è nello stato SRUN. Questo stato, tuttavia, è costituito da due stati distinti: eseguibile e in esecuzione. Lo definiamo eseguibile solo se è in coda per l’esecuzione, ma non è in esecuzione, poiché la CPU è occupata con altri processi: il problema è che il kernel non si preoccupa di distinguere tra i due stati distinti; un processo in esecuzione può anche essere espulso (passatemi il termine) dalla CPU e tornare in coda in seguito, o se un altro processo con priorità più alta lo sovrasta.

Un processo, comunque, trascorre la maggior parte del suo tempo a cavallo tra lo stato in esecuzione/eseguibile di SRUN, a meno che non sia in attesa di una risorsa. In questo contesto, una “risorsa” solitamente è legata all’I/O (come ad esempio un file o un dispositivo). Quando un processo è in attesa, non ha senso fargli ottenere la CPU, o addirittura considerarlo in coda di esecuzione: è svantaggioso perché c’è spreco di risorse, quindi viene messo a dormire ed entra nello stato SSLEEP. A questo punto il processo si addormenta, fino a quando la risorsa diventa disponibile; a quel punto
verrà nuovamente messo in coda per l’esecuzione con una priorità piuttosto elevata. Ovviamente un processo in stato SSLEEP può anche essere svegliato da un qualsiasi segnale.

Il vantaggio principale del multithreading (che indica il supporto hardware da parte di un processore di eseguire più thread) è che i singoli stati di ogni thread possono divergere l’uno dall’altro. Quindi, mentre un thread sta “dormendo”, un altro può essere in esecuzione sulla CPU. I thread trascorrono la loro vita tra lo stato eseguibile/esecuzione e inattivo (o “bloccato”).
Utilizzando un segnale speciale (TSTOP o TOSTOP) è possibile interrompere un processo oppure congelarlo (sospendendo simultaneamente tutti i suoi thread) mettendolo in uno stato di sonno profondo. L’unico modo per riprendere un processo, riportandolo “in vita” è attraverso l’uso di un altro segnale (CONT), che riporta il processo in uno stato eseguibile, consentendo nuovamente la programmazione di uno dei suoi thread.

Al termine dell’esecuzione di un processo, mediante un ritorno del main() dell’app, o attraverso la funzione exit(2), il processo stesso viene cancellato dalla memoria, morendo definitivamente. Quest’operazione terminerà tutti i thread simultaneamente: prima di poter eseguire questa operazione, il processo deve passare brevemente del tempo nello stato di zombie, che vedremo nel prossimo articolo.

Anche per questo articolo è tutto. Per eventuali domande, curiosità o feedback potete lasciare un commento qui in basso, a presto!

 

NovitàAcquista il nuovo iPhone 16 su Amazon
Approfondimenti