Gestione dei processi • Un sistema operativo multitasking è in grado di gestire più processi • Un processo corrisponde ad un programma in esecuzione. • Un programma tipicamente alterna sequenze di istruzioni CPU ad operazioni di I/O • Durante l’esecuzione di una operazione di I/O il programma non utilizza la CPU, che pertanto può essere assegnata dal sistema operativo ad un alto processo Il contesto del processo • Rappresenta tutte le informazioni che descrivono la “fotografia” corrente del programma in esecuzione • Alcune di queste informazioni sono: – Il contenuto della memoria assegnata al processo – Il contenuto attuale dei registri della CPU, incluso il Program Counter, che determina lo stato di avanzamento del programma – Informazioni accessorie utilizzate dal sistema operativo per la gestione del processo, quali la priorità corrente • Tali informazioni, escluso il contenuto della memoria sono memorizzate dal sistema operativo nel Process Control Block associato . Gli stati di un processo • Definiscono se un processo è in esecuzione, o on attesa. • Gli stati sono: – New: processo creato, non ancora entrato in esecuzione – Running: il processo sta utilizzando la CPU – Waiting: il processo è in attesa della terminazione di un’operazione di I/O – Ready: il processo potrebbe utilizzare la CPU, ma la CPU è assegnata ad un altro processo – Halted: il processo ha finito la sua esecuzione o è stato stoppato dal sistema operativo (es. in conseguenza alla generazione di un’eccezione) La sequenza degli stati L’operazione di Context Switch La schedulazione (scheduling) dei processi • Il sistema operativo ha generalmente più di un processo candidato per l’utilizzo della CPU (Ready) • Tra le caratteristiche da tener conto nella determinazione di un efficiente algoritmo di scheduling sono le seguenti: – Utilizzo della CPU: si cerca di massimizzare l’uso della CPU – Throughput: massimizzazione dei processi eseguiti (terminati) per unità di tempo – Tempo di risposta: minimizzazione del tempo di attesa in stato Ready per ogni processo Prima implementazione: FIFO • L’utilizzo di una coda First-In-First out rappresenta l’implementazione più semplice, ma anche la meno efficiente. • Si supponga, ad esempio, che nella coda siano tre processi P1, P2, P3, con il processo P1 inserito per primo nella coda. Si supponga inoltre che il processo P1 impiegherà la CPU per un tempo di 100ms, mentre gli altri due processi impiegheranno la CPU per un tempo di 1 ms. • In questo caso il tempo medio di attesa risulterà pari a (100 + 101)/3 ms. • Se invece venissero eseguiti prima P2 e P3, il tempo medio di attesa sarebbe, a parità di throughput: (1 + 2) /3 ms Shortest Job First • Con riferimento all’esempio precedente, si può intuire che l’organizzazione ideale assegnerebbe la CPU per prima al processo che la utilizzerà per il tempo minore. • Tale approccio non è tuttavia realizzabile in pratica. • E’ possibile dare una stima n+1 del tempo di esecuzione previsto dopo n passi a partire dalla stima precedentemente fatta (n) e dal tempo effettivamente impiegato al passo precedente tn: n+1 = tn +(1- )n La priorità dei processi • In molte situazioni reali, esistono processi che hanno una “importanza” maggiore, e pertanto per tali processi risulta preferibile una maggiore prontezza del sistema operativo nell’assegnazione della CPU, a scapito di altri processi meno importanti • Ciò si può ottenere assegnando una priorità ai processi. • Il sistema operativo assegnerà la CPU al processo Ready di priorità più alta. Priorità fisse e variabili • L’utilizzo di priorità fisse assegnante ai processi è adeguato per alcune classi di utilizzi, tra cui i sistemi real-time. • In altri utilizzi, quali gestione multi-utente, tale politica non è ottimale in quanto rischia di creare lunghi tempi di attesa per alcuni processi. • In un sistema time sharing si vuole invece che la CPU venga assegnata ai vari utenti in modo da minimizzare il tempo di risposta. • Ciò può essere realizzato tramite l’utilizzo di priorità variabili: la priorità di processi che utilizzano pesantemente la CPU viene abbassata, mentre la priorità di processi che eseguono prevalentemente I/O viene elevata La gestione della priorità variabile. • Il meccanismo di cambio della priorità avviene interrompendo periodicamente il processore, e verificando il processo che sta correntemente utilizzando la CPU. • Ad ogni interruzione (tick) la priorità del processo corrente può essere decrementata, mentre la priorità degli altri processi può essere incrementata. • In questa maniera si “premiano” i processi che eseguono prevalentemente I/O, con l’assunzione che tali processi utilizzeranno la CPU per un tempo breve prima di tornare nello stato wait La gestione della priorità variabile (cont.) • Conseguentemente, lo scheduler gestirà più code FIFO di processi, una per ogni priorità. • La CPU verrà assegnata al primo processo della coda corrispondente alla priorità massima • Ad ogni tick, i processi possono cambiare coda, come conseguenza del loro “comportamento” • Tale gestione pertanto fa si’ che il processo selezionato per l’utilizzo della CPU la impegnerà probabilmente per un tempo non più lungo rispetto agli altri processi che in quel momento sono ready. Programmazione concorrente • La programmazione concorrente definisce due o più flussi di esecuzione tra loro indipendenti • Le problematiche non cambiano anche considerando sistemi a processore singolo, ma sistema operativo multitasking • La difficoltà nella dimostrazione della correttezza di tali algoritmi deriva dalla necessità di tenere in considerazione tutte le possibili combinazioni derivanti dalla esecuzione indipendente dei programmi. • Gli errori dovuti alla mancata considerazione di particolari condizioni di esecuzione prendono il nome di “race conditions”, e sono in genere molto difficili da diagnosticare Un esempio: l’implementazione di una sezione critica • Prende il nome di sezione critica un segmento di codice che viene eseguito da non più di un processo per volta • L’uso delle sezioni critiche è necessario per una gestione opportuna delle risorse del sistema • Molte risorse (ad esempio dispositivi di I/O) possono infatti essere utilizzate solo da un processo per volta • Più in generale, quando più di un processo condivide delle strutture dati (ad esempio liste), ci si vuole assicurare che solo un processo per volta modifichi tali strutture per evitare situazioni di incosistenza • Si consideri ad esempio l’utilizzo di una linked list condivisa: se un processo viene interrotto dal SO nel momento in cui sta modificando i riferimenti, altri processi vedrebbero la lista in uno stato inconsistente Requisiti dell’algoritmo • L’algoritmo di mutua esclusione deve garantire i seguenti requisiti: – Mutua esclusione: solo un processo per volta deve accedere alla sezione critica – Progresso: se nessun processo sta accedendo alla sezione critica, l’assegnazione della sezione critica ad uno dei processi richiedenti deve avvenire in un tempo finito (non può essere postposta indefinitamente) – Attesa finita(bounded waiting): deve esistere un limite finito al numero di volte in cui un processo in attesa per l’accesso alla sezione critica vede negato l’accesso a favore di un altro processo Una prima implementazione della sezione critica • Si consideri il problema dell’implementazione della sezione critica per due processi, utilizzando variabili condivise tra i due processi. • In questa implementazione la struttura condivisa è un array di due boolean: var flag: array[0..1] of boolean • Il processo Pi esegue le seguenti istruzioni (j si riferisce all’indice dell’altro processo): while(flag[j]) {//Ripeti il test} flag[i] = true … //Sezione critica flag[i]= false Analisi del primo algoritmo • Ad una prima analisi l’algoritmo sembrerebbe garantire la mutua esclusione: il processo Pi attende che il processo Pj sia uscito dalla sezione critica (ovvero che flag[j] diventi false) prima di entrare nella sezione critica. • Una analisi più accurata rivela tuttavia che la mutua esclusione non viene garantita in questa sequenza (partendo da una condizione in cui nessun processo è nella sezione critica): – 1) P0 entra nel ciclo while e trova flag[1] false – 2) P1 entra nel ciclo while e trova flag[0] false – 3) P1 pone flag[1] a true ed entra nella sezione critica – 4) P0 pone flag[0] a true ed entra nella sezione critica Secondo algoritmo • Visto che la race condition derivava dal fatto che il processo Pi pone flag[i] = true dopo aver testato flag[j], allora si può pensare di invertire le due operazioni, ottenendo la sequenza: flag[i] = true while(flag[j]) {//Ripeti il test} … //Sezione critica flag[i]= false Analisi del secondo algoritmo • In questo caso la mutua esclusione viene assicurata: infatti il processo i entra nella sezione critica solo se flag[i] == true e flag[j] == false. Ma la seconda condizione è verificata solamente se il secondo processo non è nella sezione critica • Tuttavia il requisito di progresso non è soddisfatto dalla seguente possibile sequenza: – 1) P0 pone flag[0] a true – 2) P1 pone flag[1] a true • In questa condizione i due processi si bloccano in un’attesa infinita: si è generata una situazione di deadlock Algoritmo definitivo • L’algoritmo di Peterson definisce una variabile booleana aggiuntiva:In questa implementazione la struttura condivisa è un array di due boolean: var flag: array[0..1] of boolean var turn: integer • Il processo Pi esegue le seguenti istruzioni (j si riferisce all’indice dell’altro processo): flag[i] = true turn = j while(flag[j] and turn == j){} //Ripeti il test … //Sezione critica flag[i]= false Analisi dell’algoritmo di Peterson • Mutua esclusione: quando Pi entra nella sezione critica o flag[j] == false o turn = i. Se entrambi i processi sono nella sezione critica, allora flag[0] e flag[i] sono true. Poiché la variabile turn non può essere contemporaneamente i e j, ciò significa che i due processi non possono essere entrati contemporaneamente nella sezione critica. Inoltre quando il primo dei due processi era entrato nella sezione critica, l’altro processo doveva aver già assegnato la variabile turn. A questo punto, tuttavia, quest’ultimo processo troverebbe la seconda condizione true pertanto deve necessariamente attendere che l’altro processo esca dalla sezione critica Analisi algoritmo Peterson (Cont) • Progresso: il processo Pi può essere bloccato prima di entrare nella sezione critica solo e solo se flag[j] resta true e turn resta a j. Se Pj è al di fuori della sezione critica e non sta cercando di entrarci, allora flag[j] == false. Se Pj ha posto flag[j] a true, allora sta cercando di entrare nella sezione critica (eseguendo il loop while). La variabile turn può avere solo i valori i o j, e pertanto o il processo j o il processo i entrerà nella sezione critica (Progresso). • Attesa finita: supponiamo che P1 e P0 continuino a richiedere l’accesso alla sezione critica. In questo flag[0] e flag[1] sono ripetutamente posti a true. Ciò significa che il processo Pi riesce a superare il ciclo while in base alla condizione turn == j. L’assegnazione della variabile turn prima del ciclo while assicura che Pi non dovrà attendere un numero indefinito di accessi di Pj prima di accedere a sua volta alla sezione critica.