










































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
Una panoramica dettagliata sulla gestione dei processi e dei thread nei sistemi operativi, con un focus particolare sui meccanismi di sincronizzazione. Vengono esaminati concetti chiave come il context switch, la creazione e terminazione dei processi, e le diverse tecniche per la sincronizzazione, inclusi semafori e monitor. Inoltre, viene analizzato il problema del deadlock e le strategie per la sua prevenzione e gestione. Gli appunti offrono una guida completa per comprendere le sfide e le soluzioni nella gestione concorrente dei processi, fornendo esempi pratici e spiegazioni chiare dei concetti fondamentali. Si discute anche il modello a scambio di messaggi e le condizioni per lo stallo, offrendo una visione approfondita delle problematiche legate alla concorrenza nei sistemi operativi moderni. Questi appunti sono ideali per studenti universitari e professionisti del settore che desiderano approfondire le proprie conoscenze sui sistemi operativi e la gestione dei processi.
Tipologia: Appunti
1 / 50
Questa pagina non è visibile nell’anteprima
Non perderti parti importanti!











































Inizio unità 2 Processi e Thread Processo= programma in esecuzione P = (C,S) dove C è il codice eseguibile mentre S è lo stato dell’esecuzione dove sono inclusi vari registri come: program counter, altri registri della CPU… Illustrazione della struttura tipica della memoria di un processo (immagine slide 5) Text = contiene il codice eseguibile del programma data = contiene le variabili globali e statiche inizializzate heap = è l’area di memoria utilizzata per l’allocazione dinamica stack = utilizzata per la gestione delle chiamate a funzione e delle variabili locali La separazione di questi segmenti aiuta il sistema operativo a gestire la memoria in modo efficiente e sicuro. In sostanza l’immagine mostra una mappa fondamentale di come un programma vede e utilizza la memoria durante la sua esecuzione. Ora vediamo il ciclo di vita di un processo dal momento in cui ne viene chiesta la creazione al momento di terminazione. (guarda slide 7 per immagine) Quando ne viene chiesta la creazione, il kernel, crea un descrittore dove mette delle informazioni che sono l’ID (un numero) e setta lo stato del processo in NEW nessuna risorsa se non il descrittore la seconda cosa che fa il kernal è cercare un’area di memoria da allocare e una volta trovata il processo assegna il suo stato a READY, pronto per essere mandato in esecuzione. A questo punto nel descrittore troviamo tutti i registri messi a zero, incluso il PC. Mentre il registro base e il registro limite vengono impostati con valori che aiuteranno a definire l’area di memoria che il processo potrà utilizzare. Il processo ora è stato creato, ha la sua memoria e il suo descrittore. È nello stato READY in attesa di essere eseguito dalla CPU. A questo punto entra in gioco lo scheduler, una parte del sistema operativo che ha il compito di decidere quale dei processi nello stato READY debba essere il prossimo a ottenere l’uso della CPU. Lo scheduler prende una decisione e sceglie un processo fornendo l’indirizzo di memoria del descrittore del processo selezionato. Questa informazione (l’indirizzo del PCB) viene poi passata al dispatcher. Il suo ruolo è quello di prendere il processo selezionato dallo scheduler e caricarlo sulla CPU in modo da riprendere l’esecuzione. Carica i valori dei registri, inclusi quelli basi e quelli limiti e tutti quelli della CPU, che erano stati salvato nel descrittore del processo, nei rispettivi registri fisici della CPU. Assegna il bit di modo e una volta che tutti i registri sono stati ripristinati e il bit di modo impostato, il processore salta all’indirizzo contenuto nel PC. Qui entra nella fase di RUNNING. Due cose possono capitare quando il processo è in fase di RUNNING:
processi. (EXIT()) Una volta che il processo ha invocato la funzione, il controllo passa al kernel che imposta lo stato le processo a TERMINATED.
UNO A UNO : slide 24. Ogni thread a livello utente si mappa a un thread a livello kernel. PRO : Multiprocessore con più thread running per processo e le system call bloccanti non pongono problemi, il sistema è in grado di schedulare più thread. CONTRO : Context switch fra thread è costoso perché richiede il passaggio a modalità kernel e il sistema operativo deve predisporre di strutture per la memorizzazione e gestione di tutti i thread, ma questo può essere limitativo. (esempio: linux) MOLTI A MOLTI : slide 25. Diversi thread a livello utente sono mappati a diversi thread a livello kernel, questo permette al sistema operativo di creare un numero sufficiente di thread a livello kernel. PRO : In sistemi multiprocessore è possibile avere più thread running per processo, il context switch fra thread a livello utente avviene in modalità utente, a cura del sistema runtime. Il sistema operativo effettua il context switch di kernel threads che risulterà un po’ meno costoso perché queste condivideranno lo spazio di indirizzi e quindi la tabella delle pagine. Il sistema operativo è in grado di schedulare i kernel threads pronti di un dato processo anche se una o più altri kernel threads dello stesso processo sono bloccatti in attesa di un evento. CONTRO : Il modello è più complicato e richiede che lo scheduler della CPU e lo scheduler dei threads a livello utente collaborino. Slide27 il codice qui sotto vuole illustrare la concorrenza in Java, mandando in esecuzione lo stesso codice svariate volte. Possiamo notare che il comportamento risulta diverso, nonostante il medesimo codice. I due thread condividono la variabile counter. Più avanti vedremo tecniche per la sincronizzazione dei processi. I task di linux Linux utilizza la stessa rappresentazione interna sia per i processi sia per i thread che in entrambi i casi vengono chiamati task. La differenza sta nel fatto che un thread è un task che condivide lo stesso spazio degli indirizzi con il genitore. Il processo ha uno spazio diverso. Questa differenza si nota nel momento della creazione dei task effettuando una system call opportuna:
Distinguiamo due tipi di processi:
Si ha quando piu processi possono accedere e manipolare dei dati condivisi e il valore finale dei dati dipende dall’ordine degli accessi. Per prevenire la corsa critica i processi devono Sincronizzati tra loro. Osserva che quando si modifica deve essere l'unica che ha accesso all'area di memoria condivisa; quindi, l'unica soluzione è la sezione critica. La sezione critica sia quando n processi competono per accedere ad una data area condivisa. Ogni processo ha un segmento di codice, chiamato “Sezione critica”, Che contiene le istruzioni di accesso e manipolazione dei dati presenti nella memoria condivisa. La soluzione deve assicurare che un processo in esecuzione nella propria sezione critica, nessun altro processo possa essere in esecuzione nella propria sezione critica. La soluzione deve soddisfare tutte le proprietà elencate, altrimenti non funziona sempre:
può entrare nella sezione critica non può essere rimandata indefinitivamente. (assenza di deadlock).
Definiamo un'interfaccia dello spin lock alla slide 18 e andiamo a vedere delle possibili implementazioni di questa interfaccia.
Algoritmo tre (Peterson). Slide 22. I problemi visti fino ad ora portano a questo risultato. In questo algoritmo vediamo l’utilizzo delle flag e quindi dichiara se vuole entrare all’interno della sezione critica, però se vede un altro processo che vuole entrare, lascia il passo a quest’altro processo, immaginiamo come due persone che voglio entrare attraverso la stessa porta, uno dei due lascia il passo all’altro. quindi utilizza sia le flag che i turn. Processo P1 desidera entrare nella sua sezione critica, esegue i seguenti passaggi nella sezione di ingresso: dichiara l’intenzione di entrare, flag1 = true, mette la variabile turn a 2, dicendo che se l'altro processo P2 vuole entrare ha la precedenza. L'altro processo (P2) ha dichiarato la sua intenzione di entrare, flag2 = True, e il turno è del processo 2, turn = 2, quindi P1 resta in attesa, finché una delle due condizioni diventino false, ne basta una per far sì che P1 possa entrare:
Per entrambi nostri array, troviamo scritto volatile, Cosa significa? i compilatori moderni sono in grado di ottimizzare il codice per renderlo più veloce e più compatto, tra queste ottimizzazioni troviamo il riordinamento delle istruzioni: il compilatore può modificare l’ordine delle istruzioni nel codice macchia generato se ritiene che questo non alteri il risultato logico del programma dal punto di vista di un singolo thread. Specificare volatile su queste variabili inibisce queste ottimizzazioni rendendo il comportamento reale del programma il più vicino possibile a quello che il programmatore si aspetta e progetta. Nella slide 26 troviamo le operazioni da fare in ingresso, chiedere a tutti gli altri processi qual è il loro numero di ingresso e assegnarsi il massimo + 1, aspettando il proprio turno. terminato di lavorare in sezione critica, assegnare a sé stesso il ticket = 0, non ha bisogno di entrare. (per entrare il ticket parte da 1 in poi), il choosing mi dice in questo momento il thread si sta calcolando il suo ticket, ancora non lo sa, quando lo rimette a False nell’array abbiamo il valore del ticket aggiornato. nessuno dice ora è il tuo turno, e per sapere quando è arrivato il suo turno, il threads va a confrontarsi con tutti gli altri. Ciclo While siamo in attesa se “questo” o (||) “questo”. foto diagramma di sequenza sul telefono (5 foto = 5 lavagne). Il problema dell’algoritmo del fornaio è che usa θ(n) locazioni di memoria condivisa. Che è la quantita minima di memoria richiesta se si usano solo operazioni di lettura e scrittura. Stiamo parlando di una quantità di memoria che cresce con l’aumentare dei processi e questo lo rende poco pratico in sistemi con molti processi. Per queta ragione abbiamo bisogno di strumenti più potenti:
Se il lock era occupato (true): getAndSet restituisce true. La condizione while(true) è verificata, e il thread resta intrappolato nel ciclo, riprovando continuamente. Questo si chiama spin-lock. Seconda Soluzione (più potente): Compare-and-Swap (CAS) L'operazione CompareAndSwap (CAS) è un'altra istruzione atomica, ma più sofisticata della TSL. Funziona così:
progresso. Ma non l’attesa limitata perché il fair che è impostato a false di default. Un thread appena arrivato può sorpassare quelli in coda. Se mettessi il fair = True non ho comunque la certezza che tutti i thread vengano trattati in modo equo, ciò dipende dalla gestione della JVM. Per quanto riguarda l’implementazione dell’interfaccia per i Semafori , java utilizza la classe Semaphore: quando è inizializzato a 1 abbiamo un semaforo binario (mutex) quando è inizializzaro un valore maggiore di 1 abbiamo un semaforo contatore. acquire() è il nostro wait, release() è il nostro signal, se il fair è impostato a True avremo tutte e tre le nostre proprieta garantite I semafori binari sono molto simili ai lock con una differenza: in un lock solo il thread che lo acquisice (decrementa il valore) lo può rilasciare (incrementa il valore), mentre nel semaforo binario un thread può decrementare il valore ed un altro lo può incrementare.
Implementazione di CounterSemaphore basata sul context switch, quindi non useremo l’attesa attiva (busy-waiting) ma utilizzeremo la capacità del sistema operativo di bloccare un thread e risvegliarlo quando serve, questo è un approccio più efficiente perche evita lo spreco di CPU. Utilizziamo due primitive per bloccare un processo e per risvegliare un processo:
blocked.remove(): si rimuove dalla coda. count = count – 1: Finalmente, prende il permesso. if (!blocked.isEmpty()) notifyAll();: Se c'è almeno un thread in coda (!blocked.isEmpty()), sveglia tutti i thread in attesa.
Abbiamo:
Una barriera è una forma di sincronizzazione dove esiste un punto (la barriera) nell’esecuzione di ogni processo di un certo gruppo che deve essere raggiunto da tutti i processi del gruppo prima che ognuno di loro possa proseguire nell’esecuzione. done[t0].release(): t0 ha finito il suo lavoro "prima della barriera". Con release(), alza la sua mano e dice "Io ci sono!". Il suo semaforo, done[0], passa da 0 a 1. done[t1].acquire(): Ora t0 si ferma e aspetta l'altro. Tenta di acquisire un permesso dal semaforo di t1. Si sta chiedendo: "È già arrivato t1?". Se t1 non è ancora arrivato, done[t1] è ancora a 0, e t0 si blocca. Immaginiamo che t0 arrivi per primo. Si bloccherà su done[t1].acquire(). Quando t1 arriva, esegue prima done[t1].release() (sbloccando potenzialmente t0) e poi done[t0].acquire(). Poiché t0 aveva già eseguito il suo release, done[t0] ha un permesso e t1 può procedere. Allo stesso tempo, t0 viene sbloccato perché t1 ha eseguito release sul suo semaforo.