Unix è un sistema operativo multiprogrammato a divisione di tempo.
Caratteristiche del processo Unix:
Stato in cui un processo ha terminato, ma non può essere ancora eliminato perché la sua immagine è ancora necessaria.
Esempio tipico: il processo figlio termina prima del padre. Il processo è zombie finchè il padre non rileva lo stato di terminazione del figlio.
Un processo, in modo “user running“, esegue la system call read(). Una richiesta viene inoltrata al kernel per leggere i dati da un file.
Il device, una volta completata l’operazione di lettura, notificherà mediante un interrupt al kernel il completamento del task.
La routine di interrupt (ISR) verrà eseguita con una priorità molto elevata in modo da poter prelazionare qualsiasi altro task a priorità inferiore. Lo scopo fondamentale della ISR consiste nel far transitare il processo dallo stato asleep a quello di user running.
A tal punto il kernel “sveglierà” il processo dallo stato asleep in modo tale che possa continuare la sua esecuzione. Nel caso particolare considerato in precedenza, in cui la richiesta è stata effettuata da una system call read(), i dati letti verranno copiati nello spazio utente all’indirizzo dato come parametro d’ingresso alla system call stessa.
Suddiviso in due parti:
Process Structure: contiene le informazioni indispensabili per la gestione del processo, anche se è in stato swapped.
U-Area: contiene le informazioni necessarie alla gestione del processo solo quando risiede in memoria.
Il codice dei processi è rientrante: più processi possono condividere lo stesso codice (text).
Per questo motivo, il kernel gestisce una struttura dati globale, la text table, nella quale ogni elemento contiene un puntatore all’area di memoria in cui è contenuto il codice.
Text table: un elemento per ogni segmento di codice utilizzato.
Obiettivo: privilegiare i processi interattivi.
Un utente può visualizzare/impostare solo il valore di nice.
Internamente, il kernel Linux distingue tra priorità statica e priorità dinamica dei task.
Priorità statica: determina la durata di un quanto di tempo assegnato ad un task.
Priorità dinamica: determina l’ordine di schedulazione (un task non esegue se altri task eseguibili hanno maggiore priorità statica).
La priorità dinamica è fatta variare automaticamente dal kernel durante l’esecuzione.
La priorità dinamica è ottenuta aggiungendo o sottraendo un valore (bonus) alla priorità statica, calcolata in base al tempo medio di sleep del task:
Grazie alla priorità dinamica, il kernel può privilegiare i task I/O bound, e penalizzare quelli CPU bound.
Il kernel di Linux non distingue tra thread e processo.
Un thread è un particolare tipo di processo che condivide delle strutture con altri processi (ad esempio, più processi con stessa tabella delle pagine).
Un flusso di esecuzione (task) è l’unità fondamentale di schedulazione per il kernel.
Ciascun task è identificato da un Process ID (PID).
Un gruppo di task correlati (un “processo” multithreaded nel senso classico) è identificato da un Thread Group ID (TGID).
Ad ogni task è attribuita una priorità che ne determina la schedulazione:
Due categorie di task: real-time e convenzionali.
La priorità dei task convenzionali è rappresentata da un intero tra -20 e +19 (nice value).
Diversa schedulazione per task real-time e convenzionali.
Un task viene eseguito solo quando non ci sono altri task eseguibili con priorità maggiore.
I processi convenzionali sono gestiti con un’unica politica di schedulazione time-sharing (SCHED_NORMAL).
Ciascun task può essere configurato per essere schedulato secondo una tra 2 politiche disponibili:
1. SCHED_FIFO: il task esegue finchè è eseguibile o rilascia volontariamente la CPU;
2. SCHED_RR: il task viene alternato periodicamente con altri task (round robin).
Tale scheduling non è hard real-time (nessuna garanzia è fornita sui tempi di schedulazione).
Effettua la schedulazione del task (attivo) a maggiore priorità.
Se vi è un task a priorità maggiore di quello attuale, effettua il context switch.
Può essere invocata in diverse occasioni:
Effettua anche altri compiti di servizio (e.g., aggiornare la priorità, load balancing tra CPU)
Obiettivi
Ottenere uno scheduling con overhead costante al variare del numero dei processi presenti nel sistema.
Utilizzare efficientemente le architetture SMP:
Il tempo impiegato per scegliere quale processo schedulare è costante, indipendentemente da quanti task ci sono nel sistema.
Il tempo per la scelta è lo stesso sia se ci sono 10 task sia se ci sono 1000 task in esecuzione.
Sviluppato per ottenere un buon grado di interattività ed equità allo stesso tempo.
Elevata scalabilità su sistemi SMP.
A ciascun processore è associata una runqueue, ossia una lista di processi in attesa di essere eseguiti su quel processore.
Ogni runqueue ha 2 “priority array”, expired ed active.
Un elemento dell’array è una lista di processi a stessa priorità (140 liste in totale):
Il sistema mantiene una matrice (map) di 140 (MAX_PRIO) bit, ognuno dei quali indica se esiste un processo in esecuzione con tale priorità.
Trovare la coda che contiene il processo a priorità più alta è davvero semplice: basta trovare il primo bit alto della matrice!
Una volta trovata la coda si seleziona il primo processo (FIFO).
Ogni volta che un task diviene eseguibile, il suo descrittore è collocato in coda alla relativa lista (in base alla priorità dinamica) dell’array active.
Quando un task deve essere schedulato, viene letto il descrittore in cima alla lista non vuota a maggior priorità.
Quando un task si blocca, esce dalla runqueue.
Queste operazioni non dipendono dal numero di task nel sistema.
Quando un task a maggior priorità diviene attivo, esso è inserito nella relativa lista ed il task corrente viene prelazionato
Quando un task esaurisce il quanto di tempo:
Quando tutti i task hanno esaurito il loro quanto di tempo, i puntatori ai 2 priority array vengono scambiati tra loro (expired <-> active).
Queste operazioni non dipendono dal numero di task nel sistema.
In Linux qualunque processore può eseguire qualunque task.
CPU affinity: se un task ha appena eseguito su un certo processore (cache-hot), è svantaggioso spostarlo su un altro processore.
Per ottenere un throughput elevato in un sistema SMP, è necessario che il numero di task in esecuzione sia equilibrato tra i vari processori.
Il load balancing è attivato da schedule() se una runqueue è vuota, e periodicamente per ogni runqueue (a seconda che sia idle o meno).
Basato su regole euristiche.
W2K è un S.O. multithread, time-sharing.
Caratteristiche dei processi e thread W2K:
La concorrenza è realizzata dai thread, per cui un processo W2K deve contenere almeno un thread per eseguire.
Politica di scheduling dei thread: intermedia tra la politica prioritaria e quella “round-robin”.
Ad ogni thread viene associata una priorità assoluta che varia tra 0 e 31.
Viene calcolata come somma di due componenti:
Ad ogni classe viene assegnato un valore numerico nominale.
Classi di priorità di un processo (in ordine decrescente):
Priorità relative di un thread (in ordine decrescente):
Per ogni livello di priorità, lo scheduler mantiene una coda gestita con Round Robin, con quanto 10 ms.
Lo scheduler esegue il primo thread della coda a priorità più alta che contiene almeno un thread pronto.
Il thread può eseguire fino a quando:
In tutti e tre i casi, lo scheduler viene invocato nuovamente per scegliere il successivo thread.
Permette di ridurre il tempo di risposta dei thread interattivi.
All’attivazione di un thread sospeso:
I thread di ogni processo, inclusi quelli dell’executive, possono eseguire su qualsiasi processore.
Il microkernel usa una policy nota come soft affinity:
1. Introduzione ai Sistemi Operativi
5. Scheduling nei sistemi mono-processore
6. Threads, SMP
8. Scheduling Multiprocessore e Real-Time
9. Gestione dei processi nei sistemi operativi Unix/Linux e Window...
10. Introduzione alla Programmazione Concorrente
11. Sincronizzazione nel modello ad ambiente globale
12. Problemi di cooperazione nel modello ad ambiente globale
14. Sincronizzazione nel modello ad ambiente locale
15. Deadlock
16. Programmazione Multithread
18. Memoria Virtuale
20. Il File System
21. Primitive di sincronizzazione nel kernel Linux
22. Esercitazione: System call per la gestione dei processi
23. Esercitazione: Inteprocess Communication e Shared Memory
24. Esercitazione: System Call per la gestione dei semafori in Linu...
25. Esercitazione: Problema dei Produttori e dei Consumatori
26. Posix Threads
P. Ancilotti, M.Boari, A. Ciampolini, G. Lipari, “Sistemi Operativi”, Mc-Graw-Hill (Cap.7, Par. 7.4 – Cap. 8, Par. 8.2)
Approfondimenti
W. Stallings, “Operating Systems : Internals and Design Principles (5th Edition) ”, Prentice Hall (Cap. 3. Cap. 9)