1. Modelli di threading

Thread a livello utente (User-Level Threads, ULT)

  • Gestiti interamente da una libreria in user space (es. vecchie “green threads”), senza che il kernel ne sappia nulla
  • Il kernel vede un solo processo/task — nessuna visibilità sui thread creati internamente
  • Lo scheduling tra thread avviene interamente in user space, gestito dalla libreria

Vantaggi: creazione/switch tra thread molto veloce (nessun mode switch verso il kernel necessario)

Svantaggio enorme: se un thread fa una system call bloccante (es. read() che si blocca in I/O), l’intero processo si blocca — il kernel vede un solo task e non sa che ci sono altri thread pronti a girare. Inoltre non si può sfruttare il multiprocessing reale (i thread non possono girare veramente in parallelo su core diversi).

Thread a livello kernel (Kernel-Level Threads, KLT)

  • Ogni thread è visibile al kernel come un task a sé stante, con la propria struttura dati di controllo
  • Lo scheduler del kernel gestisce direttamente ogni singolo thread

Vantaggi:

  • Se un thread si blocca in una system call, gli altri thread dello stesso processo continuano a girare
  • Vero parallelismo: thread diversi dello stesso processo possono girare davvero in contemporanea su core diversi

Svantaggio: overhead maggiore rispetto agli ULT nella creazione/gestione (anche se molto meno costoso di un fork() completo, perché non copia l’intero address space)

Modello ibrido: Many-to-Many (M:N)

Una libreria user space gestisce molti thread utente, mappandoli su un numero minore di thread kernel (invece che 1:1). Esempi storici: green threads di Java, goroutine di Go.

Confronto

ModelloScheduling gestito daIl kernel vede i thread?Un blocco ne blocca altri?Parallelismo multi-core
User-level (ULT)Libreria user spaceNoSì (blocca tutto il processo)No
Kernel-level (KLT)Scheduler del kernelSìNoSì
Many-to-many (M:N)IbridoParzialmenteDipendeSì, parziale

Linux usa il modello 1:1 (kernel-level): ogni thread utente (pthread) corrisponde esattamente a un thread kernel — nessun multiplexing intermedio.


2. Thread su Linux: clone()

Linux non distingue processi e thread a livello di kernel: entrambi sono task, rappresentati dalla struttura task_struct. La system call clone() generalizza fork(), con flag che decidono cosa condividere invece di copiare:

CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD | ...
  • fork() ≈ clone() senza condivisione — copia (concettualmente) address space, file descriptor table, ecc.
  • pthread_create() ≈ clone() con condivisione di address space, file descriptor, filesystem info, gestori di segnale

3. Cosa condividono i thread

Condiviso (grazie a CLONE_VM e affini — stessa page table):

  • Text (codice)
  • Data e BSS
  • Heap
  • File descriptor table
  • Working directory, umask
  • Gestori di segnale

Se un thread scrive nell’heap, tutti gli altri thread dello stesso processo lo vedono immediatamente — non c’è copia, è letteralmente lo stesso address space.

Privato per ogni thread:

  • Stack — necessario: se due thread condividessero lo stesso stack, le chiamate a funzione e variabili locali si sovrapporrebbero in modo catastrofico
  • Registri della CPU, incluso il program counter
  • Thread ID (TID)
  • Segnali pendenti specifici del thread
  • Thread-Local Storage (TLS): variabili __thread/thread_local, che sembrano globali nel codice ma hanno una copia distinta per thread
Address space condiviso da tutti i thread del processo:
┌─────────────────────────┐
│  Stack thread 1 (privato) │
├─────────────────────────┤
│  Stack thread 2 (privato) │
├─────────────────────────┤
│  Stack thread 3 (privato) │
├─────────────────────────┤
│      Heap (condiviso)     │
├─────────────────────────┤
│  BSS / Data (condivisi)   │
├─────────────────────────┤
│      Text (condiviso)     │
└─────────────────────────┘

4. Perché questo design

  • Semplicità nel kernel: un solo scheduler, una sola struttura dati (task_struct) sia per processi che thread
  • Efficienza nella creazione: creare un thread è molto più economico di un fork() completo, perché si condivide direttamente invece di copiare (o predisporre copy-on-write per) l’intero address space

5. PCB vs TCB

Modello classico (didattico)

  • PCB (Process Control Block): struttura per un intero processo — address space, file aperti, PID, stato globale, priorità
  • TCB (Thread Control Block): struttura per un singolo thread — registri, stack privato, TID, stato del thread. Più TCB fanno riferimento a un solo PCB
┌─────────────── PCB ───────────────┐
│ address space, file, PID, ecc.     │
│  ┌─ TCB 1: registri, stack, TID    │
│  ├─ TCB 2: registri, stack, TID    │
│  └─ TCB 3: registri, stack, TID    │
└─────────────────────────────────┘

Modello reale Linux: task_struct

Non esiste una TCB separata dal PCB: ogni thread ha il proprio task_struct completo, che funge sia da PCB che da TCB (contiene sia informazioni di stato/scheduling sia registri e stack privato). Più task_struct di uno stesso processo multi-thread condividono puntatori a strutture come mm_struct (address space) e files_struct (file aperti), invece di duplicarle.

task_struct 1 ──┐
task_struct 2 ──┼──→ stesso mm_struct (address space)
task_struct 3 ──┘──→ stesso files_struct (file aperti)

(ogni task_struct ha comunque i propri registri, PC, stack privato)

Campi chiave della task_struct per l’identificazione

CampoCosa rappresentaCondiviso tra thread dello stesso gruppo?
pidID univoco del singolo task/thread (kernel)No — unico per ciascuno
tgid (Thread Group ID)ID del gruppo di thread (= “PID” visto da user space)Sì — stesso per tutti
group_leaderPuntatore al task_struct del thread principale del gruppoPunta allo stesso task per tutti
real_parent / parentPuntatore al processo padre (relazione di creazione)Dipende dalla gerarchia di creazione

Nota importante: la funzione di libreria getpid() in user space restituisce il tgid, non il pid interno del kernel. Per questo tutti i thread di uno stesso processo “sembrano” avere lo stesso PID dall’esterno.

Esempio concreto

task_struct (thread principale):
  pid = 1000
  tgid = 1000        ← è lui stesso il leader
  group_leader → se stesso
  real_parent → task_struct del processo creatore (es. bash, pid 500)

task_struct (thread 2):
  pid = 1001         ← pid kernel univoco, diverso!
  tgid = 1000        ← stesso gruppo del thread principale
  group_leader → punta al task_struct con pid 1000

task_struct (thread 3):
  pid = 1002
  tgid = 1000
  group_leader → punta al task_struct con pid 1000

Da ps o getpid(): tutti e tre mostrano PID 1000 (è il tgid). Ma in /proc/1000/task/ si vedono le sotto-directory 1000, 1001, 1002 — i veri PID kernel di ciascun thread del gruppo.

Dove risiedono PCB/TCB/task_struct

Sempre in memoria kernel, mai accessibili direttamente dallo user space — l’accesso avviene solo tramite system call gestite dal kernel.

Riassunto

AspettoPCB (modello classico)TCB (modello classico)Linux (task_struct)
RappresentaUn intero processoUn singolo threadSia processi che thread (“task”)
ContieneAddress space, file, PID, stato globaleRegistri, stack privato, TIDTutto insieme, con puntatori condivisi tra “gemelli”
Quanti per processo multi-threadUnoUno per ogni threadUno task_struct per ogni thread, tutti che condividono mm_struct/files_struct
Dove risiedeMemoria kernelMemoria kernelMemoria kernel