Processi: Stati, PCB e Scheduling
Dal file sul disco al processo vivo in RAM: cosa fa davvero Windows quando apri chrome.exe, come gestisce gli stati, il PCB e la corsa dei thread sulla CPU.
Ogni volta che fai doppio click su un eseguibile, il sistema operativo compie un piccolo miracolo che diamo per scontato: trasforma un file inerte in un'entità viva, con memoria, identità, priorità e diritto di chiedere tempo di CPU. Questa è la lezione fondante di Sistemi e Reti del quarto anno.
1. Programmi e processi: la differenza fondamentale
Programma = file statico su disco. Processo = programma in esecuzione, dinamico, con il suo spazio di indirizzamento, registri, PID e risorse assegnate dal SO.
Il programma è statico e passivo: chrome.exe è una sequenza di byte sul disco. Non fa nulla finché non lo carichi. Il processo è l'esatto opposto: dinamico e attivo. Il SO gli cuce addosso 4 aree di memoria — codice (read-only), dati, heap (malloc/new), stack (variabili locali, chiamate).
Io ho pensato di fare così per farvelo ricordare all'interrogazione: aprite Task Manager → Dettagli. Se aprite 3 volte Notepad++ vedrete 3 PID diversi, stesso file .exe. Io lo farei così per spiegarlo alla Maturità: "Il programma è la ricetta nel cassetto, il processo è quando sto cucinando davvero, con pentole (RAM) accese e timer (CPU) che gira". Se dite solo "sono la stessa cosa" vi bocciano la domanda sul PCB.
Stesso file C:\Program Files\Chrome\chrome.exe. Tre doppie finestre = tre processi separati, tre stack, tre PID. Se uno crasha, gli altri restano su. Questo isolamento è il motivo per cui il SO preferisce i processi ai semplici programmi.
2. Multiprocessing, multiprogrammazione e multitasking
Una CPU single-core esegue UNA istruzione alla volta. Come sembra che ne faccia 50? Con il time-sharing: assegna a ogni processo un quantum di 2-10 ms, poi context switch. Velocissimo da sembrare parallelo.
| Termine | Significato | Obiettivo / Quando |
|---|---|---|
| Multiprogrammazione | Più processi residenti in RAM: se uno si blocca su I/O, la CPU passa ad un altro | Massimizzare utilizzo CPU (anni '60) |
| Multitasking / Time-sharing | Switch frequentissimi (quantum piccoli) per dare illusione di simultaneità all'utente | Reattività verso l'utente |
| Multiprocessing | Più core/CPU fisiche: vero parallelismo, 8 core = 8 processi davvero simultanei | Throughput reale |
Io ho pensato di fare così quando correggo le verifiche: se confondete multiprogrammazione con multiprocessing vi metto 1 punto in meno. Io lo farei così per ricordarlo: "Multiprogrammazione = più programmi in RAM, Multiprocessing = più CPU reali". Oggi convivono: su ogni core c'è time-sharing, tra core c'è parallelismo vero. Guardate Task Manager > Prestazioni > CPU: vedete i core logici che schizzano.
Ogni cambio di contesto costa: salvare registri, PC, stack pointer del PCB uscente e caricare quelli entranti. Quantum troppo piccolo = più reattività ma più overhead. Quantum troppo grande = effetto convoglio. È il compromesso che Windows deve tarare.
3. Il ciclo di vita: gli stati di un processo
Un processo non è mai sempre RUNNING. Lo scheduler lo sposta tra 5 stati ben definiti:
| Stato | Cosa significa davvero |
|---|---|
| New | Creato, il SO sta allocando PCB, pagine, handle. Non ancora in ready queue. |
| Ready | In RAM, tutto pronto, aspetta solo la CPU. Sta nella ready queue. |
| Running | Istruzioni effettivamente sul core in questo istante. Monocore = 1 solo Running. |
| Waiting / Blocked | Bloccato su evento esterno (disco, rete, mutex). Non consuma CPU. |
| Terminated | Finito. Risorse rilasciate, resta zombie finché il padre non fa wait() sul suo exit code. |
Apri un file di 2GB. Il processo chiede lettura. Se restasse in Running sprecherebbe CPU ad aspettare il disco. Passa in Waiting, lo scheduler dà la CPU a Chrome, Teams, etc. Quando il disco finisce, interrupt → I/O completato → torna Ready. Così non sprechi cicli.
4. Il Process Control Block (PCB)
Il PCB è la carta d'identità del processo: la struttura dati con cui il SO congela e ripristina esattamente un processo. Contiene PID, stato, PC, registri, priorità, memoria, file aperti.
| Campo del PCB | Contenuto pratico | Perché serve nel vostro lab |
|---|---|---|
| PID | ID univoco (es. 4256) | Per kill, debug, analisi con Process Explorer |
| Stato | New/Ready/Running/Waiting/Terminated | Determina in quale coda metterlo |
| Program Counter | Prossima istruzione EIP/RIP | Senza, al rientro ripartiresti da capo |
| Registri CPU | EAX, EBX, SP, flags... | Context switch = salva/ripristina questi |
| Info scheduling | Priorità, puntatori ready queue | Decide chi vince la CPU |
| Memoria | Page table base, limiti | Isolamento tra processi |
| File/I/O | Handle file, socket | Chiusura risorse su Terminated |
Io ho pensato di fare così a lezione di Tecnologia: vi faccio vedere Process Hacker, aperto su chrome. Clicco su un processo e vi mostro: PID, threads, handles. Io lo farei così all'interrogazione: "Prof, il PCB è come il salvataggio di un videogioco: se staccate la corrente (interrupt), quando riaccendete (dispatch) ripartite esattamente da dove eravate, perché avevate salvato tutto". Se dite questa frase, vi do il mezzo punto in più.
Più il PCB è grande, più il context switch costa tempo di CPU buttato in salvataggi. Ecco perché i thread sono "leggeri": il loro PCB salva solo registri e stack, non tutta la memoria virtuale.
5. Le politiche di scheduling
Ready queue piena, CPU libera: chi scegli? Lo decide lo short-term scheduler. Non esiste l'algoritmo perfetto, solo compromessi.
| Algoritmo | Come funziona | Pro / Contro da interrogazione |
|---|---|---|
| FCFS First-Come First-Served | Non preemptive, FIFO fino a fine | Semplicissimo, ma effetto convoglio: P lungo blocca tutti |
| SJF Shortest Job First | Sceglie burst più breve (stimato) | Ottimo tempo medio attesa, ma serve predizione e causa starvation processi lunghi |
| Round Robin | FCFS + quantum q. Se non finisce, torna in coda | Equo, reattivo. q piccolo = tanti switch, q grande = torna FCFS |
| A priorità | Priorità numerica esterna, preemptive o no | Gestisce urgenze, ma starvation priorità basse → serve aging |
Calcolo metriche: l'esempio che esce sempre
P1=24ms, P2=3ms, P3=3ms, arrivano tutti a t=0 in ordine P1,P2,P3.
Tempo medio attesa
P1 0ms, P2 24ms, P3 27ms → media = (0+24+27)/3 = 17ms. P2 e P3 lentissimi per colpa di P1.
Ottimo teorico
Ordine P2,P3,P1 → attese: P2 0, P3 3, P1 6 → media = 3ms. Stratosferico, ma devi conoscere le durate prima.
Timeline: 0-4 P1 (rimangono 20), 4-7 P2 finisce (attesa 4), 7-10 P3 finisce (attesa 7), 10-30 P1 finisce (attesa = 4+3 =7? No: intervalli non-running = 6ms totali perché tra 4-10 non gira). Attese: P1 6ms, P2 4ms, P3 7ms → media ≈5,7ms. Più equo: P2/P3 completano entro 10ms invece di aspettare 24+.
SJF e priorità puri: se arrivano sempre processi brevi/alta priorità, quello lungo/bassa priorità muore di fame. Soluzione: aging = priorità effettiva cresce con attesa. Windows lo fa con priority boost.
Io ho pensato di fare così per la verifica scritta: vi do 3 processi con burst e vi chiedo FCFS vs RR. Io lo farei così: disegnate la timeline su carta, segnate ogni quantum, NON fate a mente. L'errore più stupido che vedo ogni anno è sommare i burst invece dei tempi di attesa. Ricordate: tempo di attesa = tempo in Ready, non tempo totale.
6. Il thread: un processo che si moltiplica
Thread = flusso leggero dentro un processo. Ha PC, registri, stack suoi. Condivide codice, dati globali, heap, file aperti con gli altri thread dello stesso processo.
Browser: un thread rendering, uno JS, uno network → stesso processo, stessa cache, ma corrono paralleli. Perché thread e non processi? Costo.
| Caratteristica | Processo pesante | Thread leggero |
|---|---|---|
| Memoria | Isolata, propria page table | Condivisa nello stesso processo |
| Comunicazione | IPC lenta (pipe, socket, shared mem) | Diretta via variabili condivise (veloce) |
| Costo creazione | Alto: copia spazio indirizzamento | Basso: riusa spazio |
| Context switch | Pesante (PCB completo) | Leggero (solo registri+stack) |
| Crash | Isolato | Un thread buca tutto il processo |
4 motivi per multithreading
Reattività UI non si blocca, Condivisione no duplicazione RAM, Economicità creazione/switch rapidi, Scalabilità vera su multi-core.
Race condition
2 thread fanno contatore++ da 0. Leggono entrambi 0, scrivono entrambi 1 → risultato 1 invece di 2. Serve mutex/semaforo (prossima lezione).
Condividere memoria senza sincronizzazione è come due persone che modificano lo stesso file Google Docs offline e poi sincronizzano: l'ultimo sovrascrive. In verifica: se vedete "variabile condivisa senza mutex" rispondete sempre race condition.
Io ho pensato di fare così per l'orale: portate l'esempio del browser multithread. Io lo farei così: "Prof, se il thread JS si blocca su un ciclo infinito, la scheda si freeza ma il processo browser può ancora chiuderla perché il thread UI è separato". Così dimostrate di aver capito isolamento parziale vs processi.
7. Multithreading e scheduling in Windows
Windows: ogni processo nasce con almeno 1 thread primario. Ne può creare altri con Win32 API. Scheduler preemptive a priorità: vince sempre il thread Ready con priorità numerica più alta, anche interrompendo quello Running.
Da 0 (zero page thread) a 31 (real-time). Processo ha 6 classi: Idle, Below Normal, Normal, Above Normal, High, Realtime. Thread ha priorità relativa dentro la classe (Idle → Time-critical). Effettiva = classe + relativa.
| Stato thread Windows | Significato |
|---|---|
| Ready | In coda, aspetta scheduler |
| Standby | Scelto come prossimo su quel core |
| Running | Sta girando sulla CPU ora |
| Waiting | Aspetta I/O, evento, timer, mutex |
| Terminated | Finito, handle chiudibile |
Thread esce da Waiting (es. lettura disco finita) → riceve bonus temporaneo di priorità, gira subito, poi bonus decade ogni quantum. Così l'app che stava caricando torna subito viva.
Io ho pensato di fare così in lab: compilatelo con x64 Native Tools, aprite Process Explorer e guardate il thread nascere. Io lo farei così per farlo sembrare vostro: spiegate che WaitForSingleObject mette il chiamante in Waiting senza busy-wait, quindi zero CPU sprecata. Nelle app moderne si usa thread pool (QueueUserWorkItem) per non creare/distruggere thread ogni volta: riuso = meno overhead.
8. Verifica e laboratorio mentale
🔬 Attività – carta e penna (da saper fare senza PC)
Esercizi che escono in verifica scritta, 15 minuti:
- Disegna il diagramma New→Ready→Running→Waiting→Terminated con le 5 frecce etichettate (ammesso, dispatch, interrupt, I/O request, I/O completato, exit).
- Dati P1=10ms, P2=1ms, P3=2ms: calcola tempo medio attesa con FCFS e con RR q=2ms. Spiega dove conviene RR.
- Spiega perché il PCB rende costoso il context switch tra processi e meno costoso quello tra thread.
- Codice: 2 thread fanno counter++ 1000 volte senza mutex. Quale valore finale ti aspetti? Perché può essere <2000?
- Windows: un thread Normal con priorità Above Normal appena uscito da Waiting. Cosa fa il priority boost e perché?
1) Guardate svg sopra. 2) FCFS: 0+10+11=21/3=7ms. RR q2: timeline 0-2 P1,2-3 P2 fine(attesa2),3-5 P3 fine(attesa3),5-13 P1 → attese P1 3ms, P2 2ms, P3 3ms → media 2.66ms → RR vince su reattività. 3) PCB processo salva page table, file table; thread solo registri/stack → meno byte copiati. 4) Race: read-modify-write non atomico, incremento perso. Valore atteso <2000, non deterministico. 5) Boost temporaneo, poi decade → reattività percepita, poi torna a base.
Vuoi passare all'azione? Accedi alla repository ufficiale GitHub del corso per scaricare i file dei laboratori pratici in formato .md.
Vuoi consultare la sezione dedicata ai laboratori sul sito? Accedi cliccando il pulsante qui sotto.
📊 Sezione dedicata ai Laboratori