






Studia grazie alle numerose risorse presenti su Docsity
Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium
Prepara i tuoi esami
Studia grazie alle numerose risorse presenti su Docsity
Prepara i tuoi esami con i documenti condivisi da studenti come te su Docsity
Trova i documenti specifici per gli esami della tua università
Preparati con lezioni e prove svolte basate sui programmi universitari!
Rispondi a reali domande d’esame e scopri la tua preparazione
Riassumi i tuoi documenti, fagli domande, convertili in quiz e mappe concettuali
Studia con prove svolte, tesine e consigli utili
Togliti ogni dubbio leggendo le risposte alle domande fatte da altri studenti come te
Esplora i documenti più scaricati per gli argomenti di studio più popolari
Ottieni i punti per scaricare
Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium
Libro di Michael Dahlin, Thomas Anderson tradotti i primi 13 capitoli.
Tipologia: Appunti
1 / 10
Questa pagina non è visibile nell’anteprima
Non perderti parti importanti!







Lezione del prof 08/03/ Capita molto spesso di dover fare più cose contemporaneamente. Fino ad ora abbiamo visto i processi: la visione è quello di un programma monolitico (Es: programma con 4 procedure); si è pensato quindi di dividere i compiti che possono essere eseguiti indipendentemente dalle altri. Thread: È un’unica sequenza di istruzioni che rappresenta un oggetto che posso schedulare separatamente dagli altri. L’impressione è quella di avere un numero illimitato di processori che eseguono un numero illimitato di thread, anche se la realtà è come nella figura sottostante:
Non sappiamo mai quando i processi, thread o istruzioni vengono eseguiti poiché dipende dallo scheduler. Le operazioni dei thread sono:
Ciclo di vita dei thread
Come per i processi anche il thread avrà un suo spazio per memorizzare le sue informazioni: questo è chiamato thread control block (TCB). In origine c’erano solo i processi. Ad un certo punto qualcuno si è accorto che i programmi abbastanza complessi potevano essere organizzati in compiti separati. Come faccio ad avere i thread se il sistema operativo non li supporta? Vengono visti come thread a livello utente, ma come un processo a livello kernel. Il sistema operativo potrebbe anche gestire i thread a livello kernel. Processo vs Thread
Esiste una Process Table contenente tutti i PCB. I thread hanno i loro TCB contente nella Thread Table, se implementato a livello utente => starà nello spazio utente, altrimenti starà nel kernel. PCB: Nome del processo (PID), memoria assegnata, risorse, device, file aperti, contiene tutte le informazioni per riferire i thread del processo con PID definito. TCB: Thread ID (TID), Stato, Contesto, Riferimento al suo stack. Ogni singola computazione è memorizzata nel TCB. Thread a livello utente: Nel caso in cui i thread sono realizzati a livello utente, la gestione viene fatta interamente dell’utente. In caso di yield (), viene messo un altro thread al posto di un altro. Il problema a livello utente è che se il thread esegue una system call e questa ci mette un po’ a finire => tutto il processo si blocca. A livello utente non abbiamo bisogno di eseguire un cambio di contesto (quindi è tutto molto più veloce), non ho bisogno di system call. Se realizzo i thread a livello utente realizzo una maggiore portabilità poiché il sistema operativo non vede la differenza tra thread e processo. Thread a livello kernel: Non cambia nulla rispetto ai processi. Se un thread si blocca qui, gli altri possono andare tranquillamente avanti. Lo scheduling dei thread viene fatto dal sistema operativo, quindi non esistono yield (). Thread Switch (Cambio di contesto) I thread come sappiamo sono soggetti a cambio di contesto: Volontaria: yield() nel caso di thread a livello utente oppure tramite interruzioni/eccezioni Involontaria: Lo scheduler decide di sospendere l’attuale thread per eseguirne un altro.
Riprendiamo (13/03/2018) – Thread Switch OSS: La yield è una supervisor call da parte dell’utente al kernel. Se un thread è a livello utente: Bisogna salvare i registri nel vecchio TCB, bisogna cambiare lo stack e mettere quello nuovo, ripristinare i valori del nuovo TCB. La stessa cosa avviene se è un thread kernel. Interruzioni: Se l’interruzione è da timer, o per via di una lettura da disco (supervisor call) il cambio di contesto è esattamente identico.
Capitolo 4 (Dal libro) Nel mondo reale – fuori dal computer – differenti attività vengono eseguite contemporaneamente. Internamente, anche il computer eseguo lavori in simultaneo. Per esempio, un server moderno potrebbe avere 8 processori, 10 dischi, 4 interfacce di rete; una workstation potrebbe avere dozzine di device IO come schermi, tastiera, mouse, camera, microfono ecc. Persino i telefoni di oggi possono avere 2 processori. Comunque, i programmatori sono abituati a pensare sequenzialmente. Quando leggi o scrivi un codice per una procedura, identifichiamo uno stato iniziale e una serie di precondizione, pensando a come possa cambiare lo stato, e come possono determinare le post condizioni. Per simulare l’interazione con il mondo reale, per gestire le risorse hardware, e per mappare applicazioni parallele per hardware paralleli, i computer devono forniscono ai programmatori per esprimere e gestire la concorrenza. Queste astrazioni sono usate sia dalle applicazioni che dal sistema operativo. Threads: Un’astrazione di base per la concorrenza: Questo capitolo si focalizzerà sulla potenza di astrazione dei thread. Definiremo i thread come un insieme di task che saranno eseguiti allo stesso tempo, ma scriveremo i codici di ognuno di essi in modo sequenziale. I thread sono usati sia dal sistema operativo, sia per i processi al livello utente. 4.1 Thread: astrazione ed interfaccia. Come illustrato nella figura 4.2, i thread
Per mappare un insieme arbitrario di thread per un insieme fissato di processori, il sistema operativo include uno scheduler che commuta chi è eseguito con chi è pronto ma non in esecuzione.
Cambiare thread avviene in modo trasparente rispetto al codice eseguito dai thread. Ricordando che il punto dell’astrazione che si vuol realizzare è quello di far apparire ogni thread come un singolo stream di esecuzione, così il programmatore non deve preoccuparsi sulla sequenza di istruzioni o se essa viene sospesa per eseguirne un’altra.
Per esempio, nella figura 4.3, vieni illustrato una vista del programmatore come
un semplice programma e 3 ( o più) possibili modi in cui il programma potrebbe essere eseguito; il tutto dipende da come lo scheduler lavora! Dal punto di vista di un thread, oltre la velocità di esecuzione, i 3 modi sono equivalenti. Inoltre, di solito nessun thread sa quali di questi ( o altri) sono effettivamente in esecuzione. Certamente, la vecotià di esecuzione di un thread e il suo interleaving con gli altri è influenzato dal lavoro che svolge lo scheduler.
La figura di lato, ci mostra i possibili modi di interleaving tra 3 thread. Quindi i programmatori non devono fare nessuna assunzione sulla velocità di esecuzione di ciascun thread.
Perché la velocità non è prevedibile? La ragione per cui il modello di programmazione dei thread usa questa assunzione è per guidare i programmatori per ragionare sulla correttezza. La realtà fisica è che la velocità di esecuzione relativa a differenti thread è influenzata da fattori fuori il loro controllo. Per esempio, un processore moderno potrebbe entrare in stallo per centinaia o migliaia di cicli se fosse presente una cache miss. Un altro fattore include come lo scheduler dà la priorità ai thread, quanti processori fisici risiedono nella macchina, quanto è grande la cache, la velocità di clock del sistema ecc.. Come risultato, la velocità di esecuzione di thread diversi è difficile da predire, può variare da hardware differenti, e può variare da differenti esecuzione nella stessa macchina. 4.2 Semplici API ed esempi Descriveremo qui le API dei thread chiamate sthreads (“simple threads”). void sthread create(thread, func, arg). Crea un nuovo thread, memorizzando informazioni su di esso nel thread. Il thread eseguirà la funzione func , la quale sarà chiamata con gli argomenti argc. void sthread yield(). La chiamata volontaria lascia il processore ad un altro thread pronto. Lo schedulare può riprendere ad eseguire il thread chiamante ogni volta che lo vuole.
int sthread join(thread). Aspetta che il thread termini la sua esecuzione se non l’ha già fatto; Quindi passa il valore restituito dal thread a sthread_exit(ret). Da notare che la join può essere chiamata solo una volta per chiascun thread. void sthread exit(ret). Termina l’esecuzione dell’attuale thread. Memorizza le il valore ret nella struttura del thread e, se esiste un altro thread che sta aspettando esegue la sthread_join(), in modo tale da svegliarlo e poter eseguire, restituendo questo valore. 4.2.1 Un programma multi-thread semplice
Nella figura qui a lato viene mostrato un semplice programma multi-thread. La funzione main() usa sthread_create() per creare 10 thread. Gli argomenti interessanti sono:
Così, sthread_create() inizializzerà lo stato del thread in modo da poter chiamare la funzione go() con l’argomento ii. Quando lo scheduler eseguirà il thread, quest’ultimo eseguirà la funzione go() con i suoi relativi argomenti e produrrà il suo relativo output. Il thread infine terminerà restituendo un valore dalla chiamata sthread_exit(ii+100). Questa chiamata memorizza il valore in un campo specifico del thread così quest’ultimo può essere recuperato dal thread iniziale al momento della chiamata sthread_join(). Il main() usa sthread_join() per aspettare che ogni thread creato termini la sua esecuzione. Quando ogni thread finisce viene prodotto in output il valore di uscita del thread. 4.3 Thread interni Ogni thread rappresenta uno stream sequenziale di esecuzione, e il sistema operativo fornisce l’illusione che ogni thread venga eseguito nel suo processore virtuale, anche se nella realtà ognuno di essi viene sospeso e rieseguito. Per capire come il sistema operativo implementa il processo di astrazione dei thread, definiremo le necessità di cui abbiamo bisogno per rappresentare lo stato di un thread. Dopo, capiremo il ciclo di vita di un thread – come il sistema operativo può creare, startare, stoppare e cancellare un thread. 4.3.1 Thread Control Block (TCB) e lo stato dei thread Il sistema operativo ha bisogno di una struttura per rappresentare lo stato di un thread; quest’ultimo è uguale ad ogni altro oggetto in questo contesto. La struttura dati che memorizza lo stato dei thread è chiamata thread control block (TCB). Per ogni thread che il sistema operativo crea, quindi viene creato il suo relativo TCB, il quale mantiene 2 tipi di informazioni:
Stato di computazione del thread
Ogni thread rappresenta la sua computazione eseguita sequenzialmente, quindi per poter creare thread multipli, dobbiamo allocare uno stato per rappresentare lo stato corrente di ogni computazione di ogni thread. Ogni thread contiene elementi di stato:
informazioni di cui abbiamo
bisogno per le procedure
nidificate che il thread ha
attualmente in esecuzione.
4.4 Dettagli d’implementazione Nella discussione precedente, abbiamo definito le operazioni base dei thread. Per fare le cose in modo concreto, descriveremo come implementare i thread molto più in dettaglio. Tipi di thread. Un sistema operativo usa i thread internamente – il kernel può essere multi thread – e fornisce anche la loro astrazione a livello utente così i processi possono essere multi thread. L’implementazione di questi due casi è quasi identica. Per semplicità ci focalizzeremo su come implementare i thread nel kernel. Questo caso è il più semplice perché tutto lo scheduling e le azioni di cambio di contesto (relativo ai thread) avvengono in un solo posto – il kernel. Discuteremo poi dei piccoli cambiamenti per estendere tale astrazione a supporto dei processi multi-thread. 4.4.1 Creare un thread Come detto in precedenza per poter creare un thread, chiamiamo la funzione sthread_create() la quale ha un puntatore ad una procedura func() e i suoi relativi argomenti args. Una volta creato il thread verrà eseguita func(args). Allocazione dello stato. Per allocare un thread, dobbiamo per prima cosa allocare il suo stato, il costruttore del thread alloca un TCB e lo stack. Nel TCB è contenuta una struttura dati con i campi che vogliamo memorizzare per salvare lo stato. Lo stack è un’area di memoria come una qualsiasi altra struttura dati allocata in memoria. Da notare che dobbiamo scegliere la grandezza per allocare lo stack. Alcune implementazione allocano uno stack a dimensione fissa, questo è un bug poiché potrebbe accadere un overflow a causa della dimensione fissata. Altri sistemi allocano uno stack iniziale di qualche dimensione e dinamicamente cresce se necessario. Inizializzazione dello stato. Quando inizializziamo il TCB, abbiamo bisogno di un nuovo insieme di registri inizializzati con qualche valore. Per prima cosa lo stack del nuovo thread deve puntare alla base del nuovo stack allocato. Poi, inizializzeremo lo stack pointer e i registri in modo tale che quando verrà fatto partire, quest’ultimo esegue la sua procedura con i suoi relativi argomenti. 4.4.2 Cancellare un thread Per cancellare un thread abbiamo bisogno di rimuoverlo dalla “ready list” così non potrà più essere eseguito e liberare lo stato precedentemente allocato per esso. C’è una piccola sottigliezza: se un thread rimuove sé stesso dalla “ready list” e libera il suo stato, la costruzione di ciò che abbiamo detto fin ora non funziona più. Poiché se un thread viene deschedulato ( e il suo stack viene deallocato), nel momento in cui viene rimesso in esecuzione non avrà più il suo stack, allora cosa dovrebbe fare? Esiste una soluzione semplice. Un thread non cancella il suo stato. Invece, un thread transisce nello stato di “ finished” spostando il TCB dalla “finished list -> ready list”, così saremo sicuri che il thread non verrà mai più eseguito. Poi, in un qualsiasi momento (quando qualcuno farà la yield o un’interruzione viene generata) viene liberato lo stato dei thread nella finished list. Così, sthread_delete() semplicemente muove il thread corrente nella finished list e dopo la yield per mettere qualche thread nella ready list. Tutto ciò rende possibile cancellare un thread in sicurezza. 4.4.3 Cambio di contesto dei thread. Quando il kernel ha thread multipli, abbiamo bisogno di un meccanismo per cambiare quasi thread sono nello stato di Running e quali nello stato di Ready.
Un cambio di contesto per i thread sospende l’esecuzione del thread corrente e ne mette in esecuzione un altro. Questo lavoro viene fatto copiando i registri correnti dei thread del processore e copiarli nel TCB che verrà messo nella ready list, e copiare il TCB del thread che deve essere messo in esecuzione nel processore. Il diavolo è nei dettagli. Ci sono alcune piccole differenze tra i due casi, ma relative a dettagli di implementazione. Descriveremo questi dettagli seguendo alcuni punti fondamentali:
Il resto di questa sezione descriverà questi problemi. Si noti che rimanderemo la discussione di un problema: Quale dei thread in stato di ready verrà eseguito? Cosa causa un cambio di contesto? Quando un thread nel kernel è in esecuzione, possono accadere 2 cause che provocano un cambio di contesto.
Per esempio, molte librerie cambiano i thread quando viene generato un timer-interrupt. Il particolare le librerie tipicamente vogliono essere sicure che nessun thread possa monopolizzare il processore, motivo per cui esiste il timer-interrupt. Come avviene il cambio di contesto per i thread? Indipendentemente se il cambio di contesto avviene tramite interrupt o una chiamata di funzione, il cambio di contesto avviene tramite 2 fasi:
I dettagli di implementazione variano a secondo se il cambio di contesto è avvento tramite interrupt o chiamata di sistema. Hardware-interrupt: Abbiamo visto cosa nel caso in cui avviene un interrupt, eccezione, o interrupt trap nei processi in esecuzione a livello utente: l’hardware e il software lavorano insieme per salvare lo stato del processo interrotto, eseguendo l’handler del kernel. Infine, bisognava ripristinare lo stato del processo interrotto. Il meccanismo è identico nel caso di un interrupt o eccezione nei thread del kernel:
Nel caso dei processi saremmo passati in kernel mode, in questo caso non c’è bisogno di farlo.
Non abbiamo nemmeno bisogno di cambiare lo stack pointer della base dell’interrupt stack del kernel. Invece, possiamo prendere o togliere lo stato salvato o le variabili dell’handler nel stack corrente a partire dallo stack pointer corrente.
In sostanza, comparando questo approccio con quello dei processi a livello utente, i più significativi cambiamenti sono:
Kernel Multi-thread con processi single-thread.
La figura illustrata qui sopra mostra 2 processi single-thread a livello utenti eseguiti in un kernel multi-thread con 3 kernel thread. Da notare che ogni processo a livello utente include i thread del processo. Ma, ogni processo ha molto più di un thread poiché ogni processo ha il suo proprio spazio di indirizzi – il processo ha la sua propria visione di memoria, del suo codice, del suo heap, delle sue variabili globali che sono differenti dal processo 2. Quindi, ogni processo – come sappiamo già – ha il suo proprio PCB, il quale necessita di più informazioni per il TCB per un kernel thread, e il cambio tra processi ha bisogno di un po’ più di lavoro. PCB VS TCB. Il TCB dei thread del kernel e i processi a livello utente sono simili, ma quest’ultimo ha bisogno di informazioni in più. Come il TCB, il PCB deve memorizzare i registri del processo quando il thread del processo non è in esecuzione. In più, il PCB ha le informazioni sullo spazio di indirizzi così quando noi cambiamo contesto da un processo ad un altro o tra un processo e il kernel, vengono utilizzate le giuste mappature della memoria virtuale. Con rispetto alla concorrenza e i thread, il PCB e ogni TCB rappresentano un thread, e la “ready list” del kernel contiene un mix di PCB dei processi e TCB dei thread del kernel. Quando lo scheduler scegli il prossimo thread da eseguire, ne prende uno di essi. Cambio di processi vs Cambio di thread. La differenza è che per cambiare i thread a livello utente, abbiamo bisogno di passare dalla user mode -> kernel mode. Lasciando da parte addizionale di cui si ha bisogno per cambiare la mappatura della memoria virtuale, il cambio di modalità può anche cambiare i dettagli di implementazioni a basso livello per salvare e ripristinare lo stato del thread. Interruzioni hardware (interrupt ed eccezioni). Quando un interrupt o un’eccezione causa un’interruzione hardware e un cambio di thread, l’hardware e il software lavorano insieme per salvare lo stato corrente e sospendere il thread. Quale stato viene salvato dall’hardware può essere leggermente differente poiché il thread può essere eseguito o in user mode o kernel mode, ma queste differenze sono relativamente piccola nei dettagli d’implementazione. Interruzioni software ( chiamate di libreria vs system call). Quando un thread del kernel accede alla libreria dei thread per creare, cancellare, sospendere, ripristinare, o cambiare thread può usare una semplice chiamata di procedura, ma quando un thread a livello utente accede alla libreria per eseguire una delle azioni menzionate in precedenza, il sistema usa le system call; poiché i PCB sono nella memoria del kernel, i thread dei processi a livello utente devono invocare il codice del kernel per salvare il loro stato.