Lezione T8 Gli scheduler CPU di Linux
←
→
Trascrizione del contenuto della pagina
Se il tuo browser non visualizza correttamente la pagina, ti preghiamo di leggere il contenuto della pagina quaggiù
Lezione T8 Gli scheduler CPU di Linux Sistemi Operativi (9 CFU), CdL Informatica, A. A. 2013/2014 Dipartimento di Scienze Fisiche, Informatiche e Matematiche Università di Modena e Reggio Emilia http://weblab.ing.unimo.it/people/andreolini/didattica/sistemi-operativi 1
Quote of the day (Meditate, gente, meditate...) “The design relies on the fact that interactive tasks, by their nature, sleep often.” Con Kolivas (1977-) Anestesista, programmatore Il pioniere del “desktop interattivo” 2
Parametri di progetto di uno scheduler (Repetita juvant) Numero e formato delle code dei processi pronti. Algoritmo di scheduling per ciascuna coda. Criterio usato per spostare un processo in una coda con priorità maggiore. Criterio usato per spostare un processo in una coda con priorità minore. Criterio usato per scegliere la coda in cui inserire inizialmente un processo. 3
LINUX v1.2 4
Coda dei processi pronti (Ve ne è una sola; semplice semplice) Esiste una unica lista contenente tutti i processi attivati sulla macchina. Il primo elemento della lista è la task_struct di init. init_task ... 5
Definizione delle priorità (Priorità statica, the UNIX way) Ogni processo ha una priorità statica (nice value o nice) (re)impostabile dall'utente. La priorità varia da -20 (priorità più alta) a +19 (priorità più bassa). Il valore di default è 10. → Schema classico delle priorità in UNIX. Non è prevista una modifica dinamica delle priorità da parte del kernel. 6
Algoritmo di scheduling (Round Robin; what else?) L'algoritmo di scheduling pesca in modalità “round robin pesata” uno dei processi in TASK_RUNNING. Un processo in attesa da più tempo viene favorito rispetto ad un processo che attende da meno tempo. A parità di attesa, un processo a priorità più alta viene favorito rispetto ad un processo a priorità più bassa. A tal scopo, ad ogni processo è associato un contatore, inizializzato al valore della priorità statica. 7
Aggiornamento del contatore (Round Robin; what else?) Ad ogni invocazione di schedule(), l'intera lista è scandita ed i contatori aggiornati: contatore = (contatore >> 1) + priority Si sceglie il processo per cui contatore è massimo. 8
Modifica dinamica della priorità (Non pervenuta) Non esiste alcun meccanismo per la modifica dinamica della priorità in funzione della natura del processo. L'unica priorità esistente è quella statica. La priorità iniziale del processo è: il valore iniziale di default (10). oppure un valore impostato con il comando nice. 9
Problemi 1/3 (Tanti, purtroppo) L'algoritmo di scheduling è O(n) rispetto al numero di processi (una invocazione di schedule() → una scansione lineare della lista). Che succede se schedule() è invocata migliaia di volte al secondo in un sistema server con migliaia di processi attivi? 10
Problemi 2/3 (Tanti, purtroppo) Non vi è alcuna differenziazione in classi; l'unico discriminante è una priorità statica. Che succede se un processo muta spesso la propria natura? Il programmatore deve reimpostare ogni volta la priorità statica a mano? Che succede ad un terminale interattivo che va in competizione con uno script di make? 11
Problemi 3/3 (Tanti, purtroppo) Vi è una unica lista globale. L'accesso alla lista va “serializzato” per impedire le modifiche concorrenti. → I processori si “accodano” per accedere alla lista. → Le prestazioni non scalano con il numero di processori. Questo non è un vero problema, dal momento che Linux v1.12 non supporta sistemi SMP. 12
LINUX v2.2 13
Differenze rispetto allo scheduler v1.2 (Classi di processi, goodness()) Lo scheduler del kernel v2.2 è una estensione di quello presente nella versione v1.2. Differenziazione dei processi in più classi tramite un insieme più ampio di priorità. Introduzione di una funzione (goodness()) per il calcolo della “bontà” di un processo (ovvero quanto è favorevole schedularlo). 14
Il nuovo schema di priorità (Da [-20, 19] a [0, 139]) Si usano 140 livelli di priorità. Livelli [0, 99]: usati da algoritmi di scheduling di tipo soft real time (SCHED_FIFO, SCHED_RR), che hanno sempre la precedenza sull'algoritmo standard. Livelli [100, 139]: corrispondono al vecchio intervallo [-20, 19] e sono utilizzati dall'algoritmo di scheduling di default (round robin pesato). 15
Classi di processi: SCHED_FIFO (Alta priorità) Impostabili staticamente dall'utente all'avvio del processo. SCHED_FIFO: FCFS senza prelazione “soft real time”. Opera ad una priorità in [0, 99]. Un processo non si ferma se non quando termina o richiede I/O. → Occhio: pericolo di stallo della macchina in caso di ciclo infinito! 16
Classi di processi: SCHED_RR (Alta priorità) Impostabili staticamente dall'utente all'avvio del processo. SCHED_RR: RR “soft real time”. Opera ad una priorità in [0, 99]. 17
Classi di processi: SCHED_OTHER (Bassa priorità) Impostabili staticamente dall'utente all'avvio del processo. SCHED_OTHER: algoritmo di scheduling RR pesato (derivato da quello presente in v1.2). Opera ad una priorità in [100, 139] ed è pertanto sempre battuto da SCHED_FIFO o SCHED_RR. → Occhio: rischio di starvation in caso di esecuzione con un processo CPU-bound soft real time! 18
Criterio di scelta del processo (Priorità + algoritmo) Algoritmo multilivello. Passo 1: si individua il processo con priorità più alta nella lista dei processi. Passo 2: si applica l'algoritmo di scheduling associato al processo. 19
La funzione goodness() (Stima dell'interattività di un processo) La funzione goodness() ritorna un peso (weight) nell'intervallo [-1000, 1000]. Il peso è usato per scegliere il processo da schedulare. -1000: mai selezionare questo processo. 1000: processo soft real time, da selezionare subito. Algoritmo: Classe == SCHED_FIFO | SCHED_RR? Sì → weight=1000 Processo gira su stesso processore? Sì → weight += bonus Processo usa le stesse aree di memoria? Sì → weight += bonus weight += priorità processo 20
Problemi 1/2 (Un po' meno) L'algoritmo di scheduling è O(n) rispetto al numero di processi (una invocazione di schedule() → una scansione lineare della lista). Che succede se schedule() è invocata migliaia di volte al secondo in un sistema server con migliaia di processi attivi? 21
Problemi 2/2 (Un po' meno) Vi è una unica lista globale. L'accesso alla lista va “serializzato” per impedire le modifiche concorrenti. → I processori si “accodano” per accedere alla lista. → Le prestazioni non scalano con il numero di processori. Questo comincia ad essere un problema, dal momento che Linux v2.2 supporta architetture SMP. 22
LINUX v2.4 23
Differenze rispetto allo scheduler v2.2 (Classi di processi, goodness()) Lo scheduler del kernel v2.4 è una estensione di quello presente nella versione v2.2. Uso di una coda contenente solo processi in stato di pronto. Uso di quanti di tempo variabili (time slice). Uso di un sistema di schedulazione a crediti. 24
Time slice (Quanto di tempo variabile) Ciascun processo può eseguire al più per un intervallo di tempo calcolato dinamicamente (time slice). Il calcolo avviene mediante un sistema a crediti: un credito → 10ms di esecuzione. un processo acquisisce un credito ogni volta che si blocca → premio all'interattività. dopo l'esecuzione di una time slice si scala un credito. nessun credito → niente processore. 25
Recrediting (Quanti crediti sono riassegnati ai processi?) Quando tutti i processi in stato TASK_RUNNING hanno esaurito i crediti, il kernel ricalcola il credito per tutti i processi (recrediting). Algoritmo di aggiornamento: next_credit = prev_credit / 2 + priority Si cerca di ridurre il credito a disposizione dei processi per non farli eseguire troppo a lungo. Se il processo è interattivo → il credito aumenta. Se il processo non è interattivo → il credito si riduce. 26
Algoritmo di scheduling (Round Robin; what else?) L'algoritmo di scheduling pesca il processo migliore tramite la funzione goodness(), in modo analogo al kernel v2.2. La funzione goodness() tiene ora in conto anche dei seguenti aspetti: affinità al processore. crediti rimanenti. 27
Problemi 1/2 (Pochi, ma grossi) L'algoritmo di scheduling è O(n) rispetto al numero di processi. Una invocazione di schedule() → una scansione lineare della run queue). Un recrediting → una scansione lineare della run queue. 28
Problemi 2/2 (Pochi, ma grossi) I processori sono inattivi durante il recrediting. → starvation dei processori (tanto più marcata quanti più sono). 29
LINUX V2.5.49 - V2.6.22 30
Differenze rispetto allo scheduler v2.4 (Parecchie; è una riscrittura da zero) Obiettivi. Favorire i processi interattivi indipendentemente dal carico di lavoro applicato al processore. Scalare bene le prestazioni dello scheduler con il numero di processi. il numero di processori. 31
Coda dei processi pronti (Una per CPU!) La coda contiene due array di priorità di 140 elementi cadauno (una coda per ciascuna priorità possibile). Array dei processi attivi (active): non hanno esaurito il tempo di esecuzione loro assegnato. Array dei processi spirati (expired): hanno esaurito il tempo di esecuzione loro assegnato. Active array Expired array alta Priorità Lista di task_struct Priorità Lista di task_struct [0] [0] [1] [1] 32 bassa [140] [140]
Il difetto del Weighted Round Robin (L'algoritmo standard in pseudocodice → O(n)) for (p = init_task; p != init_task; p++) { if (p->prio != MAX) Aging (per evitare la p->prio++; starvation del processo) if (p->prio > current->prio) Si schedula il primo processo switch_to(p); con priorità più } alta trovato. Il ciclo for cicla sull'intera lista dei processi e pesca quello a priorità più alta fra gli eseguibili. → Algoritmo O(n) rispetto al numero di processi. Bisogna cercare di evitare il ciclo for sull'intera lista. 33
Un algoritmo O(1) rispetto ai processi (Ricorda molto da vicino la tecnica del double buffering per i videogiochi) 1.Trovare in tempo O(1) la coda di priorità più elevata in active contenente la task_struct di un processo eseguibile. 2.Se non esiste una task_struct, tutti i processi sono spirati. Si scambiano gli array active ed expired (operazione fattibile in tempo O(1)) e si ripete 1. 3.Sia next=task_struct in cima alla coda di priorità più elevata (O(1)). 4.Si ricalcola la priorità di next (O(1). 5.Si effettua un cambio di contesto a next (O(1)). 6.Quando next ha usato la sua timeslice, lo si inserisce nella coda di priorità opportuna nell'array expired (O(1)). 7.Si invoca schedule() per schedulare un altro processo (O(1)).34
Come risolvere il passo 1. (Come trovare la coda con priorità più elevata senza scandirle tutte) Si usa una bitmap di 140 bit (5 interi a 32 bit). Nella coda i è presente la bit i=1 → task_struct di un processo eseguibile. Nella coda i non è presente la bit i=0 → task_struct di un processo eseguibile. L'istruzione assembly bsfl (Intel) individua in tempo costante (O(1)) il primo bit non nullo in una bitmap. 35
Stima dell'interattività (Il kernel riconosce i processi interattivi) Obiettivo della stima: aumentare in maniera dinamica la priorità di un processo interattivo. Criterio di interattività: frequenza di blocco processo. Frequenza elevata: processo → I/O-bound Frequenza bassa: processo → CPU-bound Implementazione: campo sleep_avg inserito nella task_struct del processo. Run → blocked: si sottrae a sleep_avg il tempo di esecuzione dell'ultima timelice. Blocked → run: si aggiunge a sleep_avg il tempo di blocco (fino ad un massimo di 10ms). 36
Calcolo dinamico della priorità (Il processo esegue tanto spesso quanto richiesto dalla sua natura) La funzione effective_prio(), definita nel file $LINUX/kernel/sched.c, calcola il boost di priorità da assegnare ad un processo in base alla sua interattività. Tale funzione assegna un bonus di priorità nell'intervallo [-5, +5]. Processo fortemente I/O-bound → -5. Processo fortemente CPU-bound → +5. 37
Calcolo dinamico della timeslice (Il processo esegue tanto tempo quanto richiesto dalla sua natura) La durata della timeslice è: memorizzata nel campo time_slice della task_struct. calcolata dalla funzione task_timeslice() nel file $LINUX/kernel/sched.c. Tale funzione scala la priorità statica (PS) di un processo in un valore nell'intervallo [5ms, 800ms]: PS < 120 → time_slice = (140 – PS) * 20 PS ≥ 120 → time_slice = (140 – PS) * 5 38
Alcuni valori di esempio (Valgono più di 1000 parole) Priorità PS NICE Time_slice Altissima 100 -20 800 ms Alta 110 -10 600 ms Normale 120 0 100 ms Bassa 130 10 50 ms Bassissima 139 19 5 ms 39
Una riflessione su sleep_avg (STOP! A che serve tutto questo? Qual è l'effetto sui processi?) La metrica sleep_avg è molto accurata! Un processo che si blocca spesso, ma che esaurisce la sua timeslice continuamente, non riceve un bonus grande. Effetto ricompensa per i processi interattivi. Effetto punizione per i processi non interattivi. Un task che riceve un bonus di priorità lo perde se comincia ad abusare del processore. Cambi di contesto non compensati da attese provocano la decrescita di sleep_avg e del bonus. 40
Una pecca dell'algoritmo O(1) (Un processo CPU-bound può rallentare un processo I/O-bound) Scenario. Un processo CPU-bound ed un processo I/O-bound sono in competizione per il processore. Il processo I/O-bound termina la sua timeslice per primo. Il processo CPU-bound continua a consumare la sua timeslice per molto più tempo. → Il processo CPU-bound blocca il processo I/O-bound almeno fino all'esaurimento della sua timeslice! 41
Una euristica risolutiva (La classica “pezza”) Se il processo è sufficientemente interattivo, al termine della sua timeslice non viene spostato nell'array expired viene reinserito nell'array active. Rationale: reinserendo il processo interattivo nell'array di priorità active, esso continua ad essere rischedulato immediatamente. 42
Aggiornamento delle timeslice (Rimettiamo insieme tutti i pezzi) scheduler_tick() in $LINUX/kernel/sched.c. Si decrementa time_slice nella task_struct del processo in esecuzione. Se time_slice == 0, la timeslice è spirata e bisogna decidere in che array piazzare la task_struct. Si invoca la macro TASK_INTERACTIVE() per capire se il task è sufficientemente interattivo. Si invoca la macro EXPIRED_STARVING() per capire se la runqueue ha processi in starvation. Se la runqueue non ha processi in starvation ed il task è sufficientemente interattivo, viene reinserito in active. 43
Problemi 1/2 (I vecchi problemi sono spariti; ne sorgono di nuovi) È complicato stimare l'interattività di un processo. I/O-bound → deve essere schedulato più spesso. CPU-bound → deve essere schedulato meno spesso e con timeslice più lunghe. È complicato calcolare una giusta durata. Timeslice troppo piccola → elevato aggravio legato al cambio di contesto. Timeslice troppo grande → scheduler degenera in FIFO (bene per il batch, pessimo per l'interattivo). 44
Problemi 2/2 (I vecchi problemi sono spariti; ne sorgono di nuovi) La priorità è relativa mentre la timeslice è assoluta. Due processi con NICE 0 ed 1: le timeslice sono di 100ms e 95ms → 5% di differenza. Due processi con NICE 18 e 19: le timeslice sono di 10ms e 5ms → 100% di differenza! Sono state progettate diverse euristiche per affrontare questi problemi. Problema: le euristiche possono essere sfruttate per provocare Denial of Service (fatta l'euristica, trovato l'inganno!). 45
LINUX V2.6.23- 46
Differenze rispetto allo scheduler v2.5 (Parecchie; è una riscrittura da zero) Obiettivi. Semplificare lo scheduler, togliendo tutte le euristiche viste in precedenza. Rendere lo scheduler equo (fair) verso tutti i processi. Mantenere elevata l'interattività con gli utenti. → Completely Fair Scheduler (CFS) 47
Fair Scheduling (CFS cerca di emulare il comportamento di uno scheduler ideale) CFS cerca di imitare al meglio il comportamento di uno scheduler multitasking ideale (Generalized Processor Sharing, GPS) con le seguenti caratteristiche: timeslice infinitesima. servizio simultaneo ad n processi con capacità di elaborazione pari ad 1/n. Un Servito a capacità processo massima della CPU. Tre Serviti simultaneamente processi ad un terzo della capacità massima della CPU. 48
Perché GPS? (Già, perché proprio GPS?) GPS ha latenza di servizio minima. I processi non possono essere serviti più celermente di quanto non riesca a fare GPS. Perché non implementare direttamente GPS? 49
Problema (GPS non è implementabile) GPS non è implementabile perché un processore non può servire simultaneamente più processi. No, usare più processori non vale! GPS è definito solo per un processore. No, non si può ridurre la timeslice ad un valore quasi nullo nell'algoritmo O(1). L'aggravio legato al cambio di contesto ammazzerebbe le prestazioni. → Si deve implementare una approssimazione di GPS → Ripensamento delle strutture dati. 50
Strategia implementativa 1/3 (Cambia parecchio rispetto all'O(1)) Si cambia l'ordinamento della coda di pronto. Non più FIFO con priorità. I processi sono ordinati in base a quanta CPU hanno ricevuto in meno rispetto a GPS. → In tal modo si può scegliere in maniera efficiente il processo con il maggior bisogno di CPU. 51
Strategia implementativa 2/3 (Cambia parecchio rispetto all'O(1)) Non si usa una timeslice fissa per un processo. Si usa una sola timeslice, condivisa fra tutti i processi. → Una volta esaurita la timeslice, tutti i processi devono essere stati schedulati. → Ogni processo è schedulato almeno una volta nella timeslice. →Approssimazione della “schedulazione simultanea” effettuata da GPS. 52
Strategia implementativa 3/3 (Cambia parecchio rispetto all'O(1)) Non si usa Round Robin per la scelta del processo. Si sceglie il processo che ha ricevuto meno CPU rispetto a GPS. → CFS si comporta come GPS “a tratti”. 53
Scheduling latency (La timeslice condivisa da tutti i processi) La scheduling latency è la timeslice unica condivisa fra tutti i processi. Approssimazione di una timeslice infinitesima. Valore di default: 20ms (→ applicazioni multimediali). Tunable: /proc/sys/kernel/sched_latency_ns Ciascun processo ottiene una uguale proporzione di timeslice. timeslice(task)=sched_latency_ns/nr_tasks. timeslice(task) non può essere inferiore a 4ms. Tunable per regolare la timeslice minima: /proc/sys/kernel/sched_min_granularity_ns. 54
Priorità e pesi (Il peso quantifica la priorità nelle formule dello scheduler) La priorità statica del processo (in [0, 139]) è usata per definire dei valori detti pesi (weight). I pesi servono per scalare le grandezze di un processo (fetta di timeslice associata, tempo di esecuzione effettivo) in funzione della sua priorità. I pesi sono calibrati sperimentalmente e decrescono con la priorità assoluta. Priorità=0: peso massimo Priorità=139: peso minimo 55
Calcolo dinamico del quanto di tempo (Il processo esegue quanto gli serve, entro 20 ms.) La timeslice è la scheduling latency media per processo, pesata con la frazione relativa del peso del processo considerato. timeslice(task) = [ sched_latency_ns/nr_tasks ] * [ weight(task) / Σtasks weight (task) ] Rationale: più è elevata la priorità, più è basso il valore di priorità assoluta, più è alto il peso, più aumenta la frazione di timeslice assegnata. 56
Il tempo virtuale (Scorre più o meno rapidamente a seconda della priorità) Ciascun processo ha associato un tempo virtuale di esecuzione (virtual run time). Campo vruntime della task_struct. Durante l'esecuzione di un processo, il tempo virtuale cresce: normalmente alla priorità standard (120). più lentamente a priorità più alte (120). 57
Aggiornamento del tempo virtuale (Scorre più o meno rapidamente a seconda della priorità) Ad ogni rischedulazione viene ricalcolato il peso del processo uscente. Il valore vruntime è aumentato del tempo di esecuzione delta_exec in maniera proporzionale al rapporto fra la priorità standard e quella attuale. vruntime = [vruntime+delta_exec] * [ weight(prio=120) / weight(prio) ] Rationale: più elevata è la priorità assoluta, più è alto il peso, più lentamente cresce vruntime. 58
L'algoritmo di scheduling (Molto semplice) Si sceglie il processo eseguibile il cui vruntime è minimo. Problema: vruntime può andare in overflow. In tal caso, il processo attuale è eseguito per sempre (vruntime è a 64 bit!). Soluzione: si sceglie il processo eseguibile per cui la differenza vruntime – min_vruntime è minima. → Tale differenza non è soggetta ad overflow. 59
La “coda di pronto” (Non è più una coda!) Come pescare efficientemente (O(1)) il processo con differenza vruntime – min_vruntime minima? Si usa un albero rosso-nero (red-black tree) di task_struct ordinate secondo la chiave vruntime – min_vruntime. 60
L'albero rosso-nero (Ed alcune osservazioni di contorno) Δ = vruntime - min_vruntime Inserimento: O(log n) Cancellazione: O(log n) Aggiornamento: O(log n) Estrazione minimo: O(1) Δ = 300 vruntime e min_vruntime sono aggiornati ad ogni cambio di contesto oppure Δ = 100 Δ = 400 interruzione. I processi tendono a spostarsi Δ=0 Δ = 150 Δ = 410 da sinistra verso destra man mano che eseguono. I processi a priorità più alta si spostano Viene sempre selezionato più lentamente. il processo che si trova in 61 fondo a sinistra.
Fairness fra utenti diversi (Finora i processi sono stati considerati tutti appartenenti ad un utente) Si supponga di avere 25 processi, di cui 20 relativi ad un utente A. 5 relativi ad un utente B. CFS cerca di essere fair con tutti → l'utente A ottiene una fetta di CPU molto più consistente rispetto all'utente B. → Si rende necessario estendere i meccanismi di fairness a gruppi di processi. 62
Scheduling gerarchico (Si schedulano processi e gruppi di processi) A partire dal kernel v2.6.24 è stato aggiunto il supporto per la schedulazione gerarchica (hierarchical scheduling). Si introduce una nuova struttura dati: struct sched_entity (definita in $LINUX/include/linux/sched.h) che rappresenta un gruppo di processi. Ogni entity ha un suo vruntime e un suo peso, calcolato in maniera analoga a quanto visto prima. 63
L'albero rosso-nero, task e entity (Si schedulano prima le entity e poi i singoli processi) Δ = vruntime - min_vruntime Entity rappresentante Entity rappresentante tutti i processi. il gruppo dei processi Entity rappresentante dell'utente A. 1 il gruppo dei processi dell'utente B. 1. Si parte dalla entity Δ = 300 rappresentante tutti 2 i processi. Δ = 100 Δ = 400 2. Si schedula la entity relativa ai processi del gruppo A (ha un vruntime 3 più basso). Δ=0 Δ = 150 Δ = 410 3. Si schedula il processo con più fame di CPU nel Task_struct dei processi Task_struct dei processi gruppo A (vruntime=0). 4. Si cambia contesto a tale appartenenti al gruppo appartenenti al gruppo processo. 64 dell'utente A. dell'utente B.
Puoi anche leggere