Università degli Studi di Milano Bicocca Facoltà di Scienze Matematiche Fisiche e Naturali Corso di Laurea Specialistica in Informatica

Dimensione: px
Iniziare la visualizzazioe della pagina:

Download "Università degli Studi di Milano Bicocca Facoltà di Scienze Matematiche Fisiche e Naturali Corso di Laurea Specialistica in Informatica"

Transcript

1 Università degli Studi di Milano Bicocca Facoltà di Scienze Matematiche Fisiche e Naturali Corso di Laurea Specialistica in Informatica Real-time EDF scheduling per il kernel FreeBSD: analisi, implementazione e risultati sperimentali. Relatore: Dott. Sergio Ruocco Correlatore: Prof. Francesco Tisato Tesi di Laurea di: Marco Trentini Matr Anno Accademico 2010/2011

2 In memoria di Christopher Johnson McCandless (alias Alexander Supertramp)

3 Abstract I sistemi operativi Unix-like come Linux e FreeBSD nati negli ultimi 20 anni per applicazioni in ambiti server e workstation hanno recentemente trovato impiego anche in dispositivi embedded e mobili, in molti casi per applicazioni con requisiti realtime. Tuttavia, mentre per Linux l attività di adattamento del kernel alle necessità delle applicazioni real-time è fervente, per FreeBSD non ci è nota alcuna iniziativa in tal senso. Questa tesi presenta l adattamento per FreeBSD di un framework di scheduling realtime originariamente sviluppato per Linux, e basato sulla politica EDF con un meccanismo di limitazione del tempo di esecuzione dei task real-time, detto isolamento temporale. I risultati sperimentali ottenuti mostrano come un sistema FreeBSD dotato di questa classe di scheduling possa gestire workload real-time periodici e aperiodici rispettando i vincoli di tempo di esecuzione, deadline e periodo, anche in presenza di un elevato carico di sistema. i

4 Ringraziamenti Ringrazio, in ordine sparso, la comunità di FreeBSD, di Linux e dell open source in generale, che, grazie alla loro filosofia e passione, condividono conoscenza, documenti, mailing list, e soprattutto codice sorgente, senza i quali non avrei potuto svolgere questa tesi; la mia fidanzata Laura, per avermi più volte incentivato a terminare l elaborato; i miei famigliari; il relatore Dott. Sergio Ruocco e correlatore Prof. Francesco Tisato per avermi dato la possibilità di affrontare questo argomento e per avermi supportato con la loro esperienza durante tutto il periodo di tesi. ii

5 Indice Abstract Ringraziamenti Introduzione i ii vi 1 Scheduling dei processi nei sistemi operativi Concetti base di funzionamento dello scheduler Thread scheduling Attivazione dello scheduler Tipologie e obiettivi degli algoritmi di scheduling Sistemi batch First-Come First-Served Shortest Job First Shortest Remaining Time Next Sistemi interattivi Round Robin Scheduling a priorità Lottery scheduling Altre politiche di scheduling Sistemi real-time Rate Monotonic Scheduling Earliest Deadline First Sistemi multiprocessori Scheduling a classi di priorità a lista singola Affinity scheduling e scheduling a due livelli Space sharing Gang scheduling Scheduling su micro e mono kernel Il problema dell inversione di priorità Aspetti dell hardware legati allo scheduling dei processi Aspetti di energia, potenza e temperatura Sistemi operativi Unix-like:Linux e FreeBSD Unix Linux FreeBSD Settori di impiego di Linux e di FreeBSD Scheduling in Linux Evoluzione dello scheduling e stato dell arte Kernel 1.1.x-1.2.x Kernel 2.0.x-2.2.x Kernel 2.4.x Kernel (alias O(1)) iii

6 3.1.5 Kernel (alias CFS) Supporto al real-time RT-Preempt Sched-deadline Risultati sperimentali Altre proposte per Linux real-time OCERA AQuoSA Frescor LITMUS-RT RTLinux, RTAI, Xenomai Altri algoritmi di scheduling proposti dalla comunità Staircase Rotating Staircase Deadline Scheduler (RSDL) Brain Fuck Scheduler (BFS) Scheduling in FreeBSD Evoluzione dello scheduling e stato dell arte Scheduler 4BSD Scheduler ULE Confronto con Linux Supporto al real-time Real-time EDF per FreeBSD Dettagli implementativi di DEADLINE-BSD Risultati sperimentali Analisi dei risultati Considerazioni finali Conclusioni e direzioni future 129 A Nozioni di base 130 A.1 Il ruolo del sistema operativo A.2 Esecuzione di un programma A.3 Interrupt A.4 Processo, thread e multiprogrammazione A.5 Sistemi paralleli B Elementi di complessità computazionale 143 C Scheduling: problema teorico NP-Hard 145 D Il ruolo dello scheduling nell evoluzione dei computer 147 E Standard POSIX 149 E.1 POSIX F Interrupt di clock in FreeBSD 158 Bibliografia 160 iv

7 Elenco delle tabelle 3.1 Evoluzione dello scheduling nel kernel Linux Evoluzione dello scheduling in FreeBSD Esecuzione di un while(1) per 3 secondi con diversi vincoli temporali (runtime/periodo) v

8 Elenco delle figure 1.1 (a) Thread gestiti in spazio utente. (b) Thread gestiti in kernel Un esempio di scheduling con Shortest Job First. (a) Lista dei job in ordine di arrivo con relativo tempo di esecuzione in minuti. (b) Ordine di esecuzione dei job con shortest job first Un esempio di scheduling con classi di priorità Un esempio di scheduling real-time di tre processi periodici con RMS ed EDF Gang scheduling Un esempio di inversione di priorità Tecnica a priorità ereditata Una CPU single-core con HT e una CPU dual-core senza HT Evoluzione dei sistemi Unix-like Organizzazione dello scheduling nel filone di kernel Linux 1.1.x-1.2.x Organizzazione dello scheduling nel filone di kernel Linux 2.0.x-2.2.x Organizzazione dello scheduling nel filone di kernel Linux 2.4.x Organizzazione dello scheduling nel filone di kernel Linux Esempi di schedulazione a dominio in Linux Organizzazione dello scheduling nel filone di kernel Linux Organizzazione dello scheduling nel kernel Linux con sched_deadline Esecuzione di while(1) con la classe di scheduling sched_normal nel kernel Linux Esecuzione di while(1) con la classe di scheduling sched_fifo nel kernel Linux Esecuzione di while(1) con la classe di scheduling sched_deadline nel kernel Linux Organizzazione delle strutture dati nello scheduler 4BSD di FreeBSD Organizzazione delle strutture dati nello scheduler ULE di FreeBSD Esempi di riconoscimento di topologie di CPU in ULE Rappresentazione di un task real-time in DEADLINE-BSD Organizzazione delle strutture dati nello scheduler ULE di FreeBSD con DEADLINE-BSD Esecuzione di un task in while(1) di classe deadline con runtime 100ms e deadline/periodo 500ms (zoom out) Esecuzione di un task in while(1) di classe deadline con runtime 100ms e deadline/periodo 500ms (zoom in) Esecuzione di un task in while(1) di classe deadline con runtime 1ms e deadline/periodo 10ms (zoom out) Esecuzione di un task in while(1) di classe deadline con runtime 1ms e deadline/periodo 10ms (zoom in) Esecuzione di un task in while(1) di classe deadline con runtime 200us e deadline/periodo 500us (zoom out) vi

9 5.8 Esecuzione di un task in while(1) di classe deadline con runtime 200us e deadline/periodo 500us (zoom in) Esecuzione di un task in while(1) di classe timesharing in un workload con alto carico (zoom in) Esecuzione di un task in while(1) di classe real-time in un workload con alto carico (zoom in) Esecuzione di un task in while(1) di classe deadline con runtime 20ms e deadline/periodo 100ms in un workload con alto carico (zoom in) Esecuzione di due task in while(1) di classe real-time in un workload con alto carico (zoom in) Esecuzione di due task in while(1) di classe deadline con runtime 20ms e deadline/periodo 100ms in un workload con alto carico (zoom in) A.1 Relazione tra applicazioni, il kernel e l hardware A.2 Organizzazione della memoria all interno di un elaboratore elettronico A.3 Gestione di un interrupt hardware. (a) Un interrupt occorre. (b) Dopo la gestione dell interrupt, il controllo passa allo scheduler. (c) Lo scheduler manda in esecuzione un task A.4 Multiprogrammazione A.5 Transizioni di stato dei processi A.6 Thread e processi. (a) Tre processi indipendenti. (b) Un processo con tre thread A.7 Mutua esclusione per memoria condivisa A.8 (a) Sistemi a multiprocessori con memoria condivisa. (b) Multicomputer con scambio di messaggi. (c) Sistemi distribuiti A.9 (a) UMA senza cache. (b) UMA con cache. (c) NUMA con cache vii

10 Introduzione I sistemi operativi Unix-like come Linux e FreeBSD nascono negli ultimi 20 anni per soddisfare requisiti in ambiti server e workstation. Recentemente trovato impiego anche in dispositivi embedded e mobili, in molti casi per applicazioni con requisiti real-time, come nell intrattenimento, per le applicazioni audio, video e di posizionamento, nel campo della robotica, automotive, militare, e in generale, in applicazioni di controllo dove la tempistica di lettura e manipolazione di dati diventa indispensabile per ottenere risultati corretti. In Linux l attività di adattamento del kernel alle necessità delle applicazioni real-time è fervente da anni. Alcune società, come Montavista, Windriver e Timesys, hanno sviluppato e mantengono un propria versione del kernel Linux con supporto al real-time [48, 49, 50]. La comunità opensource ha fornito diverse proposte per integrare il supporto al real-time nel kernel Linux, come OCERA [52], AQuoSA [54], Frescor [55], LITMUS-RT [56], RTLinux [59] e RT-Preempt [62], per citare le maggiori. Inoltre, ogni anno, a partire dal 1999, si tiene il Real Time Linux Workshop [61], un seminario internazionale dedicato al supporto ed impiego di Linux in ambiti real-time. In FreeBSD non ci è nota alcuna iniziativa di adattamento del kernel alle necessità delle applicazioni real-time. Questa tesi presenta l implementazione per Free- BSD di un framework di scheduling real-time originariamente sviluppato per Linux, SCHED_DEADLINE [64], basato sull algoritmo di scheduling Earliest Deadline First (EDF) (rif ), con l aggiunta di un meccanismo, noto come Constant Bandwidth Server (CBS) [14], che limita il tempo di esecuzione dei task, garantendo un isolamento temporale tra essi. Il lavoro fatto per Linux si inserisce nel contesto del progetto europeo ACTORS [69], con la partecipazione di un gruppo di società tra cui Ericsson Research, Xilinx e AKAtech SA. A causa delle sostanziali differenze tra il kernel Linux e FreeBSD, come ad esempio la gestione dei timer, dei lock, il framework di scheduling, le strutture dati, per citarne alcune, questo lavoro non può essere considerato come un semplice porting del framework di scheduling fatto per Linux, ma bensì una re-implementazione dello stesso. I risultati ottenuti mostrano come un sistema FreeBSD dotato di questa classe di scheduling possa gestire workload real-time periodici e aperiodici, rispettando i vincoli di tempo di esecuzione, deadline e periodo, anche in presenza di un elevato carico di sistema. Questo lavoro apre la possibilità di impiegare FreeBSD anche in ambiti applicativi con requisiti real-time, dove Linux è il sistema operativo Unix-like dominante. Inoltre bisogna considerare che la licenza BSD-style di FreeBSD [91] presenta meno restrizioni di quella GPL del kernel Linux [33] e questo rende FreeBSD più appetibile per le aziende nella scelta del sistema operativo da adottare. Nel Capitolo 1 - Scheduling dei processi nei sistemi operativi - troviamo le nozioni sul funzionamento dello scheduler in un sistema operativo e una panoramica degli algoritmi di scheduling più noti in letteratura per sistemi batch, sistemi interattivi, sistemi a tempo reale e sistemi a multiprocessore. viii

11 Nel Capitolo 2 - Sistemi operativi Linux e FreeBSD - troviamo un introduzione a Linux e FreeBSD, il relativo contesto storico nell evoluzione di Unix, e le caratteristiche principale del kernel Linux e di quello FreeBSD. Inoltre una sezione è dedicata ai settori di impiego dei due sistemi operativi. Il Capitolo 3 - Scheduling in Linux - descrive in dettaglio l evoluzione dello scheduling dei processi e il relativo stato dell arte nel kernel Linux. Una sezione è dedicata al supporto al real-time. Un altra sezione riporta altri algoritmi di scheduling proposti dalla comunità. Il Capitolo 4 - Scheduling in FreeBSD - descrive in dettaglio l evoluzione dello scheduling dei processi e il relativo stato dell arte nel kernel FreeBSD. Inoltre è presente un analisi di confronto rispetto allo scheduling in Linux. Il Capitolo 5 - Real-time EDF per FreeBSD: implementazione e risultati - riporta l analisi, l implementazione, i risultati sperimentali e alcune considerazioni del trapianto della classe di scheduling sched_deadline nello scheduler ULE di FreeBSD. Alcune riflessioni e proposte di miglioramento su questo lavoro sono riportate in - Conclusioni e direzioni future. L Appendice A - Nozioni di base - fornisce le nozioni e termini di base utili per capire il funzionamento delle attività di scheduling dei processi nei sistemi operativi. Nell Appendice B - Elementi di complessità computazionale - troviamo delle nozioni sulla complessità computazionale rispetto al tempo di esecuzione degli algoritmi. L Appendice C - Scheduling: problema teorico NP-HARD definisce il problema di scheduling job shop e la relativa complessità computazionale. L Appendice D - Il ruolo dello scheduling nell evoluzione dei computer - riporta come le attività di scheduling abbiano giocato un ruolo primario nell evoluzione degli elaboratori elettronici e dei sistemi operativi. Nell Appendice E - standard Posix - troviamo una breve panoramica sullo standard Posix, in particolare gli standard che riguardano le attività di scheduling e di multithreading che un sistema operativo deve adottare affinché possa essere considerato Posix-compliance. L Appendice F - Interrupt di clock in FreeBSD - introduce la gestione degli interrupt di clock in FreeBSD, attività fondamentale per la caratteristica di timesharing nello scheduling dei processi. ix

12 Capitolo 1 Scheduling dei processi nei sistemi operativi Il problema generico del riordino di attività (scheduling) si presenta in diversi ambiti, come nella gestione di produzione industriale, nella gestione di controllo di atterraggio degli aerei e di sicuro nel calcolo scientifico. Queste problematiche si rifanno alla classica nozione del problema di sequenza di attività, dove si vuole ordinare una sequenza di attività al fine di ottenere un obiettivo prefissato, come la riduzione dei costi nella produzione industriale, una sequenza di atterraggio sicura in un aeroporto, l aumento del throughput in un sistema di calcolo o il rispetto di vincoli temporali di esecuzione nelle applicazioni real-time. Nella scienza dei calcolatori, la maggior attività di scheduling riguarda lo scheduling dei processi, ossia il problema di come organizzare l esecuzione dei vari processi sulle risorse di calcolo disponibili, al fine di raggiungere certi obiettivi. Ritroviamo il problema dello scheduling di processi nel kernel dei sistemi operativi ma anche a livello applicativo, nei sistemi distribuiti/paralleli (e.g. GRID) 1. Il problema dello scheduling è stato uno dei principali fattori nell evoluzione degli elaboratori elettronici e dei sistemi operativi (si veda l appendice D). A tutt oggi lo scheduling dei processi è un argomento di forte interesse, sia in ambito industriale/commerciale che in quello accademico per la ricerca scientifica. Ritroviamo le motivazioni di questo interesse nei seguenti aspetti principali: una maggior potenza di calcolo: oggigiorno architetture a multiprocessore sono sempre più alla portata di tutti, mentre la comunità scientifica ha a disposizione potenti sistemi paralleli per risolvere problemi complessi (si veda l appendice B); in questi sistemi con molte unità di calcolo è importante avere un algoritmo di scheduling costruito ad hoc per ottenere certi obiettivi, come massimizzare l utilizzo delle risorse (e quindi bilanciando il carico sulle risorse di calcolo) oppure massimizzare il throughput di sistema; la nuova tecnologia, in particolare i dispositivi embedded e mobili, richiede esigenze di real-time e una migliore interattività: questi obiettivi rientrano nelle attività di scheduling di processi, rispettivamente cercando di rispettare le deadline di esecuzione dei processi e migliorando il relativo response time; in un mondo sempre più tecnologico è importante fare green computing, tenendo sotto controllo aspetti come temperatura, energia e potenza dei sistemi di calcolo: l attività di svuotamento/riempimento di una cache, causata spesso dagli switch di contesto, richiede energia; far andare al 100% un core in un sistema multiprocessore 1 Altre attività di scheduling nei sistemi operativi riguardano il problema di come pianificare le diverse scritture/letture su dispositivi di I/O come hard disk (I/O scheduling) e in quale sequenza far transitare i pacchetti di networking, garantendo ad esempio servizi di Quality of Service (packet scheduling) 1

13 CAPITOLO 1. SCHEDULING DEI PROCESSI NEI SISTEMI OPERATIVI 2 con altri core in stato di idle è uno spreco di energia. Questi sono solo due esempi di come le attività di scheduling possono contribuire alla green computing. Questo capitolo fornisce le nozioni sul funzionamento dello scheduler nei sistemi operativi. La prima sezione introduce il ruolo dello scheduler nel kernel, i concetti di prelazione in modalità kernel/utente e lo scheduling di thread implementati in spazio utente. Nella seconda sezione troviamo tipologie, obiettivi e algoritmi di scheduling più noti per sistemi batch, interattivi, real-time e per sistemi con architetture a multiprocessori. La terza sezione mette a confronto le implementazioni degli scheduler su micro e mono kernel. Il problema di inversione di priorità con alcune possibile soluzioni viene presentato nella quarta sezione. Nella quinta sezione sono presenti alcuni aspetti dell hardware legati allo scheduling dei processi come la distanza di cache e la caratteristica di hyperthreading nelle architetture multiprocessori. Nella sesta sezione si porta qualche esempio di come le attività di scheduling possono giocare un ruolo primario nel tenere sotto controllo aspetti di energia, potenza e temperatura nei sistemi di calcolo. In questo capitolo, salvo dove diversamente specificato, con il termine thread si fa riferimento all entità schedulabile in kernel. 1.1 Concetti base di funzionamento dello scheduler Con la tecnica della multiprogrammazione (paragrafo A.4) diversi processi in stato di pronto competono per l utilizzo delle risorse di calcolo. L alternanza di esecuzione in CPU tra diversi processi avviene tramite switch di contesto (paragrafo A.4). La scelta di quale processo mandare in esecuzione è fatta da una parte del kernel chiamata scheduler e l algoritmo che implementa le politiche di decisione è chiamato algoritmo di scheduling. La suddivisione del tempo di CPU è spesso affidata ad un interrupt hardware (paragrafo A.3) che, ad intervalli regolari, segnala alla CPU la fine di un quanto di computazione (chiamato anche tick). Allo scadere di ogni tick può essere invocato lo scheduler per determinare se prelazionare il task attualmente in esecuzione (ad esempio quando il task esaurisce il suo quanto di esecuzione). Inoltre in quei sistemi con architetture a multiprocessori dove ogni risorsa di calcolo mantiene un proprio set di task da eseguire, allo scadere di ogni tick (o dopo un certo numero di tick) si può verificare se il carico di lavoro (numero di task in stato di pronto) è ben distribuito sulle varie risorse di calcolo, ed eventualmente effettuare un bilanciamento di carico. L invocazione dello scheduler, può avvenire in altre circostanze tra cui: quando un processo termina la sua normale esecuzione bisogna schedulare un nuovo processo; quando un processo si blocca per I/O, per un semaforo o per altre ragioni, un altro processo viene schedulato per l esecuzione in CPU; dopo la gestione di interrupt di I/O bisogna schedulare un nuovo processo, che potrebbe essere il processo che è stato interrotto o uno dei processi che dipendeva da quell interrupt, o anche un terzo processo; alla creazione di un nuovo processo bisogna decidere se mandare in esecuzione il padre o il figlio, o un altro processo; quando sono utilizzate alcune system call relative alle attività di scheduling (e.g. rilascio spontaneo della risorsa di calcolo da parte del task attualmente in esecuzione, cambio di classe di scheduling e/o di priorità di un task). In una decisione di scheduling, se non ci sono processi in stato di pronto, solitamente viene schedulato un processo di idle, il cui compito è quello di verificare in modo periodico la presenza di nuovi task in stato di pronto. Dopo la gestione di un interrupt, un algoritmo di scheduling nonpreemptive rischedula il

14 CAPITOLO 1. SCHEDULING DEI PROCESSI NEI SISTEMI OPERATIVI 3 processo che era in esecuzione durante l interrupt, o al più può schedulare un processo più importante (e.g. con più alta priorità) tornato in stato di pronto. In questo modo un processo può computare fino a quando si blocca per I/O o rilascia spontaneamente la CPU. Un algoritmo di scheduling preemptive lascia in esecuzione un processo fino al termine del suo quanto di esecuzione (un certo numero di tick) o quando si blocca per I/O, e quindi schedula un nuovo processo. Alcuni sistemi operativi permettono la caratteristica di prelazione di task solo in modalità utente, ma non in modalità kernel. Nell ultimo caso un task eseguito in modalità kernel può essere prelazionato solo quando termina le sue operazioni e ritorna in spazio utente, quando richiede azioni di I/O oppure, supponendo che non si siano disabilitati gli interrupt durante la sua esecuzione, da un interrupt (e.g. interrupt di clock). Un kernel nonpreemptive semplifica le operazioni di sincronizzazione nel kernel stesso ma impedisce ad esempio che un task real-time vada subito in esecuzione mentre è già in esecuzione un task in modalità kernel, e questo può comportare il mancato rispetto di una deadline di esecuzione del task, che, nel caso migliore, si traduce in perdita di dati Thread scheduling Nell implementazione dei thead in spazio utente (paragrafo A.4), lo switch tra i thread di uno stesso processo avviene tramite una procedura locale, che salva le informazioni del thread in esecuzione e sceglie il prossimo thread da mandare in esecuzione, caricando i nuovi valori nei registri di CPU. Lo scheduler non è quello del kernel ma bensì uno scheduler di libreria dedicato ai thread. Inoltre, lo scheduling non richiede ne trap ne switch di contesto ne tanto meno un svuotamento di cache. Questo velocizza la schedulazione dei thread (almeno un ordine di grandezza più veloce rispetto a trappare nel kernel). Con l implementazione del thread in spazio utente è possibile scegliere l algoritmo di scheduling più congruo alle caratteristiche dell applicazione stessa. Inoltre, se un thread è in attesa di un altro thread, non c è un trap in kernel poiché il tutto viene gestito dalla libreria dei thread linkata al processo stesso. La Figura 1.1 mostra le due implementazioni possibili di thread. Figura 1.1: (a) Thread gestiti in spazio utente. (b) Thread gestiti in kernel Una delle maggiori problematiche nell utilizzo dei thread in spazio utente riguarda la gestione delle chiamate di sistema bloccanti. Infatti un thread in attesa di I/O comporta il blocco di tutti gli altri thread associati allo stesso processo, poiché il processo viene messo in stato di attesa. La stessa cosa succede se un thread richiede un page fault (una richiesta di codice o dati che non sono stati caricati in memoria e che devono essere prelevati da disco). Un altra problematica riguarda come distribuire in modo equo il tempo di CPU tra i thread di uno stesso processo che, in genere, può avvenire solo invocando nel thread

15 CAPITOLO 1. SCHEDULING DEI PROCESSI NEI SISTEMI OPERATIVI 4 una speciale funzione (e non in modo trasparente come tramite un interrupt hardware). Nell implementazione dei thead in kernel, lo stesso è a conoscenza dei singoli thread dei processi. Quando un thread si blocca in attesa di I/O o per un page fault, lo scheduler del kernel, secondo la sua politica di scheduling, può mandare in esecuzione un altro thread dello stesso processo o di un altro processo. La ripartizione del tempo di CPU tra thread avviene nel modo usuale, tramite un interrupt hardware. Il maggior svantaggio dell implementazione dei thread in spazio kernel riguarda l overhead introdotto nelle transizioni tra spazio utente e kernel. Sono possibili implementazioni di thread miste, come quella descritta nelle prossima sezione Attivazione dello scheduler La soluzione della gestione dei thread proposta da Anderson & c. [3] chiamata attivazione dello scheduler cerca di utilizzare le proprietà migliori delle implementazioni di thread in spazio utente e kernel: il minimo overhead (evitando transizioni tra spazio utente e kernel) e l alta flessibilità che caratterizzano l implementazione dei thread in spazio utente combinato con alcune caratteristiche tipiche della soluzione in kernel, che permette una distribuzione equa del tempo di esecuzione tra i thread e permette di eseguire un altro thread dello stesso processo, anche quando questo ha un thread in stato di blocco. L idea che sta alla base di questa soluzione è la seguente: la gestione dei thread avviene in spazio utente e ogni processo ha assegnato un numero di processori virtuali (richiesti da lui o assegnati dal kernel, 1 di default) sui quali richiedere l esecuzione dei suoi thread. Quando il kernel rileva che un thread è stato bloccato (e.g. per un page fault o per una chiamata di sistema bloccante) allora il kernel notifica questo evento al processo del thread (chiamando una specifica funzione del processo e passando come parametri le informazioni del thread in questione). In questo modo il processo può aggiornare la propria tabella dei thread e schedulare l esecuzione di un altro thread. Un comportamento analogo avviene quando il kernel rileva che un thread può uscire dalla condizione di blocco. Quando un interrupt hardware occorre, la CPU passa in modalità kernel e il thread che era in esecuzione viene momentaneamente sospeso. Se l evento non riguarda il processo del thread sospeso allora, quando termina la gestione dell interrupt, viene ripristinata l esecuzione del thread, altrimenti viene notificato l evento (e la sospensione del thread) al processo del thread sospeso, il quale aggiornerà la propria tabella dei thread per poi schedulare l esecuzione di un altro thread (o di quello sospeso). 1.2 Tipologie e obiettivi degli algoritmi di scheduling L algoritmo di scheduling e le relative politiche dipendono fortemente dal tipo e dagli obiettivi del contesto applicativo. Inoltre dipendono dall architettura hardware dei sistemi, le architetture a multiprocessore ne sono un esempio. Possiamo suddividere gli ambiti applicativi in tre categorie principali: batch, interattivi e sistemi real-time. Nei sistemi batch (e.g. sistemi bancari e assicurativi) l interattività è messa in secondo piano, dando più importanza alle prestazioni di calcolo. In questo caso si usano algoritmi nonpreemptive oppure algoritmi preemptive ma con un lungo periodo di esecuzione per ogni processo (riducendo l overhead di switch di contesto). Nei sistemi interattivi (e.g. workstation e server) vengono impiegati algoritmi preemptive, distribuendo equamente il tempo di CPU a tutti gli utenti e cercando di ottenere tempi di risposta rapidi. Nei sistemi real-time, che ritroviamo ad esempio nel campo della robotica, militare, e in dispositivi embedded e mobili, ci sono dei vincoli ben precisi sui tempi di esecuzione delle applicazioni. In questi sistemi è indispensabile avere un algoritmo preemptive che tenga in considerazione le deadline di esecuzione dei processi. Nelle prossime sezioni verranno presentati alcuni degli algoritmi di scheduling più noti per le varie categorie dei sistemi. Ulteriori informazioni sulle caratteristiche degli algoritmi di scheduling possono essere trovate nella sezione di [1].

16 CAPITOLO 1. SCHEDULING DEI PROCESSI NEI SISTEMI OPERATIVI Sistemi batch Nei sistemi batch troviamo in prevalenza processi CPU bound e, in genere, vengono utilizzate le seguenti tre metriche per verificare le prestazioni del sistema: throughput, ossia quantità di processi eseguiti in un determinato periodo di tempo, il delta di completamento, ossia una media del tempo intercorso dal momento in cui un processo è pronto per essere eseguito fino al suo completamento e infine l utilizzo della CPU. Un utilizzo di CPU vicino al 100% può indicare la necessità di avere maggior potenza di calcolo. Throughput e delta di completamento sono spesso in contrapposizione tra loro: un algoritmo che massimizza il throughput non necessariamente minimizza il delta di completamento. Seguono alcuni degli algoritmi di scheduling più noti per i sistemi batch. Ulteriori dettagli sullo scheduling nei sistemi batch possono essere trovati nella sezione di [1] First-Come First-Served L algoritmo di scheduling noto come First-Come First-Served (nonpreemptive) schedula il processo a seconda dell ordine di arrivo. Una lista mantiene l ordine di arrivo dei processi in stato di pronto: in testa alla lista c è il prossimo processo che verrà mandato in esecuzione, in coda l ultimo processo che è andato in stato di pronto. In caso di processi misti, i processi di I/O bound possono essere rallentati da processi di CPU bound Shortest Job First Nel caso in cui si possa determinare i tempi di esecuzione dei processi (anche con una stima basata su misurazioni passate), l algoritmo di scheduling Shortest Job First (nonpreemptive) schedula il processo con il tempo di esecuzione più breve. Questo permette di ridurre i delta di completamento dei processi (per quei processi che, al momento della schedulazione, sono già in stato di pronto), aumentando il throughput di sistema. La Figura 1.2 mostra un esempio di scheduling con Shortest Job First. Figura 1.2: Un esempio di scheduling con Shortest Job First. (a) Lista dei job in ordine di arrivo con relativo tempo di esecuzione in minuti. (b) Ordine di esecuzione dei job con shortest job first Shortest Remaining Time Next L algoritmo di scheduling Shortest Remaining Time Next, sceglie sempre il processo con tempo di esecuzione rimanente più breve, con la possibilità di interrompere il processo attualmente in esecuzione (preemptive). Questo algoritmo è ideale nei sistemi in cui si vuole dare priorità di esecuzione ai processi brevi Sistemi interattivi Nei sistemi interattivi troviamo in prevalenza processi I/O bound. L obiettivo primario di questi sistemi è di minimizzare il tempo di risposta dei processi degli utenti, ossia il tempo intercorso tra la sottomissione di un job e l ottenimento del relativo output. Un altro obiettivo è quello di cercare di soddisfare le aspettative degli utenti (e.g. l apertura di un browser dovrebbe avere la priorità su una scrittura in hard-disk).

17 CAPITOLO 1. SCHEDULING DEI PROCESSI NEI SISTEMI OPERATIVI 6 Seguono alcuni degli algoritmi di scheduling più noti per i sistemi interattivi. Ulteriori dettagli sullo scheduling nei sistemi interattivi nella sezione di [1] Round Robin L algoritmo di scheduling Round Robin schedula i processi in stato di pronto con una fifo e permette la loro esecuzione per un quanto di tempo (un certo numero di tick), il cui valore è definito a priori. Allo scadere del quanto avviene un contex-switch e il task viene messo in coda alla lista. Quanti di esecuzione troppo brevi possono introdurre molto overhead nel sistema (per via dei frequenti contex-switch) riducendo l efficienza di CPU, mentre quanti di esecuzione troppo lunghi allungano i tempi di risposta dei processi Scheduling a priorità Quando è necessario distinguere i processi in base a caratteristiche specifiche può essere usato uno scheduling a priorità: ad ogni processo è assegnata una priorità e viene schedulato il processo con priorità più alta. Le priorità possono essere assegnate in modo statico (e.g. in base all importanza dei processi) o dinamico (e.g. in base all utilizzo del proprio quanto di esecuzione) e alcuni sistemi operativi danno la possibilità all utente, tramite un comando (e.g. nice su Unix), di variare la priorità dei processi. Lo scheduling a classe di priorità raggruppa i processi (in stato di pronto) in classi, ognuna delle quali ha una priorità e un quanto di esecuzione (entrambi costanti per ciascun processo della classe). La Figura 1.3 mostra un esempio di scheduling con classi di priorità. Figura 1.3: Un esempio di scheduling con classi di priorità. I processi all interno di una stessa classe possono essere schedulati in round-robin con timesharing. All occorrenza dopo un certo numero di quanti di esecuzione possono essere declassati di priorità (feedback scheduling), evitando che processi di alta priorità monopolizzino la CPU ma allo stesso tempo concedendo un maggior tempo di esecuzione ai processi prioritari. Inoltre, a seconda delle esigenza di sistema è possibile evitare il declassamento per alcune classi di priorità Lottery scheduling Esistono anche algoritmi di scheduling che si basano sulla probabilità che un evento si verifichi. L idea alla base del lottery scheduling [9] è di avere un numero finito di biglietti associati a delle risorse, come al tempo di CPU, e di assegnare questi biglietti ai vari processi. Quando bisogna prendere una decisione su quale processo schedulare, viene estratto in modo random un biglietto e il processo che possiede quel biglietto ottiene la risorsa (in questo caso la CPU). Quindi più biglietti ha il processo e più è alta la probabilità che venga schedulato. I processi ritenuti più importanti possono avere più biglietti al fine di aumentare la probabilità che vengano schedulati e quindi aumentare il tempo di esecuzione in CPU. Inoltre, a seconda delle esigenze, i processi possono

18 CAPITOLO 1. SCHEDULING DEI PROCESSI NEI SISTEMI OPERATIVI 7 cedere/richiedere i biglietti, ad esempio un processo che dipende da operazioni di un secondo processo può cedere momentaneamente a quest ultimo i suoi biglietti, o parte di essi, per aumentare le probabilità che il processo da cui dipende vada in esecuzione, eliminando al dipendenza Altre politiche di scheduling Un algoritmo di scheduling che vuole garantire un equa distribuzione del tempo di CPU ai vari processi tenendo in considerazione il tempo di arrivo dei processi stessi, può schedulare i processi con il più basso rapporto tra il tempo effettivamente consumato in CPU e il tempo di esecuzione concesso al processo. Il primo è recuperabile dalla tabella dei processi, il secondo può essere calcolato dividendo il tempo trascorso da quando il processo è stato sottomesso per il numero di processi in stato di pronto. Esistono algoritmi di scheduling che garantiscono un equa distribuzione del tempo di CPU ai vari processi tenendo in considerazione il proprietario dei processi. Questo per evitare che un utente che ha molti processi ottenga un maggior tempo di utilizzo di CPU rispetto a un utente che ha meno processi attivi Sistemi real-time In un sistema operativo real-time i task hanno dei vincoli temporali di esecuzione bene definiti (e.g. una deadline entro la quale il task deve terminare la sua esecuzione): è compito del sistema operativo, ed in particolare dello scheduler, garantire il rispetto di questi vincoli temporali. Le caratteristiche principali di un sistema operativo real-time (RTOS) sono le seguenti: un kernel il più possibile prelazionabile: ossia ridurre al minimo la possibilità di eseguire codice (e.g. gestori di interrupt) con la caratteristica di prelazione disabilitata e comunque assicurarsi che queste latenze siano minime, in quanto potrebbero comportare un mancato rispetto di qualche scadenza temporale di task real-time; supporto di informazioni sui vincoli temporali nelle struttura informativa dei task; supporto nello scheduler di (almeno) un algoritmo di scheduling real-time; test di ammissione per nuovi task real-time; supporto alla caratteristica di prevedibilità : l idea è di evitare che processi molto importanti possano non rispettare qualche scadenza, preferendo, se possibile, di non rispettare scadenze di processi meno rilevanti; Oltre a queste caratteristiche software fondamentali anche l hardware deve essere predisposto per il real-time, ad esempio supportando in modo nativo timer ad alta risoluzione, utilizzabili dallo scheduler per gestire i task real-time. Ci sono sistemi in tempo reale dove le deadline devono essere assolutamente rispettate (hard real time), e sistemi dove, in modo occasionale, una mancata scadenza viene tollerata (soft real time). Nei sistemi in tempo reale, in genere, lo scheduler viene attivato da un evento esterno, che può essere periodico o aperiodico. Possiamo dire che un sistema in tempo reale è schedulabile se riusciamo a garantire i vincoli temporali di esecuzione di tutti i processi real-time sottomessi. Seguono due degli algoritmi di scheduling più noti per i sistemi real-time. Ulteriori dettagli sullo scheduling nei sistemi real-time nelle sezioni e 7.5 di [1].

19 CAPITOLO 1. SCHEDULING DEI PROCESSI NEI SISTEMI OPERATIVI Rate Monotonic Scheduling L algoritmo di scheduling Rate Monotonic Scheduling (RMS) può essere usato in sistemi con queste caratteristiche: ogni processo real-time periodico deve essere completato entro il suo periodo; non c è dipendenza tra processi; ogni processo necessita lo stesso tempo di CPU su ogni burst; i processi non periodici non hanno deadline di esecuzione e l overhead per la prelazione di processi è trascurabile. L algoritmo assegna a ogni processo un priorità pari alla frequenza di occorrenza del suo evento, in modo lineare con il rate (e.g. 33Hz priorità 33). In una decisione di scheduling, lo scheduler manda in esecuzione il processo in stato di pronto con priorità maggiore Earliest Deadline First L algoritmo di scheduling Earliest Deadline First (EDF) può essere usato in sistemi con processi real-time periodici aperiodici o sporadici. Lo scheduler mantiene una lista di processi in stato di pronto ordinati in base alle loro deadline di esecuzione: in una decisione di scheduling viene mandato in esecuzione il processo con la deadline più prossima alla scadenza (quella di valore minore). La Figura 1.4 mostra un esempio di scheduling di tre processi periodici applicando sia RMS che EDF. I burst di esecuzione di ciascun processo, devono essere schedulati in Figura 1.4: Un esempio di scheduling real-time di tre processi periodici con RMS ed EDF. ordine, quindi A1 prima di A2, B1 prima di B2, e così via. Quindi la deadline di esecuzione di A1 è t = 30, quella di B1 t = 40, quella di C1 t = 50 e così via per gli altri. Come è possibile vedere dalla figura, utilizzando l algoritmo RMS, la deadline di esecuzione di C1 non viene rispettata. Con EDF, invece, vengono rispettate tutte le deadline di esecuzione dei processi Sistemi multiprocessori Nei sistemi a multiprocessore le attività di scheduling devono tenere in considerazione ulteriori aspetti come il bilanciamento di carico e l affinità dei task di CPU. Seguono alcuni degli algoritmi di scheduling più noti per i sistemi a multiprocessore. Per ulteriori dettagli sullo scheduling su multiprocessori si rimanda alla sezione di [1] Scheduling a classi di priorità a lista singola Nel caso di processi indipendenti, un possibile algoritmo di scheduling a suddivisione del tempo potrebbe prevedere un unica struttura contenente un set di liste di processi in stato di pronto, una per ogni priorità. All interno delle liste i processi sono gestiti in

20 CAPITOLO 1. SCHEDULING DEI PROCESSI NEI SISTEMI OPERATIVI 9 round-robin. Ogni qual volta si decide di mandare in esecuzione un processo su una CPU, lo scheduler prende il mutex della struttura e schedula il primo processo della lista non vuota con priorità più alta. In questo modo da un lato abbiamo un bilanciamento di carico automatico, dall altro una forte competizione per l accesso alla struttura dati di scheduling, soprattutto con un numero elevato di risorse di calcolo (che potrebbe portare alcune CPU a rimane in attesa di poter accedere alla struttura dati, introducendo di fatto tempi morti) Affinity scheduling e scheduling a due livelli Se un thread è stato di recente in esecuzione su una certa risorsa di calcolo è molto probabile che nella cache della stessa (così come nella memoria di sistema) ci siano ancora informazioni utili per quel thread. Quindi, schedulando i thread sulle stesse risorse di calcolo sulle quali sono stati mandati in esecuzione di recente si potranno utilizzare le informazioni che sono già presenti in cache, senza doverle prendere dalla memoria (o in caso di informazioni in memoria di sistema dal disco fisso), che comporterebbe overhead per il sistema. Questa tecnica è chiamata affinity scheduling. Un modo per sfruttare questa tecnica è di usare un algoritmo di scheduling a due livelli. In un primo livello l algoritmo distribuisce i thread in stato di pronto sulle varie risorse di calcolo, tenendo in considerazione su quale risorsa di calcolo il thread è stato eseguito di recente o, nel caso di un nuovo thread, cercando di bilanciare il carico di lavoro sulle varie risorse di calcolo. In questo modo ogni CPU/core ha la sua lista di processi in stato di pronto che può eseguire. La reale schedulazione dei thread (secondo livello) è fatta su ogni risorsa di calcolo in modo indipendente, usando un algoritmo a priorità o altri metodi. Nel caso una CPU/core abbia terminato i propri thread l algoritmo può prevedere uno spostamento di thread da altre CPU. Lo scheduling a due livelli oltre a tenere in considerazione l affinità tra thread e CPU, riducendo l overhead per page fault, effettua un bilanciamento di carico tra le CPU del sistema. Inoltre riduce fortemente la competizione di accesso alle liste di thread in stato di pronto tra le varie CPU (che occorre solo quando una CPU ha esaurito i thread a lei assegnati, per spostare alcuni processi da altre CPU) Space sharing La tecnica che permette di schedulare thread di gruppo nello stesso tempo su diverse CPU è chiamata space sharing. Dato un gruppo di thread, un semplice algoritmo di space sharing potrebbe associare una CPU a ogni thread; il gruppo di thread verrebbe schedulato non appena ci sono abbastanza CPU libere (per la schedulazione dei gruppi di thread potrebbe essere schedulato prima il gruppo che richiede meno CPU e con tempo di esecuzione globale inferiore, oppure con un semplice first-come first-served). Ogni thread mantiene la CPU fino alla sua terminazione. Questo algoritmo assume che ogni gruppo di thread abbia un numero inferiore o al più uguale al numero di CPU/core fisicamente disponibili. Una variante di questo algoritmo potrebbe prevedere una variazione dinamica del numero di thread del gruppo in base al numero di CPU disponibili (quindi l applicativo deve avere il modo di capire quante risorse di calcolo sono disponibili prima della creazione dei suoi thread e questo può essere fatto con una chiamata di sistema che restituisce il numero di CPU libere in quel momento). Di fatto la tecnica di space sharing da un lato elimina la multiprogrammazione, e di conseguenza anche l overhead introdotto dagli switch di contesto, dall altro introduce tempi morti, sia quando alcune CPU rimangono in attesa di essere utilizzate, sia quando un thread rimane in attesa di I/O Gang scheduling L algoritmo gang scheduling schedula gruppi di thread sia in base allo spazio sia in base al tempo. L algoritmo permette di schedulare thread di gruppo in modo simultaneo,

21 CAPITOLO 1. SCHEDULING DEI PROCESSI NEI SISTEMI OPERATIVI 10 utilizzando risorse di calcolo che sono condivise nel tempo, come mostrato nella Figura 1.5. L idea è di schedulare i thread in modo sincrono sulle CPU, in modo tale che ad ogni Figura 1.5: Gang scheduling. quanto di esecuzione siano schedulati thread di uno stesso gruppo. 1.3 Scheduling su micro e mono kernel Nei sistemi monolitici il kernel contiene tutti i servizi di base, inclusi il gestore della memoria, i driver, lo scheduler, e così via. I servizi del kernel sono messi a disposizione dei programmi utente tramite un set di chiamate di sistema. In un sistema monolitico lo scheduler è incluso nel kernel e i programmi utenti non possono agire sulla sua logica e politica di funzionamento ma sono limitati alla variazioni di alcuni parametri (e.g. aumentare o diminuire la priorità di schedulazione di un processo). Ritroviamo esempi di sistemi operativi monolitici nella maggior parte dei sistemi Unix-like, come Linux e FreeBSD. In un sistema a microkernel i servizi e le funzionalità del sistema operativo sono suddivisi in piccoli moduli e solo il modulo che fornisce le funzionalità essenziali viene eseguito in spazio kernel. Gli altri moduli sono eseguiti in spazio utente. Riducendo la quantità di software che viene eseguito in modalità kernel, si riduce la possibilità che un bug nel kernel possa portare instabilità all intero sistema. L idea che sta dietro ai sistemi a microkernel è quella di fornire in kernel i meccanismi per fare qualcosa ma non la relativa politica e logica, che vengono implementate in spazio utente (cosiddetta separazione del meccanismo dalla politica. Ad esempio il meccanismo nel kernel per lo scheduling di processi potrebbe essere quello di mandare in esecuzione il processo con priorità più alta; le politiche e relative logiche di assegnamento delle priorità ai processi possono essere implementate in spazio utente. Esempi di sistemi operativi con microkernel sono MINIX e L4. Ulteriori dettagli sulla struttura di un sistema operativo nella sezione 1.7 di [1]. 1.4 Il problema dell inversione di priorità Il problema di inversione di priorità si manifesta in uno scheduling a priorità quando un task di alta priorità aspetta il rilascio di una risorsa acquisita da un task di più bassa priorità, la quale esecuzione però (che permetterebbe il rilascio della risorsa) è stata interrotta da altri eventi, come ad esempio la prelazione da parte di un terzo task di media priorità. In questo caso il task di alta priorità non può proseguire la sua esecuzione fino a quando il task di bassa priorità non verrà eseguito e rilascerà la risorsa contesa, e questo avverrà quando lo scheduler deciderà di mandare in esecuzione il task di bassa priorità (e.g. non ci sono task in stato di pronto la cui priorità è superiore a quella del task di bassa priorità). La figura Figura 1.6 mostra un esempio di inversione di priorità

22 CAPITOLO 1. SCHEDULING DEI PROCESSI NEI SISTEMI OPERATIVI 11 tra tre task: il task C, meno prioritario, va in esecuzione e prende il lock della risorsa X; a questo punto viene prelazionato da un task di maggiore priorità, il task B. Il task B a sua volta viene prelazionato dal task A, di priorità maggiore, che ha bisogno del lock della risorsa X (mantenuto dal task C). Il task C può rilasciare il lock della risorsa X solo dopo che il task B rilascia la CPU. A questo punto il task C rilascia il lock che viene preso dal task A. Esistono diverse soluzioni per evitare problemi di inversioni di priorità: Figura 1.6: Un esempio di inversione di priorità. disabilitando la prelazione per i task che acquisiscono una risorsa; proibendo a task di differenti priorità di acquisire una stessa risorsa; tecnica a priorità ereditata: se un task di alta priorità è in attesa che un task di più bassa priorità rilasci la risorsa contesa, allora quest ultimo eredita la priorità del primo fino a quando rilascia la risorsa; vale la proprietà transitiva e quindi se il proprietario di un lock (per il quale un altro task di più alta priorità ne sta aspettando il rilascio) è bloccato da un secondo lock, allora il proprietario di questo secondo lock eredita la priorità del task bloccato sul primo lock. La figura Figura 1.7 mostra l utilizzo della tecnica a priorità ereditata per l esempio di inversione di priorità, mostrato nella Figura 1.6. Figura 1.7: Tecnica a priorità ereditata. associando una priorità ad ogni lock, di numero pari o superiore alla priorità di ogni possibile task che può acquisirlo; quindi il task che acquisisce un lock eredita anche la sua priorità; smart scheduling: se un thread che ha preso uno spin lock viene prelazionato per lo scadere del suo quanto di computazione e se i successivi thread richiedono il medesimo mutex, questi rimangono in attesa fino a quando non viene schedulato il thread che può rilasciare il mutex. Per evitare che diversi thread rimangano in attesa, consumando cicli di CPU, lo scheduler può concedere più tempo affinché un thread, che ha preso un mutex, lo possa rilasciare prima che venga prelazionato da un altro thread. L informazione di lock su un mutex può essere gestita nella tabella dei processi. In un sistema con scheduling a priorità è fondamentale implementare una delle tecniche sopra citate per evitare problemi di inversione di priorità che possono portare a situazioni di stallo, come avvenne nella missione Mars Pathfinder [11].

23 CAPITOLO 1. SCHEDULING DEI PROCESSI NEI SISTEMI OPERATIVI Aspetti dell hardware legati allo scheduling dei processi Il tuning di uno scheduler è sempre più legato alle caratteristiche delle architetture sulle quali opererà e questo per sfruttare le potenzialità dell architettura stessa. Nel modello SMP su architetture multiprocessori (sezione A.5) lo scheduler del sistema operativo ha a disposizione tutte le risorse di calcolo disponibili sul sistema per mandare in esecuzione i processi/thread. E quindi importante che il kernel sia a conoscenza dei dettagli architetturali delle risorse di calcolo, al fine di effettuare una schedulazione il più possibile efficiente, rispettando gli obiettivi del sistema. Distanza di cache Su architetture UMA con CPU multicore, così come su architetture NUMA con CPU monocore/multicore è importante conoscere la distanza di cache/memoria tra le varie risorse di calcolo: questo informazione, che esprime sostanzialmente il livello di condivisione di cache/memoria tra due core (e.g. in NUMA, due core su due nodi diversi avranno una distanza di cache/memoria maggiore rispetto a due core di uno stesso nodo), può essere utilizzata in una decisione di scheduling quando si vuole mantenere il più possibile la caratteristica di affinità di core (sezione ) del processo che deve essere schedulato: se un processo è stato di recente in esecuzione su un certo core è probabile 2 che alcuni delle sue informazioni (codice e dati) siano ancora presenti nei livelli di cache del core e nella relativa memoria di sistema. Tra le risorse di calcolo disponibili in un dato momento (in idle, quelle meno cariche o quelle su cui il task può essere mandato in esecuzione) schedulare un processo su un core con minor distanza di cache rispetto all ultimo core che ha eseguito il processo stesso, può evitare di introdurre overhead nel sistema, dovuto al recupero in cache e/o in memoria delle sue informazioni (codice e dati) necessari per l esecuzione del processo. Hyperthreading La caratteristica multithreading/hyperthreading (sezione A.5) permette di mantenere in CPU lo stato di due o più thread: questo è reso possibile replicando il set di registri in CPU (registri general-purpose, di controllo, APIC e i registri funzionali). La figura Figura 1.8 mette a confronto una CPU single-core con HT (hyperthreading) e una CPU dual-core senza HT. Notare come nella CPU con HT l unità AS (che contiene il set di registri) sia replicata, permettendo di fatto di mantenere contemporaneamente lo stato (ma non l esecuzione) di due processi distinti. La caratteristica di hyperthreading deve essere presa in considerazione in una decisione di scheduling. Ad esempio su un sistema a 2 CPU fisiche con hyperthreading a 2 thread (l OS vede 4 risorse di calcolo), schedulando 2 task sulle 2 CPU logiche che fanno parte di un unica CPU fisica (un solo task può essere in esecuzione in un dato istante) e lasciando l altra CPU fisica in idle, risulterebbe meno efficiente rispetto ad ignorare la caratteristica di hyperthreading (e quindi l OS vederebbe due sole risorse di calcolo) schedulando un task per ogni CPU fisica (qui di fatto abbiamo 2 task in esecuzione contemporaneamente). 1.6 Aspetti di energia, potenza e temperatura Le attività di scheduling possono contribuire a tenere sotto controllo aspetti come temperatura, energia, potenza e anche costi dei sistemi di calcolo. Seguono alcuni esempi. Su sistemi a multiprocessore un buon algoritmo di bilanciamento di carico previene la possibilità di avere core con carichi di lavoro molto sbilanciati e questo riduce il consumo 2 dipende anche dalla dimensione di cache/memoria e dalle azioni dei processi che sono andati in esecuzione dopo di lui sul medesimo core

24 CAPITOLO 1. SCHEDULING DEI PROCESSI NEI SISTEMI OPERATIVI 13 Figura 1.8: Una CPU single-core con HT e una CPU dual-core senza HT. di energia del sistema, così come riduce la temperatura del sistema stesso. Nei sistemi mobili embedded è fondamentale evitare sprechi di consumo di energia per allungare il più possibile la durata delle batterie: uno svuotamento di cache, così come un cache-hint sono operazioni che richiedono un consumo di energia, e spesso queste operazioni sono indotte da switch di contesto forzate dallo scheduler (e.g. lunghezza dei quanti di esecuzione). Su questi sistemi e importante quindi avere un buon compromesso tra prestazioni e consumo di energia. In fase di design di un sistema di calcolo su architettura multicore con obiettivi prefissati, progettando uno scheduler che sfrutta al meglio tutte le caratteristiche architetturali delle risorse di calcolo (vedi la sezione precedente 1.5) può far risparmiare l installazione di qualche unità di CPU, riducendo di fatto la potenza (in termini di Watt) e i costi richiesti dal sistema.

25 Capitolo 2 Sistemi operativi Unix-like:Linux e FreeBSD Questo capitolo introduce i sistemi operativi Linux e FreeBSD sui quali verranno analizzate e messe a confronto, nei capitoli successivi, le relative attività di scheduling di processi. Inoltre, in questo capitolo i due sistemi operativi saranno posizionati nel contesto storico di Unix, dal quale derivano. La prima sezione introduce il sistema operativo Unix, la sua evoluzione nei molteplici sistemi Unix-like che oggi conosciamo e i punti di forza del suo successo. La seconda e terza sezione introducono rispettivamente i sistemi operativi Linux e FreeBSD con una panoramica delle principali caratteristiche dei relativi kernel. Infine, nella quarta sezione troviamo alcuni esempi di settori di impiego dei due sistemi operativi. 2.1 Unix Oggigiorno con il termine Unix 1 si fa riferimento a una famiglia di sistemi operativi che condividono alcune scelte di progettazione e che implementano simili interfacce di programmazione (API). Dobbiamo però ricordare che Unix è stato anche un sistema operativo specifico, dal quale derivarono la maggior parte dei sistemi operativi Unix-like che oggi conosciamo. Unix fu sviluppato nei laboratori Bell Labs da Ken Thompson e Dennis Ritchie nel 1969 sulle idee fallimentari di MULTICS (vedere appendice D). Inizialmente sviluppato su PDP-7 [5], nel 1971 Unix fu portato su PDP-11 [6]. Nel 1973 Unix fu riscritto utilizzando il linguaggio C. Poiché il codice sorgente di UNIX fu reso disponibile liberamente, diverse organizzazioni ne svilupparono una propria versione. Una delle prime versioni di Unix che vide la luce al di fuori dei laboratori Bell Labs fu Unix System, Sesta Edizione, chiamata anche V6. Nel 1977 sempre Bell Labs rilascia Unix System III e nel 1982 AT&T rilascia la sua versione di Unix, System V. Oltre a AT&T un altro importante attore che contribuì allo sviluppo di Unix fu l università di California a Berkeley. Le varianti di Unix di Berkeley sono note con il nome di BSD, acronimo di Berkeley Software Distribuitions. La prima release di BSD fu 1BSD nel 1977, sostanzialmente una collezione di patch e software addizionale per la versione di Unix dei Bell Labs, alla quale seguì 2BSD nel Il primo Unix di Berkeley standalone fu 3BSD rilasciato nel 1979, alla quale seguì il filone 4.xBSD che terminò nel 1994 con l ultima release ufficiale di Berkley, la 4.4BSD. Tra le molte caratteristiche che ritroviamo nelle varie release di BSD citiamo il sistema di gestione di memoria virtuale, il controllo dei job e l implementazione dello stack TCP/IP. Nel 1992 William Jolitz porta il sistema 4.3BSD-Lite (la versione di 4.3BSD liberamente 1 Il termine Unix in origine fu Unics (acronimo di UNiplexeed Information and Computing Service, derivato dal termine MULTICS); cambiò in Unix per evidenziare il fatto di poter elaborare più informazioni alla volta (la x di Multiplexeed) 14

26 CAPITOLO 2. SISTEMI OPERATIVI UNIX-LIKE:LINUX E FREEBSD 15 distribuibile) su architettura intel 80386, e da qui nasce 386BSD. A partire dai sistemi 4.3BSD-Lite, 4.4BSD-Lite e 386BSD si sviluppano diverse varianti BSD-like: FreeBSD, OpenBSD e NetBSD (da FreeBSD si dirama un altro progetto noto come DragonFlyBSD). Tra il 1980 e il 1990 diverse aziende produttrici di macchine server introducono la propria versione commerciale di Unix. Questi sistemi, basati sulla versione di Unix di AT&T o su quella di Berkeley, furono progettati intorno alle caratteristiche hardware proprie dell architettura dei loro server. Tra le maggiori ricordiamo HP-UX di Hewlett Packard, AIX di IBM, IRIX di SGI, Solaris e SunOS di Sun. Al fine di permettere di scrivere programmi in grado di essere eseguiti su diversi sistemi UNIX, IEEE sviluppa a partire dal 1988 lo standard POSIX (acronimo di Portable Operating System Interface), anche noto con il nome , che definisce un minimo insieme di chiamate di sistema, ma anche comandi utenti, che un sistema UNIX deve supportare per essere POSIX-compliance. Nel 1984, Andrew Tanenbaum sviluppa per scopi educativi MINIX, un piccolo clone di UNIX con il supporto POSIX. A partire dalle idee di MINIX, Linus Torvalds, uno studente finlandese, sviluppa Linux (1991), o meglio il kernel Linux dal quale poi si basano tutte le varie distribuzioni Linux open e commerciali che oggi conosciamo. La Figura 2.1 mostra l evoluzione dei sistemi Unix-like, a partire da Unix. L originale ed elegante design del sistema Unix, seguito da diversi anni di innovazione, Figura 2.1: Evoluzione dei sistemi Unix-like evoluzione e miglioramenti, ha avuto come risultato un potente, robusto e stabile modello di progettazione di sistemi operativi. Sono molteplici i punti di forza di Unix. Primo fra tutti è la sua semplicità: mentre altri sistemi operativi implementano migliaia di system call con un design poco chiaro, Unix implementa qualche centinaia di system call con un design preciso, anche se essenziale. Inoltre, in Unix la maggior parte delle attività

27 CAPITOLO 2. SISTEMI OPERATIVI UNIX-LIKE:LINUX E FREEBSD 16 è gestita tramite il concetto di file, che semplifica molto l utilizzo e la manipolazione dei dati e dei dispositivi di I/O. Il terzo punto di forza è che il kernel Unix e le relative utility di sistema sono scritte in codice C, che permette di essere portato su molteplici architetture hardware e condiviso con migliaia di sviluppatori. Ritroviamo altri punti di forza di Unix nei meccanismi di creazione, gestione e comunicazione (IPC) di processi (vedere sezione A.4), che permettono la progettazione di programmi semplici, che fanno una cosa e la fanno bene. Molteplici programmi semplici possono interagire tra loro per realizzare attività complesse. Oggigiorno i sistemi operativi Unix-like sono sistemi operativi robusti che supportano il multitasking (vedere sezione A.4) con possibilità di prelazionare attività anche in kernel space, il multithreading (vedere sezione A.4), implementano meccanismi di memoria virtuale, paginazione a richiesta, hanno il supporto a librerie condivise, al networking TCP/IP e molte altre caratteristiche. Alcune varianti di Unix scalano bene fino a centinaia di processori mentre altre sono progettate per girare su piccoli dispositivi embedded. I sistemi operativi Unix-like devono il loro successo alla semplicità ed eleganza del loro design, secondo la filosofia propria di Unix. Ulteriori informazioni sulla storia e sulle caratteristiche di Unix possono essere trovate nel capito 10 di [1], e in [4]. 2.2 Linux Linux è un sistema operativo Unix-like molto popolare impiegato in diversi settori, come nelle workstation, nei server, nei multicomputer e anche in diversi sistemi embedded e mobili. Linux nasce dalle idee di uno studente finlandese, Linus Torvalds, che, prendendo spunto da Minix, sviluppa un nuovo sistema operativo. La prima release di Linux, la 0.01, viene rilasciata nel 1991 (per il contesto storico si veda la sezione 2.1 sulla storia di Unix). Nel corso degli anni vengono aggiunte nuove funzionalità, come un nuovo file system, il supporto alle socket TCP/IP di BSD e diversi driver di dispositivi. Inoltre, la maggior parte del software per UNIX viene portato in Linux, rendendo disponibile all utente svariati applicativi. La release 1.0 viene rilasciata nel 1994 e la versione 2.0 nel 1996 (nel Novembre del 2011 siamo alla versione 3.1). Ulteriori dettagli sulla storia di Linux nella sezione 10.1 di [1]. Le versioni del kernel Linux sono rilasciate sotto licenza GPL (GNU Public License) che permette di distribuire il codice sorgente. 2 Il successo di Linux è in larga parte dovuto proprio alla possibilità di distribuire il codice sorgente; in questo modo sviluppatori di tutto il mondo possono aggiungere, modificare e migliorare il codice, rendendo di fatto lo sviluppo del kernel linux un progetto collaborativo. Linux è un sistema operativo Unix-like ma non è Unix. Benché sia in larga parte Posix compliance ed incorpori molte idee e meccanismi che ritroviamo nei sistemi Unix, il codice del kernel Linux non è un diretto discendente del codice sorgente di Unix, a differenza di altri Unix-like come FreeBSD per esempio. Oggigiorno esistono più di 500 distribuzioni di Linux (un elenco dettagliato si trova in [22]) per settori di impiego specifici. Alla base di tutte le distribuzioni Linux c è il kernel Linux (alcune distribuzioni forniscono delle patch di kernel proprie, ad esempio per modificare lo scheduler), librerie C, toolchain e utility di base come shell utente. La maggior parte delle distribuzioni forniscono dei tool per installare una miriade di applicazioni free (e commerciali) come il sistema a finestre X, i desktop GNOME, KDE e molte altre. Kernel Linux Il kernel Linux racchiude tutti quelle componenti fondamentali di un sistema operativo (sezione A.1 per ulteriori dettagli) come i gestori degli interrupt, lo scheduler, i device 2 La versione più recente di GNU GPL è la 3.0; gli sviluppatori del kernel linux hanno deciso di rimanere alla versione 2.0. Una copia della licenza è presente nel file COPYING nell albero dei sorgenti del kernel, oppure è disponibile online in

28 CAPITOLO 2. SISTEMI OPERATIVI UNIX-LIKE:LINUX E FREEBSD 17 driver, il gestore della memoria e diverse system call per fornire servizi di networking, di file system, di IPC e molti altri. Il kernel Linux è quindi monolitico (sezione 1.3 per ulteriori dettagli): tutte le sue componenti sono eseguite in un unico spazio di indirizzamento, in modalità kernel. La modalità di comunicazione tra le varie componenti è nella maggior parte dei casi quella classica a chiamata di funzione. La struttura del kernel Linux è modulare, nel senso che è possibile caricare in modo dinamico moduli del kernel specifici per certe funzionalità (e.g. device driver o supporto per un certo file system). Oltre alle caratteristiche tipiche dei sistemi Unix-like come il multiuser, la multiprogrammazione e il multithreading, il kernel Linux ha la caratteristica di kernel preemption, ossia è possibile prelazionare codice in spazio kernel. Quest ultima caratteristica è molto importante per il supporto al real-time. Un altra caratteristica di rilievo del kernel Linux è il supporto all SMP (sezione A.5 per i dettagli). L implementazione dei thead è fatta in spazio kernel (sezione A.4 per i dettagli). Per quanto riguarda gli aspetti di scheduling di processi, esistono diverse proposte per ambienti interattivi, batch e real-time. Per ulteriori dettagli sulla struttura e componenti del kernel Linux si consiglia la lettura del libro di Rober Love [23] e la documentazione presente nei riferimenti [25, 29]. 2.3 FreeBSD FreeBSD [86] è un sistema operativo Unix-like che trova il suo maggior impiego nell ambito dei server per la fruizione dei servizi Internet. Inoltre viene utilizzato nelle workstation e in alcuni sistemi embedded. Il progetto FreeBSD ha le sue origini agli inizi del 1993 con la patch Unoffical 386BSD PatchKit proposta da Nate Williams, Rod Grimes e Jordan Hubbard, cha andava a risolvere alcuni problemi della versione ufficiale di 386BSD di Bill jolitz (per il contesto storico si veda la sezione 2.1 sulla storia di Unix ). Il nome FreeBSD fu coniato da David Greenman. La prima versione di FreeBSD, la 1.0, rilasciata nel Dicembre del 1993 e basata su 4.3BSD-Lite (Net/2) e su molte parti di 386BSD, riscosse un grande interesse al punto che nel maggio del 1994 fu rilasciato FreeBSD 1.1. A causa di alcuni processi legali in cui Novel rivendicava il diritto di autore di parte del codice contenuto nelle versione BSD Net/2, il progetto di FreeBSD fu costretto a reinventare se stesso a partire dalla versione 4.4BSD-Lite. Nel Novembre del 1994 fu rilasciato FreeBSD 2.0 che riscosse un grande successo, sopratutto tra i provider internet (ISP). Tra il 1994 e il 1998 seguirono una serie di release di FreeBSD 2.X e nell Ottobre del 1998 fu rilasciala la prima release del filone FreeBSD 3.X, che segnò la fine dello sviluppo di FreeBSD 2.X. A partire dagli inizi del 1999 iniziò lo sviluppo di FreeBSD 4.X, che si concluse con la release FreeBSD 4.11, rilasciata nel Febbraio del Matthew Dillom, uno sviluppatore di FreeBSD e Amiga, forka la release FreeBSD 4.8 dando vita al progetto DragonFly BSD [96, 97]. Dopo FreeBSD 4.X seguì lo sviluppo di FreeBSD 5.x (metà del Febbraio del 2004) e quello di FreeBSD 6.X (Luglio del Novembre del 2008). Negli ultimi mesi del 2007 iniziò lo sviluppo di FreeBSD 7.X con la prima release rilasciata nel Febbraio del 2008, l ultima, FreeBSD 7.3, nel Marzo del In Agosto del 2009 inizia lo sviluppo di FreeBSD 8.X e nel Novembre dello stesso anno viene rilasciata la prima release, FreeBSD 8.0, a partire dalla quale inizia lo sviluppo di FreeBSD 9.X. La release più recente di FreeBSD, FreeBSD 8.2 è stata rilasciata nel Febbraio del Ulteriori informazioni sulla storia di FreeBSD nel capitolo 1 di [92] e nella sezione 1.3 di [90]. Le versioni di FreeBSD sono rilasciate con una licenza semplificata della licenza BSD 3, 3 Esistono alcune varianti della licenza BSD. Per i dettagli si rimanda a

29 CAPITOLO 2. SISTEMI OPERATIVI UNIX-LIKE:LINUX E FREEBSD 18 chiamata licenza FreeBSD o licenza BSD semplificata [91]. Anche la licenza di FreeBSD, come quella per il kernel Linux, permette di distribuire liberamente tutto il codice sorgente 4 : questo permette a chiunque di contribuire a migliorare il progetto FreeBSD. A differenza delle svariate distribuzioni Linux che hanno come denominatore comune il relativo kernel, FreeBSD fornisce sia il kernel sia le utility a livello utente, come un programma di installazione, diverse shell utente, tool per lo sviluppo in diversi linguaggi di programmazione e tool per la gestione di migliaia di applicazioni di terze parti (sia free che commerciali). Per ulteriori informazioni su FreeBSD si consiglia di consultare l ottima documentazione disponibile in [88]. Kernel FreeBSD L architettura del kernel di FreeBSD è molto simile a quella del kernel Linux: si tratta di un kernel monolitico (rif. sezione 1.3), con un unico spazio di indirizzamento. Il kernel include le componenti fondamentali, come i gestori degli interrupt, lo scheduler, i device driver, il gestore della memoria e diverse system call per fornire servizi di gestione di processi e IPC, di networking, di file system e molti altri. Queste componenti comunicano tra loro in modo sincrono, tramite la tecnica a chiamata di funzione. Anche per FreeBSD, come in Linux, è possibile caricare in modo dinamico moduli del kernel specifici per certe funzionalità (e.g. device driver o supporto per un certo file system). In FreeBSD ritroviamo caratteristiche quali il multiuser, la multiprogrammazione, il multithreading, l implementazione dei thread in kernel, la possibilità di prelazionare codice in spazio kernel e il supporto all SMP. Inoltre FreeBSD fornisce una compatibilità binaria per applicativi compilati per Linux, SCO, SVR4, BSDI and NetBSD. Le differenze tra il kernel Linux e quello di FreeBSD non sono quindi tanto architetturali ma piuttosto implementative o di metodo, ad esempio possiamo trovare delle differenze sull implementazione dei gestori di interrupt o sulla implementazione dello scheduler. Per quanto riguarda gli aspetti di scheduling, allo stato attuale esistono due implementazioni per lo scheduling di processi nel kernel di FreeBSD: una, chiamata BSD, ereditata dal sistema BSD, e un altra più recente, dal nome ULE, che introduce nuove interessanti caratteristiche. Il kernel Linux ha di gran lunga molte più implementazioni per lo scheduling di processi rispetto a quelle di FreeBSD, che sostanzialmente coprono gli ambienti server e workstation. Per ulteriori informazioni sulla struttura e componenti del kernel di FreeBSD si rimanda a [92, 93]. 2.4 Settori di impiego di Linux e di FreeBSD Spesso i settori di impiego di un sistema operativo rispecchiamo la possibilità o meno di eseguire il sistema su determinate architetture hardware. Per esempio l architettura x86 è tipica dei sistemi server e workstation mentre l architettura ARM e MIPS sono tipiche dei sistemi embedded. Il kernel Linux può essere eseguito su una moltitudine di architetture hardware, come diverse architetture ARM, Freescale, IBM, x86, MIPS, PowerPC, SPARC e molte altre. Per un elenco dettagliato si rimanda a [27]. Di conseguenza i settori di impiego di Linux sono molteplici, nei server per la fruizioni di servizi Internet (e.g. RedHat e Debian), nelle workstation (e.g. Ubunto e Fedora), nei mainframe (e.g. architettura IBM Z), nei sistemi per il super calcolo (e.g. architettura IBM Blu Gene), in diversi sistemi embedded e mobili (e.g. Android per i dispositivi mobili), e nei sistemi real-time (distribuzioni Montavista e WindRiver). 4 La licenza di FreeBSD pone meno restrizioni della licenza GPLv2 del kernel Linux

30 CAPITOLO 2. SISTEMI OPERATIVI UNIX-LIKE:LINUX E FREEBSD 19 Attualmente FreeBSD può essere eseguito su un numero molto inferiore di architetture hardware rispetto al kernel Linux 5 quali x86 (IA-32), x86-64 e NEC PC-9801 in Tier 1 (ossia completamente supportate), SPARC, IA-64, PowerPC e ARM in Tier 2 (ossia il supporto c è ma è ancora in via di sviluppo), MIPS e Alpha in Tier 3 (ossia a un livello ancora sperimentale). I settori di impiego di FreeBSD sono di gran lunga inferiori a quelli del kernel Linux. FreeBSD trova il suo maggior utilizzo nei sistemi server per la fruizione dei servizi Internet e in una percentuale molto inferiore nei sistemi embedded (e.g. alcuni modelli di TV della Panasonic) e nelle workstation 6. Inoltre merita titolo di menzione il fatto che parte del codice di FreeBSD sia stato incorporato in altri sistemi operativi tra cui VxWorks (sistema RTOS di WindRiver), Darwin (il core di Apple Mac OS X), lo stesso kernel Linux e alcune versioni di Windows per il supporto alle socket BSD. Altri sistemi derivati da FreeBSD in [87]. 5 NetBSD, della famiglia di BSD, supporta un numero di architetture hardware superiori rispetto a FreeBSD. Per i dettagli si rimanda a 6 Il progetto PCBSD [98] ha come obiettivo l utilizzo di FreeBSD come desktop

31 Capitolo 3 Scheduling in Linux Questo capitolo descrive in dettaglio il funzionamento dello scheduling dei processi e il relativo stato dell arte nel kernel Linux. La prima sezione descrive l evoluzione dello scheduling nei vari filoni di kernel, partendo dalla versione 1.1.X del 1994 fino ad arrivare allo stato dell arte dei giorni odierni, versione 3.1 nel Novembre I filoni di kernel sono stati suddivisi sulla base di cambiamenti significativi nelle attività di scheduling. Per ogni filone di kernel individuato vengono riportate politiche, strutture dati, algoritmi, caratteristiche e obiettivi che caratterizzano le modifiche delle attività di scheduling. La seconda sezione riporta le principali proposte per il supporto al real-time in Linux, come la patch Preempt-RT e la classe di scheduling sched_deadline con alcuni test sperimentali. Nell ultima sezione troviamo altri algoritmi di scheduling proposti dalla comunità, come RSDL e BFS. 3.1 Evoluzione dello scheduling e stato dell arte Possiamo individuare l evoluzione dello scheduling nel kernel Linux in 5 filoni principali: kernel 1.1.x-1.2.x ( ); kernel 2.0.x-2.2.x ( ); kernel 2.4.x ( ) kernel ( ); kernel ( Novembre 2011). Nel filone di kernel Linux 1.x lo scheduling si basa sulla tecnica a priorità con time sharing, il kernel è non preemptive e non c è il supporto a SMP. Nel filone di kernel Linux 2.0.x-2.2.x si introduce il meccanismo delle classi di priorità, secondo lo standard POSIX. Il calcolo delle priorità dinamiche per le classi di task utente tiene conto delle affinità dei task in CPU e favorisce i task I/O bound, in genere interattivi. In questo filone si introduce un supporto di base all SMP. Nel filone di kernel Linux 2.4.x si apportano alcune migliorie al supporto SMP e al calcolo delle priorità dei task utente. Nel filone di kernel Linux si introduce il cosiddetto scheduler O(1) con una rivisitazione della gestione delle priorità e del riconoscimento dei task interattivi. Inoltre troviamo un kernel preemptive e l uso della tecnica a priorità ereditata per il problema di priorità inversa. Ogni risorsa di calcolo mantiene una propria lista di task che può eseguire, ed è stato introdotto un bilanciamento di carico periodico basato sul domain scheduling. Nel filone di kernel si introduce una nuova tecnica di scheduling modulare e la politica CFS che migliora l equità di esecuzione in CPU per i task utente. Inoltre si 20

32 CAPITOLO 3. SCHEDULING IN LINUX 21 introduce la tecnica di group scheduling, che permette di distribuire il tempo di calcolo a gruppi di task utente. Nelle successive sezioni vengono analizzati e messi a confronto le varie implementazioni degli scheduler nei 5 filoni di kernel individuati, secondo il seguente schema: tipologia e obiettivo; strutture dati; politica e algoritmo; features; chiamate di sistema; considerazioni; Nella Tabella 5.1 sono riportate le caratteristiche principali degli scheduler nei 5 filoni di kernel Linux individuati Kernel 1.1.x-1.2.x Come riferimento si è preso il Kernel Tipologia e obiettivo Nel filone del kernel Linux 1.x abbiamo uno scheduling di processi basato sulla tecnica a priorità con time-sharing: in una decisione di scheduling si manda in esecuzione il task con priorità maggiore. La priorità in questo caso è il valore del quanto esecutivo a disposizione del task. La prelazione può avvenire solo in spazio utente, quando il task in esecuzione esaurisce il suo quanto di tempo o quando si blocca per I/O; non è possibile prelazionare task in spazio kernel. La ricerca del prossimo task da mandare in esecuzione ha una complessità temporale di O(n), con n numero di task sul sistema, risultando poco efficiente con alti carichi di lavoro. L obiettivo è di distribuire in modo equo il tempo di esecuzione tra tutti i task del sistema minimizzando i tempi di risposta per i task interattivi, in genere I/O bound. In questo filone del kernel non sono supportate architetture a multiprocessore. Strutture dati Tutti i task, anche quelli che non sono in stato di pronto, confluiscono in un unica lista. Le informazioni per le attività di scheduling contenute nella struttura dati informativa di ogni task (task_struct) sono le seguenti: state: rappresenta lo stato del task ( 1 unrunnable, 0 runnable, > 0 stopped); priority: il quanto esecutivo iniziale (valori tra 1 e 35); counter: il numero di tick che il task ha ancora a disposizione (valori tra 0 e 70). L organizzazione delle strutture dati utilizzate nella attività di scheduling viene schematizzata nella Figura 3.1.

33 CAPITOLO 3. SCHEDULING IN LINUX 22 Tabella 3.1: Evoluzione dello scheduling nel kernel Linux Filone di Kernel Tipologia e Politica Strutture Dati Algoritmo Feature Obiettivi 1.1.x-1.2.x ( ) scheduling a priorità; timesharing per tutti i task tramite interrupt hardware; preemptive in user space e kernel nonpreemptive; si schedula il task con il più lungo quanto esecutivo ancora a disposizione; unica lista di task dove confluiscono tutti i task del sistema; suddivisione del tempo di CPU in epoche; scansione dell intera lista con complessità temporale O(n), con n numero di task; solo per sistemi monoprocessore; distribuire in modo equo il tempo di esecuzione tra tutti task; minimizzare i tempi di risposta per task interattivi (I/O bound); 2.0.x-2.2.x ( ) scheduling a classi di priorità; time-sharing solo per alcune classi di task (e.g. ordinari) tramite interrupt hardware; preemptive in user space e kernel nonpreemptive; si schedula il task con il maggior grado di bontà unica lista per i task in stato di pronto; gli altri task sono organizzati in liste in base all evento atteso. suddivisione del tempo di CPU in epoche solo per i task ordinari; scansione dell intera lista con complessità temporale O(n), con n numero di task; supporto ai task real-time (POSIX); supporto SMP (con affinity); massima priorità ai task realtime; distribuire in modo equo il tempo di esecuzione tra tutti task ordinari; minimizzare i tempi di risposta per task interattivi (I/O bound); supporto di base per SMP. 2.4.x ( ) scheduling a classi di priorità; time-sharing solo per alcune classi di task (e.g. ordinari) tramite interrupt hardware; preemptive in user space e kernel nonpreemptive; si schedula il task con il maggior grado di bontà unica lista per i task in stato di pronto; gli altri task sono organizzati in liste in base all evento atteso suddivisione del tempo di CPU in epoche solo per i task ordinari; scansione dell intera lista con complessità temporale O(n), con n numero di task; supporto ai task real-time (POSIX); supporto rivisitato SMP (con affinity); supporto HyperThreading massima priorità ai task realtime; distribuire in modo equo il tempo di esecuzione tra tutti task ordinari; minimizzare i tempi di risposta per task interattivi (I/O bound); supporto SMP ( ) aka O(1) scheduling a classi di priorità: si schedula il task con priorità maggiore; timesharing solo per alcune classi di task (e.g. ordinari) tramite interrupt hardware; kernel preemptive; una lista di task in stato di pronto per ogni risorsa di calcolo (organizzata da due code di task, active ed expired); gli altri task sono organizzati in liste in base all evento atteso suddivisione del tempo di CPU in epoche solo per i task ordinari (tramite lo switch delle liste active ed expired); l algoritmo considera la prima lista di task nella coda active, non vuota, partendo dalla priorità maggiore e recupera il primo task in essa; complessità temporale O(1) supporto ai task real-time (POSIX); euristica per task interattivi; bilanciamento di carico con domain scheduling per arch. UMA, NUMA, SMP e SMT; supporto SMP (con affinity) e Hyper-Threading (SMT) massima priorità ai task realtime; distribuire in modo equo il tempo di esecuzione tra tutti task ordinari; minimizzare i tempi di risposta per task interattivi (I/O bound); supporto SMP ( Novembre 2011) aka CFS scheduling modulare; preemptive in user and kernel space; modulo per task realtime (POSIX) con scheduling a priorità; modulo CFS per task ordinari con scheduling basato sull equità dei tempi di esecuzione dei task. un albero red-black per CPU per i task ordinari (in stato di pronto); una lista per CPU per i task real-time (in stato di pronto); i task in attesa sono organizzati in liste in base all evento atteso per il modulo real-time si schedula il primo task trovato nella prima lista non vuota (partendo dalla priorità maggiore) con complessità temporale O(1); per il modulo CFS si schedula il task più a sinistra dell albero (quello che è stato eseguito meno in CPU rispetto agli altri) con complessità temporale O(log n); supporto ai task real-time (POSIX); scheduler modulare; group scheduling ; bilanciamento di carico con domain scheduling per arch. UMA, NUMA, SMP e SMT; supporto SMP (con affinity); supporto Hyper- Threading (SMT) distribuire in modo equo le risorse di calcolo per i task ordinari; possibilità di garantire tempi di esecuzione a task real-time e ordinari; possibilità di raggruppare task in gruppi; supportare in modo efficiente architetture a multiprocessore

34 CAPITOLO 3. SCHEDULING IN LINUX 23 Figura 3.1: Organizzazione dello scheduling nel filone di kernel Linux 1.1.x-1.2.x Politica La prelazione di task può avvenire solo se il task è in esecuzione in modalità utente; un task eseguito in modalità kernel (e.g. mentre si esegue una chiamata di sistema) non può essere prelazionato e questo per facilitare le operazioni di sincronizzazione, evitando problemi di race condition (sezione A.4 per i dettagli). La decisione di quale task mandare in esecuzione viene presa in base al numero di tick ancora a disposizione dei task (counter): viene schedulato in CPU il task che ha il più lungo quanto esecutivo ancora a disposizione. In questo modo i task che utilizzano la CPU per brevi periodi di tempo, tipica caratteristica dei task I/O bound, sono prioritari rispetto a quelli CPU bound. Algoritmo L algoritmo di scheduling in questo filone divide il tempo di CPU in epoche. In ogni epoca ogni task ha assegnato il suo quanto esecutivo (ossia un certo numero di tick) e l epoca termina quando tutti i task, in stato di pronto, hanno esaurito il loro quanto esecutivo, quindi lo scheduler ricalcola i nuovi quanti per tutti i task e inizia una nuova epoca. Ogni task ha il suo quanto esecutivo: questo valore rappresenta il numero di tick assegnati inizialmente al task (priority). Allo scadere di ogni tick, viene decrementato di una unità il quanto esecutivo (counter) del task che in quel momento si trova in esecuzione. Un task, se non viene interrotto (per richieste di I/O o per interrupt), rimane in esecuzione fino al termine del suo quanto esecutivo. L algoritmo di scheduling consiste in una scansione dell intera lista dei task, dall inizio alla fine, alla ricerca del task, in stato di pronto, con il più lungo quanto esecutivo ancora a disposizione. Se tutti i task (in stato di pronto) nella lista hanno il proprio quanto esecutivo a 0 (fine epoca), allora lo scheduler riassegna i quanti esecutivi a tutti i task (anche a quelli non in stato di pronto), semplicemente dimezzando il numero di tick ancora a disposizione e sommando la relativa priorità. La divisione serve solo per i task che non si trovano in quel momento in stato di pronto: in questo modo da un lato si evita che, una volta che tornano in stato di pronto, questi task abbiano a disposizione quanti esecutivi troppo lunghi, dall altro si cerca di tenere in considerazione il fatto che non avevano esaurito del tutto il loro precedente quanto esecutivo. La priorità dei task è inizialmente settata ad un valore costante, pari a 15 tick. Questa priorità può in seguito essere modificata, entro un certo range, tramite la syscall sys_nice(). Quando viene creato un nuovo processo, il numero di tick a disposizione (counter) del processo padre viene dimezzato e il processo figlio assume il medesimo quanto esecutivo.

35 CAPITOLO 3. SCHEDULING IN LINUX 24 In questo modo si evita ad esempio che un utente monopolizzi la CPU creando tanti processi. Chiamate di Sistema In questo filone di kernel le principali system call attinenti allo scheduling di processi sono le seguenti: sys_nice(): permette (al solo super-user) di settare una nuova priorità (priority) ad un task nel range 1-35; sys_setpriority(): permette di assegnare il quanto esecutivo iniziale (priority in un range tra 1 e 15) a un singolo task fornendo il suo PID, oppure a un gruppo di task, sulla base dell identificativo di proprietario o di gruppo. Considerazioni Lo scheduler non conosce a priori se un processo è I/O bound o CPU bound. Con molti nuovi processi in stato di pronto, i processi CPU bound sono messi sullo stesso piano dei processi I/O bound e, nel caso in cui alcuni processi I/O bound sia preceduti da molti processi CPU bound, prima che ai primi sia concessa l esecuzione bisogna attendere che i processi precedenti (CPU bound) esauriscano il loro quanto esecutivo (o cambino stato). Di fatto, in questo caso, i processi CPU bound sono eseguiti prima di quelli I/O bound (per un discorso di ordine nella lista dei task). Un lungo quanto esecutivo porterebbe ad allungare i tempi di risposta per i processi I/O bound. Non è sempre vero che un task I/O bound sia un task interattivo e che uno CPU bound non lo sia. In alcuni casi, task CPU bound interattivi possono ottenere lunghi tempi di risposta per via della priorità esecutiva che viene data ai task I/O bound. Ogni volta che viene invocato lo scheduler è necessario una scansione di tutta la lista dei task per decidere quale task mandare in esecuzione. La complessità computazionale rispetto al tempo di questo algoritmo è O(n) con n numero dei task del sistema. Su sistemi con molti task si introduce molto overhead. Notare che in lista sono mantenuti tutti i task, indipendentemente dallo stato in cui si trovano (di fatto in una decisione di scheduling solo i task in stato di pronto sono presi in considerazione). Ulteriori dettagli sullo scheduling di processi nel filone di kernel Linux 1.1.x-1.2.x possono essere trovati visionando direttamente i sorgenti del kernel [24], in particolare: kernel/sched.c: contiene le principali funzioni per lo scheduling dei processi; kernel/sys.c: contiene la maggior parte delle system call; include/linux/sched.h: contiene le principali strutture dati utilizzate per operazioni di scheduling Kernel 2.0.x-2.2.x Come riferimento si è preso il Kernel Tipologia e obiettivo In questo filone di kernel Linux 2.0.x e 2.2.x lo scheduling di processi è organizzato con classi di priorità, secondo lo standard POSIX: in una decisione di scheduling si manda in esecuzione il task di priorità maggiore. I task delle classi real-time 1 hanno priorità statiche e dei quanti di esecuzione: possono 1 Il concetto di real-time POSIX si basa sulla tecnica a priorità e non tiene conto di vincoli temporali di esecuzione richiesti da alcune applicazioni real-time. Per ulteriori dettagli si rimanda alle sezioni e 3.2

36 CAPITOLO 3. SCHEDULING IN LINUX 25 essere prelazionati solo da task di maggiore priorità, quando passano in stato di attesa (sleep o I/O) o quando esauriscono il loro quanto esecutivo (solo una delle due classi ha la gestione del quanto). I task utente hanno delle time slice e le relative priorità sono calcolate sulla base di un indice, chiamato grado di bontà, che tiene in considerazione l affinità dei task in CPU e favorisce task I/O bound, in genere interattivi. Il calcolo del grado di bontà di tutti i task utente avviene in modo periodico, con una complessità temporale di O(n), con n numero di task sul sistema. La prelazione di task può avvenire solo se il task è in esecuzione in modalità utente; un task eseguito in modalità kernel non può essere prelazionato. Inoltre, in questo filone si introduce il supporto alle architetture multiprocessore, secondo il modello SMP. Tutte le risorse di calcolo condividono un unica lista di task. L obiettivo delle attività di scheduling in questo filone è di dare massima priorità esecutiva ai task real-time POSIX, distribuire in modo equo il tempo di esecuzione tra tutti i task utente minimizzando i tempi di risposta per i task interattivi, in genere I/O bound e avere un supporto di base per le architetture a multiprocessore, secondo il modello SMP. Strutture dati Tutti i task in stato di pronto confluiscono in una lista (runqueue); i task che non si trovano in stato di pronto confluiscono in liste organizzate in base all evento atteso (concettualmente possiamo immaginare un unica lista che raggruppa i task in attesa). In questo modo in una decisione di scheduling sono esaminati solo i task che si trovano in stato di pronto, riducendo il tempo necessario per scorrere tutta la lista. Le informazioni per le attività di scheduling contenute nella struttura dati informativa di ogni task (task_struct) sono le seguenti: state: rappresenta lo stato del task ( 1 unrunnable, 0 runnable, > 0 stopped); need_resched: flag che indica che è necessario invocare lo scheduler; policy: la classe di priorità di appartenenza dei task (sched_fifo, sched_rr e sched_other); rt_priority: il valore di priorità dei task real-time (solo per sched_fifo, sched_rr con range 1-99); priority: il quanto esecutivo iniziale (solo per sched_rr e sched_other con range 1-40); counter: il numero di tick che il task ha ancora a disposizione (solo per sched_rr e sched_other con range 0-80); avg_slice: media dei numeri di cicli eseguiti sull ultima CPU has_cpu: indica se il task è in esecuzione su una CPU; processor: identifica l ultima CPU che ha eseguito il task. Inoltre ogni CPU ha assegnato una struttura dati (schedule_data) dove viene riportato il task attualmente in esecuzione sulla CPU (curr) e quando questo è stato mandato in esecuzione dallo scheduler (last_schedule). L organizzazione delle strutture dati utilizzate nella attività di scheduling viene schematizzata nella Figura 3.2. Politica La prelazione di task può avvenire solo se il task è in esecuzione in modalità utente; un task eseguito in modalità kernel non può essere prelazionato e questo per facilitare le operazioni di sincronizzazione, evitando problemi di race condition (sezione A.4 per i dettagli). I task sono organizzati in 3 classi di priorità (policy): sched_fifo, sched_rr e sched_other, secondo lo standard POSIX. Le prime due sono dedicate ai cosiddetti task real-time, mentre l ultima rappresenta i task ordinari. Per tutte le classi di priorità abbiamo una prelazione quando il task in esecuzione va in stato di attesa (per I/O o

37 CAPITOLO 3. SCHEDULING IN LINUX 26 Figura 3.2: Organizzazione dello scheduling nel filone di kernel Linux 2.0.x-2.2.x quando rilascia in modo volontario la CPU). I task di tipo sched_fifo, gestiti con tecnica fifo, non hanno quanti esecutivi e sono prelazionati solo da nuovi task in stato di pronto con priorità maggiore. I i task real-time sched_rr hanno quanti esecutivi e al loro termine sono prelazionati secondo la tecnica del round robin (quando un task esaurisce il suo quanto viene messo in coda alla lista) oltre che da task in stato di pronto con maggiore priorità. I task sched_other come i sched_rr hanno quanti esecutivi e sono prelazionati alla fine dei loro quanti o nel caso in cui si presenta un nuovo task di tipo real-time o sched_other di maggiore priorità. La decisione di quale task mandare in esecuzione viene presa calcolando per ogni task un grado di bontà: viene schedulato in CPU il task con il valore dell indice più alto. Un task di tipo real-time avrà un grado di bontà sempre maggiore di un task ordinario e quindi

38 CAPITOLO 3. SCHEDULING IN LINUX 27 avrà precedenza di esecuzione rispetto a quest ultimo. Calcolo del grado di bontà Per i task delle classi sched_fifo e sched_rr il grado di bontà viene dato direttamente dal valore della relativa priorità (rt_priority con range 1-99) a cui viene sommato il valore costante 1000; per i task della classe sched_other la bontà dei task è data dalla somma del valore della relativa priorità, che in questo caso rappresenta il quanto esecutivo iniziale (priority con range 1-40) e dal valore dei tick ancora a disposizione del task (counter con range 1-80). Un task sched_other che ha esaurito il proprio quanto esecutivo otterrà un grado di bontà pari a 0. A parità di grado di bontà sarà selezionato il primo task trovato nella lista. Ulteriori bonus vengono concessi sia nel caso in cui un task condivida lo spazio di indirizzamento con il task precedentemente in esecuzione (bonus di 1) sia nel caso in cui un task ha avuto la sua ultima esecuzione proprio sulla CPU considerata (bonus di 15 per arch i386); questi bonus vengono concessi per beneficiare dei dati già presenti in cache e in memoria, evitando page fault e/o cache miss. Algoritmo L algoritmo di scheduling in questo filone di kernel divide il tempo di CPU in epoche. Un epoca termina quando tutti i task real-time in stato di pronto terminano, o passano in un altro stato, e quando tutti i task della classe sched_other hanno esaurito il proprio quanto esecutivo, quindi lo scheduler ricalcola i nuovi quanti per tutti i task, anche quelli che non si trovano in stato di pronto (ma solo per le classi sched_other e sched_rr). I nuovi quanti esecutivi sono dati dalla somma del quanto esecutivo iniziale (priority) e dalla metà del numero di tick ancora a disposizione (counter) del task (solo per i task che non si trovano in stato di pronto). In questo modo il numero di tick a disposizione dei task non può andare oltre il doppio del valore del quanto esecutivo iniziale (priority) e questo per evitare che task di lungo termine possano accrescere in modo indefinito il proprio quanto esecutivo a disposizione, di fatto rendendoli sempre più prioritari rispetto a nuovi task. Lo scheduler, ogni qual volta viene invocato, scansione l intera lista dei task in stato di pronto (runqueue), dalla testa alla coda. Se il task esaminato è di tipo real-rime round robin e ha esaurito il suo quanto esecutivo, allora il task viene spostato in coda alla lista e viene ripristinato il quanto esecutivo a disposizione (counter con il valore di priority). Se il task esaminato non è più in stato di pronto allora viene rimosso dalla lista. Quindi per ogni task (che non sia già in esecuzione su altre CPU) viene calcolato il relativo grado di bontà e viene schedulato in CPU il task con il valore di bontà più alto. Quando un task rilascia la CPU, tramite apposita chiamata di sistema sched_yield(), il task viene messo in testa alla lista per favorirlo nella prossima decisione di scheduling (rispetto a task di pari bontà). Quando viene creato un nuovo processo, il numero di tick a disposizione (counter) del processo padre viene dimezzato e il processo figlio assume il medesimo quanto esecutivo. In questo modo si evita ad esempio che un utente monopolizzi la CPU creando tanti processi. Features In questo filone di kernel, troviamo l introduzione al supporto alle architetture a multiprocessore, secondo il modello SMP (sezione A.5 per i dettagli). SMP L idea è quella di schedulare task sulle stesse CPU sulle quali sono stati eseguiti in precedenza e questo per evitare page fault e/o cache misses, riducendo quindi l overhead.

39 CAPITOLO 3. SCHEDULING IN LINUX 28 Una struttura dati dedicata per ogni CPU contiene l informazione su quale task attualmente è in esecuzione sulla CPU e quando questo è stato mandato in esecuzione dallo scheduler. Inoltre nella struttura informativa dei task sono contenute alcune informazioni per il supporto SMP come il riferimento all ultima CPU sulla quale il task è andato in esecuzione e la relativa media del numero di cicli di CPU eseguiti. Quando lo scheduler viene invocato su una certa CPU, nel calcolo dell indice di bontà viene concesso un bonus (un incremento di valore dell indice di una certa costante, 15 per arch i386) se l ultima esecuzione del task preso in considerazione è avvenuta sulla CPU considerata. Inoltre viene stimato il numero di cicli che un task deve compiere affinché possa sporcare in modo significativo la cache di CPU. Questa informazione viene utilizzata per decidere se prelazionare o meno un processo in esecuzione su una CPU. Se l attuale processo in esecuzione ha compiuto un numero di cicli di CPU minore del numero stimato, allora non c è prelazione, poiché è più conveniente (in termini di overhead) lasciare in esecuzione l attuale processo (questo controllo non viene effettuato se il nuovo processo è di tipo real-time). Quando un task passa in stato di pronto o quando un task viene prelazionato su una certa CPU si cerca di determinare se il nuovo processo possa andare in esecuzione su una CPU, secondo i seguenti step, nell ordine: se l ultima CPU che ha eseguito il task è in idle, il task viene mandato in esecuzione sulla stessa, altrimenti si sceglie la prima, eventuale, CPU in idle; se il nuovo processo è di tipo real-time si valuta la prelazione dei task in esecuzione sulle CPU disponibili; se il processo non è real-time si valuta la prelazione dei task in esecuzione sulle CPU disponibili in favore di quello nuovo se entrambi non richiedono un lock di kernel e se il task in esecuzione ho eseguito un numero di cicli inferiore a quello necessario per sporcare in modo significativo la relativa cache; la valutazione di prelazione di un task consiste nel confrontare gli indici di bontà dei task attualmente in esecuzione sulle CPU (tutte nel caso il processo sia real-time e solo quelle per cui sono soddisfatte le due condizioni precedenti nel caso in cui il nuovo processo sia ordinario) con quello del nuovo task. Nel caso in cui l indice di bontà del nuovo task sia maggiore di quello dei task attualmente in esecuzione sulle CPU allora si prelaziona il task che ha la maggiore differenza (positiva). Chiamate di Sistema In questo filone di kernel le principali system call attinenti allo scheduling di processi sono le seguenti: setscheduler(): permette di assegnare a un task la relativa classe di priorità (policy) e la priorità dedicata ai task real-time (rt_priority); sys_sched_yield(): permette a un task di rilasciare la CPU in modo spontaneo; sys_setpriority(): permette di assegnare il quanto esecutivo iniziale (priority in un range tra 1 e 40 tick) a un singolo task fornendo il suo PID, oppure a un gruppo di task sulla base dell identificativo di proprietario o di gruppo. Considerazioni Anche per questo filone di kernel valgono le stesse considerazione fatte per il filone precedente riguardo alla gestione dei task I/O e CPU bound (si veda la sezione Considerazioni in 3.1.1). Ogni volta che viene invocato lo scheduler è necessario una scansione di tutta la lista dei task in stato di pronto per decidere quale task mandare in esecuzione, con una

40 CAPITOLO 3. SCHEDULING IN LINUX 29 complessità computazionale rispetto al tempo pari a O(n), con n numero dei task del sistema in stato di pronto. Su sistemi con molti task si introduce molto overhead. Per questo motivo l algoritmo di scheduling non scala bene rispetto al numero di processi. L introduzione delle classi di task real-time permette di dare una maggiore priorità di esecuzione ad alcuni task. Ad ogni modo il supporto per le applicazioni real-time è ancora debole. In prima istanza abbiamo un kernel nonpreemptive, ossia un task in esecuzione in modalità kernel (e.g. una chiamata di sistema) non può essere prelazionato da altri task. In questo modo un task real-time che necessità di andare in esecuzione rapidamente dovrà attendere il completamento del task eseguito in modalità kernel. Un altra importante questione riguarda i vincoli temporali di esecuzione richiesti da alcune applicazioni real-time: lo scheduler non tiene conto di questo aspetto. Inoltre non sono considerati alcuni problemi tipici in uno scheduler real-time come il problema dell inversione di priorità, che può accadere quando un task real-time è in attesa che un task di priorità inferiore liberi una risorsa a lui necessaria (sezione 1.4 per i dettagli). Per quanto riguarda il supporto SMP, l utilizzo di un unica lista di task da un lato permette un bilanciamento di carico automatico tra le diverse risorse di calcolo, dall altro, soprattutto su sistemi con molte CPU, quando lo scheduler invocato su diverse risorse di calcolo tenta di accedere alla stessa lista, introduce dei ritardi (e quindi overhead) per via dei meccanismi di accesso utilizzati per la mutua esclusione (e.g. spinlock). Per questi motivi l algoritmo di scheduling non scala bene rispetto al numero di processori. Ulteriori dettagli sullo scheduling dei processi nel filone di kernel Linux 2.0.x e 2.2.x possono essere trovati nel Capitolo 10 di [20] e visionando i sorgenti di Linux [24], in particolare i file: kernel/sched.c: contiene le principali funzioni per lo scheduling dei processi; kernel/sys.c: contiene la maggior parte delle system call; include/linux/sched.h: contiene le principali strutture dati utilizzate per operazioni di scheduling Kernel 2.4.x Come riferimento si è preso il Kernel Tipologia e obiettivo La tipologia e obiettivo di scheduling nel filone di kernel Linux 2.4.x seguono quelli del filone precedente (sezione Tipologia e obiettivo in 3.1.2), con alcuni migliorie nel calcolo del grado di bontà e nel supporto SMP. L obiettivo è di dare massima priorità esecutiva ai task real-time POSIX, distribuire in modo equo il tempo di esecuzione tra tutti i task utente minimizzando i tempi di risposta per i task interattivi, in genere I/O bound e avere un supporto di base per le architetture a multiprocessore, secondo il modello SMP. Strutture dati Tutti i task in stato di pronto confluiscono in una lista (runqueue); i task che non si trovano in stato di pronto confluiscono in liste organizzate in base all evento atteso (concettualmente possiamo immaginare un unica lista che raggruppa i task non in stato di pronto). In questo modo in una decisione di scheduling sono esaminati solo i task che si trovano in stato di pronto, riducendo il tempo necessario per scorrere tutta la lista. La struttura dati informativa di ogni task (task_struct) mantiene sostanzialmente gli stessi campi rispetto a quella del filone precedente, con l aggiunta di alcuni campi per il supporto SMP. Per l attività di scheduling le informazioni principali utilizzate sono le seguenti:

41 CAPITOLO 3. SCHEDULING IN LINUX 30 state: rappresenta lo stato del task (-1 unrunnable, 0 runnable, > 0 stopped); need_resched: flag che indica che è necessario invocare lo scheduler; policy: la classe di priorità di appartenenza del task (sched_fifo, sched_rr e sched_other); rt_priority: il valore di priorità per i task real-time delle classi sched_fifo e sched_rr (range 1-99, dove 1 è la priorità più bassa e 99 quella più alta); nice: viene utilizzato per calcolare il quanto esecutivo che il task ha a disposizione all inizio di un epoca (solo per sched_rr e sched_other; range tra -20 e +19); counter: il numero di tick che il task ha ancora a disposizione (solo per sched_rr e sched_other con range 0-22); cpus_allowed: una bit mask che indica su quali CPU il task può essere eseguito; cpu_runnable: una bit mask che indica quale eventuale CPU sta eseguendo il task; processor: identifica l ultima CPU che ha eseguito il task. Inoltre una struttura dati (schedule_data) dedicata per ogni CPU contiene l informazione su quale task attualmente è in esecuzione sulla CPU (curr) e quando questo è stato mandato in esecuzione dallo scheduler (last_schedule). L organizzazione delle strutture dati utilizzate per le attività di scheduling nel filone di kernel Linux 2.4.x viene schematizzata nella Figura 3.3. Politica Anche nel filone del kernel 2.4.x la prelazione di task può avvenire solo se il task è in esecuzione in modalità utente; un task eseguito in modalità kernel non può essere prelazionato. I task sono organizzati in 3 classi di priorità (policy): sched_fifo, sched_rr e sched_other. Per i dettagli sul significato di queste classi vedere la sezione del filone di kernel precedente. La decisione di quale task mandare in esecuzione viene presa calcolando per ogni task un grado di bontà: viene schedulato in CPU il task con il valore dell indice più alto. Un task di tipo real-time avrà un grado di bontà sempre maggiore di un task ordinario e quindi avrà precedenza di esecuzione rispetto a quest ultimo. Calcolo del grado di bontà Rispetto al filone di kernel precedente è stato modificato il calcolo del grado di bontà per i task della classe sched_other. Per i task delle classi sched_fifo e sched_rr il grado di bontà viene dato direttamente dal valore della relativa priorità (rt_priority con range 1-99) a cui viene sommato il valore costante Per i task della classe sched_other la bontà dei task è data dalla seguente formula: bont = counter nice Il range dell indice di bontà della classe sched_other è tra 2 e 77. Un task sched_other che ha esaurito il proprio quanto esecutivo otterrà un grado di bontà pari a 0. A parità di grado di bontà tra diversi task sarà selezionato il primo task trovato scandendo la lista. Ulteriori bonus vengono concessi sia nel caso in cui un task condivida lo spazio di indirizzamento con il task precedentemente in esecuzione (bonus di 1) sia nel caso in cui un task ha avuto la sua ultima esecuzione proprio sulla CPU considerata (bonus di 15 per arch i386); questi bonus vengono concessi per beneficiare dei dati già presenti in cache e in memoria, evitando page fault e/o cache miss. Algoritmo Come per il filone di kernel precedente l algoritmo di scheduling divide il tempo di CPU in epoche. Un epoca termina quando tutti i task real-time in stato di pronto terminano,

42 CAPITOLO 3. SCHEDULING IN LINUX 31 Figura 3.3: Organizzazione dello scheduling nel filone di kernel Linux 2.4.x o passano in un altro stato e quando tutti i task della classe sched_other hanno esaurito il proprio quanto esecutivo, quindi lo scheduler ricalcola i nuovi quanti per tutti i task, anche quelli che non si trovano in stato di pronto (ma solo per le classi sched_other e sched_rr). I nuovi quanti esecutivi sono dati dalla somma del quanto esecutivo iniziale ( (20 nice)/4 + 1 per arch i386 ottenendo valori nel range tra 1 e 11) e dalla metà del numero di tick ancora a disposizione (counter) del task (> 0 solo per i task che non si trovano in stato di pronto). In questo modo il numero di tick a disposizione dei task non può andare oltre il doppio del valore del quanto esecutivo iniziale e questo per evitare che task di lungo termine possano accrescere in modo indefinito il proprio quanto esecutivo a disposizione, di fatto rendendoli sempre più prioritari rispetto a nuovi task. Lo scheduler, ogni qual volta viene invocato, scansione l intera lista dei task in stato di

43 CAPITOLO 3. SCHEDULING IN LINUX 32 pronto (runqueue), dalla testa alla coda. Se il task esaminato è di tipo real-rime round robin e ha esaurito il suo quanto esecutivo, allora il task viene spostato in coda alla lista e ne viene ripristinato il quanto esecutivo a disposizione ((20 nice)/4 + 1). Se il task esaminato non è più in stato di pronto allora viene rimosso dalla lista. Quindi per ogni task (che non sia già in esecuzione su altre CPU e che sia eseguibile sulla CPU considerata) viene calcolato il relativo grado di bontà e viene schedulato in CPU il task con il valore di bontà più alto. Quando un task rilascia la CPU, tramite apposita chiamata di sistema sys_sched_yield(), lo stesso viene messo in testa alla lista dei task, per favorirlo nella prossima decisione di scheduling (rispetto a task di pari bontà). Quando viene creato un nuovo processo, il numero di tick a disposizione (counter) del processo padre viene dimezzato e il processo figlio assume il medesimo quanto esecutivo. In questo modo si evita ad esempio che un utente monopolizzi la CPU creando tanti processi. Quando su una CPU non ci sono task da eseguire, viene eseguito il thread swapper, con PID 0, uno per ogni CPU. Features In questo filone di kernel, troviamo una rivisitazione del supporto SMP (sezione A.5 per i dettagli). SMP L accesso alle strutture dati condivise da parte di più task (su diverse CPU) è supportato grazie alle tecniche di disabilitazione degli interrupt e di spin lock che evitano problemi di race condition, permettendo un accesso in mutua esclusione. Inoltre gli interrupt vengono utilizzati per forzare una nuova schedulazione sulle CPU. Rispetto al filone di kernel precedente il supporto SMP è stato migliorato e semplificato. L idea è sempre quella di schedulare task sulle stesse CPU sulle quali sono stati eseguiti in precedenza e questo per evitare page fault e/o cache miss, riducendo quindi l overhead. Quando un task passa in stato di pronto e quando un task viene prelazionato su una certa CPU si cerca di mettere in atto questa politica verificando le seguenti condizioni, nell ordine: se l ultima CPU che ha eseguito il task è in idle, il task viene mandato in esecuzione sulla stessa; tra tutte le CPU in idle si seleziona quella in idle da più tempo (dove, probabilmente, abbiamo cache con dati meno utili) e si manda in esecuzione il task; se non ci sono CPU in idle si confrontano gli indici di bontà dei task attualmente in esecuzione sulle CPU (sulle quali il task può essere eseguito) con quello del task considerato: nel caso in cui l indice di bontà del task considerato sia maggiore di quello dei task attualmente in esecuzione sulle CPU considerate allora si prelaziona il task che ha la maggiore differenza (positiva). Inoltre in questo filone di kernel si introduce il supporto alla tecnologia Hyper-Threading (sezione A.5 per i dettagli). In questo caso il kernel Linux vede una CPU Hyper-threading come se fossero n CPU distinte (con n numero di thread supportati in CPU). Nel caso in cui una CPU Hyper-Threading sia in idle questa viene preferita rispetto alle altre, eventuali, CPU in idle (di fatto invalidando la regola nella quale si preferisce la CPU in idle da più tempo). Chiamate di Sistema In questo filone di kernel le principali system call attinenti allo scheduling di processi sono le seguenti:

44 CAPITOLO 3. SCHEDULING IN LINUX 33 setscheduler(): permette di assegnare a un task la relativa classe di priorità (policy) e la priorità dedicata ai task real-time (rt_priority); sys_sched_yield(): permette a un task di rilasciare la CPU in modo spontaneo; sys_setpriority(): permette di assegnare il valore di nice (in un range tra -20 e 19) a un singolo task fornendo il suo PID, oppure a un gruppo di task sulla base dell identificativo di proprietario o di gruppo. Considerazioni Rispetto al filone di kernel 2.0.x e 2.2.x dove il numero di tick a disposizione dei task (counter) poteva assumere valori compresi tra 0 e 80, nel filone di kernel 2.4.x il range si è ristretto a La diminuzione del massimo quanto esecutivo che un task (delle classi sched_other e sched_rr) può ottenere all interno di un epoca comporta una serie di conseguenze: i contex-switch avvengono con una maggior frequenza e questo introduce un maggior overhead; le epoche hanno una minor durata e quindi le eventuali modifiche di priorità (che determina il quanto esecutivo) dei task (delle classi sched_other e sched_rr) sono prese in considerazione più velocemente (a fine epoca quando vengono ricalcolati i quanti esecutivi); riducendo la durata dei quanti esecutivi dei task, i task in stato di pronto sono mandati in esecuzione più velocemente, riducendo i tempi di risposta, soprattutto per i task I/O bound (in genere task interattivi). Per quanto riguarda le considerazioni sulla complessità computazionale rispetto al tempo, sulla gestione dei task I/O e CPU bound, sul real-time e sul supporto SMP, valgono le stesse considerazioni fatte per il filone precedente (si veda la sezione Considerazioni in 3.1.2). Ulteriori dettagli sullo scheduling dei processi nel kernel Linux filone 2.4.x possono essere trovati nel Capitolo 11 di [21] e visionando i sorgenti di Linux [24] in particolare i file: kernel/sched.c: contiene le principali funzioni per lo scheduling dei processi; kernel/sys.c: contiene la maggior parte delle system call; include/linux/sched.h: contiene le principali strutture dati utilizzate per operazioni di scheduling Kernel (alias O(1)) Come riferimento si è preso il Kernel Tipologia e obiettivo Nel filone di kernel troviamo diversi cambiamenti rispetto al filone precedente per quanto riguarda le attività di scheduling. E stato introdotto il cosiddetto scheduler O(1). Il nome deriva dal fatto che il tempo impiegato per scegliere un nuovo task da mandare in esecuzione (e il calcolo delle priorità dinamiche) è costante e non dipendete più dal numero di task presenti nel sistema, come avveniva per lo scheduler nei filoni di kernel precedenti. Lo scheduler O(1) è uno scheduler a classi di priorità, secondo lo standard POSIX: in una decisione di scheduling si manda in esecuzione il task di priorità maggiore. La gestione delle priorità e dei quanti esecutivi è stata rivisitata; inoltre è stato migliorato il riconoscimento dei task interattivi, introducendo un euristica di interattività, al fine di concedere loro una maggior priorità esecutiva. Altre caratteristiche importanti introdotte in questo filone di kernel riguardano la possibilità di prelazionare task che sono eseguiti in kernel space e l uso della tecnica a priorità ereditata per il problema di priorità inversa. Queste due caratteristiche aumentando la

45 CAPITOLO 3. SCHEDULING IN LINUX 34 reattività del sistema sui cambi di priorità dei task. Il supporto alle architetture multiprocessore (secondo il modello SMP) è stato rivisitato per migliorarne le prestazioni. Ogni risorsa di calcolo (e.g. CPU o core) ha associato una lista di task che può eseguire. E stato introdotto un bilanciamento di carico periodico basato sul domain scheduling con supporto alle architetture NUMA, UMA, CPU hyperthreading e CPU multicore, le cui decisioni tengono conto delle affinità dei task in CPU. L obiettivo delle politiche di scheduling in questo filone è quello di dare massima priorità ai task real-time (POSIX), distribuire in modo equo le risorse di calcolo ai task ordinari cercando di minimizzare i tempi di risposta per i task interattivi (in genere I/O bound) e sfruttare in modo efficiente le caratteristiche dei sistemi a multiprocessore, secondo il modello SMP. Strutture dati Nei filoni di kernel precedenti esisteva un unica lista di task alla quale le CPU facevano riferimento per le attività di scheduling. In questo filone ogni risorsa di calcolo mantiene una propria lista di processi che può eseguire. La struttura dati informativa di ogni task (task_struct) è stata rivisitata. Per l attività di scheduling le informazioni utilizzate sono le seguenti: state: rappresenta lo stato del task (-1 unrunnable, 0 runnable, > 0 stopped); flags: una bit mask di flag tra cui quella che indica che è necessario invocare lo scheduler; policy: la classe di priorità di appartenenza del task (sched_fifo, sched_rr, sched_normal e sched_batch ); prio: priorità effettiva dei processi, viene utilizzata dallo scheduler per determinare il prossimo task da eseguire (range tra 0 (priorità più alta) e 139 (priorità più bassa)); static_prio: priorità statica dei processi, viene utilizzata per calcolare il quanto esecutivo che il task ha a disposizione e per determinare la priorità normal_prio (range tra ; solo per sched_rr e sched_normal/batch ); normal_prio: priorità attesa dei task che tiene conto degli eventuali bonus/penalità nel caso di task ordinari e del valore di rt_priority (a logica inversa) nel caso di task real-time; run_list: puntatore al precedente e prossimo task nella lista; array: puntatore alla lista associata al task; sleep_avg: media del tempo di sleep del task; timestamp: riporta quando il task è stato inserito nella lista o quando è avvenuto l ultimo switch che ha coinvolto il task; last_ran: informazione temporale di quando è avvenuto l ultimo switch che ha rimpiazzato il task; cpus_allowed : una bit mask che indica su quali CPU il task può essere eseguito; time_slice: il numero di tick che il task ha ancora a disposizione (solo per sched_rr e sched_normal/batch); first_time_slice: indica se il task non ha mai esaurito il suo quanto; rt_priority: il valore di priorità per i task real-time delle classi sched_fifo e sched_rr (da 1 (priorità più bassa) a 99 (priorità più alta)); pi_waiters: una lista di riferimenti a task che sono in attesa di rilascio di un mutex real-time impegnato da questo task; Ogni risorsa di calcolo (e.g. CPU o core) ha associato una struttura dati runqueue (struttura rq ) dedicata alle attività di scheduling. Le informazioni più rilevanti contenute nella struttura sono le seguenti: nr_running: numero di task in stato di pronto assegnati alla CPU; cpu_load: fattori di carico di CPU sulla base di una media dei task mantenuti; nr_switches: numero di switch realizzati sulla CPU; nr_uninterruptible: numero di task, appartenenti a questa CPU, che sono nello stato task_uninterruptible ;

46 CAPITOLO 3. SCHEDULING IN LINUX 35 expired_timestamp: informazione temporale di quando è stato inserito il primo task nella lista expired; curr: puntatore al task attualmente in esecuzione su questa CPU; idle: puntatore al task di swapper per questa CPU; active: puntatore alla lista dei processi attivi ; expired: puntatore alla lista dei processi scaduti ; arrays[2]: le due liste dei processi scaduti e attivi ; best_expired_prio: il valore di priorità più alta tra i processi scaduti ; nr_iowait: numero di task, associato a questa CPU, che sono in attesa di I/O; sd: puntatore alla struttura di dominio di questa CPU; active_balance: indica se alcuni task possono migrare in liste di altre CPU; migration_thread: puntatore al thread utilizzato per forzare la migrazione di thread tra liste di diverse CPU; migration_queue: lista di processi da rimuovere da questa CPU; Le liste dei task (active e expired) sono organizzate con una struttura prio_array che contiene il numero di task nella lista (nr_active), una bitmask (bitmap) e l array di liste (queue), una per ogni valore di priorità (0-139). L organizzazione delle strutture dati utilizzate nelle attività di scheduling nel filone di kernel Linux viene schematizzata nella Figura 3.4. Politica In questo filone si introduce la caratteristica di kernel preemption (sezione 1.1 per i dettagli): un task che sta eseguendo codice in kernel space (ad esempio una system call) può essere prelazionato (sempre che non sia in qualche regione critica) in favore di un altro task di priorità maggiore. I task sono organizzati in 4 classi di priorità (policy): sched_fifo, sched_rr, sched_normal e sched_batch. Le prime due sono dedicate ai task real-time (POSIX), mentre le ultime due rappresentano i task ordinari. Per tutte le classi di priorità abbiamo una prelazione quando il task in esecuzione va in attesa di I/O o quando rilascia in modo volontario la CPU. I task di tipo sched_fifo, gestiti con tecnica first-in first-out, non hanno quanti esecutivi e sono prelazionati da nuovi task in stato di pronto con priorità maggiore; i task real-time sched_rr hanno quanti esecutivi e al loro termine sono prelazionati secondo la tecnica del round robin (quando un task esaurisce il suo quanto viene messo in coda alla lista) oltre che da task in stato di pronto con maggiore priorità; i task sched_normal come i sched_rr hanno quanti esecutivi e possono essere prelazionati alla fine dei loro quanti o nel caso in cui si presenta un nuovo task con una maggiore priorità di esecuzione (e.g. task real-time). La classe sched_batch rappresenta i task CPU bound, gestiti dallo scheduler come i task della classe sched_normal ma con il relativo valore di sleep_avg sempre a 0 (questi task non possono essere considerati mai come task interattivi). Calcolo del quanto esecutivo Il quanto esecutivo (time_slice) di un task (sched_normal/batch e sched_rr) viene calcolato sulla base del valore della priorità statica del task (static_prio) secondo la seguente formula: se static_prio < 120 time_slice*= (140 static_prio) 2

47 CAPITOLO 3. SCHEDULING IN LINUX 36 Figura 3.4: Organizzazione dello scheduling nel filone di kernel Linux

48 CAPITOLO 3. SCHEDULING IN LINUX 37 se static_prio >= 120 time_slice*= (140 static_prio) 0.5 (*) per i386 (range tra 1-80, dove 1 è 5ms) In questo modo si concede un maggiore tempo di esecuzione (quanti esecutivi più lunghi) ai task con priorità più alte (e quindi valori bassi di static_prio). Calcolo della priorità La priorità attesa di un task (normal_prio) nel caso di task real-time è determinata dal valore di rt_priority a logica inversa (ossia il valore 1 di rt_priority assume il valore 100 in normal_prio, ecc.), mentre nel caso di task ordinari è determinata dal valore di static_prio più eventuali bonus/penalità, in questo modo: normal_prio = max(100, min(static_prio bonus, 139)) Quindi per i task ordinari il range è tra 100 (priorità più alta) e 139 (priorità più bassa). Il valore di bonus (range tra -5 e +5) dipende dal comportamento passato del task ed in particolare da quanto mediamente il task è stato in sleep (sleep_avg). Più il task è stato in sleep (si considera un massimo di un secondo) e maggiore sarà il suo bonus (massimo 5). In questo modo i task che sono stati poco in sleep sono penalizzati (e.g. i task della classe sched_batch), mentre si agevolano (concedendo loro qualche punto di priorità in più) i task che sono rimasti più tempo in sleep. La caratteristica di rimare spesso in sleep è tipica, in genere, dei task interattivi. La priorità effettiva utilizzata dallo scheduler per scegliere quale task mandare in esecuzione è contenuta in prio (range tra 0-139). Solitamente assume il valore di normal_prio. Per evitare il problema di priorità inversa si utilizza la tecnica a priorità ereditata (sezione 1.4 per i dettagli): quando il task in esecuzione ha preso un mutex real-time, per cui almeno un altro task di priorità maggiore ne sta aspettando il rilascio, allora la priorità del task in esecuzione assume lo stesso valore di priorità del task in attesa di rilascio del mutex (quello con valore maggiore di priorità nel caso di più task in attesa); in questo modo viene concessa la possibilità al task di rilasciare al più presto il mutex. Il valore di prio (e di normal_prio) viene ricalcolato ad ogni tick (o dopo l esecuzione delle chiamate di sistema che modificano i valori di static_prio o rt_priority). A parità di priorità tra diversi task, sarà selezionato il primo task trovato scandendo la lista (associata alla relativa priorità). Euristica per l interattività Un task (di tipo sched_normal) è considerato interattivo se vale la seguente disuguaglianza: prio <= static_prio delta dove delta assume valori maggiori al diminuire della priorità static_prio (e quindi valori di static_prio più grandi). L idea che sta alla base è la seguente: più è bassa la priorità del task è più tempo il task deve rimanere in sleep affinché possa essere considerato interattivo. Liste active ed expired Ogni risorsa di calcolo mantiene una lista (active) di processi che può eseguire. La struttura della lista è organizzata come un array di liste di processi, una per ogni priorità

49 CAPITOLO 3. SCHEDULING IN LINUX 38 (0-139) dove confluiscono tutti i task, in stato di pronto, assegnati alla risorsa di calcolo. In una seconda lista (expired) confluiscono i task della classe sched_normal/batch che hanno esaurito il proprio quanto esecutivo. Lo spostamento dei task dalla lista active a quella expired avviene a seconda se il task è considerato interattivo o meno. Se il task non è considerato interattivo (e.g. per sched_batch) allora viene spostato; se il task è considerato interattivo, questo viene spostato solo se non ci sono task nella lista expired con priorità maggiore oppure se c è almeno un task nella lista expired che sta aspettando da troppo tempo; altrimenti il task rimane nella lista active (in coda alla relativa lista e con il quanto esecutivo ripristinato). I task di tipo real-time, fino a quando sono in stato di pronto, restano nella lista active. Quando un task real-time della classe shed_rr esaurisce il suo quanto esecutivo, questo viene ripristinato e il task viene messo in coda alla relativa lista. Quando nella struttura active non ci sono più task in stato di pronto da eseguire allora la struttura expired diventa active e vice versa. Questa logica a due liste simula il comportamento delle epoche che ritroviamo nei filoni di kernel precedenti: l obiettivo è sempre quello di evitare che task ordinari con alta priorità e di lunga esecuzione impediscano l esecuzione di task ordinari di più bassa priorità. L utilizzo delle due liste evita di dover ricalcolare, per ogni task, il nuovo quanto esecutivo ad ogni fine epoca, come avveniva per i filoni di kernel precedenti. Algoritmo La struttura della liste active ed expired contengono una bitmask e un array di 140 liste di task, una per ogni valore di priorità. L organizzazione delle liste è mantenuta in modo tale che il primo elemento sia quello da selezionare per l esecuzione. La decisione di quale lista di priorità considerare è determinata dalla bitmask (140 bit, uno per ogni priorità: bit alto significa che c è almeno un task in stato di pronto; bit basso significa che non ci sono task da considerare per la priorità rappresentata dal bit): viene selezionato il valore di priorità corrispondente al primo bit alto trovato nella bitmask (partendo da 0). Se ci sono task in stato di pronto allora si verifica che la lista active ne contenga almeno uno. In caso negativo tutti i task in stato di pronto si trovano nella lista expired e quindi le due liste vengono scambiate. A questo punto si determina quale task mandare in esecuzione. Per farlo si verifica nella bitmask il primo valore di priorità con il bit alto e si seleziona il primo task della lista corrispondente. Per determinare il primo bit alto della bitmask si utilizza l istruzione assembler bsfl (che richiede qualche decina di cicli di clock). Il nome O(1) dello scheduler deriva proprio dal fatto che la scelta del prossimo task da mandare in esecuzione avviene in tempo sempre costante, indipendentemente dal numero di task in stato di pronto nel sistema. Quando un task rilascia la CPU, tramite apposita chiamata di sistema sched_yield(), lo stesso rimane in stato di pronto: se è di tipo sched_normal/bacth viene spostato nella lista expired, se è di tipo sched_fifo rimane nella sua posizione, nella lista active; mentre se è di tipo sched_rr viene messo in coda alla lista dei task della medesima priorità. Quando viene creato un nuovo processo, il numero di tick a disposizione del processo padre viene dimezzato e il processo figlio assume il medesimo quanto esecutivo. In questo modo si evita ad esempio che un utente monopolizzi la CPU creando tanti processi. Quando su una CPU non ci sono task da eseguire, viene eseguito il thread di swapper, uno per ogni CPU. Inoltre allo scadere di ogni tick vengono effettuate queste operazioni di scheduling: se sulla CPU considerata è in esecuzione il thread di swapper si verifica se ci sono task in stato di pronto sulla stessa CPU e in caso positivo si forza una nuova schedulazione; se il task in esecuzione è di tipo sched_normal/batch o sched_rr si decrementa il relativo quanto esecutivo di una unità; allo scadere del quanto nel caso di

50 CAPITOLO 3. SCHEDULING IN LINUX 39 sched_normal/batch si ricalcola il quanto e la priorità effettiva del task (prio) e si valuta se spostare il task nella lista expired; nel caso di sched_rr si ripristina il quanto e si posizione il task in coda alla relativa lista di priorità, sempre nella lista active; si verifica il bilanciamento di carico del sistema (dopo un certo numero di tick). Lo scheduler considera solo i task che sono in stato di pronto, inclusi nella struttura runqueue. I task in stato di blocco, in attesa di qualche evento, sono posizionati in un altra struttura dati (waitqueue). Ogni tipo di evento ha associato una struttura dati waitqueue che contiene un riferimento alla lista dei task che sono in attesa di quel particolare evento ed uno spinlock, per assicurare un accesso in mutua esclusione alla struttura stessa. Features Il supporto alle architetture multiprocessore, secondo il modello SMP, è stato rivisitato per migliorarne le prestazioni ed è stato introdotto un bilanciamento di carico basato sul domain scheduling con supporto alle architetture NUMA, UMA, CPU hyper-threading e CPU multicore. SMP L accesso alle strutture dati condivise da parte di più task è supportato grazie alle tecniche di disabilitazione degli interrupt e di spin lock che evitano problemi di race condition, permettendo un accesso in mutua esclusione. Inoltre gli interrupt vengono utilizzati per forzare attività di scheduling sulle CPU (come il bilanciamento di carico). In un sistema a multiprocessore, dove ogni risorsa di calcolo ha la propria lista di task da eseguire, quando un task passa in stato di pronto e non è associato ancora a nessuna CPU, lo scheduler determina su quale lista tra le diverse CPU del sistema far confluire il task, secondo le seguenti regole: tra le eventuali CPU in idle, si da preferenza all ultima CPU sulla quale il task è stato eseguito o alla CPU che sta eseguendo il controllo (chiamata CPU locale) o alla prima CPU trovata in idle, in questo ordine; se il carico di lavoro dell ultima CPU che ha eseguito il task è minore del carico della CPU locale, allora si sceglie la prima; se il processo è stato eseguito di recente, si seleziona la lista dell ultima CPU che lo ha eseguito; se portando il task sulla lista della CPU locale riduce lo sbilanciamento di carico del sistema, allora la lista è quella della CPU locale; Come per il filone precedente, anche in questo filone c è il supporto alla tecnologia Hyper- Threading (vedi il paragrafo A.5 per i dettagli). Il kernel vede una CPU Hyper-threading come se fossero n CPU logiche distinte (con n numero di thread supportati). L organizzazione dello scheduling in questo filone di kernel prevede che ogni CPU abbia una propria lista di task che può eseguire. In un dato istante un task si trova in una sola lista di una certa CPU. Questa scelta di progettazione è stata indotta da due aspetti principali: se un task viene eseguito sempre sulla stessa CPU può beneficiare dei propri dati che sono stati caricati in cache durante la sua esecuzione (e nella ram locale in caso di NUMA); l altro aspetto riguarda l accesso alla lista dei task in stato di pronto che, in caso di un unica lista, porterebbe ad una competizione di accesso alla lista tra le diverse CPU del sistema. Entrambi gli aspetti riducono fortemente i possibili overhead che si avrebbero senza di essi. Con questo tipo di organizzazione è necessario verificare in modo periodico il bilanciamento di carico tra le varie CPU del sistema evitando di avere alcune CPU pesantemente

51 CAPITOLO 3. SCHEDULING IN LINUX 40 cariche e altre in idle. Il bilanciamento di carico inoltre deve tenere in considerazione la topologia dell architettura multiprocessore del sistema e questo sfruttarne le caratteristiche, migliorando le prestazioni di scheduling. Il bilanciamento di carico in questo filone di kernel si basa sulla nozione di schedulazione a dominio con supporto alle architetture UMA, NUMA, hyper-threading (aka SMT) e CPU multicore. Domain scheduling La schedulazione a dominio, introdotta a partire dal kernel 2.6.7, organizza le CPU del sistema in domini. Un dominio contiene uno o più gruppi; un gruppo contiene una o più CPU. A seconda del tipo di architettura del sistema l organizzazione dei domini può essere gerarchica e quindi un dominio può avere un dominio padre e uno o più domini figli. Il bilanciamento di carico avviene sempre tra gruppi appartenenti allo stesso dominio, partendo dal dominio di più basso livello. Ad esempio, in una architettura NUMA composta da due nodi ognuno dei quali ha quattro CPU (a core singolo) avremmo un primo dominio a due gruppi, dove ogni gruppo rappresenta un nodo (e quindi ingloba le relative quattro CPU) e con due sotto-domini, ognuno dei quali è composto da quattro gruppi, uno per CPU del relativo nodo che rappresenta. In un sistema con architetture UMA con una CPU a doppio core, avremmo un unico dominio a due gruppi, uno per ogni core. Questi due esempi sono rappresentati nella Figura 3.5. Bilanciamento di carico La procedura del bilanciamento di carico viene attivata in modo periodico su ogni CPU del sistema con una frequenza che dipende dal carico di lavoro della CPU considerata: in caso di idle la frequenza sarà maggiore, mentre diminuirà all aumentare del carico di lavoro della CPU. Per ogni livello di dominio del sistema viene verificato il bilanciamento di carico tra i gruppi dello stesso dominio, partendo dal dominio base associato alla CPU considerata (d ora in poi CPU locale) fino al dominio di più alto livello (che può coincidere con il primo). La procedura di bilanciamento di carico si sviluppa nei seguenti punti: all interno del dominio considerato si stabilisce qual è l eventuale gruppo che ha il maggior carico di lavoro e il numero di task che devono essere spostati nella lista della CPU locale affinché sia ristabilito il bilanciamento di carico tra i gruppi del dominio (se la CPU locale fa parte del gruppo trovato allora questo non viene preso in considerazione e la procedura termina); all interno del gruppo con maggior carico di lavoro si stabilisce quale CPU mantiene il maggior numero di task e, alcuni di questi vengono spostati nella lista della CPU locale; nel caso in cui fallisca qualche spostamento di task da una lista all altra allora viene attivato il task di migrazione che va alla ricerca di una CPU in idle e sposta il task incriminato su di essa; La logica di scelta dei task che devono essere migrati da una lista di CPU ad un altra segue questi punti: si prendono in considerazione prima gli eventuali task trovati nella lista expired e poi quelli nella lista active; il task non deve essere già in esecuzione; la CPU di destinazione deve essere una tra quelle permesse dal task (cpus_allow);

52 CAPITOLO 3. SCHEDULING IN LINUX 41 Figura 3.5: Esempi di schedulazione a dominio in Linux almeno una di queste condizioni deve essere soddisfatta: il task non è considerato cache hot per la CPU sorgente (ossia non è stato recentemente eseguito in CPU), oppure ci sono stati dei problemi nello spostare altri task (si parla di bilanciamento forzato). Chiamate di Sistema In questo filone di kernel le principali system call attinenti allo scheduling di processi sono le seguenti: sched_setscheduler(): permette di assegnare a un task la relativa classe di priorità (policy) e la priorità dedicata ai task real-time (rt_priority); sys_sched_yield(): permette a un task di rilasciare la CPU in modo spontaneo; sys_setpriority(): permette di assegnare il valore di priorità statica a un singolo task fornendo il suo PID, oppure a un gruppo di task sulla base dell identificativo di proprietario o di gruppo; sys_sched_setaffinity(): permette di valorizzare il campo cpus_allow del task, determinando su quali CPU il task può essere eseguito.

53 CAPITOLO 3. SCHEDULING IN LINUX 42 Considerazioni L algoritmo di scheduling introdotto nel filone di kernel scala bene rispetto al numero di processi del sistema: l algoritmo seleziona il processo da mandare in esecuzione sempre in un tempo costante, indipendentemente dal numero di processi in stato di pronto nel sistema. Inoltre l algoritmo scala bene rispetto al numero di risorse di calcolo nel sistema: ogni risorsa di calcolo ha la propria lista di processi che può mandare in esecuzione e quindi l accesso alla lista dei processi da schedulare non è in competizione con altre risorse di calcolo. Inoltre l introduzione di un euristica che migliora il riconoscimento dell interattività di un task permette di minimizzare i tempi di risposta delle applicazione interattive, anche su sistemi con alti carichi. Il supporto per le applicazioni real-time è stato migliorato: un kernel preemptive permette di prelazionare subito un task eseguito in modalità kernel in favore di un task real-time di maggior priorità. Inoltre sono state introdotte delle tecniche per contrastare problemi come l inversione di priorità. Questo scheduler non tiene però in considerazione eventuali vincoli temporali di task real-time dovrebbero rispettare; inoltre diversi blocchi di codice sono ancora eseguiti in interrupt context o comunque senza la possibilità di essere prelazionati (e.g. disabilitando gli interrupt). Questo scheduler quindi non può ancora essere considerato uno scheduler real-time a tutti gli effetti. Ulteriori dettagli sullo scheduling dei processi nel kernel Linux filone possono essere trovati nel Capitolo 7 di [22] e visionando i sorgenti di Linux [24] in particolare i file: kernel/sched.c: contiene le principali funzioni per lo scheduling dei processi; kernel/sys.c: contiene la maggior parte delle system call; include/linux/sched.h: contiene le principali strutture dati utilizzate per operazioni di scheduling Kernel (alias CFS) Come riferimento si è preso il Kernel , salvo dove diversamente specificato. Tipologia e obiettivo Nel filone di kernel sono stati apportati cambiamenti significativi per quanto riguarda l attività di scheduling dei processi. Ingo Molnar ha introdotto una nuova tecnica di scheduling a classi, che permette di implementare nuove politiche di scheduling in moduli, azionati dallo scheduler. Sempre Ingo Molnar ha sviluppato il cosiddetto scheduler CFS (Completely Fair Scheduler), una nuova politica di scheduling per i task ordinari, che cerca di allocare in modo equo il tempo di esecuzione delle risorse di calcolo ai task ordinari. Per quanto riguarda i task real time ritroviamo la stessa politica di scheduling a classi di priorità (incapsulata in un modulo), attuata nel filone di kernel precedente, secondo lo standard POSIX: in una decisione di scheduling si schedula il task real-time di maggiore priorità, con una complessità computazionale temporale di O(1). La politica di scheduling CFS per i task ordinari (incapsulata in un modulo) fraziona equamente un periodo di esecuzione in CPU (chiamato latenza di target) ai task ordinari in competizione di esecuzione: in una decisione di scheduling si schedula il task ordinario con il minor tempo di esecuzione disponibile, ossia il task che è stato meno in esecuzione in CPU rispetto agli altri. La complessità computazionale temporale per CFS è, nel caso peggiore, di O(log n), con n numero di task ordinari sul sistema (per via della gestione dell albero binario che mantiene i task ordinari). In questo filone di kernel viene introdotta la tecnica di group scheduling, che permette di raggruppare task in gruppi (sulla base di diversi criteri, come l id di sessione o l id

54 CAPITOLO 3. SCHEDULING IN LINUX 43 utente) e quindi allocare tempi di esecuzione in CPU ai gruppi stessi (e non più a singoli task). Inoltre è possibile allocare un tempo di esecuzione deterministico (all interno di un periodo) per i task real-time e per quelli ordinari, potendo quindi garantire un tempo di esecuzione certo ai task ordinari. La caratteristica di preemption del kernel e l utilizzo della tecnica a priorità ereditata aumentano la reattività del sistema ai cambi di priorità dei task. Il supporto alle architetture multiprocessore (modello SMP) è stato riorganizzato: anche per questo filone di kernel ogni risorsa di calcolo (e.g. CPU o core) ha associato una lista di task che può eseguire e il bilanciamento di carico si basa sulla schedulazione a dominio (con supporto alle architetture NUMA, UMA, CPU multicore e CPU hyper-threading). L obiettivo delle politiche di scheduling in questo filone è di distribuire in modo equo le risorse di calcolo ai task ordinari cercando di minimizzare i tempi di risposta per i task interattivi (in genere I/O bound), di avere dei meccanismi che garantiscano tempi di esecuzione sia per i task real time sia per quelli ordinari e di supportare in modo efficiente le architetture multiprocessore, secondo il modello SMP. Classi di scheduling Il codice dello scheduler è stato riorganizzato, secondo la metodologia delle classi di scheduling: una gerarchia estendibile di moduli per lo scheduler. L idea è di incapsulare i dettagli delle politiche di scheduling in moduli che saranno poi azionati dal core dello scheduler. In questo modo si possono implementare nuove politiche di scheduling (e quindi nuovi moduli) senza aver la necessità di modificare il core dello scheduler. Le classi di scheduling sono implementate attraverso la struttura sched_class, che contiene dei puntatori a un set di funzioni di base che sono invocate a seconda dell evento di scheduling che si presenta. Seguono le funzioni principali: enqueue_task(): inserisce un task in stato di pronto nella lista di task utilizzata dalla classe; dequeue_task(): toglie un task non più in stato di pronto dalla lista di task utilizzata dalla classe;; yield_task(): questa funzione sostanzialmente invoca dequeue_task() seguita da enqueue_task(); check_preempt_curr(): verifica se il task che è passato in stato di pronto può prelazionare il task in quel momento in esecuzione; pick_next_task(): determina il prossimo task da mandare in esecuzione; put_prev_task(): questa funzione viene invocata quando il task in esecuzione sta per essere prelazionato; pre_schedule(): invocata dallo scheduler prima di decidere il prossimo task da mandare in esecuzione; post_schedule(): invocata dallo scheduler dopo uno switch di contesto; set_curr_task(): questa funzione è invocata quando un task cambia la sua classe di scheduling o il suo gruppo; task_tick(): gestione dei tick del task in esecuzione; task_fork(): con questa funzione il core dello scheduler offre la possibilità ai moduli di scheduling di gestire i nuovi task. Ad esempio il modulo di CFS utilizza questa funzione per gestire lo scheduling di gruppo, mentre il modulo che gestisce i task real-time non la utilizza;

55 CAPITOLO 3. SCHEDULING IN LINUX 44 select_task_rq (): questa funzione permette ad ogni classe di implementare la propria logica di scelta della lista di CPU sulla quale verrà agganciato un task passato in stato di pronto (e.g. su operazioni di fork(), wake(), exec()); Ogni modulo di scheduling implementa queste funzioni a seconda delle strutture dati, delle politiche e degli algoritmi previsti per il modulo stesso. Nel filone di kernel ritroviamo due moduli di scheduling: modulo real-time (file sched_rt.c) che implementa la politica di scheduling per i task real-time secondo lo standard POSIX; CFS (file sched_fair.c) che implementa la classe di scheduling per i task ordinari; Strutture dati Come per il filone di kernel precedente, anche in questo, ogni risorsa di calcolo mantiene una propria lista di processi in stato di pronto che può eseguire. La struttura dati informativa di ogni task (task_struct) è stata rivisitata. Per l attività di scheduling le informazioni utilizzate sono le seguenti: state: rappresenta lo stato del task (-1 unrunnable, 0 runnable, > 0 stopped); flags: una bit mask di flag tra cui quella che indica che è necessario invocare lo scheduler; prio: priorità effettiva dei processi con range tra 0 (priorità più alta) e 139 (priorità più bassa); normalmente assume il valore di normal_prio; per i task real-time viene utilizzata per decidere il prossimo task da mandare in esecuzione, mentre per i task ordinari viene utilizzata per assegnare il peso al task (che sarà poi utilizzato nel calcolo della slice di CPU da assegnare al task stesso); static_prio: priorità statica dei processi, viene utilizzata per determinare la priorità normal_prio anche sulla base dei valori di nice (range tra ; solo per i task ordinari); normal_prio: priorità attesa dei task (range tra 0 e 139); nel caso di task ordinari assume il valore di static_prio, nel caso di task real-time assume il valore di rt_priority (a logica inversa); rt_priority: il valore di priorità per i task real-time delle classi sched_fifo e sched_rr (da 0 (priorità più bassa) a 99 (priorità più alta)); sched_class: puntatore alla classe di scheduling di appartenenza del task (e.g. real-time o cfs); se: struttura informativa dell entità del task ordinario (CFS); rt: struttura informativa dell entità del task real time; policy: la classe di scheduling di appartenenza del task (sched_fifo, sched_rr, sched_normal, sched_batch e sched_idle ); cpus_allowed: una bit mask che indica su quali CPU il task può essere eseguito; pid: identificativo univo del task; pi_waiters: una lista di riferimenti a task che sono in attesa di rilascio di un mutex real-time impegnato da questo task; Informazioni specifiche per i task di classe real-time sono mantenute nella struttura dati sched_rt_entity (campo rt di task_struct): run_list: puntatore alla lista di CPU di appartenenza del task; time_slice: il numero di tick che il task ha ancora a disposizione (solo per sched_rr); Informazioni specifiche per i task di classe CFS sono mantenute nella struttura dati sched_entity (campo se di task_struct): load: struttura dati che contiene l informazione sul peso del task, derivato dal valore di prio e utilizzato nel calcolo di vruntime e nelle operazioni di bilanciamento di carico; exec_start: tempo di inizio esecuzione; sum_exec_runtime: tempo totale di esecuzione;

56 CAPITOLO 3. SCHEDULING IN LINUX 45 vruntime: contiene il tempo (in nanosecondo) di esecuzione virtuale, normalizzato in base ai task che competono per l utilizzo della risorsa di calcolo; in una decisione di scheduling viene preso quello con il minor valore. Ogni risorsa di calcolo (e.g. CPU o core) mantiene il proprio set di task da eseguire in una struttura dati dedicata (di tipo rq ). Le informazioni di scheduling più rilevanti contenute nella struttura sono le seguenti: nr_running: numero di task (ordinari e real time) in stato di pronto assegnati alla CPU; cpu_load: fattori di carico di CPU sulla base di una media dei task mantenuti; load: carico globale di tutti i task assegnati alla CPU; nr_switches: numero di switch realizzati sulla CPU; cfs: struttura dati che mantiene i task ordinari di questa CPU; rt: struttura dati che mantiene i task real time di questa CPU; nr_uninterruptible: numero di task, appartenenti a questa CPU, che sono nello stato task_uninterruptible; curr: puntatore al task attualmente in esecuzione su questa CPU; idle: puntatore al task di swapper per questa CPU; nr_iowait: numero di task, associato a questa CPU, che sono in attesa di I/O; sd: puntatore alla struttura di dominio di questa CPU; active_balance: indica se alcuni task possono migrare in liste di altre CPU; migration_thread: puntatore al thread utilizzato per forzare la migrazione di thread tra liste di diverse CPU; migration_queue: lista di processi da rimuovere da questa CPU; Il set di task ordinari di ogni risorsa di calcolo (campo cfs di rq) viene mantenuto con la struttura dati cfs_rq, i cui campi principali sono i seguenti: load: riporta informazioni di carico dei task mantenuti; nr_running: numero di task mantenuti; exec_clock: tempo di esecuzione globale dei task ordinati mantenuti; min_vruntime: il più piccolo valore di vruntime tra tutti i task mantenuti; tasks_timeline: punta alla radice dell albero rb; rb_leftmost: punta al nodo più a sinistra dell albero rb; curr: punta al nodo attualmente in esecuzione; next: punta al prossimo nodo che andrà in esecuzione; last: punta all ultimo nodo che andrà in esecuzione; Il set di task real time di ogni risorsa di calcolo (campo rt di rq) viene mantenuto con la struttura dati rt_rq, i cui campi principali sono i seguenti: active: lista dei task real-time (struttura rt_prio_array composta da una bitmap e da un array di liste, una per ogni valore di priorità); rt_nr_running: numero di task mantenuti; highest_prio.curr: valore di priorità più grande tra i task mantenuti; highest_prio.next: secondo valore di priorità più grande tra i task mantenuti; I task che non si trovano in stato di pronto confluiscono in liste organizzate in base all evento atteso (concettualmente possiamo immaginare un unica lista che raggruppa i task non in stato di pronto). L organizzazione delle strutture dati utilizzate nella attività di scheduling nel filone di kernel Linux viene schematizzata nella Figura 3.6.

57 CAPITOLO 3. SCHEDULING IN LINUX 46 Figura 3.6: Organizzazione dello scheduling nel filone di kernel Linux

58 CAPITOLO 3. SCHEDULING IN LINUX 47 Politiche ed algoritmi Secondo la nuova organizzazione dello scheduler modulare, in questo filone di kernel, ritroviamo due moduli di scheduling: il modulo real-time che implementa le classi di scheduling per i task real-time (sched_fifo e sched_rr), secondo lo standard POSIX.; il modulo CFS che implementa le classi di scheduling per i task ordinari (sched_normal, sched_batch e sched_idle). Modulo real-time Questo modulo implementa le classi di scheduling per i task real-time, sched_fifo e sched_rr, secondo lo standard POSIX. Politica I task di tipo sched_fifo, gestiti con tecnica first-in first-out, non hanno quanti esecutivi e sono prelazionati da nuovi task in stato di pronto con priorità maggiore; i task real-time sched_rr hanno quanti esecutivi e al loro termine sono prelazionati secondo la tecnica del round robin (quando un task esaurisce il suo quanto viene messo in coda alla lista) oltre che da task in stato di pronto con maggiore priorità; Il quanto esecutivo di un task sched_rr (time_slice) è costante e dipende dal tipo di architettura considerata: time_slice = (100 HZ/1000) Su architettura x86 HZ di default vale 1000, quindi il quanto esecutivo è di 100 msec. Calcolo della priorità La priorità attesa di un task (normal_prio) nel caso di task real-time è determinata dal valore di rt_priority a logica inversa (ossia il valore 0 di rt_priority assume il valore 99 in normal_prio, ecc.). La priorità effettiva utilizzata dallo scheduler per scegliere quale task real-time mandare in esecuzione è contenuta in prio (range tra 0-99). Solitamente assume il valore di normal_prio. Per evitare il problema di priorità inversa si utilizza la tecnica di priorità ereditata (sezione 1.4 per i dettagli): quando il task in esecuzione ha preso un mutex real-time, per cui almeno un altro task di priorità maggiore ne sta aspettando il rilascio, allora la priorità del task in esecuzione assume lo stesso valore di priorità del task in attesa di rilascio del mutex (quello con valore maggiore di priorità nel caso di più task in attesa); in questo modo viene concessa la possibilità al task di rilasciare al più presto il mutex. Il valore di prio (e di normal_prio) viene ricalcolato ad ogni tick (o dopo l esecuzione delle chiamate di sistema che modificano il valore di rt_priority). A parità di priorità tra diversi task, sarà selezionato il primo task trovato scandendo la lista (associata alla relativa priorità). Algoritmo Ogni risorsa di calcolo mantiene una lista processi real-time che può eseguire in una propria struttura dati (campo active in rt_rq). La struttura è organizzata con un array di 100 liste di processi, una per ogni priorità, dove confluiscono tutti i task real-time, in stato di pronto, assegnati alla risorsa di calcolo. Oltre all array c è una bitmask che segnala la presenza di task nei diversi valori di priorità. L organizzazione delle liste è mantenuta in modo tale che il primo task sia quello da selezionare per l esecuzione. La decisione di quale lista di priorità considerare è determinata dalla bitmask (100 bit, uno per ogni priorità: bit alto significa che c è almeno un task; bit basso significa che non ci sono task da considerare per la priorità rappresentata dal bit). Per determinare quale task mandare in esecuzione si verifica nella bitmask il primo valore di priorità con il bit alto e si seleziona il primo task della lista corrispondente. Per determinare il primo bit alto della bitmask si utilizza l istruzione assembler bsfl (che richiede qualche decina di cicli di clock). Quindi la scelta del prossimo task real-time da mandare in esecuzione avviene in tempo sempre costante (O(1)), indipendentemente dal numero di task in stato di pronto nel sistema (come avveniva per il filone precedente).

59 CAPITOLO 3. SCHEDULING IN LINUX 48 Modulo CFS CFS sta per Scheduler completamente equo e sostituisce la precedente politica di scheduling sui processi ordinari e la logica sull interattività dei processi. Politica Come dice l autore, l 80% della progettazione di CFS può essere sintetizzata nella seguente affermazione: CFS modella una CPU multi-tasking ideale su hardware reale. Una CPU multi-tasking ideale (che in realtà non esiste) è una CPU che può ripartire in modo equo la sua capacità di calcolo ai vari task che competono per essa, eseguendo ciascun task in parallelo, alla velocità di 1/nr_task. Per esempio, se ci sono 2 task che devono essere eseguiti, una CPU ideale li esegue ognuno al 50% della totale velocità, in parallelo. Su hardware reale, un solo task può essere in esecuzione in un dato istante, quindi si introduce il concetto di tempo di esecuzione virtuale. Questo tempo rappresenta il tempo di esecuzione virtuale che un task dovrebbe ottenere ipotizzando di usare una CPU ideale. In pratica, il tempo di esecuzione virtuale di un task viene calcolato frazionando un certo periodo di tempo di esecuzione di CPU in base al numero totale di task che devono essere eseguiti. In CFS il tempo di esecuzione virtuale di ogni task viene tracciato nella variabile vruntime (in nanosecondi). In questo modo è possibile misurare in modo accurato il tempo di CPU atteso che un task dovrebbe ottenere. Su un hardware ideale, in ogni dato istante tutti i task dovrebbero avere lo stesso valore di vruntime, ossia tutti i task dovrebbero essere eseguiti simultaneamente e nessun task dovrebbe risultare sbilanciato riguardo alla condivisione del tempo di CPU. La logica di CFS per scegliere quale task mandare in esecuzione si basa sul valore di vruntime ed è molto semplice: in una decisione di scheduling si manda in esecuzione il task con il più piccolo valore di vruntime (ossia il task che è stato meno in esecuzione rispetto agli altri). CFS tenta sempre di suddividere il tempo di CPU tra i diversi task che devono essere eseguiti, allo stesso modo di un hardware multi-tasking ideale. Classi di scheduling Il modulo CFS implementa le classi di scheduling per i task ordinari, sched_normal, sched_batch e sched_idle. Per questi task non esiste più il concetto di quanto esecutivo (infatti non viene tracciato nelle informazioni dei task); bensì viene calcolato un valore di slice di CPU assegnato al task, ossia il tempo di esecuzione virtuale. I task ordinari possono essere prelazionati anche prima dello scadere della slice assegnata da task real-time o da altri task ordinari, secondo la politica di CFS sopra descritta. Per i task sched_batch la prelazione è meno frequente rispetto a quelli in sched_normal, permettendo ai task di essere eseguiti più a lungo, beneficiando dei dati in cache, a discapito dell interattività. Questa politica è ideale per i task batch. La classe sched_idle schedula task perfino più lentamente di quanto fa il valore 19 di nice e viene utilizzata per evitare problemi di inversioni di priorità che possono portare a problemi di stallo. Calcolo della priorità La priorità attesa di un task (normal_prio) nel caso di task ordinari assume il valore di static_prio. Il valore di static_prio (range ) riflette i valori di nice (range ). La priorità effettiva di un task ordinario è contenuta in prio (range tra ) e nel caso dei task ordinari coincide con il valore di normal_prio. Il valore di prio è utilizzato per assegnare il peso ai task ordinari (campo load.weight della struttura se). Il peso di un task viene utilizzato per determinare il tempo di esecuzione virtuale di CPU da assegnare al task: maggiore è il peso (e quindi la priorità) e maggiore sarà il tempo di esecuzione (in un periodo definito) concesso al task. Calcolo di vruntime Il valore di vruntime rappresenta il tempo di esecuzione virtuale che un task dovrebbe ottenere ipotizzando di usare una CPU ideale, con l obiettivo di

60 CAPITOLO 3. SCHEDULING IN LINUX 49 ripartire in modo equo la capacità di calcolo della CPU tra tutti i task che competono per essa. Questo tempo di esecuzione viene determinato frazionando un periodo temporale di esecuzione in CPU sulla base del numero di task ordinari che competono per l esecuzione, ed in modo proporzionale ai pesi dei task. A parità di priorità (e quindi di peso), ciascun task riceverà una porzione del periodo esecutivo pari a 1/n, con n numero di task. Ogni task ha associato un peso, campo load.weight, determinato in modo tale che a priorità maggiori corrispondano pesi maggiori. Questo peso viene tenuto in considerazione nel calcolo di vruntime e permette di assegnare tempi di esecuzione proporzionali ai pesi dei task. Latenza di target Il periodo di tempo utilizzato nel calcolo di vruntime viene chiamato latenza di target: a periodi corti corrispondono frequenti switch di contesto, migliorando i tempi di risposta (e quindi l interattività) a discapito del throughput. Per esempio con un target di latenza pari a 10 ms e con 4 task di pari priorità, ogni task riceverà un valore di vruntime pari a 2,5 ms. Notare che man mano che il numero di task che competono per l esecuzione tende all infinito, i valori di vruntime tendono a zero, portando a frequenti switch di contesto. Per evitare questo problema CFS impone un valore minimo di esecuzione da assegnare ai task (di default 1 ms), chiamato granularità minima. Per attuare questa granularità minima, CFS aumenta in modo proporzionale il target di latenza (periodo di CPU). Per esempio, supponendo di avere un target di latenza pari a 10 ms e 20 task (di pari priorità) che competono per l esecuzione in CPU, senza il vincolo della granularità minima avremmo un valore di vruntime di ciascun task pari a 0,5 ms. Con il vincolo di granularità a 1 ms, il target di latenza aumenta a 20 ms e i valori di vruntime di ciascun task sono pari a 1 ms. Quindi man mano che il carico di lavoro aumenta, le politiche di CFS tendono a massimizzare il throughput a discapito della latenza e quindi della interattività. Ci sono state delle proposte da parte della comunità per migliorare la latenza su sistemi con alti carichi di lavoro. Maggiori informazioni sono disponibili in [38]. Albero red-black La progettazione di CFS è piuttosto radicale: la lista dei task ordinari non viene rappresentata da array di liste di priorità, ma da un albero rbtree ordinato in base ai valori di vruntime. Non ci sono artefatti come lo switch tra liste active ed expired, come accadeva nel precedente filone di kernel. Le struttura dati ad albero red-black è una forma di albero binario semi-bilanciato. Ogni nodo dell albero ha un valore e fino a due figli: il valore di un nodo è maggiore di tutti i valori dei nodi che si trovano alla sua sinistra e minore di tutti i valori dei nodi che si trovano alla sua destra. Ogni nodo in un albero red-black è di coloro rosso o nero, con il nodo radice sempre di colore nero. Esistono rigorose regole su come i nodi dovrebbero essere colorati e, in particolare, su come i colori dei nodi devono essere utilizzati per prendere decisioni di quando e come bilanciare l albero. I dettagli di questi meccanismi sono disponibili in [35]. Per le proprietà e regole dell albero red-black la complessità temporale per effettuare inserimenti, cancellazioni e ricerche è pari, nel caso peggiore, a O(log n). Un altra importate proprietà dell albero è la seguente: il valore del più lungo percorso (numero di archi) verso un nodo foglia nell albero sarà sempre minore o uguale al doppio del valore del percorso più breve verso un nodo foglia. Questa proprietà ci assicura che l albero sia sempre semi-bilanciato. Gli alberi red-black sono usati in diversi componenti del kernel linux (come per l I/O scheduling, per il driver CD/DVD, per il file system ext3, e altri ancora). Ulteriori informazioni sono disponibili in [36]. Algoritmo Ogni risorsa di calcolo mantiene una lista processi che può eseguire in una proprio albero rbtree (campo cfs di rq). CFS mantiene i task ordinari che devono essere eseguiti nell albero rbtree, dove la chiave di ordinamento è vruntime. In una scelta di scheduling, CFS prende il task più a sinistra dell albero, che ha il più piccolo valore della

61 CAPITOLO 3. SCHEDULING IN LINUX 50 chiave, e che quindi è stato in esecuzione per minor tempo. Man mano che il sistema evolve nel tempo, i task eseguiti sono reinseriti nell albero a partire da destra e per le proprietà dell albero rbtree pian piano si sposteranno sempre più a sinistra per poi ottenere nuovamente l esecuzione in CPU, in un tempo deterministico. Quando viene richiesta una nuova schedulazione (e.g. se il task in esecuzione richiede I/O) si tiene traccia del tempo di esecuzione utilizzato dal task ordinario: questo tempo è aggiunto alla variabile vruntime del task. Non appena questo valore diventa più grande del valore del task che nell albero sta alla destra del task eseguito, allora si avrà un nuovo task che sta alla posizione più a sinistra dell albero, e il task corrente viene prelazionato e viene reinserito nell albero, partendo da destra. CFS mantiene, per ogni risorsa di calcolo, la variabile cfs.min_vruntime, un valore crescente che tiene traccia del più piccolo valore di vruntime tra tutti i task mantenuti nell albero. Questo valore viene utilizzato quando bisogna inserire un nuovo task nell albero. CFS usa una granularità di nanosecondi. La politica di CFS non ha una nozione di timeslice come nel precedente scheduler, e non ci sono euristiche. Esiste un parametro di tuning, sysctl_sched_min_granularity, che può essere usato per impostare la granularità minima, ossia il minimo tempo di esecuzione da allocare per i task. Il valore di questo parametro riflette gli obiettivi del workload: piccoli valori sono ideali per minimizzare le latenze e quindi avere brevi tempi di risposta, a discapito del throughput (workload server/workstation); valori più grandi sono necessari per ottenere dei tempi di esecuzioni più lunghi con l obiettivo di massimizzare il throughput (mainframe o HPC), a discapito delle latenze/interattività. Il valore di default (1 millisecondo) è opportuno per workload da workstation. Lo scheduler CFS, per come è stato progettato, non è incline a nessun attacco ad oggi esistente, a differenza di alcune euristiche che ritroviamo nella maggior parte degli altri scheduler. Le simulazioni fiftyp.c, thud.c, chew.c, ring-test.c, massive_intr.c lavorano tutte egregiamente con CFS e non hanno conseguenze con l interattività, producendo i comportamenti previsti [40]. Core scheduler L attività di scheduling principale (dove avviene lo switch di contesto) è contenuta nella funzione schedule() (file sched.c). Le azioni principali di questa funzione sono, nell ordine, le seguenti: pre_schedule(): azioni di pre-scheduling (implementate nel modulo di scheduling associato al task in esecuzione); idle_balance(): se la CPU (sulla quale la funzione viene eseguita) è in idle si cerca di vedere si possono spostare eventuali altri task da altre CPU; put_prev_task(): azioni sul task attualmente in esecuzione (implementata nei moduli di scheduling); pick_next_task(): scelta del prossimo task da mandare in esecuzione (implementata nei moduli di scheduling); context_switch(): esegue lo switch di contesto (istruzioni assembler per caricare/salvare i registri di CPU); post_schedule(): azioni di post-scheduling (implementata nei moduli di scheduling); Quando su una CPU non ci sono task da eseguire, viene eseguito il thread di swapper, uno per ogni CPU. Inoltre allo scadere di ogni tick vengono effettuate queste operazioni di scheduling:

62 CAPITOLO 3. SCHEDULING IN LINUX 51 (modulo real-time) se il task in esecuzione è di tipo real-time sched_fifo non si fa nulla, mentre se è di tipo sched_rr si decrementa il relativo quanto esecutivo e, quando raggiunge il valore 0 si ripristina il quanto di default e si sposta il task in coda alla lista della relativa priorità; (modulo CFS) si verifica se il task ha esaurito il suo slice e in caso positivo si verifica se prelazionare il task (andando a vedere se il task che sta nell albero alla sua destra ha un minor tempo di esecuzione virtuale); si verifica il bilanciamento di carico del sistema. Features In questo filone di kernel è stata introdotta la caratteristica del group scheduling che permette di raggruppare task in gruppi, secondo diversi criteri, al fine di permette di attuare le politiche di scheduling su gruppi di task. Come nel filone di kernel precedente ritroviamo un supporto alle architetture multiprocessore (NUMA, UMA, CPU hyper-threading e CPU multicore), secondo il modello SMP con un bilanciamento di carico basato sul domain scheduling. Group scheduling Le politiche di scheduling sono applicate su task individuali. A volte, può essere utile raggruppare task in gruppi e attuare le politiche di scheduling su questi gruppi di task. Per esempio, potrebbe essere utile fornire un equo tempo di CPU per ogni utente sul sistema: in questo caso i gruppi rappresentano gli utenti, e all interno dei gruppi ci sono i task degli utenti. In questo filone di kernel viene introdotta la caratteristica di group scheduling, ossia un meccanismo di aggregazione/partizionamento di task e dei loro eventuali figli in gruppi gerarchici. Un gruppo (chiamato cgroup) associa un set di task a uno o più sotto-sistemi. Un sotto-sistema è un modulo che utilizza la caratteristica di schedulazione di gruppo per raggruppare task in modo opportuno (e.g. in base alla cpu/memoria che i gruppi di task possono utilizzare). Una gerarchia è un insieme di sotto-sistemi e di gruppi di task arrangiati in un albero, in modo tale che ciascun task del sistema sia esattamente in un solo gruppo della gerarchia. Possono esserci più gerarchie e ognuna di queste ha associato una porzione di file system virtuale (di tipo cgroup). L utente può gestire i gruppi tramite il file system virtuale assegnato alla gerarchia. Un esempio di sotto-sistema e quindi di modulo per la schedulazione di gruppo è il cpuset, che permette di assegnare risorse di calcolo (intese come coppia CPU/memoria) a gruppi di task. In questo modo i gruppi di task possono essere eseguiti solo su specifiche CPU e possono utilizzare solo alcuni banchi di memoria. La gestione dei gruppi avviene tramite il file system virtuale di tipo cgroup. Ulteriori dettagli sul cpuset possono essere trovati in [40]. Esistono delle define di compilazione specifiche per abilitare il group scheduling: CONFIG_CGROUP_SCHED abilita lo scheduling di gruppo, permettendo di raggruppare task in gruppi; CONFIG_RT_GROUP_SCHED permette di raggruppare task real-time; CONFIG_FAIR_GROUP_SCHED permette di raggruppare task ordinari. Per abilitare l utilizzo del pseudo filesystem cgroup è necessaria l opzione di compilazione del kernel CONFIG_CGROUPS. Quando si abilità l opzione CONFIG_FAIR_GROUP_SCHED, ogni gruppo di task ordinari, creato sotto il filesystem cgroup, ha il parametro cpu.shares. Questo parametro

63 CAPITOLO 3. SCHEDULING IN LINUX 52 permette di definire la capacità di calcolo del gruppo (in microsecondi), all interno del periodo di esecuzione di CPU (latenza di target). Per maggiori informazioni sulla schedulazione di gruppo si rimanda alla documentazione presente in [37, 39]. Real-time group scheduling Questa caratteristica permette di schedulare gruppi di task real-time POSIX, allocando ad ogni gruppo una porzione di un periodo esecutivo in CPU. Il tempo di CPU allocato a un certo gruppo non può essere utilizzato da altri gruppi di task real-time. Eventuale tempo non allocato a gruppi di task real-time viene utilizzato per eseguire task ordinari. Inoltre, se è stato allocato del tempo di CPU per eseguire task real-time e questo non viene effettivamente utilizzato (e.g. perché i task real-time sono in stato di attesa di qualche evento), allora questo tempo di CPU viene impiegato per eseguire task ordinari. La caratteristica di real-time group scheduling viene configurata da due parametri, sotto il file system virtuale /proc: /proc/sys/kernel/sched_rt_period_us specifica il periodo di scheduling (in microsecondi, default usec. o 1 sec.); /proc/sys/kernel/sched_rt_runtime_us specifica un limite globale sul tempo di CPU che deve essere allocato per tutti i task real-time del sistema all interno del periodo sopra specificato (default 0.95 sec.). Se la caratteristica CONFIG_RT_GROUP_SCHED è abilitata, questo parametro è inteso come il tempo totale di CPU dedicato a tutti i gruppi di task real-time definiti. Notare che i valori di default permettono di riservare del tempo di CPU (50 msec. ogni secondo) per eseguire task ordinari, evitando situazioni in cui task di classe real-time monopolizzano l esecuzione in CPU. La gestione dei gruppi avviene attraverso il filesystem cgroup, specificando il tempo di CPU da allocare ai gruppi tramite /cgroup/<cgroup>/sched_rt_runtime_us e /cgroup/<cgroup>/sched_rt_period_us, con il vincolo che la banda di utilizzo dei figli non può essere maggiore di quella padre. Per maggiori informazioni sulla schedulazione di gruppo di task di classe real-time si rimanda alla documentazione presente in [41]. Session-ID-based group scheduling Nel kernel è stato fatto un commit [42] che abilita il group scheduling sulla base del session-id dei task (CONFIG_SCHED_AUTOGROUP=y nella configurazione del kernel, abilitato di default). L informazione del session-id viene utilizzata per raggruppare task che condividono la stessa sessione. Ad esempio, tutti gli applicativi eseguiti in una shell utente hanno il medesimo session-id, quello della shell. Utilizzando l informazione del session-id è possibile raggruppare task in gruppi e distribuire in modo equo il tempo di esecuzione di CPU a questi gruppi, secondo le politiche di CFS. E possibile variare la capacità di calcolo dei gruppi settando il relativo valore di nice, tramite un parametro posto sotto il file system virtuale /proc: /proc/<pid>/autogroup: visualizza e setta il valore di nice del gruppo di appartenenza del task. Ulteriori considerazioni sul group scheduling basato sul session-id in [43].

64 CAPITOLO 3. SCHEDULING IN LINUX 53 SMP L accesso alle strutture dati condivise da parte di più task è supportato grazie alle tecniche di disabilitazione degli interrupt e tramite l uso di spin lock che evitano problemi di race condition, permettendo un accesso in mutua esclusione. Inoltre gli interrupt vengono utilizzati per forzare attività di scheduling sulle CPU (come il bilanciamento di carico). In un sistema multiprocessore quando un task passa in stato di pronto (e.g. su operazioni di fork(), wake(), exec()) bisogna selezionare una risorsa di calcolo alla quale assegnare il task. Con l introduzione dello scheduling modulare, questa scelta è affidata al modulo di scheduling di appartenenza del task (funzione select_task_rq()). Per quanto riguarda il modulo real-time la logica di scelta della CPU per il nuovo task è la seguente (implementata nella funzione select_task_rq_rt): se il task in esecuzione sulla CPU sulla quale si sta facendo il controllo (d ora in avanti CPU locale) è real-time, allora si cerca di assegnare il nuovo task ad un altra CPU, altrimenti si assegna il task alla CPU locale. L idea è sia di evitare di sovraccaricare le liste di task real-time, sia di prelazionare frequentemente task real-time. In questo modo i task real-time possono beneficiare dei dati in cache, evitando frequenti page fault e cache miss 2. La ricerca di un altra CPU alla quale assegnare il task avviene in questo modo: si costruisce una mappa delle CPU che hanno in esecuzione un task la cui priorità (campo prio) sia inferiore a quella del nuovo task real-time. Tra queste CPU si seleziona quella che più favorisce l affinità del task e la topologia, in questo ordine: se tra queste CPU è presente la CPU che ha eseguito per ultima il task (d ora in avanti CPU-hot), allora si seleziona quest ultima, beneficiando di eventuali dati già presenti in cache/memoria; sulla base della schedulazione a dominio (e a seconda se la caratteristica di affinità sia abilitata per i domini considerati) si seleziona la CPU (tra quelle presenti in mappa) con minore distanza di cache per la CPU-hot (partendo dal gruppo di CPU di appartenenza della CPU-hot); se le precedenti assunzioni non trovano un match, allora si seleziona in modo randomico una tra le CPU contenute nella mappa precedentemente costruita; Il modulo CFS, nella scelta di quale CPU assegnare al nuovo task, utilizza la seguente politica (implementata nella funzione select_task_rq_fair()): basandosi sulla schedulazione a dominio, seleziona la CPU (compatibilmente con il campo cpus_allowed) che ha il minor carico di lavoro (per i confronti si utilizza il campo load.weght delle strutture rq di ciascuna CPU). Come per il filone precedente, anche in questo filone c è il supporto alla tecnologia Hyper-Threading (si veda il paragrafo A.5 per i dettagli). Il kernel Linux vede una CPU Hyperthreading come se fossero n CPU distinte (con n numero di thread supportati). Come per il filone precedente, ogni CPU mantiene il proprio set di task che può eseguire. In un dato istante un task si trova in un solo set di una certa CPU. Questa scelta di progettazione è stata indotta da due aspetti principali: se un task viene eseguito sempre sulla stessa CPU può beneficiare dei propri dati che sono stati caricati in cache durante la sua esecuzione (e nella ram locale in caso di NUMA); l altro aspetto riguarda l accesso 2 Nel kernel è stato fatto un commit [28] che modifica questo comportamento. Se il task real-time passato in stato di pronto ha una priorità più alta rispetto al task real-time running sulla CPU locale, allora la politica è quella di tenere il task di più alta priorità sulla CPU locale, prelazionando il task di più bassa priorità e provando a mandare in esecuzione quest ultimo su altre CPU. Questo permette un esecuzione più rapida di task real-time di alta priorità evitando attese dovute alla migrazione su altre CPU.

65 CAPITOLO 3. SCHEDULING IN LINUX 54 alla lista dei task in stato di pronto che, in caso di un unica lista, porterebbe ad una competizione di accesso alla lista tra le diverse CPU del sistema. Entrambi gli aspetti riducono fortemente i possibili overhead che si avrebbero senza di essi. Con questo tipo di organizzazione è necessario verificare il bilanciamento di carico tra le varie CPU del sistema evitando di avere alcune CPU pesantemente cariche e altre in idle. Un buon bilanciamento di carico inoltre deve tenere in considerazione la topologia dell architettura multiprocessore e questo per sfruttare le caratteristiche delle CPU, migliorando le prestazioni di scheduling. Il bilanciamento di carico in questo filone di kernel si basa sulla nozione di schedulazione a dominio con supporto alle architetture UMA, NUMA, hyper-threading (aka SMT) e CPU multicore (sezione A.5 per i dettagli). Domain scheduling La schedulazione a dominio organizza le CPU del sistema in domini. Un dominio contiene uno o più gruppi; un gruppo contiene una o più CPU. A seconda del tipo di architettura del sistema l organizzazione dei domini può essere gerarchica e quindi un dominio può avere un dominio padre e uno o più domini figli. Il bilanciamento di carico avviene sempre tra gruppi appartenenti allo stesso dominio, partendo dal dominio di più basso livello. Ad esempio, in una architettura NUMA composta da due nodi ognuno dei quali ha quattro CPU (a core singolo) avremmo un primo dominio a due gruppi, dove ogni gruppo rappresenta un nodo (e quindi ingloba le relative quattro CPU) e con due sotto-domini, ognuno dei quali è composto da quattro gruppi, uno per CPU del relativo nodo che rappresenta. In un sistema con architetture UMA con una CPU a doppio core, avremmo un unico dominio a due gruppi, uno per ogni core. Questi due esempi sono rappresentati nella Figura 3.5. Load balancing La procedura di bilanciamento di carico viene attivata in modo periodico (check allo scadere di ogni tick) su ogni CPU del sistema, con una frequenza che dipende da alcune variabili, come il numero di CPU presenti nel sistema, il fattore di carico del dominio interessato e il tempo trascorso dall ultimo bilanciamento di carico effettuato. Nel caso in cui ci siano più risorse di calcolo in idle, allora si cerca di affidare la procedura di bilanciamento di carico ad una di queste risorse. La nomina di questa risorsa di calcolo avviene in questo modo: si cerca di scegliere una risorsa in idle che appartiene a un gruppo in semi-idle (ossia con almeno una CPU/core in idle e almeno una CPU/core non in idle ) e con la caratteristica del powersaving abilitata. L idea è di evitare di affidare il bilanciamento di carico a CPU che appartengono a gruppi completamente in idle, essendo queste più indicate ad eseguire processi (che molto probabilmente saranno a loro assegnati dalla procedura di bilanciamento di carico) piuttosto che a introdurre overhead. Se, al momento della verifica, non c è almeno un gruppo in semi-idle, allora ciascuna CPU in idle effettua in modo autonomo la procedura di bilanciamento di carico (allo stesso modo delle CPU non in idle). La procedura di bilanciamento di carico ( rebalance_domains()) si attua per ogni dominio di appartenenza della CPU locale, e si sviluppa nei seguenti punti ( load_balance()): tra tutti i gruppi del dominio si cerca quello più occupato e si stabilisce quale sia il carico di lavoro (variabile imbalance) da spostare sulla CPU locale affinché sia ristabilito il bilanciamento; tra tutte le risorse di calcolo appartenenti al gruppo più occupato si stabilisce quale sia quella maggiormente carica; dalla CPU maggiormente carica si tenta di spostare task ordinari (il numero dipende dal parametro imbalance) verso la CPU locale;

66 CAPITOLO 3. SCHEDULING IN LINUX 55 nel caso in cui fallisca qualche spostamento di task da una lista all altra, allora viene attivato il task di migrazione che va alla ricerca di una nuova CPU e sposta il task incriminato su di essa; La logica di scelta dei task che devono essere migrati da una lista di CPU ad un altra si basa sul peso (contenuto in load.weight) dei task stessi: in particolare se questo valore diviso 2 risulta maggiore del valore di imbalance, allora il task risulta idoneo allo spostamento. Inoltre il task non può essere migrato se: il task è già in esecuzione; non può migrare sulla CPU locale (per via di cpus_allowed) oppure è cache-hot sull attuale CPU. Se nessuna delle due precedenti condizione non è soddisfatta allora il task viene migrato se è considerato cache-cold (ossia non è stato recentemente eseguito in CPU) oppure ci sono stati diversi precedenti tentativi di trasferire task che non sono andati a buon fine. Il bilanciamento di carico del sistema (funzione load_balance(), sopra descritta) viene attivato anche quando una risorsa di calcolo va in idle. Chiamate di sistema In questo filone di kernel le principali system call attinenti allo scheduling di processi sono le seguenti: sched_setscheduler(): che permette di modificare la classe di priorità (policy) e la priorità dedicata ai task real-time (rt_priority); sys_sched_yield(): permette a un task di rilasciare la CPU in modo spontaneo; sys_setpriority(): permette di assegnare il valore di priorità statica a un singolo task ordinario, fornendo il suo PID, oppure a un gruppo di task sulla base dell identificativo di proprietario o di gruppo; sys_nice(): permette di modificare la priorità statica di un processo ordinario; sys_sched_setaffinity(): permette di valorizzare il campo cpus_allowed del task, determinando su quali CPU il task può essere eseguito. Considerazioni Nel filone di kernel sono state introdotte diverse novità per quanto riguarda le attività di scheduling di processi. La caratteristica dello scheduling modulare permette di implementare nuove politiche di scheduling in moduli, che sono poi azionati dallo scheduler, senza che ci sia la necessità di riorganizzare il core dello scheduler. Le due politiche di scheduling in questo filone, real-time e CFS, scalano bene rispetto al numero di processi del sistema: il modulo real-time sceglie il prossimo task da mandare in esecuzione in un tempo di O(1), mentre il modulo CFS lo fa, nel caso peggiore, in O(log n). Inoltre l algoritmo scala bene rispetto al numero di risorse di calcolo nel sistema: ogni risorsa di calcolo ha la propria lista di processi che può mandare in esecuzione e quindi l accesso alla lista dei processi da schedulare non è in competizione con altre risorse di calcolo. La caratteristica della schedulazione di gruppo permette l aggregazione/partizionamento di task e dei loro eventuali figli in gruppi gerarchici, e questo può essere utile per distribuire la potenza di calcolo del sistema a gruppi di task del sistema e non più a singoli task. Inoltre è possibile garantire all interno di un periodo del tempo di esecuzione sia per i task real-time sia per quelli ordinari. In questo modo, per esempio, è possibile mandare in

67 CAPITOLO 3. SCHEDULING IN LINUX 56 esecuzione task ordinari anche in presenza di task real-time, che normalmente avrebbero la precedenza di esecuzione in CPU. Per quanto riguarda il supporto al real-time non ci sono grosse differenze rispetto al filone di kernel precedente: benché il kernel sia preemptive diversi blocchi di codice kernel sono ancora eseguiti in interrupt context o comunque senza la possibilità di essere prelazionati (e.g. disabilitando gli interrupt), inoltre lo scheduler non tiene in considerazione i vincoli temporali di esecuzione richiesti da alcune applicazioni real-time. Ulteriori dettagli sullo scheduling dei processi nel filone di kernel Linux possono essere trovati nel capitolo 4 del libro di Robert Love [23], nella documentazione nei sorgenti del kernel Linux in [37] e visionando i sorgenti Linux [24], in particolare: kernel/sched.c: core dello scheduler, contiene le principali funzioni per lo scheduling dei processi; kernel/sys.c: contiene la maggior parte delle system call; include/linux/sched.h: contiene le principali strutture dati utilizzate per operazioni di scheduling. kernel/sched_fair.c: implementa il modulo CFS; kernel/sched_rt.c: implementa il modulo real-time (POSIX). L annuncio della tecnica di scheduling modulare e del modulo di scheduling CFS è disponibile in [34].

68 CAPITOLO 3. SCHEDULING IN LINUX Supporto al real-time In Linux l attività di adattamento del kernel alle necessità delle applicazioni real-time è fervente da anni. Esistono diverse proposte di supporto al real-time per il kernel Linux. Tuttavia nessuna di queste, nel momento in cui scrivo, è stata incorporata nell albero dei sorgenti ufficiali. Un sistema operativo real-time (RTOS) deve supportare le seguenti caratteristiche fondamentali: una politica di scheduling che tenga conto dei vincoli temporali di esecuzione dei task e un kernel il più possibile prelazionabile. Per entrambi questi aspetti sono state proposte diverse soluzioni per il kernel Linux: quelle che hanno riscosso un maggior interesse sono SCHED_DEADLINE [65, 66, 67], che implementa l algoritmo di scheduling real-time EDF (sezione ) e RT-Preempt [62], che aumenta notevolmente la caratteristica di preemption nel kernel. Ulteriori informazioni sul supporto al real-time in Linux sono disponibili in [47] RT-Preempt La proposta RT-Preempt [62], sviluppata principalmente da Ingo Molnar e Thomas Gleixner, soddisfa uno dei principali requisiti affinché un sistema operativo possa essere considerato un sistema operativo real-time (RTOS): avere un kernel molto prelazionabile. Prelazione nel filone ufficiale del kernel Linux Tradizionalmente il kernel Linux permette di prelazionare un processo solo in certe circostanze: quando il codice in kernel-space si blocca su un mutex o rilascia in modo esplicito l esecuzione a un altro task; quando si torna in user-space da una chiamata di sistema o da una gestione di un interrupt; quando l esecuzione è in user-space. Nel filone di kernel ufficiale, se si sta eseguendo codice in spazio kernel e si presenta un evento che richiede l immediata esecuzione di un altro task (e.g. un task real-time con una deadline di esecuzione), questo task potrà andare in esecuzione non appena il kernel rilascia il controllo. Nei casi peggiori, questa latenza di esecuzione può essere nell ordine di centinaia di millisecondi o più. Nel filone di kernel 2.6.x sono state introdotte due caratteristiche per ridurre queste latenze di prelazione: CONFIG_PREEMPT_VOLUNTARY e CONFIG_PREEMPT. L opzione di compilazione CONFIG_PREEMPT_VOLUNTARY introduce dei check nelle parti di codice in kernel che sono causa di lunghe latenze, permettendo al kernel di rilasciare in modo volontario l esecuzione a task prioritari. Tuttavia questa caratteristica permette di ridurre le latenze dall ordine di secondi o più in latenze nell ordine di centinai di millisecondi e di fatto non elimina queste latenze. Inoltre bisogna tenere in considerazione che i gestori degli interrupt sono eseguiti in un conteso interrompibile (cosiddetto contesto di interrupt). L altra caratteristica, CONFIG_PREEMPT, permette di prelazionare tutto il codice in kernel space al di fuori dei gestori di interrupt e dalle sezioni di codice protette da spinlock. Rispetto alla prima è più aggressiva e, mentre per la prima la prelazione avviene in modo volontario (tramite l invocazione di una chiamata di sistema), per la seconda questa prelazione può essere anche non volontaria. In questo caso le latenze si riducono nell ordine di unità di millisecondi. Tuttavia anche in questo caso possiamo avere ancora delle latenze maggiori per la gestione degli interrupt e per il codice protetto da spinlock. L opzione CONFIG_PREEMPT_VOLUNTARY rispetto a CONFIG_PREEMPT ha un minor impatto sull efficienza del sistema in termini di throughput.

69 CAPITOLO 3. SCHEDULING IN LINUX 58 Prelazione con RT-Preempt La patch RT-Preempt aumenta notevolmente la caratteristica di preemption nel kernel Linux, apportando delle modifiche sostanziali in punti critici del kernel: alcune primitive di lock in kernel-space (e.g spinlock) sono state re-implementate tramite mutex-rt, prelazionabili; sezioni di codice critiche protette ad esempio da spinlock_t e rwlock_r sono ora prelazionabili. E ancora possibile rendere una sezione di codice di kernel non prelazionabile tramite raw_spinlock_t; quando si utilizzano semafori e spinlock (che possono mandare in attesa i processi), per risolvere problemi di inversione di priorità è stata adottata la tecnica a priorità ereditata (sezione 1.4); i gestori di interrupt sono stati convertiti in thread kernel prelazionabili, eseguiti in un contesto di processo (prelazionabile) e non più nel contesto interrupt (non prelazionabile); ad ogni modo è ancora possibile registrare un IRQ in contesto di interrupt; le vecchie API Linux per i timer sono state convertite in infrastrutture separate per timer kernel ad alta risoluzione, più uno dedicato ai timeout, portando in spazio utente timer POSIX ad alta risoluzione. Considerazioni Un kernel molto prelazionabile reagisce rapidamente ai cambi di priorità dei task. Questa è una caratteristica fondamentale per un sistema operativo real-time. Tuttavia nel caso di workload di task real-time con deadline di esecuzione, è indispensabile schedulare questi task con un algoritmo di scheduling che tenga conto dei relativi vincoli temporali di esecuzione dei task, garantendo il rispetto delle deadline. Inoltre in alcuni workload può essere proficuo deadicare una CPU ad eseguire solo i processi real-time, mentre le altre eseguono gli altri processi, e parte del kernel, come i gestori degli interrupt. In questo modo si evita che processi di più alta priorità, come i gestori di interrupt, possano interrompere l esecuzione dei task real-time. Ulteriori informazioni su come dedicare l esecuzione dei gestori di interrupt e dei processi su determinate CPU possono essere trovate in [46]. Per una lista di architetture supportate e testate con la patch RT-Preempt, e su come configurare e compilare un kernel con il relativo supporto, e per altre utili informazioni si rimanda ai riferimenti [62, 63] Sched-deadline Le politiche di scheduling presenti nel kernel Linux (mentre scrivo l ultima versione ufficiale è la 3.1) non garantiscono il rispetto di vincoli temporali di esecuzione richieste dalle applicazioni real-time, poiché Linux non è stato pensato per essere un sistema operativo real-time (RTOS). sched_deadline è un nuovo modulo per lo scheduler Linux che implementa l algoritmo di scheduling real-time Earliest Deadline First (EDF) (rif ), con l aggiunta di un meccanismo, noto come Constant Bandwidth Server (CBS) [14], che garantisce l isolamento temporale dei task. Lo sviluppo è mantenuto da un gruppo di società (tra cui Ericsson Research, Evidence srl, AKAtech) ed è inserito nel contesto del progetto europeo ACTORS [69]. La home page del progetto di SCHED_DEADLINE è in [68] e i principali sviluppatori sono Dario Faggioli e Michael Trimarchi. Gli annunci delle versioni di SCHED_DEADLINE sono disponibili in [65, 66, 67]. Questa classe di scheduling è stata anche discussa nel Real Time Linux Workshops 2009 [45, 64].

70 CAPITOLO 3. SCHEDULING IN LINUX 59 Le prossime sezioni fanno riferimento alla versione 3 di SCHED_DEADLINE (rilasciata nel Dicembre 2010). Motivazioni Le politiche di scheduling che ritroviamo nel kernel ufficiale, implementate nei moduli sched_fair e sched_rt, funzionano egregiamente nel loro contesto applicativo. Ad ogni modo le decisioni di scheduling di entrambi i moduli non si basano su vincoli temporali di esecuzione dei task. Benché sia possibile con sched_fair (CFS) assegnare una parte del tempo di esecuzione di CPU stabilito (latenza di target) a un task ordinario tramite la caratteristica del group scheduling, non ci sono garanzie che il vincolo sia rispettato, inoltre la latenza di target è fissa per tutti i task ordinari e viene modulata dal numero di task ordinari che competono per l esecuzione. Per i task real-time POSIX, sched_rt, grazie alla caratteristica del group scheduling, possiamo solo specificare una banda di utilizzo globale per tutti i task real-time Posix, con il vincolo che un sotto-gruppo deve avere una banda minore o al più uguale del suo progenitore. Inoltre con le due politiche sched_fair e sched_rt il lasso di tempo che intercorre tra due burst di esecuzione consecutivi di un task non è deterministico e non può essere stabilito a priori, in quanto dipende fortemente dal numero di task che competono per l esecuzione in CPU. Il rispetto di questi vincoli è fondamentale nelle applicazioni real-time per ottenere risultati corretti. Senza uno scheduling che tenga conto di questi vincoli, di fatto, non è possibile fare uno studio di fattibilità del sistema, e non ci sono garanzie che i vincoli temporali di esecuzione dei task siano rispettati in ogni circostanza. Classe di scheduling La classe di scheduling sched_deadline è stata progettata come modulo di scheduling, lasciando inalterate le caratteristiche di funzionamento degli altri moduli, sched_fair, sched_rt e sched_idle. Per quanto riguarda la priorità esecutiva del modulo sched_deadline, è stato deciso di dare un priorità maggiore rispetto ai moduli sched_fair e sched_rt. Questo significa che un task sched_deadline in stato di pronto vincerà la competizione per l acquisizione della risorsa di calcolo rispetto a un task sched_fair, o sched_rt. Questa scelta è stata motivata dalla seguenti ragioni: per rispettare i vincoli temporali dei task sched_deadline è importante mandarli in esecuzione appena passano in stato di pronto; tramite una gestione della capacità di calcolo dei task sched_deadline è possibile definire, all interno di una finestra temporale, il tempo di esecuzione globale da allocare a questi task. In questo modo è possibile riservare del tempo di esecuzione (all interno della finestra temporale) per task di altre classi. Strutture dati Nella struttura informativa di ogni task (task_struct) sono state aggiunte le informazioni per i task sched_deadline, in particolare: rb_node: nodo associato al task dell albero red-black; nr_cpus_allowed: maschera di CPU sulle quali il task può essere mandato in esecuzione; dl_runtime: massimo tempo di esecuzione per ogni istanza del task (budget, in us); dl_deadline: deadline relativa di ogni istanza del task (in us); dl_period: periodo di tempo tra due istanze del task (in us); dl_bw: rapporto tra dl_runtime e dl_period (ossia banda di esecuzione del task); runtime: tempo rimanente per l istanza corrente del task (in us);

71 CAPITOLO 3. SCHEDULING IN LINUX 60 exec_start: tempo di sistema in cui è stato aggiornato il runtime dell istanza del task (in us); deadline: deadline assoluta per l istanza corrente del task (in us); flags: flags di scheduling; dl_throttled: indica se il task ha esaurito il suo budget di esecuzione; dl_new: indica se è stata attivata una nuova istanza del task; dl_timer: timer utilizzato per attivare nuove istanze del task; stats: informazioni statistiche sul task; Ogni risorsa di calcolo mantiene il proprio set di task da eseguire in una struttura dati dedicata (di tipo rq). Per la gestione della lista dei task sched_deadline viene utilizzato un albero red-black, ordinato in base al campo deadline associato ai task. Alla struttura rq sono state aggiunte informazioni per la gestione dei task sched_deadline, in particolare è stata aggiunta la struttura dati dl_rq dl che mantiene l albero dei task sched_deadline. I campi principali della struttura dati dl_rq sono i seguenti: rb_root: radice dell albero red-black; rb_leftmost: punta al nodo più a sinistra dell albero; dl_nr_running: numero di task deadline (in stato di pronto) mantenuti nell albero; earliest: contiene la deadline del task in esecuzione e del prossimo task che andrà in esecuzione (usate per decisioni di migrazione di task); pushable_dl_tasks_root: radice dell albero red-black che mantiene i task che possono essere migrati; pushable_dl_tasks_leftmost: punta al nodo più a sinistra dell albero che mantiene i task che possono essere migrati; L organizzazione delle strutture dati di scheduling nel kernel Linux con sched_deadline viene rappresentata nella Figura 3.7. Politica e algoritmo Un task di classe sched_deadline ottiene una fase di computazione in CPU (chiamata istanza o anche burst di esecuzione), che può essere attivata in modo periodico, sporadico, o aperiodico, a seconda dell implementazione del task stesso. La durata (massima) di computazione dell istanza di un task è contenuta nel campo dl_runtime della struttura informativa del task, mentre il lasso di tempo entro il quale questa computazione deve avvenire, chiamata deadline relativa, è contenuto nel campo dl_deadline. La deadline assoluta, campo deadline, viene calcolata di volta in volta ed è pari al tempo assoluto del sistema nel quale viene attivata l istanza del task, sommato alla sua deadline relativa. Il periodo di tempo entro il quale può essere attivata una nuova istanza del task è contenuto nel campo dl_period. Per attivazione di un istanza del task si intende il primo passaggio in stato di pronto del task all interno del periodo. La banda di utilizzo dei task, campo dl_bw, è determinata dal rapporto tra il budget assegnato e il relativo periodo, ossia dl_runtime/dl_period. In questa implementazione il periodo dl_period coincide con la deadline relativa dl_deadline. Questo significa che il budget di ogni istanza del task (dl_runtime) deve essere consumato all interno di tutto il periodo (dl_period). Questa assunzione ci permette di utilizzare un semplice test di ammissione per validare il workload real-time, come vedremo in seguito. In una decisione di scheduling l algoritmo EDF seleziona il task da mandare in esecuzione sulla base dei valori delle deadline assolute (campo deadline) dei task: viene mandato in esecuzione il task con il minor valore di deadline, ossia la deadline più prossima alla scadenza.

72 CAPITOLO 3. SCHEDULING IN LINUX 61 Figura 3.7: Organizzazione dello scheduling nel kernel Linux con sched_deadline

73 CAPITOLO 3. SCHEDULING IN LINUX 62 La tecnica dell isolamento temporale, (CBS) [14], prevede un controllo sul tempo di esecuzione dei task real-time: un task non può rimanere in esecuzione oltre il suo budget dl_runtime. Questo controllo viene implementato in due modi: tramite l uso di timer ad alta risoluzione (hrtimer), oppure tramite una gestione basata sul tick di clock del sistema. L utilizzo dell uno esclude l altro. Gestione della capacità di calcolo e test di ammissione Per cercare di rispettare i vincoli temporali di esecuzione, e quindi effettuare uno scheduling con deadline, è importate mandare in esecuzione i task deadline prima possibile. Inoltre è importante poter verificare la schedulabilità del workload real-time, ossia utilizzare un controllo di ammissione per i task real-time da sottomettere. Senza questi due meccanismi il rispetto dei vincoli temporali esecutivi dei task non potrebbero essere garantiti. La gestione della capacità esecutiva dei task deadline è regolata dai seguenti parametri di sistema, posti sotto il file system virtuale /proc: /proc/sys/kernel/sched_dl_runtime_us (default a usec o 0.5 sec); /proc/sys/kernel/sched_dl_period_us (default a usec o 1 sec); Questi parametri restituiscono (se letti) o settano (se scritti) il budget totale di esecuzione per i task deadline all interno del relativo periodo, per ogni CPU del sistema. I valori di default permettono di riservare un tempo di CPU pari a 500 millisecondi ogni secondo per eseguire task appartenente alle altri classi utente. Questa gestione della capacità di calcolo per i task di classe sched_deadline, insieme all assunzione che l informazione di periodo e di deadline dei task coincidono, ci permettono di definire il seguente controllo di ammissione : n dl_bw <= C (sched_dl_runtime_us/sched_dl_period_us) i=0 con C numero di CPU del dominio e n numero di task di classe deadline in stato di pronto. Questo controllo garantisce una capacità di calcolo sufficiente per rispettare i vincoli temporali di esecuzione del workload real-time. Ogni task deadline sottomesso nel sistema, viene validato dal controllo di ammissione: se il controllo fallisce, il task non può essere inserito. Settando il parametro /proc/sys/kernel/sched_dl_runtime_us a 1 si disabilita il controllo di ammissione. In questo caso però non ci sono garanzie che ci sia una capacità di calcolo sufficiente per rispettare le deadline esecutive dei task sottomessi. A differenza della gestione della capacità di calcolo per i task real-time Posix (sezione per i dettagli), questa gestione non prevede l utilizzo di un timer e di un contatore di tick globale per limitare la capacità esecutiva dei task in modo periodico. Infatti, l implementazione della classe sched_deadline prevede già un meccanismo di limitazione periodica della capacità esecutiva per ogni task (isolamento temporale). Supporto SMP La schedulazione su piattaforme multiprocessori del kernel Linux (a partire dal filone di kernel 2.6.x), conosciuta come runqueue distribuita, prevede che ogni risorsa di calcolo abbia la propria lista di processi da eseguire (in realtà una lista per i processi sched_rt e una per quelli sched_fair); all occorrenza, i task possono migrare da una lista all altra. Per la gestione dei task della classe sched_deadline è stata prevista un ulteriore lista di task per ogni CPU. La lista è implementata tramite un albero red-black 3.1.5, come per la lista dei task ordinari. La politica di migrazione per i task sched_deadline tra runqueue di diverse risorse di calcolo è la seguente: su un sistema di m CPU si tenta di

74 CAPITOLO 3. SCHEDULING IN LINUX 63 mandare in esecuzione i primi m task la cui deadline è più prossima alla scadenza, tenendo sempre in considerazione l eventuale affinità dei task sulle CPU. Inoltre è previsto anche un bilanciamento di carico statico, quando nuovi task real-time entrano nel workload. Chiamate di sistema Alcune syscall permettono la gestione in userspace dei task della classe sched_deadline: sched_setscheduler_ex()permette di modificare i vincoli temporali di esecuzione dei task (come dl_runtime e dl_period) sched_yield() è stata modificata per dare la possibilità ai task real-time periodici di attendere la fine della loro istanza corrente Risultati sperimentali Per verificare l implementazione di SCHED_DEADLINE sono stati condotti alcuni test. In particolare si vuole verificare come viene schedulato un task in while(1), nelle diverse classi di scheduling. L ambiente di test è composto da un architettura i386 con monoprocessore Intel 2.0Ghz e da una Debian Il kernel, integrato con il supporto di SCHED_DEADLINE v3 [70], è stato compilato con le seguenti opzioni: CONFIG_HIGH_RES_TIMERS, CONFIG_PREEMPT, CONFIG_HZ_1000, CONFIG_SCHED_DEBUG, CONFIG_SCHEDSTATS e CONFIG_SCHED_TRACER. Per tracciare gli switch di contesto dei task che competono per l esecuzione in CPU è stata utilizzata l infrastruttura di Linux ftrace con il tracer sched_switch (ulteriori dettagli in [30]). I dati raccolti con ftrace sono stati convertiti in formato VCD tramite un apposito tool [31], e visualizzati in modalità grafica con GtkWave. Per mandare in esecuzione i task è stato utilizzato il tool schedtool. I test effettuati sono i seguenti: esecuzione del task con la classe sched_normal (CFS); esecuzione del task con la classe sched_fifo; esecuzione del task con la classe sched_deadline; Nel primo test il task in while(1) è stato mandato in esecuzione con la classe sched_normal: in questo caso le politiche di scheduling sono quelle del modulo CFS. Nella Figura 3.8 è possibile vedere gli eventi di scheduling durante l esecuzione del task test, in una finestra temporale di 100 millisecondi. Il colore giallo significa che il task non è in stato di pronto, il verde che è in esecuzione in CPU e il rosso significa che il task è in stato di pronto, in attesa di essere mandato in esecuzione. Come possiamo notare, quando ci sono più task della classe sched_normal che competono per l esecuzione (e.g. i task Xorg, test e gnome-terminal), la politica di CFS distribuisce equamente il tempo di esecuzione in CPU. I parametri di tuning di CFS sono quelli di default, ossia 6 millisecondi per sched_latency_ns (finestra temporale utilizzata per distribuire il tempo di esecuzione ai task) e 750 microsecondi per sched_min_granularity_ns (tempo minimo di esecuzione di un task CFS). Notare che il task test, essendo un while(1), e quindi sempre in stato di pronto, è il task che utilizza maggiormente la CPU. Il secondo test vole verificare le politiche di scheduling del modulo real-time Posix. Nella Figura 3.9 è possibile vedere gli eventi di scheduling durante l esecuzione del task test_fifo in una finestra temporale di 2 secondi. I parametri della gestione della capacità di calcolo per i task real-time, sched_rt_period_us e sched_rt_runtime_us sono settati rispettivamente a 500 e 100 millisecondi. Questo significa che il sistema garantisce una banda di esecuzione per i task real-time di (almeno)

75 CAPITOLO 3. SCHEDULING IN LINUX 64 Figura 3.8: Esecuzione di while(1) con la classe di scheduling sched_normal nel kernel Linux Figura 3.9: Esecuzione di while(1) con la classe di scheduling sched_fifo nel kernel Linux millisecondi ogni 500. Per verificarlo, oltre al task real-time test_fifo ne abbiamo eseguito un altro, test_cfs, sempre un while(1) ma di classe sched_normal. Come possiamo notare nella Figura 3.9 il task test_fifo viene mandato in esecuzione per 100 millisecondi ogni 500; il restante lasso di tempo (400 millisecondi) viene concesso ai task

76 CAPITOLO 3. SCHEDULING IN LINUX 65 di altre classi (come test_cfs). Il terzo test vuole verificare le politiche di scheduling del modulo SCHED_DEADLINE. Nella Figura 3.10 è possibile vedere gli eventi di scheduling durante l esecuzione del task test_deadline in una finestra temporale di 1 secondo. Figura 3.10: Esecuzione di while(1) con la classe di scheduling sched_deadline nel kernel Linux I parametri della gestione della capacità di calcolo per i task real-time, sched_rt_period_us e sched_rt_runtime_us sono settati rispettivamente a 500 e 100 millisecondi, mentre quelli per i task della classe sched_deadline, sched_dl_period_us e sched_dl_runtime_us sono settati rispettivamente a 500 e 100 millisecondi. Il sistema garantisce una banda di esecuzione per i task deadline di (almeno) 100 millisecondi ogni 500. Il task test_deadline è stato mandato in esecuzione con i seguenti parametri: dl_runtime pari a 50 millisecondi; dl_deadline (e dl_period) pari a 250 millisecondi. In questo modo si vuole concedere al task un budget di esecuzione di 50 millisecondi ogni 250. Per verificare il rispetto dei vincoli di esecuzione del task in un workload con alto carico, sono stati mandati in esecuzione altri due task in while(1) (il while(1) è un ottimo modo per simulare un workload con alto carico), uno di classe sched_normal e l altro di classe sched_fifo. Come possiamo notare nella Figura 3.10 il task test_deadline viene mandato in esecuzione per 50 millisecondi ogni 250, garantendo il rispetto dei vincoli temporali di esecuzione, mentre il restante lasso di tempo (200 millisecondi) viene concesso ai task di altre classi (come test_fifo e test_cfs). Considerazioni Rispetto alle altre proposte di supporto al real-time nel kernel Linux, questo lavoro è allineato al filone di kernel ufficiale. L uso della progettazione di scheduling modulare permette di mantenere inalterato il funzionamento delle classi di scheduling esistenti. Non ci sono assunzioni sulle caratteristiche dei task, che possono essere periodici, sporadici o aperiodici. Un altra caratteristica importante riguarda l isolamento temporale tra i task gestiti: i vincoli di esecuzione temporali assegnati ai task non sono influenzati dal numero di task sul sistema o dal comportamento dei task stessi; un task che richiede maggiore tempo esecutivo non può monopolizzare il processore. Altri risultati sperimentali e ulteriori informazioni su sched_deadline possono essere visio-

77 CAPITOLO 3. SCHEDULING IN LINUX 66 nati in [71, 72, 64, 75] e visionando i sorgenti Linux [70] (con la patch di SCHED_DEADLINE), in particolare: kernel/sched.c: contiene la maggior parte delle funzioni per lo scheduling dei processi; kernel/sys.c: contiene la maggior parte delle chiamate di sistema; include/linux/sched.h: contiene la maggior parte delle strutture dati utilizzate per operazioni di scheduling. kernel/sched_dl.c: implementa il modulo sched_deadline Altre proposte per Linux real-time Alcune società, come Montavista [50], Windriver [48] e Timesys [49], mantengono un propria versione del kernel Linux con supporto al real-time. Inoltre la comunità opensource ha fornito diverse proposte di miglioramento del supporto al real-time in Linux. Nessuna di queste però è stata, ad oggi, integrata nel kernel Linux ufficiale e, con l evoluzione dello stesso, la maggior parte di queste estensioni sono diventate obsolete. Segue una panoramica delle estensioni principali. Ulteriori informazioni ed articoli sul real-time in Linux possono essere trovati in [44] OCERA Uno scheduler real-time per la versione del kernel Linux 2.4 è stato sviluppato all interno del progetto Europeo OCERA, ed è disponibile come codice sorgente [51, 52, 53]. Per minimizzare le modifiche al codice del kernel, lo scheduler real-time è stato sviluppato come una piccola patch associata ad un modulo del kernel. La patch esporta al modulo gli eventi di scheduling rilevanti. L approccio è semplice e flessibile ma i punti nel kernel che sono stati modificati per esportare gli eventi di scheduling sono critici, rendendo un compito arduo portare la patch nelle nuove versioni del kernel Linux AQuoSA I risultati di OCERA danno alla luce l architettura software AQuoSA [54]. Sostanzialmente si tratta del porting di OCERA nel kernel 2.6, con l aggiunta di una libreria per portare in spazio utente gli eventi di scheduling. Non supporta piattaforme multiprocessore ne il meccanismo a classi di scheduling [34] Frescor Un framework real-time basato sul kernel 2.6 è stato proposto dal progetto Europeo Frescor [55]. Si basa su AQuoSA, con l aggiunta di un middleware complesso per gestire le prestazioni del sistema con supporto alla Quality of Service (QoS). Soffre delle stesse mancanze di AQuoSA LITMUS-RT LITMUS-RT [56] è un estensione soft real-time per il kernel Linux con un focus sullo scheduling real-time multiprocessore e sulla sincronizzazione, con l obiettivo di fornire una piattaforma sperimentale per i sistemi di ricerca basati sul real-time. Mette a disposizione un meccanismo di scheduler modulare a plugin, con la possibilità di incapsulare diversi algoritmi di scheduling real-time come EDF e PFAIR.

78 CAPITOLO 3. SCHEDULING IN LINUX RTLinux, RTAI, Xenomai L approccio dell astrazione degli interrupt [57] consiste nell avere uno strato di astrazione hardware virtuale sotto il kernel Linux, avendo come risultato una sorta di sistema RTOS multithread con Linux standard e tutti i suoi processi in esecuzione con la minima priorità. Questo approccio è stato implementato con successo in diversi framework, tra cui i più noti sono RTLinux [59], RTAI [58] e Xenomai [60].

79 CAPITOLO 3. SCHEDULING IN LINUX Altri algoritmi di scheduling proposti dalla comunità Il kernel Linux ha ricevuto molte proposte di miglioramento per quanto riguarda le attività di scheduling dei processi. Alcune di queste sono state importate ufficialmente nel kernel Linux (come lo scheduling modulare e CFS), altre sono state adottate per progetti specifici (come BFS per Android), e tutte hanno ricevuto commenti e critiche da parte degli sviluppatori. Di seguito riportiamo tre delle principali proposte di modifica dello scheduler di Linux che non sono state incorporate nel kernel ufficiale: Staircase e RSDL (Rotating Staircase Deadline Scheduler) di Con Kolivas e BFS (Brain Fuck Scheduler ) di Ingo Molnar. Ulteriori proposte di scheduling di processi nel kernel Linux possono essere trovate in LWN.net nella pagina degli articoli del kernel Linux (dal 2004 in poi) disponibile in [25] Staircase Questa patch per le attività di scheduling è stata proposta [76] da Con Kolivas per il filone di kernel con l intento di migliorare l equità di esecuzione in CPU per i task ordinari, concendendo allo stesso tempo una maggiore priorità esecutiva alle applicazioni interattive. Politica e algoritmo Staircase modifica la politica di scheduling della classe sched_normal e i meccanismi per l interattività: la tecnica delle lista active ed expired viene sostituita da una singola runqueue a priorità multilivello discendente (sezione per i dettagli), dove in ogni lista i task sono schedulati secondo l algoritmo round robin. Il livello di priorità iniziale dei task è determinato dalla relativa priorità di base; quando un task esaurisce il proprio time slice, viene declassato di priorità e spostato nella relativa lista. All ultimo livello, quando un task esaurisce il suo time slice, il task viene spostato nella lista di priorità che precede la lista di priorità da cui era partito il task (quindi priorità di base meno n, dove n è il numero di volte che il task ha raggiunto l ultimo livello); per ogni livello iniziale (dalla seconda volta in poi) al task viene concesso una maggiore time slice, rispetto a quella di base, pari al valore della time slice di base moltiplicata per il numero di volte che il task ha raggiunto l ultimo livello. La time slice per i livelli successivi (quando il task verrà declassato) sarà pari alla sua time slice di base. Considerazioni Questa nuova politica per i task ordinari della classe sched_normal da un lato evita che un task di alta priorità possa rallentare la schedulazione di task di più bassa priorità (grazie al meccanismo del declassamento) e contemporaneamente concede a task di più alta priorità un maggior tempo esecutivo. Inoltre, poiché i task interattivi tendono a frequenti operazioni di I/O (e quindi la relativa time slice viene spesa lentamente), questi tendono a rimanere a livelli di priorità superiori rispetto ai task CPU bound, che invece spendono la relativa time slice velocemente. In questo modo i task interattivi tendono ad avere una priorità esecutiva maggiore rispetto ai task CPU bound. L annuncio di questa patch è disponibile in [76], mentre in [77] si possono trovare ulteriori dettagli tecnici. Alcuni utenti hanno segnalato delle problematiche durante l utilizzo della patch Staircase con workload interattivi, come nella segnalazione di Ingo Molnar [78] Rotating Staircase Deadline Scheduler (RSDL) Prendendo spunto dal suo lavoro precedente Staircase, Con Kolivas propone una nuova patch che modifica le politiche di scheduling per i task ordinari della classe sched_normal nel filone di kernel [79]. Questa patch mantiene il meccanismo della runqueue

80 CAPITOLO 3. SCHEDULING IN LINUX 69 a priorità multilivello discendente già presente in Staircase con l aggiunta di una gestione di epoche minori e maggiori (rotating e deadline). L obiettivo è di mantenere a tutti i costi equità di calcolo sulla base dei valori di nice, avere delle latenze ben definite e fornire una buona interattività, eliminando tutte le relative euristiche presenti nel kernel ufficiale. Politica e algoritmo Ogni CPU mantiene due array (active ed expired) di liste di processi, una lista per ogni livello di priorità. In ogni lista i task sono schedulati in round robin. Ogni task ha associato una priorità iniziale, determinata dalla priorità statica del task e dal valore di nice, e una quota di esecuzione (time slice) che può utilizzare nei vari livelli di priorità. Ogni livello di priorità ha associato una quota esecutiva. Quando un task esaurisce il suo time slice (la sua quota) viene spostato nel livello di priorità appena inferiore, quindi la sua quota (e time slice) viene ripristinata. Quando un livello esaurisce la propria quota, tutti gli eventuali task rimanenti in quel livello (solo quelli in stato di pronto) sono spostati nel livello di priorità appena inferiore. Grazie a questo meccanismo (chiamato rotazione minore) i task in attesa nei livelli di priorità bassi attendono un periodo di tempo ben definito prima che gli altri processi si ritrovino nei loro stessi livelli di priorità. In questo modo la massima latenza che ogni processo deve attendere prima di essere mandato in esecuzione è ben definita e può essere calcolata a priori. Quando un task esaurisce la sua quota di tempo nel livello di priorità più basso, il task viene spostato nel secondo array (expired) di liste di priorità, nel relativo livello di priorità iniziale. I task posti nell array expired non vengono eseguiti. Quando non ci sono più task nell array active avviene uno switch tra i due array (chiamato rotazione maggiore) con un ripristino delle quote esecutive dei vari livelli e dei task stessi. Considerazioni La tecnica del declassamento di priorità, ereditata da Staircase, evita che task di alta priorità monopolizzino la CPU, pur concedendo loro un maggiore utilizzo di CPU. Rispetto a Staircase l introduzione di una gestione di epoche minori e maggiori permette di avere latenze di esecuzione ben definite. I task interattivi che tendono a consumare la loro quota esecutiva lentamente tendono a restare a livelli di priorità alti e questo riduce le relative latenze di esecuzione. L annuncio di questa patch con ulteriori dettagli tecnici disponibili in [79]. Alcuni utenti hanno segnalato delle problematiche durante l utilizzo della patch RSDL con applicazioni che si basano sul server X. Alcune di queste considerazioni possono essere trovate in [80] Brain Fuck Scheduler (BFS) Per il filone di kernel , Con Kolivas, propone una patch che modifica in modo sostanziale l organizzazione delle strutture dati e le politiche per le attività di scheduling dei processi ordinari (classe sched_normal). L obbiettivo di BFS è di implementare uno scheduler dal design semplice, con un attenzione particolare per migliorare il response time delle applicazioni interattive, eliminando le precedenti euristiche per l interattività che sono complesse, impossibili da modellare e spesso tarate su workload specifici [81]. Strutture dati e classi di scheduling L organizzazione strutturale di BFS prevede un unica runqueue (un array di liste) di processi in stato di pronto per tutto il sistema, indipendentemente dal numero di core/cpu presenti, con una complessità temporale di gestione dei processi di O(n). La scelta di utilizzare un unica runqueue si basa sulle seguenti motivazioni: se da un lato avere una runqueue per ogni CPU migliora latenza ed equità di esecuzione dei processi locali, migliorando la scalabilità del sistema (per via dei lock locali), dall altro, per ottenere

81 CAPITOLO 3. SCHEDULING IN LINUX 70 un buon bilanciamento del sistema, e quindi per migliorare latenza ed equità tra le varie runqueue, è necessario introdurre un sistema di bilanciamento di carico che richiede un sostanziale overhead, oltre a lock multipli tra le varie runqueue. BFS, avendo un unica runqueue, non ha bisogno del bilanciamento di carico e quindi non supporta la schedulazione a dominio. Inoltre BFS non supporta la caratteristica della schedulazione di gruppo cgroup (sezione 3.1.5). BFS mantiene le classi dei task real-time Posix sched_rr e sched_fifo mentre sostituisce la classe per i task ordinari: la nuova politica, che in parte prende spunto da RSDL, si basa su una deadline virtuale, in modo simile all algoritmo EEVDF (Earliest Eligible Virtual Deadline First). Interattività BFS tiene traccia di quanto un processo ha utilizzato la CPU e non di quanto è in stato di attesa. L utilizzo effettivo di CPU dei task viene determinato utilizzando il TSC clock, con una risoluzione di nanosecondi, piuttosto di basarsi sulla frequenza del tick di timer (HZ), che solitamente è nell ordine di millisecondi. Determinare la caratteristica di interattività di un processo in base al tempo di esecuzione o di attesa di un processo (come avviene nel filone di kernel ufficiale) può portare a risultati inesatti, come considerare un processo interattivo quando non lo è affatto: non è sempre facile determinare se un processo è andato in attesa in modo volontario, per esempio in attesa di un movimento del mouse, oppure in modo involontario, per esempio in attesa di un attività da parte del kernel. Di fatto si dovrebbero introdurre diverse euristiche per coprire tutte le casistiche possibili. In BFS un task è considerato interattivo sulla base della relativa quota di esecuzione rimasta, favorendo task la cui deadline effettiva è prossima alla scadenza. Deadline virtuale Secondo Con Kolivas, la chiave per ottenere basse latenze di esecuzione, equità di schedulazione e una distribuzione dei livelli di nice è nel meccanismo della deadline virtuale. Uno dei pochi parametri configurabile di BFS è rr_interval, l intervallo di round robin (range tra 1 e 5000 millisecondi). Questo è il massimo tempo di esecuzione che un task della classe sched_normal può utilizzare in CPU quando è presente un altro task (sched_normal) con medesimo livello di nice. Il valore di default è di 6 millisecondi su sistemi a monoprocessore ed è incrementato in modo progressivo su sistemi a multiprocessori in modo tale da ridurre la competizione di cache ed aumentare il throughput. Diminuendo il valore diminuisce la latenza di esecuzione dei task ordinari ma diminuisce anche il throughput; aumentando il valore aumenta il throughput a discapito della latenza di esecuzione dei task. Ogni task ordinario ha una quota di esecuzione (time_slice) che, all inizio, è pari al valore di rr_interval (e viene decrementato ad ogni tick di timer, quanto il task è in esecuzione) e una deadline virtuale calcolata con la seguente formula: deadline_virtuale = (jif f ies + prio_ratio + rr_interval), prio_ratio è così calcolato: partendo da un valore di 0 per il livello base di nice (- 20) viene incrementato del 10% per ogni livello di nice che si discosta da quello base (quindi per il livello -15 prio_ratio assume 50% ossia 0,5). Questa deadline virtuale non indica il lasso di tempo entro il quale deve essere consumato il quanto a disposizione del task, bensì viene utilizzata per determinare il prossimo task da mandare in esecuzione con l obiettivo di distribuire in modo equo il tempo di esecuzione in CPU ai task ordinari.

82 CAPITOLO 3. SCHEDULING IN LINUX 71 Politica e algoritmo In una decisione di scheduling viene selezionato il task con il più piccolo valore della deadline virtuale. Quando un task esaurisce il suo time slice, viene posto in coda alla runqueue e il time slice e la relative deadline sono ricalcolati. Quando un task va in attesa, viene posto in coda alla runqueue e i relativi time slice e deadline rimangono inalterati. In entrambi i casi si scansiona tutta la runqueue alla ricerca del prossimo task da mandare in esecuzione (in O(n) con n numero di task totali), quello con deadline virtuale inferiore. Quando si incontra task con deadline virtuale scaduta (ossia i jiffies sono maggiori della deadline) allora i task sono scelti in FIFO (solo quelli con deadline scaduta). L unica runqueue di BFS è organizzata come un array di 103 liste di task, una per ogni livello di priorità. Le prime 100 sono dedicate ai così detti task real-time Posix (classi sched_fifo e sched_rr), mentre le rimanenti 3 sono dedicate alle classi sched_iso, sched_normal e sched_idleprio (in ordine di priorità decrescente). Una bitmap viene utilizzata per indicare la presenza di task (in stato di pronto) nelle varie liste (un bit per ogni lista). La ricerca del prossimo task da mandare in esecuzione avviene nel seguente modo: tramite la bitmap si verifica il primo livello di priorità (partendo dal più prioritario, ossia con priorità pari a 0) che contiene almeno un task in stato di pronto; se il livello di priorità appartiene al range dei task real-time Posix (sched_rr e sched_fifo) allora si prende il primo task della relativa lista (affine alla CPU considerata) e la ricerca è conclusa (in O(1)); se il livello di priorità è sched_iso i task sono presi in FIFO in modo simile a sched_rr; se il livello di priorità corrisponde a sched_normal o sched_idleprio allora la ricerca del task diventa O(n), prendendo il task con minore deadline virtuale, o il primo task con deadline scaduta. I task della classe sched_iso sono prioritari rispetto a quelli della classe sched_normal e sched_idleprio e sono schedulati in round robin con time slice pari al valore di rr_interval. Il valore posto in /proc/sys/kernel/iso_cpu rappresenta la percentuale massima di utilizzo della CPU da parte dei task sched_iso (70% come default). Raggiunto questo limite di utilizzo i task sched_iso sono degradati alla classe sched_normal e schedulati secondo la relativa politica. La classe sched_iso permette a utenti ordinari di schedulare i propri task in modo simile a quanto avviene per i task real time (standard Posix), quest ultimi utilizzabili solo con determinati privilegi. I task della classe sched_idleprio sono schedulati solo quando si hanno risorse di calcolo in idle. L idea è di schedulare task di bassa priorità in background (non interattivi e comunque con lunghi tempi di response time) senza interferire con task in foreground delle altri classi disponibili. Scalabilità e ottimizzazioni BFS ha un singolo lock globale sulla runqueue per le operazioni di inserimento, modifica e cancellazione di task; quando un task viene mandato in esecuzione su una CPU, lo stesso viene tolto dalla runqueue, per dare la possibilità di modificare le informazioni associate alla struttura del task in modo indipendente dal lock globale (e.g. durante un tick di timer). BFS inserisce un task passato in stato di pronto nella runqueue in O(1) e verifica se questo può prelazionare un task in esecuzione su qualche CPU in O(n), con n numero totale di CPU sul sistema. La maggior limitazione di BFS riguarda la scalabilità: infatti utilizzando un unica runqueue globale per tutto il sistema, al crescere del numero di CPU aumentano le richieste di lock sulla runqueue, con il conseguente aumento delle latenze di accesso. Su piccoli numeri

83 CAPITOLO 3. SCHEDULING IN LINUX 72 di CPU, BFS risulta essere più efficiente rispetto ad altri algoritmi di scheduling con runqueue locali (una per ogni CPU). Infatti quest ultimi per bilanciare il carico di sistema, devono eseguire in modo periodico algoritmi di bilanciamento di carico per ridistribuire i task sulle runqueue del sistema e questo può comportare un overhead notevole (di fatto parte del tempo di esecuzione in CPU viene utilizzato per bilanciare il carico di sistema). BFS per migliorare le prestazioni su sistemi SMP adotta le seguenti politiche: quando un task viene mandato in esecuzione su una CPU viene mantenuta una copia locale della struttura informativa del task stesso in modo tale da poterla aggiornare evitando preziosi lock sulla runqueue globale; il concetto di distanza di cache (sezione 1.5), che rappresenta la distanza tra l ultima CPU che ha eseguito il task e la CPU sulla quale avviene la decisione, viene utilizzato in una decisione di scheduling: questa distanza viene utilizzata come moltiplicatore per il valore di deadline virtuale. L informazione contenuta in sched_domain viene utilizzata per determinare la distanza di cache. Se le due CPU (quella sulla quale avviene al decisione di scheduling e l ultima CPU che ha eseguito il task considerato) sono considerate a cache condivisa (ad esempio appartengono allo stesso core SMT o a due core diversi ma con cache in comune) il moltiplicatore assume il valore di una unità e di fatto non altera il valore di deadline virtuale; se le due CPU sono considerate a cache non condivisa (ad esempio appartengono a due core con cache non in comune) il moltiplicatore assume il valore di due unità e di fatto raddoppia il valore di deadline virtuale; se, in un architettura NUMA, le due CPU appartengono a due diversi nodi, il moltiplicatore assume il valore di quattro unità e di fatto quadruplica il valore di deadline virtuale. In questo modo in una decisione di scheduling si preferiscono task con cache-hot (ossia task che molto probabilmente hanno già dei dati presenti nella cache della CPU); se ci sono più CPU in idle sulle quali un task può essere eseguito, la scelta della CPU in idle avviene in questo ordine di preferenza (rispetto all ultima CPU che ha eseguito il task): stesso core, cache idle o non-idle, thread idle; core diverso, stessa cache, cache idle o non-idle, thread idle; stesso nodo, CPU diversa, cache in idle, thread idle; stesso nodo, CPU diversa, cache non-idle, thread idle; stesso core, thread non-idle; core diverso, stessa cache, thread non-idle; stesso nodo, CPU diversa, thread non-idle; nodo diverso, CPU diversa, cache idle, thread idle; nodo diverso, CPU diversa, cache non-idle, thread idle; nodo diverso, CPU diversa, thread non-idle. Come si può notare, nella scelta della CPU/core/thread sulla quale mandare in esecuzione un task c è una preferenza di scelta (rispetto all ultima CPU che ha eseguito il task) composta dalle seguenti caratteristiche, nell ordine: thread idle, medesima cache, e cache idle. Considerazioni Uno degli obiettivi di BFS è di ridurre l overhead di sistema. Questa è una caratteristica molto ricercata nei sistemi embedded e real-time, dove l overhead di sistema comporta l interruzione ed il ritardo di esecuzione delle applicazioni, ma anche un consumo di energia, che spesso, soprattutto nei dispositivi mobili, è molto prezioso. BFS è stato incorporato nella release CyanogenMod del progetto Android [85] (castomizzazione del

84 CAPITOLO 3. SCHEDULING IN LINUX 73 kernel linux per i dispositivi mobili) riscontrando un incremento di prestazioni rispetto all utilizzo dello scheduler CFS. Ulteriori dettagli tecnici di BFS possono essere trovati in [81, 82]. Alcuni benchmark sono disponbibili in [84]. I sorgenti sono disponibili sotto forma di patch in [83].

85 Capitolo 4 Scheduling in FreeBSD Questo capitolo descrive l evoluzione dello scheduling in FreeBSD, individuata in due implementazioni: lo storico 4BSD, ereditato da 4.3BSD e il più recente ULE, sviluppato a partire da FreeBSD 5.X (2003) e divenuto lo scheduler di default per le architetture amd64, i386 e Powerpc nel Marzo del 2008 (FreeBSD 7.X). Per ogni scheduler vengono riportate politiche, strutture dati, algoritmi, caratteristiche e relativi obiettivi. Nell ultima sezione troviamo un analisi di confronto tra le politiche di scheduling di processi del kernel Linux e FreeBSD con alcune considerazioni sul mancato supporto al real-time in FreeBSD. In appendice F viene riportata la gestione degli interrupt di clock in FreeBSD, dalla quale dipendono alcune attività di scheduling dei processi (e.g. per la gestione del quanto esecutivo). 4.1 Evoluzione dello scheduling e stato dell arte L architettura del kernel di FreeBSD [86] ha subito un notevole cambiamento a partire dalla release 5.0 con il progetto FreeBSD SMPng Project [94], che introduce il supporto all SMP. Obiettivi della nuova architettura sono un alta scalabilità (come numero di unità di calcolo), basse latenze per gli interrupt e un kernel maggiormente prelazionabile. Il modello SMP (sezione A.5 per i dettagli) presenta delle nuove opportunità e sfide nelle attività di scheduling di processi, con l obiettivo di avere una gestione intelligente delle risorse di calcolo nei sistemi multi-processore 1. Il precedente scheduler, 4BSD, ottiene ottimi risultati nell interattività e risulta efficiente solo con piccoli carichi, per via della sua complessità temporale di O(n), con n numero di processi nel sistema. Inoltre ha un supporto di base per le architetture a multiprocessore. Questi fattori hanno portato ad una riscrittura dello scheduler di FreeBSD, con la nascita di un nuovo scheduler, dal nome ULE [95]. Possiamo individuare l evoluzione dello scheduling dei processi in FreeBSD in due scheduler principali: 4BSD (default in FreeBSD 1.X-6.X); ULE (default 2 a partire da FreeBSD 7.X). Nelle successive sezioni vengono analizzate e messe a confronto le due implementazioni, secondo il seguente schema: 1 Notare che lo scheduler è una delle tante componenti del kernel che devono essere adattate per sfruttare al meglio il modello SMP 2 Nel momento in cui scrivo ULE non è compatibile con l architettura Sparc64 74

86 CAPITOLO 4. SCHEDULING IN FREEBSD 75 tipologia e obiettivo; strutture dati; politica e algoritmo; features; chiamate di sistema; considerazioni; Nella Tabella 4.1 sono riportate le caratteristiche principali delle due implementazioni.

87 CAPITOLO 4. SCHEDULING IN FREEBSD 76 Tabella 4.1: Evoluzione dello scheduling in FreeBSD Scheduler Tipologia e Politica Strutture Dati Algoritmo Feature Obiettivi 4BSD scheduling a classi di priorità, si schedula il task con priorità maggiore; timesharing per i task utente tramite interrupt hardware; priorità dinamiche per i task utente, statiche per i task kernel; kernel preemptive; una coda di task in stato di pronto per CPU; i task in attesa sono organizzati in liste in base all evento atteso; si determina la lista di priorità maggiore non vuota e si selezione il primo task, con complessità temporale di O(1); calcolo periodico delle priorità dinamiche dei task time-sharing, con complessità temporale di O(n) dove n è il numero di task totale del sistema; supporto ai task real-time POSIX; tecnica priorità ereditata per l inversione di priorità; supporto di base a SMP per CPU multicore/multithreading (SMT e HTT) con bilanciamento di carico statico con affinità di CPU; massima priorità ai task kernel; minimizzare i tempi di risposta per i task interattivi (I/O bound); supporto SMP. ULE scheduling a classi di priorità, si schedula il task con priorità maggiore; timesharing per i task utente tramite interrupt hardware; priorità dinamiche per i task utente, statiche per i task kernel; kernel preemptive; tre code di task in stato di pronto per CPU; i task in attesa sono organizzati in liste in base all evento atteso; tra le code di task real-time, timeshare e idle si determina (nell ordine) la lista di priorità maggiore non vuota e si selezione il primo task, con complessità temporale di O(1); calcolo periodico delle priorità dinamiche dei task time-sharing, con complessità temporale di O(1) supporto ai task real-time POSIX; tecnica priorità ereditata per l inversione di priorità; supporto SMP per CPU multicore/multithreading (SMT e HTT) con un bilanciamento di carico statico e periodico che tiene conto della distanza di cache e dell affinità dei task; massima priorità ai task kernel; minimizzare i tempi di risposta per i task interattivi (I/O bound); ridurre la complessità temporale sulle attività di scheduling; supporto migliorato per SMP;

88 CAPITOLO 4. SCHEDULING IN FREEBSD Scheduler 4BSD Come riferimento si sono presi i sorgenti di FreeBSD 8.2. Tipologia e obiettivo Lo scheduler di FreeBSD 4BSD si basa sullo scheduler del sistema operativo 4.3BSD, il sistema operativo da cui FreeBSD deriva. Lo scheduler di 4.3BSD [8] è un adattamento dello scheduler tradizionale di UNIX [7]. 4BSD è uno scheduler a classi di priorità: in una decisione di scheduling si manda in esecuzione il task con priorità maggiore. I task delle classi kernel hanno priorità statiche e non hanno time slice: possono essere prelazionati solo da processi di maggiore priorità o quando passano in stato di attesa (sleep o attesa di I/O). I task delle classi utente hanno delle time slice e le relative priorità sono calcolate in modo dinamico, con lo scopo di favorire i task interattivi, I/O bound, a discapito dei task CPU bound. Il calcolo delle priorità dinamiche avviene in modo periodico, con una complessità temporale di O(n), con n numero totale di task sul sistema, risultando poco efficiente con alti carichi di lavoro. In 4BSD si utilizza la tecnica a priorità ereditata per il problema di priorità inversa. Altre caratteristiche di 4BSD sono un supporto alle classi real-time POSIX e la possibilità di prelazionare task in spazio kernel, aumentando la reattività del sistema sui cambi di priorità dei task. Inoltre lo scheduler 4BSD ha un supporto di base alle architetture a multiprocessore (CPU multicore e core multi-threading SMT e HTT), secondo il modello SMP. Ogni risorsa di calcolo (e.g. CPU o core) ha associato una lista di task che può eseguire. Il bilanciamento di carico avviene in modo statico, in fase di decisione della CPU sulla quale assegnare un nuovo task. Nella decisione si tiene conto dell eventuale affinità dei task, cercando di assegnare task sulle stesse CPU sulle quali sono stati eseguiti in precedenza. L obiettivo di 4BSD è dare massima priorità esecutiva ai task in spazio kernel, minimizzare i tempi di risposta per i task utente interattivi (I/O bound) e avere un supporto di base per le architetture a multiprocessore, secondo il modello SMP 3. Strutture dati Ogni risorsa di calcolo mantiene una propria lista di processi che può eseguire (in stato di pronto). I task che sono in attesa di qualche evento (e.g. sono in sleep o in attesa di I/O) sono organizzati in liste, raggruppate secondo l evento atteso (per semplicità si può pensare ad un unica lista globale di attesa). Le informazioni di ogni processo sono contenute nella struttura dati proc. Per l attività di scheduling le informazioni più rilevanti sono le seguenti: p_list: puntatori al prossimo e precedente processo nella medesima lista; p_threads: puntatori al primo e ultimo thread del processo; p_slock: spin lock del processo; p_stats: statistiche del processo; p_state: stato del processo (new, normal, zombie); p_pptr: puntatore al processo padre; p_children: puntatore alla lista dei processi figli; p_mtx: lock per questa struttura; p_cpulimit: limite di utilizzo di CPU (in secondi); p_nice: valore di nice; p_numthreads: numero di thread; 3 Inizialmente il supporto SMP non era presente nello scheduler 4BSD, è stato introdotto, come altre migliorie, durante diversi anni di sviluppo

89 CAPITOLO 4. SCHEDULING IN FREEBSD 78 p_sched: informazioni specifiche dello scheduler (e.g. 4BSD e ULE); p_singlethread: informazioni del thread (in caso di processo a singolo thread); Ogni processo può avere uno o più thread. Il thread è l entità utilizzata nelle decisioni di scheduling. Le informazioni di ogni thread sono contenute nella struttura dati thread. Per l attività di scheduling le informazioni più rilevanti sono le seguenti: td_proc: puntatore al processo associato; td_plist: puntatori al prossimo e precedente thread del processo associato; td_runq: puntatori al prossimo e precedente thread della coda di run associata; td_slpq: puntatori al prossimo e precedente thread della coda di wait associata; td_lockq: puntatori al prossimo e precedente thread della coda di lock associata; td_cpuset: maschera di affinità di CPU; td_tid: id del thread; td_flags: maschera di flag del thread; td_lastcpu: ultima CPU che ha eseguito il thread; td_oncpu: attuale CPU di esecuzione del thread; td_estcpu: utilizzo di CPU del thread (solo per 4BSD); td_slptick: numero globale di tick (su base hz) salvato prima di andare in sleep; td_rqindex: indice della coda di run; td_base_pri: priorità base dei thread kernel; td_priority: priorità effettiva; td_pri_class: classe di scheduling (ithd, kern, real-time, timeshare, idle); td_user_pri: priorità dinamica (solo per task della classe timeshare); td_base_user_pri: priorità base dei thread utente; td_state: stato del thread (inactive, inhibited, ready, runq, running); td_critnest: numero di regioni critiche di codice mantenute; td_sched: informazioni specifiche dello scheduler (e.g. 4BSD e ULE); Per lo scheduler 4BSD la struttura td_sched contiene, tra altre, le seguenti informazioni: ts_cpticks: numero di tick (su base stathz) di utilizzo di CPU; ts_slptime: secondi passati in attesa; ts_runq: coda di run associata; Ogni risorsa di calcolo (e.g. CPU o core) ha associato una lista di task che può eseguire. Le liste dei task sono organizzate con una struttura runq che contiene l array di liste (rq_queues) e una bitmask (rq_status) che segnala la presenza o meno di task nelle relative liste. Ogni singola lista accorpa 4 valori di priorità 4 : avendo un range di priorità 0-255, ci sono 64 liste. L organizzazione delle strutture dati utilizzate dallo scheduler 4BSD viene schematizzata nella Figura L accorpamento serve per ridurre il costo (overhead) di gestione delle liste. All interno di ogni singola lista non c è un ordinamento dei thread sulla base delle 4 priorità.

90 CAPITOLO 4. SCHEDULING IN FREEBSD 79 Figura 4.1: Organizzazione delle strutture dati nello scheduler 4BSD di FreeBSD

91 CAPITOLO 4. SCHEDULING IN FREEBSD 80 Politica e classi di scheduling I livelli di priorità hanno un range da 0 a 255. Questo range viene suddiviso in 5 classi. Due classi sono dedicate a gestire task in spazio kernel: ithd, con range 0-63, gestisce i top half e kern, con range , gestisce i bottom half. Le restanti tre classi gestiscono i task in spazio utente: real-time, con range , gestisce i task real-time 5, timeshare, con range , gestisce i task time-sharing e idle, con range , gestisce i task di idle. La priorità dei task è inversamente proporzionale al relativo valore (valore 0 priorità più alta, valore 255 priorità più bassa). I processi delle classi in spazio utente hanno delle time slice: al termine del loro quanto di esecuzione sono schedulati in round robin. I processi delle classi real-time e idle hanno delle priorità statiche mentre quelle della classe timeshare sono dinamiche. I processi delle classi in spazio kernel, ithd e kern, hanno priorità statiche e non hanno time slice: possono essere prelazionati solo da processi di maggiore priorità o quando passano in stato di attesa (sleep o attesa di I/O). Politiche in spazio utente La politica di scheduling per i task utente favorisce i task interattivi, I/O bound, a discapito dei task CPU bound. In genere, i task interattivi hanno corti burst di computazione, seguiti da periodi di inattività o da richieste di I/O. La priorità dei task della classe timeshare viene calcolata in modo dinamico sulla base del comportamento dei task stessi, in particolare sulla base del tempo di esecuzione in CPU. L idea è di variare la priorità dei task in modo proporzionale al periodo di tempo di esecuzione in CPU. In questo modo i processi CPU bound tendono ad un declassamento di priorità, mentre i processi I/O bound tendono a mantenere i propri valori di priorità, e, nel caso di inattività prolungata, possono perfino aumentare la relativa priorità. Quando i processi interattivi (I/O bound) passano in stato di pronto prelazionano processi ordinari di tipo CPU bound. L obiettivo di questa politica è di dare priorità ai task interattivi riducendo i relativi tempi di risposta. Quando viene creato un nuovo processo, questo eredita la priorità del processo padre, compreso il relativo tempo di esecuzione in CPU (campo td_estcpu). Un altra politica di scheduling di ULE è quella che cerca di minimizzare il thrashing, ossia quel fenomeno che occorre quando la quantità di memoria libera a disposizione dei processi utente è tale per cui è necessario più tempo per la gestione dei page fault e dello swap su disco di quanto ne sia necessario per eseguire i processi che fanno richiesta di nuova memoria. Un buon indice di questo fenomeno è il rapporto tra pagine di memoria libere e richieste di nuove pagine. Quando questo rapporto raggiunge una certa soglia critica, il demone pageout tenta di ridurlo mandando in sleep i processi che richiedono nuove pagine di memoria e liberando le altre pagine di memoria assegnate agli stessi. In questo modo le pagine liberate possono essere assegnate ad altri processi che competono per l esecuzione, senza dover effettuare delle operazioni di swap (ossia quando le pagine in memoria di un certo processo vengono salvate sulla porzione del disco dedicata allo swap). Quando ci sarà un numero sufficiente di pagine libere allora il demone pageout risveglierà i processi che aveva messo in attesa. Per le politiche sopra citate, la priorità di questi processi tenderà ad aumentare (per via del tempo passato in sleep), rendendoli prioritari per l esecuzione in CPU. Calcolo della priorità Per tutte le classi i valori effettivi di priorità dei task, ossia quelli considerati in una decisione di scheduling, sono mantenuti nel campo td_priority. La 5 Il termine real-time qui si riferisce al fatto che, tra le classi di task utente, questa è quella prioritaria.

92 CAPITOLO 4. SCHEDULING IN FREEBSD 81 priorità statica di un thread in spazio kernel (0-127) è mantenuta nel campo td_base_pri. La priorità statica di un thread in spazio utente ( ) è mantenuta nel campo td_base_user_pri. I task della classe timeshare hanno una priorità dinamica, mantenuta nel campo td_user_pri e calcolata sulla base di alcuni parametri, come l utilizzo di CPU e il valore di nice. In circostanze particolari può succedere che il valore contenuto in td_priority sia diverso da quello contenuto in td_base_pri, nel caso di thread in spazio kernel, o da quello contenuto in td_base_user_pri, nel caso di thread in spazio utente (o td_user_pri per i task time-sharing); per esempio, per evitare il problema di priorità inversa si utilizza la tecnica a priorità ereditata (sezione 1.4 per i dettagli): quando il task in esecuzione tenta di accedere a una regione critica tramite lock, già preso da un altro task di priorità inferiore e per la quale esiste una lista di task in attesa, allora la priorità del task che mantiene il lock (e di tutti i task in attesa di rilascio dello stesso lock), eredita il valore di priorità del task in esecuzione (solo se di valore maggiore); così facendo il task che mantiene il lock, e tutti quelli in lista di attesa, potranno andare in esecuzione più rapidamente (per via dell aumento di priorità) rendendo la risorsa condivisa disponibile al task di priorità maggiore. Una volta rilasciata la risorsa condivisa la priorità dei task ritornerà al valore precedente, contenuto in td_base_pri o td_base_user_pri. Priorità dinamica La priorità dinamica (td_user_pri) di un thread della classe timeshare viene determinata da due parametri associati al thread stesso: l utilizzo di CPU (campo td_estcpu) e il valore di nice (campo p_nice). Il range di valori per nice è tra -20 e +20 con un valore di default pari a 0. Valori negativi incrementano la priorità (diminuendo il suo valore) mentre valori positivi la decrementano (aumentando il suo valore). Per ogni thread della classe timeshare che si trova in stato di pronto, la relativa priorità (td_user_pri e quindi td_base_user_pri) viene ricalcolata in modo periodico (di default una volta al secondo) sulla base della seguente formula (contenuta nella funzione resetpriority()): ( ) td_estcpu td_user_pri = PRI_MIN_TIMESHARE + +(p_nice 20) 8 Valori minori di PRI_MIN_TIMESHARE sono settati a PRI_MIN_TIMESHARE e valori maggiori di PRI_MAX_TIMESHARE sono settati a PRI_MAX_TIMESHARE. Con questa formula la priorità di un thread utente decresce linearmente (assume valori maggiori) sulla base dell utilizzo di CPU del thread stesso: più il thread resta in esecuzione in CPU e minore sarà la sua priorità. Il parametro nice, modificabile dall utente, permette di aumentare (con valori negativi) o diminuire (con valori positivi) la priorità di un thread sempre di un fattore costante (da -40 a 0), potendo ridurre l aumento del valore di priorità dovuto all utilizzo di CPU. L utilizzo di CPU td_estcpu del thread in esecuzione viene incrementato ad ogni tick del clock stathz (funzione sched_clock()). Nel caso di thread con lunghi periodi di esecuzione, il valore di td_estcpu aumenterebbe considerevolmente fino ad ottenere, nel calcolo della priorità td_user_pri, sempre il valore di PRI_MAX_TIMESHARE. Per evitare questo problema, il valore di td_estcpu viene aggiornato (funzione decay_cpu() invocata prima di resetpriority()), sulla base della seguente formula: td_estcpu = (2 load) (2 load + 1) td_estcpu dove il valore di load rappresenta una media di carico (basato sul numero di thread che competono per l esecuzione) calcolata sull intervallo di un minuto, precedente al momento del calcolo (la formula è contenuta nella funzione loadav()). In questo modo in ogni secondo si dimentica una percentuale di utilizzo di CPU sulla base del carico di lavoro del sistema. Minore è il carico di lavoro e maggiore sarà la

93 CAPITOLO 4. SCHEDULING IN FREEBSD 82 percentuale di utilizzo di CPU che si dimentica ogni secondo. Si può dimostrare 6 che, sulla base delle formule precedenti e di alcuni parametri di sistema, si dimentica il 90% di utilizzo di CPU ogni n secondi con n = 5 load. Le priorità dei thread della classe timeshare che non si trovano in stato di pronto viene ricalcolata (in resetpriority()) solo quanto i thread tornano in stato di pronto (nella funzione sched_wakeup()). In questo caso per il calcolo di td_estcpu viene utilizzata la seguente formula: ( ) ts_slptime (2 load) td_estcpu = td_estcpu (2 load + 1) dove ts_slptime viene settata a 0 quando un thread passa in stato di attesa ed incrementata di una unità per ogni secondo di attesa. Notare che se il thread viene risvegliato prima che sia passato un secondo, allora td_estcpu assume semplicemente il valore precedente. Inoltre se il thread è stato in attesa per più di n secondi, con n = 5 load, allora td_estcpu viene settato a 0. Questo permette di aumentare la priorità (diminuendo il valore di td_user_pri) dei thread con lunghi periodi di attesa. Algoritmo Ritroviamo attività di scheduling in diverse parti del kernel. L algoritmo applicato in una decisione di scheduling è il seguente: si determina la lista di maggior priorità che abbia almeno un thread in stato di pronto e si seleziona il primo thread trovato nella medesima lista. Questa parte di codice è contenuta nella funzione runq_choose() e la sua complessità temporale è di O(1): la ricerca del thread da mandare in esecuzione avviene sempre in un tempo costante, indipendentemente dal numero di processi nel sistema. La ricerca del prossimo thread da mandare in esecuzione avviene nella funzione mi_switch(), la parte di codice, indipendente dall architettura utilizzata, che implementa uno switch di contesto. Questa funzione viene chiamata in modo sincrono in diversi punti del kernel, tra i quali: nella funzione ithread_loop(), dopo aver processato tutti gli interrupt pendenti; nella procedura di shutdown del sistema, boot, per permettere di eseguire velocemente thread interrupt; nella funzione critical_exit(), per permettere di prelazionare un thread kernel che non mantiene più regioni critiche e per il quale è stata richiesto uno switch di contesto; nelle funzioni sched_relinquish e yield, quando un thread rilascia in modo spontaneo l esecuzione in CPU; nel processo kernel idlepoll, per pollare eventuali attività di dispositivi di networking; nelle funzioni thread_suspend_check e thread_suspend_switch, quando un thread viene sospeso; nella funzione maybe_preempt(), se il thread passato in stato di pronto (e.g. nuovo thread aggiunto alla lista di run o modifica di priorità di un thread già in lista) ha una priorità maggiore del thread in esecuzione; nella funzione sched_preempt(), invocata nella gestione degli IPI (interprocessor interrupt); nella funzione sched_bind(), quando si associa una nuova CPU ad un thread; 6 La dimostrazione è contenuta nel file sched_4bsd.c

94 CAPITOLO 4. SCHEDULING IN FREEBSD 83 nel thread kernel idle di ogni CPU, nel caso in cui ci sia almeno un thead in stato di pronto nella relativa coda; nella funzione sleepq_wait(), per mettere un thread nella lista di sleep fino a quando verrà risvegliato (usata ad esempio nella sleep). La funzione mi_switch() viene invocata anche in modo asincrono tramite il meccanismo AST (asynchronous system trap), quando viene processata una trap software asincrona con il flag TDF_NEEDRESCHED attivo. Questo meccanismo è utile ad esempio quando, dopo la gestione di un interrupt, si vuole mandare in esecuzione un thread diverso da quello che era in esecuzione prima dell interrupt. Il controllo di una AST avviene alla fine della gestione di un interrupt, di una trap o di una chiamata di sistema (solo se il controllo sta per tornare in spazio utente). In questo caso si verifica se il campo td_flags del thread in spazio utente interrotto ha il flag TDF_NEEDRESCHED attivo. Il flag TDF_NEEDRESCHED viene attivato nei seguenti punti del kernel: nelle funzioni resetpriority_thread e sched_add se la priorità del thread manipolato è maggiore di quella del thread in esecuzione (in questo caso si setta la flag del thread in esecuzione); nella funzione sched_clock() quando il thread (utente) in esecuzione ha esaurito il suo time slice a disposizione; nella funzione sched_affinity() se il thread manipolato si trova su una coda di CPU sulla quale non può più essere mandato in esecuzione (in questo caso si setta la flag del thread manipolato); Il calcolo delle priorità dinamiche dei thread in stato di pronto della classe timeshare avviene nella funzione schedcpu(). Questa funzione viene invocata in modo periodico dal processo kernel schedcpu. La frequenza del calcolo dipende dal parametro di configurazione del kernel HZ. Il valore di default ha un valore di 1000 (o 100 su architetture mips e arm). La complessità temporale della funzione schedcpu() è di O(n), con n numero dei thread totali nel sistema (anche quelli che non si trovano in stato di pronto). Il calcolo periodico delle priorità dinamiche è una della maggiori ragioni per cui lo scheduler 4BSD ha scarse prestazioni con alti carichi di lavoro. Time slice La verifica del time slice di un thread utente avviene nella funzione sched_clock (invocata con una frequenza pari al valore di stathz) e consiste nel verificare se il thread in esecuzione ha esaurito il suo quanto a disposizione, tramite la seguente disuguaglianza: ticks PCPU_GET(switchticks) >= sched_quantum dove sched_quantum è pari a HZ/10, PCPU_GET(switchticks) corrisponde al numero di tick memorizzati quando il thread è andato in esecuzione (ultimo switch di contesto) e ticks sono i tick totali. Notare che nel conteggio della time slice di un thread utente rientra anche l eventuale periodo di tempo consumato in spazio kernel (ad esempio per eseguire chiamate di sistema). Quando un thread esaurisce il proprio time slice viene messo in coda alla lista (tecnica a round robin). Se un thread va in stato di attesa non perde la sua posizione in lista: quando torna in stato di pronto occupa la stessa posizione che aveva in precedenza. I valori delle time slice per i processi utente influiscono sull interattività e sulla capacità di throughput del sistema. Con bassi valori di time slice aumenta la frequenza degli switch di contesto, permettendo

95 CAPITOLO 4. SCHEDULING IN FREEBSD 84 allo scheduler di mandare in esecuzione processi utente di più alta priorità (e.g. interattivi) in modo più rapido. Frequenti switch di contesto comportano perdita di dati in cache con conseguenti azioni di cache-miss e page-fault che si traducono in un aumento di overhead per il sistema, a discapito del throughput. Quindi più il valore della time slice è basso migliore è l interattività del sistema, a discapito del throughput; mentre a lunghi valori di time slice corrisponde una migliore capacità di throughput, a discapito dell interattività. Il valore di default della time slice usato in 4BSD è di 100 millisecondi. Questo valore è stato ottenuto in modo empirico come il valore più lungo che può essere utilizzato mantenendo un soddisfacente grado di responsività del sistema per i task interattivi. Risulta sorprendente come questo valore sia rimasto inalterato per più di 20 anni. Infatti benché sia stato stimato all epoca, sulla base di sistemi centralizzati con molti utenti, rimane tuttavia valido sui sistemi decentralizzati di oggi (workstation): la necessità di avere una migliore reattività nei sistemi odierni rispetto a quelli del passato viene compensata dal fatto che, avendo sistemi decentralizzati, il numero di task sulle code risulta inferiore rispetto a quello presente nei sistemi centralizzati del passato. Features In 4BSD è possibile abilitare il supporto allo scheduling real-time secondo lo standard POSIX. Inoltre tramite opzioni di compilazione è possibile migliorare la reattività del kernel ai cambi di priorità dei task. Un altra caratteristica di 4BSD è il supporto di base all SMP. POSIX real-time Il supporto alle classe di scheduling real-time secondo lo standard POSIX (P1003_1B, sezione E per i dettagli) può essere incluso nel kernel di FreeBSD utilizzando l opzione di compilazione _KPOSIX_PRIORITY_SCHEDULING. In questo modo si abilitano le classe di scheduling POSIX sched_fifo, sched_rr e sched_other e le relative chiamate di sistema. Le classi di scheduling sched_fifo e sched_rr con i rispettivi valori di priorità 0-31 sono mappate nella classe utente real-time di 4BSD, nel relativo range di valori La classe sched_other con valori 0-63 viene mappata nella classe utente timeshare con valori Durante queste conversioni (in entrambe le direzioni) si tiene conto del fatto che secondo POSIX a valori minori di priorità corrispondono priorità minori, al contrario di FreeBSD e di altri sistemi Unix-like. Notare che il supporto per lo scheduling di processi all estensione real-time POSIX non è pienamente conforme allo standard (P1003_1B): i task della classe sched_fifo non dovrebbero avere timeslice. Kernel preemptive Esistono alcune opzioni di compilazione del kernel che permettono di migliorare la reattività del sistema ai cambi di priorità dei thread. Senza queste opzioni su un cambio di priorità di un thread, o quando un thread passa in stato di pronto possiamo avere un prelazione del thread in esecuzione (maybe_resched()) solo in spazio utente, tramite il meccanismo AST (settando la flag TDF_NEEDRESCHED), quando la priorità del nuovo thread è maggiore (valore minore) di quella del thread in esecuzione. Non è possibile prelazionare in modo volontario un thread in spazio kernel. Con le seguenti opzioni è possibile forzare la prelazione in spazio kernel del thread in esecuzione in favore di thread di maggiore priorità. PREEMPTION: con questa caratteristica abilitata, su un cambio di priorità di un thread, la prelazione in spazio kernel del thread in esecuzione (funzione maybe_preempt()) avviene se tutte le seguenti condizioni sono soddisfatte: (a) il nuovo thread ha

96 CAPITOLO 4. SCHEDULING IN FREEBSD 85 SMP una priorità maggiore di quello in esecuzione, inoltre la classe di scheduling del thread in esecuzione è diversa da ithd e quella del nuovo thread è diversa da idle; (b) il thread attualmente in esecuzione non è in nessuna sezione critica del kernel (altrimenti la prelazione avviene appena esce da tutte le regioni critiche). Di fatto questa caratteristica rende il kernel FreeBSD maggiormente prelazionabile, permettendo di prelazionare thread in spazio kernel. In questo modo si migliora la reattività del sistema a reagire più rapidamente ai cambi di priorità dei thread, a discapito del throughput; IPI_PREEMPTION: con questa caratteristica abilitata, ogni qualvolta un thread viene aggiunto (e.g. nuovo thread o aggiornamento della priorità di un thread esistente) alla lista di una CPU diversa da quella locale (che sta eseguendo la funzione sched_add()), si verifica se inviare alla CPU target un interrupt di prelazione di tipo IPI_PREEMPT (nella funzione kick_other_cpu()). L invio avviene se il nuovo thread ha una priorità maggiore di quello in esecuzione sulla CPU target, inoltre la classe di scheduling del thread in esecuzione deve essere diversa da ithd. Sulla CPU target la gestione di un interrupt IPI_PREEMPT comporta la chiamata alla funzione sched_preempt() e la prelazione del thread in esecuzione avviene solo se questo non è in nessuna sezione critica del kernel (altrimenti avviene appena esce da tutte le regioni critiche). Di fatto questa caratteristica è l estensione di PREEMPTION su SMP. FULL_PREEMPTION: questa caratteristica modifica la condizione di prelazione delle precedenti caratteristiche, permettendo di prelazionare anche thread della classe ithd; viene utilizzata solo per scopi di debug durante lo sviluppo del kernel e di fatto permette la prelazione anche dei bottom half, aumentando in modo spropositato la frequenza degli switch di contesto. In questo caso si hanno scarse prestazione sia di interattività sia di throughput; Lo scheduler 4BSD ha un supporto di base alle architetture a multiprocessore, secondo il modello SMP (sezione A.5 per i dettagli). In 4BSD sono supportate CPU multicore e core multi-threading SMT e HTT (rif. sezione A.5): ogni singolo core logico (in caso di SMT e HTT) o fisico (in caso di multicore) viene riconosciuto come una risorsa di calcolo (d ora in avanti identificata con il termine generico CPU). Ad esempio, su un architettura con 2 CPU ognuna da 2 core, dove ogni core possiede un multi-threading a 2 thread, 4BSD vedrà in totale 8 risorse di calcolo (CPU), sulle quali distribuire il carico di lavoro. Il bilanciamento di carico avviene in modo statico, in fase di decisione della CPU sulla quale assegnare un nuovo task: non viene fatto un bilanciamento di carico periodico. Nella decisione si tiene conto dell eventuale affinità dei task: si cerca di assegnare task sulle stesse CPU sulle quali sono stati eseguiti in precedenza e questo per evitare page fault e/o cache misses, riducendo quindi l overhead. Le politiche di bilanciamento di carico non tengono conto però ne della distanza di cache (e questo per una mancanza di riconoscimento della topologia dell architettura) ne della distinzione tra core fisico e logico (ulteriori dettagli nella sezione 1.5). Strutture dati Come è possibile notare nella figura Figura 4.1, ad ogni CPU è associata una lista di processi, runq_pcpu, che possono essere mandati in esecuzione sulla CPU, mentre la variabile runq_length tiene il conto del numero di thread presenti nella lista. Nella struttura informativa dei thread sono contenute alcune informazioni per il supporto SMP come il riferimento all ultima CPU sulla quale il task è andato in esecuzione (td_lastcpu) e la maschera di affinità di CPU (td_cpuset). Inoltre ogni CPU ha associato una struttura dati di tipo pcpu nella quale viene riportato, tra le altre informazioni, il thread attualmente in esecuzione sulla CPU. Oltre alle liste associate ad ogni CPU, 4BSD mantiene una lista globale runq: quando

97 CAPITOLO 4. SCHEDULING IN FREEBSD 86 non si riesce a determinare una CPU alla quale associare un thread, allora il thread viene inserito in questa lista. Load balancing Quando un task passa in stato di pronto (e.g. viene creato un nuovo thread utente o kernel, o quando viene modificata la priorità effettiva di un thread) bisogna decidere su quale lista di CPU assegnare il thread. Questa decisione avviene nella funzione sched_add() e si articola nelle seguenti condizioni, nell ordine: se il thread mantiene dei lock su una certa CPU (campo td_pinned), allora viene assegnato alla CPU medesima, identificata dal campo td_lastcpu; se il thread è stato messo in bound su una certa CPU (flag TDF_BOUND nel campo td_flags) allora viene assegnato a quella CPU; tra le CPU sulle quali il thread può essere mandato in esecuzione (campo td_cpuset) si prende quella con minor carico, verificando la relativa variabile runq_length (questa condizione si trova nella funzione sched_pickcpu()); se nessuna delle precedenti condizioni è verificata allora il thread viene posizionato nella lista globale runq; Se è stata trovata una CPU alla quale assegnare il thread e se questa CPU è diversa dalla CPU sulla quale si sta eseguendo la funzione sched_add(), allora tramite la funzione kick_other_cpu() si valuta se inviare un interrupt alla CPU individuata: se la CPU è in idle allora si invia un interrupt IPI_AST; se la priorità del nuovo thread è minore o uguale a quella del thread in esecuzione sulla CPU target, allora non viene inviato nessun interrupt, altrimenti esistono due possibilità: se sono abilitate le caratteristiche del kernel IPI_PREEMPTION e PREEMPTION allora viene inviato un interrupt IPI_PREEMPT (che forza subito uno switch di contesto sulla CPU target), altrimenti si setta la flag TDF_NEEDRESCHED del thread in esecuzione sulla CPU target e si manda a questa un interrupt IPI_AST (e quindi ci sarà una prelazione non appena il thead in esecuzione torna in spazio utente o quando esaurisce il suo quanto a disposizione). Se non è stata trovata nessuna CPU alla quale assegnare il thread e quindi il thread è stato messo nella lista globale runq, allora si cerca di risvegliare tramite un interrupt IPI_AST una CPU in idle (o più a seconda di alcuni parametri di sistema configurabili). Se invece la CPU trovata è la stessa CPU sulla quale si sta eseguendo la funzione sched_add() (oppure se non è stata trovata nessuna CPU alla quale assegnare il thread ed inoltre non è stata risvegliata almeno una CPU trovata in idle) e se la priorità del nuovo thread è maggiore di quella del thread in esecuzione, allora, se l aggiunta del thread non è stata indotta da uno switch di contesto e se la caratteristica del kernel PREEMPTION è abilitata, si prelazione il thread in esecuzione, altrimenti si setta la relativa flag TDF_NEEDRESCHED. Decisione di scheduling La decisione (locale alla CPU) di quale thread mandare in esecuzione (invocata nei punti riportati in 4.1.1, paragrafo Algoritmo ) si articola nei seguenti punti (funzione sched_choose()): nella coda globale runq si determina l eventuale lista di maggiore priorità che contiene almeno un thread; in questa lista si cerca l eventuale primo thread la cui ultima esecuzione in CPU è avvenuta sulla CPU locale, altrimenti si prende il primo thread della lista; nella coda locale runq_pcpu si determina il thread di maggiore priorità (tramite la funzione runq_choose());

98 CAPITOLO 4. SCHEDULING IN FREEBSD 87 dei due thread determinati nei precedenti punti si seleziona quello con priorità maggiore (valore minore di td_priority); Notare che con questa politica è possibile che si esegua thread sulle CPU alle quali il thread non è affine (campo td_cpuset) e questo per evitare che alcuni thread non siano mai eseguiti. Chiamate di sistema Seguono le principali chiamate di sistema utilizzate per lo scheduling di processi in FreeBSD: getpriority(), setpriority(): ritorna o modifica il valore di nice di uno o più thread su base pid del processo, id di gruppo di processi o id utente; rtprio(): modifica o recupera la classe di priorità (RTP_PRIO_REALTIME, RTP_PRIO_NORMAL, RTP_PRIO_IDLE mappate nelle classi di 4BSD PRI_REALTIME, PRI_TIMESHARE, PRI_IDLE) e il relativo valore di un thread utente su base pid; yield(): forza una prelazione del thread utente in esecuzione (e imposta la relativa priorità al minimo); cpuset(), cpuset_setid(), cpuset_getid(): creano, settano e ritornano il cpuset associato al thread o al processo specificato; cpuset_getaffinity(), cpuset_setaffinity(): ritorna o modifica la maschera delle CPU da associare al thread, al processo o al cpuset specificato; Inoltre, abilitando l estensione real-time secondo lo standard POSIX (P1003_1B), si hanno a disposizione le relative chiamate di sistema: sched_setparam(), sched_getparam(): modifica o recupera la priorità di un thread; sched_setscheduler(), sched_getscheduler(): modifica o recupera la classe di scheduling e la relativa priorità di un thread; sched_yield(): forza una prelazione del thread utente in esecuzione (senza modificare la relativa priorità); sched_get_priority_max(), sched_get_priority_min(): ritorna priorità massima e minima delle classe di scheduling; sched_rr_get_interval(): ritorna il time slice del thread (totale e non effettivo); Oltre alle chiamate di sistema esistono anche alcune sysctl relative allo scheduling di processi: kern.sched.quantum: imposta o recupera il time slice per i thread utente (in tick di clock, default hz/10); kern.sched.ipiwakeup.enable: abilita o disabilita la possibilità di risvegliare CPU in idle; kern.sched.ipiwakeup.usemask: utilizza la maschera di CPU per determinare quali CPU in idle risvegliare; kern.sched.ipiwakeup.useloop: utilizza un loop per determinare quali CPU in idle risvegliare; kern.sched.ipiwakeup.onecpu: impone di risvegliare al massimo una CPU in idle alla volta; kern.sched.ipiwakeup.htt2: considera anche i core SMT o HTT tra le CPU in idle che si possono risvegliare.

99 CAPITOLO 4. SCHEDULING IN FREEBSD 88 Considerazioni Il calcolo periodico delle priorità dinamiche dei task utente permette di considerare i task I/O bound prioritari rispetto a quelli CPU bound. Visto che, in genere, i task interattivi sono anche I/O bound, questa politica migliora l interattività del sistema. Questa attività periodica (default ogni 100 millisecondi su mips e arm e ogni secondo sulle altre) ha una complessità temporale di O(n) con n il numero di task totali del sistema (anche quelli che non sono in stato di pronto). Con alti carichi di lavoro si introduce molto overhead a discapito delle prestazioni (sia in termini di interattività sia in termini di throughput). L equità viene rispettata solo a livello di thread: poiché un thread eredita la priorità del processo padre, l equità tra processi non è garantita: se ci sono due processi e uno dei due crea 100 thread allora a questo (o meglio ai suoi thread) verrà concesso un maggior tempo in esecuzione in CPU. Inoltre non esiste un meccanismo che garantisca equità tra diversi utenti. Lo scheduler 4BSD scala bene rispetto al numero di CPU del sistema: ogni risorsa di calcolo ha una propria lista di processi che può mandare in esecuzione e quindi l accesso alla lista di processi da schedulare non è in competizione tra le risorse di calcolo. Il supporto SMP non è però efficiente, per i seguenti motivi: (a) nella scelta della CPU alla quale assegnare un thread non viene considerata la distanza di cache tra l ultima CPU che ha eseguito il thread e le altre CPU disponibili e questo può portare a scelte non efficienti (rif. paragrafo distanza di cache in 1.5); (b) nella scelta della CPU alla quale assegnare un thread non si tiene in considerazione il fatto che la CPU disponibile sia di tipo logico (e.g. SMT o HTT) o fisico (rif. paragrafo Hyperthreading in 1.5); (c) non c è un bilanciamento di carico periodico. Il kernel di FreeBSD con lo scheduler 4BSD non può essere considerato un RTOS (rif. sezione 1.2.3): benché il kernel sia in buona parte prelazionabile, manca un algoritmo di scheduling che tenga conto dei vincoli temporali di esecuzione dei task. Ulteriori dettagli sullo scheduler 4BSD possono essere trovati nel libro The Design and Implementation of the FreeBSD Operating System [92] e visionando i sorgenti di FreeBSD, in particolare: sched_4bsd.c: contiene le funzioni per lo scheduling di processi dello scheduler 4BSD; kern_synch.c e kern_switch.c: contengono altre funzioni per lo scheduling di processi; proc.h: contiene le principali strutture dati per lo scheduling di processi; priority.h: contiene le principali define delle classi di priorità; p1003_1b.c: contiene le chiamate di sistema POSIX per l estensione real-time dello scheduling di processi;

100 CAPITOLO 4. SCHEDULING IN FREEBSD Scheduler ULE Come riferimento si sono presi i sorgenti di FreeBSD 8.2. Tipologia e obiettivo Lo scheduler ULE [95] è stato sviluppato nel contesto del progetto FreeBSD SMPng Project [94]. ULE come 4BSD è uno scheduler a classi di priorità: in una decisione di scheduling si manda in esecuzione il task con priorità maggiore. I task delle classi kernel hanno priorità statiche e non hanno time slice: possono essere prelazionati solo da processi di maggiore priorità o quando passano in stato di attesa (sleep o attesa di I/O). I task delle classi utente hanno delle time slice e le relative priorità sono calcolate in modo dinamico, con lo scopo di favorire i task interattivi, I/O bound, a discapito dei task CPU bound. Rispetto a 4BSD, dove il calcolo delle priorità dinamiche avveniva in modo periodico, con una complessità temporale di O(n), in ULE la priorità dinamica viene aggiornata solo per il task in esecuzione, con una complessità temporale di O(1), risultando efficiente anche con alti carichi di lavoro. Come per 4BSD, ULE utilizza la tecnica a priorità ereditata per il problema di priorità inversa. Altre caratteristiche di ULE sono un supporto alle classi di scheduling real-time POSIX e la possibilità di prelazionare task in spazio kernel, aumentando la reattività del sistema sui cambi di priorità dei task. Il supporto all SMP è stato migliorato in ULE, introducendo un riconoscimento della topologia delle CPU: nelle decisioni di bilanciamento di carico, oltre all affinità dei task in CPU, si tiene conto della distanza di cache. Inoltre, rispetto a 4BSD, dove il bilanciamento di carico avveniva solo in modo statico, in ULE abbiamo anche un bilanciamento di carico periodico. L obiettivo di ULE è dare massima priorità esecutiva ai task in spazio kernel, minimizzare i tempi di risposta per i task utente interattivi (I/O bound), ridurre la complessità temporale nelle decisioni di scheduling e migliorare il supporto alle architetture a multiprocessore, secondo il modello SMP. Strutture dati Ogni risorsa di calcolo mantiene 3 code per i thread in stato di pronto. Ogni coda è organizzata come un array di liste di thread, una per ogni valore di priorità gestita dalla coda. Una coda è dedicata per la gestione dei task delle classi in spazio kernel (top e bottom half) e per la gestione dei task utente della classe real-time. Un altra coda gestisce i task utente della classe timeshare e l ultima coda gestisce i task utente della classe di idle. I task che sono in attesa di qualche evento (e.g. sono in sleep o in attesa di I/O) sono organizzati in liste, raggruppate secondo l evento atteso (per semplicità si può pensare ad un unica lista globale di attesa). Le informazioni di ogni processo sono contenute nella struttura dati proc (condivisa con lo scheduler 4BSD). Per l attività di scheduling le informazioni più rilevanti sono le seguenti: p_list: puntatori al prossimo e precedente processo nella medesima lista; p_threads: puntatori al primo e ultimo thread del processo; p_slock: spin lock del processo; p_stats: statistiche del processo; p_state: stato del processo (new, normal, zombie); p_pptr: puntatore al processo padre; p_children: puntatore alla lista dei processi figli;

101 CAPITOLO 4. SCHEDULING IN FREEBSD 90 p_mtx: lock per questa struttura; p_cpulimit: limite di utilizzo della CPU (in secondi); p_nice: valore di nice; p_numthreads: numero di thread; p_sched: informazioni specifiche dello scheduler (e.g. 4BSD e ULE); p_singlethread: informazioni del thread (in caso di processo a singolo thread); Ogni processo può avere uno o più thread. Il thread è l entità utilizzata nelle decisioni di scheduling. Le informazioni di ogni thread sono contenute nella struttura dati thread (condivisa con lo scheduler 4BSD). Per l attività di scheduling le informazioni più rilevanti sono le seguenti: td_proc: puntatore al processo associato; td_plist: puntatori al prossimo e precedente thread del processo associato; td_runq: puntatori al prossimo e precedente thread della coda di run associata; td_slpq: puntatori al prossimo e precedente thread della coda di wait associata; td_lockq: puntatori al prossimo e precedente thread della coda di lock associata; td_cpuset: maschera di affinità di CPU; td_tid: id del thread; td_flags: maschera di flag del thread; td_lastcpu: ultima CPU che ha eseguito il thread; td_oncpu: attuale CPU di esecuzione del thread; td_estcpu: utilizzo di CPU del thread (solo per 4BSD); td_slptick: numero globale di tick (su base hz) salvato prima di andare in sleep; td_rqindex: indice della coda di run; td_base_pri: priorità base dei thread kernel; td_priority: priorità effettiva; td_pri_class: classe di scheduling (ithd, kern, real-time, timeshare, idle); td_user_pri: priorità dinamica (solo per task della classe timeshare); td_base_user_pri: priorità base dei thread utente; td_state: stato del thread (inactive, inhibited, ready, runq, running); td_critnest: numero di regioni critiche di codice mantenute; td_sched: informazioni specifiche dello scheduler (e.g. 4BSD e ULE); Per lo scheduler ULE la struttura td_sched contiene, tra altre, le seguenti informazioni: ts_runq: coda di run associata; ts_flags: flags; ts_cpu: CPU con maggior affinità; ts_rltick: tick globali (su base hz) memorizzati prima dell ultimo switch di contesto (usato per determinare l affinità in CPU); ts_slice: tick locali (su base stathz) di slice rimanenti; ts_slptime: tick locali (stathz scalati su base hz) di attesa volontaria (usato per determinare lo score di interattività del thread); ts_runtime: tick locali (stathz scalati su base hz) di esecuzione in CPU (usato per determinare lo score di interattività del thread); ts_ltick: ultimo tick globale (su base hz) nell ultima esecuzione in CPU; ts_ftick: primo tick globale (su base hz) nell ultima esecuzione in CPU; ts_ticks: contatore di tick globale (su base hz) del thread (mantenuto nella finestra temporale di (hz 10) + hz); ts_incrtick: tick globale (su base hz) occorso nell ultima occorrenza di sched_tick() quando il thread era in esecuzione;

102 CAPITOLO 4. SCHEDULING IN FREEBSD 91 Ogni risorsa di calcolo (e.g. CPU o core), per le attività di scheduling, mantiene una struttura dati di tipo tdq, così composta: tdq_lock: lock della struttura; tdq_cg: struttura della topologia della CPU; tdq_load: carico di CPU globale (numero totale di thread mantenuti); tdq_sysload: come tdq_load sottratto del carico dovuto ai thread bottom half; tdq_transferable: numero di thread che possono essere migrati (che non sono in regione critiche di codice); tdq_switchcnt: numero di switch nel tick corrente (su base stathz); tdq_oldswitchcnt: numero di switch nel tick precedente (su base stathz); tdq_lowpri: priorità più bassa dei thread gestiti; tdq_ipipending: IPI pendenti; tdq_idx: prossimo indice di priorità della coda timeshare; tdq_ridx: precedente indice di priorità della coda timeshare che punta a una lista vuota; tdq_realtime: coda di run per i task in spazio kernel e per i task in spazio utente della classe real-time; tdq_timeshare: coda di run per i task utente della classe time-sharing; tdq_idle: coda di run per i task utente della classe di idle; Le code dei thread sono organizzate con una struttura runq che contiene l array di liste (rq_queues) e una bitmask (rq_status) che segnala la presenza o meno di task nelle relative liste. Ogni singola lista accorpa 4 valori di priorità 7 : avendo un range di priorità 0-255, ci sono 64 liste di task in ogni coda 8. L organizzazione delle strutture dati utilizzate dallo scheduler ULE viene schematizzata nella Figura L accorpamento serve per ridurre il costo (overhead) di gestione delle liste. All interno di ogni singola lista non c è un ordinamento dei thread sulla base delle 4 priorità. 8 Di queste 64 liste di task, ogni coda utilizza solo le liste delle classi di priorità gestite dalla coda stessa.

103 CAPITOLO 4. SCHEDULING IN FREEBSD 92 Figura 4.2: Organizzazione delle strutture dati nello scheduler ULE di FreeBSD

104 CAPITOLO 4. SCHEDULING IN FREEBSD 93 Politica e classi di scheduling I livelli di priorità hanno un range da 0 a 255. Questo range viene suddiviso in 5 classi. Due classi sono dedicate a gestire task in spazio kernel: ithd, con range 0-63, gestisce i top half e kern, con range , gestisce i bottom half. Le restanti tre classi gestiscono i task in spazio utente: real-time, con range , gestisce i task real-time 9, timeshare, con range , gestisce i task time-sharing e idle, con range , gestisce i task di idle. La priorità dei task è inversamente proporzionale al relativo valore (valore 0 priorità più alta, valore 255 priorità più bassa). I processi delle classi in spazio utente hanno delle time slice: al termine del loro quanto di esecuzione sono schedulati in round robin. I processi delle classi real-time e idle hanno delle priorità statiche mentre quelle della classe timeshare sono dinamiche. I processi delle classi in spazio kernel, ithd e kern, hanno priorità statiche e non hanno time slice: possono essere prelazionati solo da processi di maggiore priorità o quando passano in stato di attesa (sleep o attesa di I/O). Politiche in spazio utente La politica di scheduling per i task utente favorisce i task interattivi, I/O bound, a discapito dei task CPU bound. L interattività di un sistema, e quindi l esperienza utente, è legata alla responsività del sistema stesso, ossia alla prontezza del sistema a reagire alle richieste dell utente. Per migliorare l interattività è necessario minimizzare i tempi di risposta dei task interattivi (di solito I/O bound), considerandoli prioritari rispetto ad altri task (e.g. task CPU bound). In questo modo la caratteristica di response time viene favorita a discapito del throughput. I task interattivi, in genere, hanno lunghi tempi di attesa, dovuti all attesa di input da parte dell utente, seguiti da brevi burst di esecuzione in CPU, dovuti all elaborazione delle richieste dell utente. In ULE l interattività di un task viene determinata sulla base del tempo di esecuzione in CPU e del tempo di attesa volontaria del task stesso. Il tempo di attesa non volontaria (e.g. il tempo trascorso da quando il task va in stato di pronto a quando viene mandato in esecuzione) non è un buon indice per determinare lo score di interattività di un task, poiché può essere causato da fattori esterni al comportamento del task stesso, come un alto carico di lavoro che può aumentare l attesa di esecuzione in CPU del task. Il tempo di attesa volontaria viene registrato conteggiando il numero di tick (ts_slptime) trascorsi dal momento in cui il task va in sleep al momento in cui torna in stato di pronto (wakeup). Il tempo di esecuzione del task è semplicemente il numero di tick passati in esecuzione (ts_runtime). Più il rapporto tra tempo di attesa volontaria e tempo di esecuzione assume valori maggiori e più il task viene considerato interattivo. A task interattivi vengono associate alte priorità. In questo modo, in una decisione di scheduling, i task interattivi hanno priorità di esecuzione, riducendo i relativi tempi di risposta. Quando viene creato un nuovo processo, questo eredita dal processo padre (in sched_fork_thread()) la priorità, il tempo di esecuzione in CPU, il tempo di attesa volontaria e altre informazioni sui tick di utilizzo. Inoltre se la somma del tempo di esecuzione e del tempo di attesa volontaria supera una certa soglia (SCHED_SLP_RUN_FORK, con hz a 1000 assume un valore di tick stathz, scalati su base hz), allora questi due parametri del thread figlio vengono frazionati (in sched_interact_fork()) dal rapporto tra la somma dei due parametri e la soglia limite, dimenticando parte della storia del padre. Questa politica permette di determinare rapidamente se il thread figlio è interattivo o meno, indipendentemente dal comportamento del padre. Sempre nella funzione sched_fork_thread() il parametro del tempo di esecuzione in CPU del padre viene incrementato di un tick stathz (scalato su base hz, ossia (hz << 10)/stathz). Inoltre quando un thread termina la sua esecuzione, il relativo tempo di esecuzione in CPU viene aggiunto a quello del padre (in sched_exit_thread()). 9 Il termine real-time qui si riferisce al fatto che, tra le classi di task utente, questa è quella prioritaria.

105 CAPITOLO 4. SCHEDULING IN FREEBSD 94 Aumentando il tempo di esecuzione in CPU del thread padre, si penalizza lo score di interattività e quindi la priorità del thread stesso. Queste ultime due politiche penalizzano il forking di thread, con l intento di evitare che un processo possa monopolizzare l esecuzione in CPU creando tanti thread. Un altra politica di ULE, come per 4BSD, è quella che cerca di minimizzare il thrashing, ossia quel fenomeno che occorre quando la quantità di memoria libera a disposizione dei processi utente è tale per cui è necessario più tempo per la gestione dei page fault e dello swap su disco di quanto ne sia necessario per eseguire i processi che fanno richiesta di nuova memoria. Un buon indice di questo fenomeno è il rapporto tra pagine di memoria libere e richieste di nuove pagine. Quando questo rapporto raggiunge una certa soglia critica, il demone pageout tenta di ridurlo mandando in sleep i processi che richiedono nuove pagine di memoria e liberando le altre pagine di memoria assegnate agli stessi. In questo modo le pagine liberate possono essere assegnate ad altri processi che competono per l esecuzione, senza dover effettuare delle operazioni di swap (ossia quando le pagine in memoria di un certo processo vengono salvate sulla porzione del disco dedicata allo swap). Quando ci sarà un numero sufficiente di pagine libere allora il demone pageout risveglierà i processi che aveva messo in attesa. Per le politiche sopra citate, la priorità di questi processi tenderà ad aumentare (per via del tempo passato in attesa volontaria), rendendoli prioritari per l esecuzione in CPU. Calcolo della priorità I valori effettivi di priorità dei task, ossia quelli considerati in una decisione di scheduling, sono mantenuti nel campo td_priority. Solo i thread della classe timeshare hanno delle priorità dinamiche, ossia calcolate sulla base del comportamento del thread durante l esecuzione. Per gli altri thread le priorità sono assegnata in modo statico. La priorità statica di un thread in spazio kernel (0-127) è mantenuta nel campo td_base_pri. La priorità statica di un thread in spazio utente ( ) è mantenuta nel campo td_base_user_pri. I task della classe timeshare hanno una priorità dinamica, mantenuta nel campo td_user_pri e calcolata sulla base di alcuni parametri, come lo score di interattività e il valore di nice. In circostanze particolari può succedere che il valore contenuto in td_priority sia diverso da quello contenuto in td_base_pri, nel caso di thread in spazio kernel, o da quello contenuto in td_base_user_pri, nel caso di thread in spazio utente (o td_user_pri per i task time-sharing); per esempio, per evitare il problema di priorità inversa si utilizza la tecnica a priorità ereditata (sezione 1.4 per i dettagli): quando il task in esecuzione tenta di accedere a una regione critica tramite lock, già preso da un altro task di priorità inferiore e per la quale esiste una lista di task in attesa, allora la priorità del task che mantiene il lock (e di tutti i task in attesa di rilascio dello stesso lock), eredita il valore di priorità del task in esecuzione (solo se di valore maggiore); così facendo il task che mantiene il lock, e tutti quelli in lista di attesa, potranno andare in esecuzione più rapidamente (per via dell aumento di priorità) rendendo la risorsa condivisa disponibile al task di priorità maggiore. Una volta rilasciata la risorsa condivisa la priorità dei task ritornerà al valore precedente, contenuto in td_base_pri o td_base_user_pri. Priorità dinamica La priorità dinamica (td_user_pri) di un thread della classe timeshare viene determinata da due parametri associati al thread stesso: lo score di interattività (score) e il valore di nice (campo p_nice). Lo score di interattività viene determinato (nella funzione sched_interact_score()) dal rapporto tra il tempo di esecuzione ts_runtime e tempo di attesa volontaria ts_slptime, in questo modo:

106 CAPITOLO 4. SCHEDULING IN FREEBSD 95 (a) se il tempo di attesa è superiore al tempo di esecuzione allora, score = m ( ts_slptime ts_runtime ) se il tempo di esecuzione è superiore al tempo di attesa allora, m score = (m ( ts_runtime ts_slptime )) + m se il tempo di esecuzione è pari al tempo di attesa allora, dove score = m Maximum Interactive Score m = 2 Di default Maximum Interactive Score è impostato a 100. In questo modo si avrà uno score nella metà inferiore del valore massimo per i thread in cui il tempo di attesa supera il tempo di esecuzione e score nella metà superiore del valore massimo per i thread in cui il tempo di esecuzione supera il tempo di attesa. Un thread viene considerato interattivo se il relativo score (sommato al valore di nice) è minore di una certa soglia configurabile (SCHED_INTERACT_THRESH di default a 30). Quindi la caratteristica di interattività è inversamente proporzionale al valore dello score. I valori del tempo di esecuzione e di quello in attesa possono crescere al punto tale da interpretare in modo errato il comportamento recente del thread: per questo motivo, quando la relativa somma raggiunge un limite configurabile (SCHED_SLP_RUN_MAX, di default impostato a hz ), sono ridotti entrambi a una frazione dei loro valori. Questa politica permette di preservare il comportamento recente del task e di tenere in considerazione solo una frazione del comportamento passato. In questo modo si riesce a individuare rapidamente cambiamenti di task da interattivi e non interattivi. Il controllo avviene nella funzione sched_interact_update() e si sviluppa nei seguenti punti, nell ordine: se la somma tra ts_runtime e ts_slptime è inferiore al limite non si applica nessuna riduzione; se la somma tra ts_runtime e ts_slptime va oltre il doppio del limite allora l addendo di valore maggiore assume lo stesso valore del limite, l altro il valore 1; se la somma tra ts_runtime e ts_slptime va oltre un quinto del limite allora i valori dei due addendi sono dimezzati entrambi; altrimenti i valori di ts_runtime e ts_slptime sono entrambi ridotti di un quinto del rispettivo valore; Il valore di p_nice, con range tra -20 e +20 (default 0), contribuisce a determinare lo score di interattività ed il valore della priorità dinamica td_user_pri (e quindi td_base_user_pri) del thread. Valori negativi incrementano la priorità del thread (e riducono lo score), mentre valori positivi decrementano la priorità del thread (e aumentano lo score). La priorità dinamica td_user_pri (e quindi td_base_user_pri) di un thread della classe timeshare viene calcolata nella funzione sched_priority() nel seguente modo: se la somma tra score e p_nice sta sotto la soglia di interattività (SCHED_INTERACT_THRESH di default a 30) allora,

107 CAPITOLO 4. SCHEDULING IN FREEBSD 96 delta = ( PRI_MAX_REALTIME PRI_MIN_REALTIME ) SCHED_INTERACT_THRESH pri = PRI_MIN_REALTIME + (delta (score + p_nice)) altrimenti la priorità viene determinata in questo modo: pri = PRI_MIN_TIMESHARE p_nice + SCHED_PRI_TICKS SCHED_PRI_TICKS può assumere un range di valori da 0 a 23: più il thread è stato in esecuzione di recente in CPU e maggiore è il valore di SCHED_PRI_TICKS (la formula è contenuta nel file sched_ule.c). Notare che se il thread viene considerato interattivo viene posizionato nella coda real-time con un valore di priorità nel relativo range, determinato dallo score e dal valore di p_nice. Un thread che non viene considerato interattivo viene posizionato nella coda timeshare con un valore di priorità nel relativo range, determinato dal valore di p_nice e dall utilizzo recente di CPU del thread, con l intento di assegnare maggiori priorità (e quindi valori bassi) ai thread che sono stati di recente in esecuzione in CPU, beneficiando dei relativi dati in cache. Algoritmo Ritroviamo attività di scheduling in diverse parti del kernel. L algoritmo applicato in una decisione di scheduling è il seguente: si determina la lista di maggior priorità che abbia almeno un thread in stato di pronto e si seleziona il primo thread trovato nella medesima lista. La ricerca del task avviene nelle 3 code di task, realtime, timeshare e idle, nell ordine. Per la coda di task timeshare la ricerca non inizia sempre dal primo indice di priorità 0 (come per le altre due code), ma bensì dall indice contenuto nel campo tdq_ridx (con un eventuale giro di wrap). Ad ogni tick di clock (nella funzione sched_clock()) si controlla se la lista di task referenziata dall indice contiene o meno dei task. Se la lista è vuota allora l indice viene incrementato di una unità (con un wrap a 64, numero totale delle liste nella coda). Questo evita che alcuni thread di più alta priorità possano impedire l esecuzione di thread di priorità inferiore. La ricerca del prossimo thread da mandare in esecuzione è contenuta nella funzione tdq_choose() e la sua complessità temporale è di O(1): la ricerca avviene sempre in un tempo costante, indipendentemente dal numero di processi nel sistema. La funzione tdq_choose() viene invocata ogni volta che si decide di fare uno switch di contesto, implementato nella funzione mi_switch() (codice indipendente dall architettura hardware). La funzione mi_switch() viene chiamata in modo sincrono in diversi punti del kernel, tra i quali: nella funzione ithread_loop(), dopo aver processato tutti gli interrupt pendenti; nella procedura di shutdown del sistema, boot, per permettere di eseguire velocemente thread interrupt; nella funzione critical_exit(), per permettere di prelazionare un thread kernel che non mantiene più regioni critiche e per il quale è stata richiesto uno switch di contesto; nelle funzioni sched_relinquish() e yield(), quando un thread rilascia in modo spontaneo l esecuzione in CPU; nel processo kernel idlepoll, per pollare eventuali attività di dispositivi di networking; nelle funzioni thread_suspend_check() e thread_suspend_switch(), quando un thread viene sospeso;

108 CAPITOLO 4. SCHEDULING IN FREEBSD 97 nella funzione sched_preempt(), invocata nella gestione di un IPI_PREEMPT; nella funzione sched_bind(), nel caso in cui la CPU associata al thread sia diversa da quella sulla quale si sta eseguendo il thread stesso; nel thread kernel idle di ogni CPU, nel caso in cui ci sia almeno un thead in stato di pronto nella relativa coda; nel thread kernel idle di ogni CPU, quando, per le politiche di bilanciamento di carico, viene spostato sulla coda della CPU in idle un thread preso dalla coda di un altra CPU; nella funzione sleepq_wait(), per mettere un thread nella lista di sleep fino a quando verrà risvegliato (usata ad esempio nella sleep()). La funzione mi_switch() viene invocata anche in modo asincrono tramite il meccanismo AST (asynchronous system trap), quando viene processata una trap software asincrona con il flag TDF_NEEDRESCHED attivo. Questo meccanismo è utile ad esempio quando, dopo la gestione di un interrupt, si vuole mandare in esecuzione un thread diverso da quello che era in esecuzione prima dell interrupt. Il controllo di una AST avviene alla fine della gestione di un interrupt, di una trap o di una chiamata di sistema (solo se il controllo sta per tornare in spazio utente). In questo caso si verifica se il campo td_flags del thread in spazio utente interrotto ha il flag TDF_NEEDRESCHED attivo. Il flag TDF_NEEDRESCHED viene attivato nei seguenti punti del kernel: nella funzione sched_add() se la priorità del thread aggiunto alla coda è maggiore di quella del thread in esecuzione (in questo caso si setta la flag del thread in esecuzione); nella funzione sched_clock() quando il thread (utente) in esecuzione ha esaurito il suo time slice a disposizione; nella funzione sched_affinity() se il thread manipolato si trova su una coda di CPU sulla quale non può più essere mandato in esecuzione (in questo caso si setta la flag del thread manipolato); Il calcolo delle priorità dinamiche dei thread in stato di pronto della classe timeshare avviene nella funzione sched_priority(). Questa funzione viene invocata nei seguenti punti: nella funzione sched_nice() per tutti i thread del processo per il quale è stato modificato il valore di nice; nella funzione sched_fork() per calcolare la priorità del nuovo thread figlio (preceduta da sched_interact_fork()) e per ricalcolare la priorità del thread padre (preceduta da sched_interact_update()); nella funzione sched_exit_thread(), alla terminazione di un thread, per ricalcolare la priorità del thread padre (preceduta da sched_interact_update()); nella funzione sched_clock(), ad ogni tick di clock (stathz), per ricalcolare la priorità del thread in esecuzione (preceduta da sched_interact_update()); nella funzione sched_add(), per ricalcolare la priorità del thread da aggiungere; In 4BSD il calcolo delle priorità dinamiche dei task nella classe timeshare avviene in modo periodico e globale (per tutti i task), con una complessità temporale di O(n), con n numero dei thread totali nel sistema. In ULE la priorità dinamica viene calcolata solo per il task in esecuzione o quando un task passa nello stato di pronto. In questo caso la complessità temporale è di O(1). Avendo una complessità temporale costante, indipendente dal numero di task sul sistema, ULE risulta efficiente anche con alti carichi di lavoro.

109 CAPITOLO 4. SCHEDULING IN FREEBSD 98 Time slice I thread delle classi utente hanno delle time slice. Il time slice del thread in esecuzione viene decrementato nella funzione sched_clock(), ad ogni tick di clock (con una frequenza pari al valore di stathz). Quando il suo valore raggiunge 0, si attiva il flag TDF_NEEDRESCHED per forzare una rischedulazione e si resetta la time slice al valore di sched_slice, di default pari a stathz/10. Il time slice di un thread viene resettato (al valore sched_slice) anche nella funzione sched_wakeup(), quando un thread passo in stato di pronto. I thread di pari priorità al termine del loro time slice sono schedulati in round robin. Features In ULE, come in 4BSD, è possibile abilitare il supporto allo scheduling real-time secondo lo standard POSIX. Inoltre tramite opzioni di compilazione è possibile migliorare la reattività del kernel ai cambi di priorità dei task. Una delle nuove caratteristiche di rilievo di ULE riguarda la rivisitazione al supporto alle architetture multiprocessore, secondo il modello SMP. POSIX real-time Il supporto alle classe di scheduling real-time secondo lo standard POSIX (P1003_1B, sezione E per i dettagli) può essere incluso nel kernel di FreeBSD utilizzando l opzione di compilazione _KPOSIX_PRIORITY_SCHEDULING. In questo modo si abilitano le classe di scheduling POSIX sched_fifo, sched_rr e sched_other e le relative chiamate di sistema. Le classi di scheduling sched_fifo e sched_rr con i rispettivi valori di priorità 0-31 sono mappate nella classe utente real-time di ULE, nel relativo range di valori La classe sched_other con valori 0-63 viene mappata nella classe utente timeshare,nel relativo range di valori Durante queste conversioni (in entrambe le direzioni) si tiene conto del fatto che secondo POSIX a valori minori di priorità corrispondono priorità minori, al contrario di FreeBSD e di altri sistemi Unix-like. Notare che il supporto per lo scheduling di processi all estensione real-time POSIX non è pienamente conforme allo standard (P1003_1B): i task della classe sched_fifo non dovrebbero avere timeslice. Kernel preemptive Esistono alcune opzioni di compilazione del kernel che permettono di migliorare la reattività del sistema ai cambi di priorità dei thread. Senza queste opzioni su un cambio di priorità di un thread, o quando un thread passa in stato di pronto, possiamo avere una prelazione del thread in esecuzione (in sched_setpreempt()) solo nei seguenti casi: se la priorità del nuovo thread è maggiore di quella del thread in esecuzione allora viene forzata una prelazione tramite il meccanismo AST (e quindi al ritorno in spazio utente), settando la flag TDF_NEEDRESCHED del thread in esecuzione; (funzione sched_shouldpreempt()) se il thread in esecuzione ha una priorità nel range dei valori della classe di idle, allora questo viene prelazionato anche in spazio kernel, appena esce dall ultima sezione critica di codice mantenuta (in critical_exit()). Con le seguenti opzioni è possibile forzare la prelazione in spazio kernel del thread in esecuzione quando la priorità del nuovo thread sta sotto una certa soglia di valori. PREEMPTION: con questa caratteristica abilitata, su un cambio di priorità di un thread, la prelazione in spazio kernel del thread in esecuzione (funzione sched_setpreempt()) avviene se tutte le seguenti condizioni sono soddisfatte: (a) il nuovo thread ha una priorità maggiore (valore minore) di quello in esecuzione; (b) il valore di priorità del nuovo thread rientra nel range dei valori della classe ithd; (c) il thread attualmente in esecuzione non è in nessuna sezione critica del kernel (la prelazione avviene appena esce da tutte le regioni critiche, in critical_exit()). Questa caratteristica permette di prelazionare thread in spazio kernel in favore di thread della classe ithd;

110 CAPITOLO 4. SCHEDULING IN FREEBSD 99 FULL_PREEMPTION: questa caratteristica elimina la condizione (b) di prelazione della precedente opzione, permettendo di fatto di prelazionare thread in spazio kernel sui cambi di priorità dei thread di tutte le classi, sia utente che kernel. Con questa opzione un thread utente può prelazionare immediatamente un altro thread utente in esecuzione con priorità minore, perfino se quest ultimo si trova in spazio kernel (con l unico vincolo di non essere in nessuna regione critica di codice). Notare che se il thread in esecuzione appartiene ad una classe kernel (e che quindi non può essere prelazionato tramite il meccanismo di AST), e se non mantiene nessuna sezione critica di codice, allora questo thread non verrà mai prelazionato (se non quando il thread in esecuzione va in I/O, rilascia in modo spontaneo la CPU, o entra ed esce da almeno una regione critica). SMP Il principale obiettivo di ULE per il supporto SMP è riuscire a trovare un buon compromesso tra i costi necessari per la migrazione dei thread tra diverse CPU e l utilizzo efficiente di tutte le risorse di calcolo disponibili. Quando un thread è in esecuzione su una CPU parte dei suoi dati sono mantenuti nei vari livelli di cache della CPU stessa. Una migrazione del thread su un altra CPU comporta non solo un costo dovuto a dover riportare nella cache della nuova CPU gli stessi dati che aveva in quella vecchia ma anche un costo di invalidazione dei dati in cache sulla vecchia CPU. Questo comporta un overhead sia in termini di tempo sia di consumi di energia. ULE ha tre meccanismi per il bilanciamento di carico delle CPU. Il primo prevede un bilanciamento statico del sistema ogni volta che un thread passa in stato di pronto o quando cambia la relativa priorità, in modo simile a quanto avviene per 4BSD. Il secondo, chiamato pull cpu migration, consiste nel migrare task su CPU in idle prendendoli da altre CPU non in idle. Il terzo, chiamato push cpu migration, consiste nel valutare in modo periodico il bilanciamento di carico del sistema ed eventualmente ripristinarlo. ULE, come 4BSD, supporta architetture con CPU multicore e multithreading (SMT e HTT): ogni singolo core logico (in caso di SMT e HTT) o fisico (in caso di multicore) viene riconosciuto come una risorsa di calcolo (d ora in avanti identificata con il termine generico CPU), sulla quale distribuire il carico di lavoro. A differenza di 4BSD però in ULE tutte le risorse di calcolo non sono considerate sullo stesso livello, e nelle decisioni per il bilanciamento di carico, si tiene conto sia della distanza di cache sia della affinità in CPU dei thread. La caratteristica di distanza di cache viene valutata sulla base di un riconoscimento della topologia delle CPU del sistema. Strutture dati Come è possibile notare nella figura Figura 4.2, ad ogni CPU è associata una struttura dati di tipo tdq, che mantiene, oltre alle code di task che possono essere eseguiti sulla CPU, alcune informazioni che vengono utilizzate nelle decisioni di bilanciamento di carico, come il carico di cpu (campo tdq_load, numero totale di thread mantenuti sulle code della CPU), il numero di thread che possono essere trasferiti su altre CPU (campo tdq_transferable) e il gruppo di CPU di appartenenza (campo tdq_cg). Nella struttura informativa dei thread (struttura thread) sono contenute altre informazioni per il supporto SMP, come la maschera delle CPU sulle quali il thread può essere eseguito (td_cpuset) e la CPU considerata più affine al thread (campo ts_cpu). Topologia di CPU Il riconoscimento della topologia di CPU in ULE organizza le CPU in gruppi gerarchici. La struttura di ogni gruppo (cpu_group), è così composta: cg_parent: puntatore al gruppo padre (unico); cg_child: puntatore al primo gruppo figlio (uno o più); cg_mask: maschera di cpu di questo gruppo; cg_count: numero totale di cpu in questo gruppo;

111 CAPITOLO 4. SCHEDULING IN FREEBSD 100 cg_children: numero totale di gruppi figli; cg_level: livelli di cache condivisi ( none, l1, l2, l3); cg_flags: tipologia di core del gruppo (htt, smt, any); Ogni gruppo contiene una o più CPU che possono condividere o meno i diversi livelli di cache. L organizzazione dei gruppi prevede una gerarchica che, a seconda del tipo di architettura del sistema, può essere formata da un minimo di 1 livello ad un massimo di 2 livelli. Ogni gruppo può avere un gruppo padre (tranne il gruppo in cima alla gerarchia) e uno o più gruppi figli (tranne i gruppi foglia). Ad esempio, in un sistema con architetture UMA con 2 CPU a doppio core che condividono il livello 1 di cache, avremmo 2 livelli di gerarchia, per un totale di 4 gruppi: il primo gruppo (group_1) in cima alla gerarchia racchiude i 2 package e quindi avrà un unico gruppo figlio (group_2). Questo secondo gruppo rappresenta i 4 core del sistema e quindi avrà due gruppi figli (group_3 e group_4) i quali racchiudono ognuno 2 core. Mentre, in un sistema con architetture UMA con 1 CPU a singolo core 5HTT (5 hyperthreading), avremmo un unico livello di gerarchia, per un totale di 2 gruppi: il primo gruppo (group_1) in cima alla gerarchia racchiude l unico package e quindi avrà un unico gruppo figlio (group_2) che racchiude i 5 hyper-threading del sistema. Questi due esempi sono rappresentati nella Figura 4.3. Load balancing ULE effettua un bilanciamento di carico in tre diversi momenti: ogni volta che bisogna decide su quale coda di CPU associare un nuovo thread (funzione sched_add()), in modo periodico tramite il meccanismo di push cpu migration e ogni volta che una CPU è in idle tramite il meccanismo di pull cpu migration. Quando viene risvegliato un thread, ne viene creato uno nuovo o quando ne viene modificata la relativa priorità, bisogna decidere su quale coda di CPU assegnare il thread (funzione sched_add()). La ricerca della CPU avviene nella funzione sched_pickcpu() e si articola nei seguenti punti, nell ordine: se il thread mantiene dei lock su una CPU (campo td_pinned) oppure è stato prelazionato in favore di un altro thread (flag SRQ_OURSELF), allora viene assegnato all ultima CPU sulla quale è andato in esecuzione, identificata dal campo ts_cpu; se la CPU affine del thread (campo ts_cpu) rientra tra le CPU sulle quali il thread può essere mandato in esecuzione (campo td_cpuset), allora il thread viene assegnato a questa CPU nei seguenti due casi: (a) se sulla CPU affine si stanno eseguendo solo thread della classe di idle (campo tdq_lowpri); (b) se la priorità del thread è maggiore (valore minore) del valore più basso di priorità (campo tdq_lowpri) dei thread posti sulla coda della CPU affine e se c è ancora affinità per questa CPU (ossia se il campo ts_rltick è maggiore del numero di tick globali ticks sottratto del valore 2); partendo dal gruppo della CPU affine al thread (campo tdq_cg), e risalendo ai gruppi padre, si determina il nuovo gruppo di CPU più affine al thread. Un gruppo viene considerato affine se il numero di tick globali (su base hz) memorizzati prima dell ultimo switch di contesto del thread (campo ts_rltick) risultano maggiori del numero di tick globali ticks sottratto dal valore di livello di condivisione di cache nel gruppo considerato (campo cg_level, con valori 0, 1 per L1, 2 per L2 e 3 per L3). Se si riesce a determinare il gruppo, allora in questo si seleziona la CPU con minor carico la cui massima priorità dei suoi thread (campo tdq_lowpri) sia minore della priorità del nuovo thread. Se non si riesce a determinare il gruppo, oppure nel gruppo non si riesce a determinare una CPU con le condizioni precedenti, allora si seleziona la CPU con minor carico sul sistema. una volta determinata la CPU target nel precedente punto, si determina se sia più efficiente assegnare il thread alla CPU locale (la CPU sulla quale si sta eseguendo

112 CAPITOLO 4. SCHEDULING IN FREEBSD 101 Figura 4.3: Esempi di riconoscimento di topologie di CPU in ULE la funzione sched_add()) secondo le seguenti condizioni: (a) il thread può essere mandato in esecuzione sulla CPU locale (campo td_cpuset); (b) la priorità del nuovo thread è maggiore della priorità più alta dei thread tdq_lowpri della CPU locale tdq_lowpri; (c) sulla CPU target non si stanno eseguendo thread della sola classe di idle. Se queste condizioni sono soddisfatte allora il thread viene assegnato alla CPU locale, altrimenti a quella target. Se il thread non è stato associato alla CPU locale, allora tramite la funzione tdq_notify() si invia un interrupt IPI_PREEMPT alla CPU target (che invocherà la funzione sched_setpreempt()), secondo le condizioni contenute in sched_shouldpreempt (vedere la sezione Kernel preemptive in 4.1.2), più la condizione seguente: il nuovo thread ha una priorità della classe real-time o maggiore e il thread in esecuzione sulla CPU target ha una priorità della classe timeshare o minore. Se il thread è stato associato alla CPU locale si valuta se prelazionare il thread in esecuzione sulla stessa, invocando la funzione sched_setpreempt() (vedere la sezione

113 CAPITOLO 4. SCHEDULING IN FREEBSD 102 Kernel preemptive in 4.1.2). Pull cpu migration Questa politica evita di avere alcune CPU in idle mentre ci sono altre CPU con alti carichi di lavoro. Gli scenari tipici sono quelli in cui ci sono una parte di processi che terminano velocemente solo su alcune CPU. Questa politica di bilanciamento, invocata nel thread di idle sched_idletd di ogni CPU, si sviluppa nei seguenti punti (funzione tdq_idled()): tramite la funzione sched_highest() si determina la CPU dalla quale prendere un thread; nella scelta si tiene conto della topologia delle CPU: la ricerca incomincia tra le CPU del gruppo di appartenenza della CPU di idle e consiste nel determinare la CPU con il maggior carico (campo tdq_load); nel caso di gruppi di CPU con core fisici esiste una soglia minima di carico (steal_thresh modificabile, di default a 2) oltre la quale è possibile cedere thread; in caso di gruppi di CPU logiche, quindi SMT o HTT, questa soglia è impostata a 1; se non viene trovata nessuna CPU nel gruppo della CPU in idle allora la ricerca riparte dal relativo gruppo padre, attraversando tutti i gruppi figli, in modo ricorsivo (fino alla radice della gerarchia). tramite la funzione (tdq_move()) si determina quale thread spostare dalla coda della CPU selezionata nel punto precedente: la politica di selezione del thread è la stessa utilizzata nella decisione di quale sia il prossimo thread da mandare in esecuzione (sezione Algoritmo in 4.1.2) con un controllo aggiuntivo che verifica se il thread può essere eseguito sulla CPU target (campo td_cpuset) e se può migrare (campo td_pinned). Notare che, poiché viene trasferito un unico thread sulle CPU in idle, questa politica evita solo di avere CPU in idle, non effettua un bilanciamento di carico (su alti carichi). Push cpu migration La politica di push cpu migration permette di ripristinare il bilanciamento quando il carico non è distribuito in modo uniforme, anche in assenza di CPU in idle. Questa politica di bilanciamento viene eseguita su un unica CPU (quella di boot) in modo periodico (invocata nella funzione sched_clock()) con una frequenza (espressa in tick su base stathz) che varia, in modo randomico, nel range 0.5 balance_interval 1.5 balance_interval, dove balance_interval è configurabile e il valore di default è pari al valore di stathz. La funzione sched_balance() si sviluppa nei seguenti punti: partendo dalla radice dell albero della gerarchia di gruppi e scendendo fino alle foglie, per ogni gruppo di CPU che abbia un gruppo padre si determina la coda di CPU con maggior carico e quella con minor carico (il carico viene determinato dal campo tdq_load); il numero di thread che si trasferiscono nella coda con minor carico è pari alla metà della differenza di carico tra le due code; la politica di selezione dei thread da trasferire (in tdq_move()) è la stessa utilizzata nella decisione di quale sia il prossimo thread da mandare in esecuzione (sezione Algoritmo in 4.1.2) con un controllo aggiuntivo che verifica se il thread può essere eseguito sulla CPU target (campo td_cpuset) e se può migrare (campo td_pinned). alla CPU dove si sono trasferiti i thread viene inviato un interrupt IPI_PREEMPT per rischedulare il nuovo workload (funzione sched_setpreempt()) Chiamate di sistema Seguono le principali chiamate di sistema utilizzate per lo scheduling di processi in FreeBSD: getpriority(), setpriority(): ritorna o modifica il valore di nice di uno o più thread su base pid del processo, id di gruppo di processi o id utente;

114 CAPITOLO 4. SCHEDULING IN FREEBSD 103 rtprio(): modifica o recupera la classe di priorità (RTP_PRIO_REALTIME, RTP_PRIO_NORMAL, RTP_PRIO_IDLE mappate nelle classi di 4BSD PRI_REALTIME, PRI_TIMESHARE, PRI_IDLE) e il relativo valore di un thread utente su base pid; yield(): forza una prelazione del thread utente in esecuzione (e imposta la relativa priorità al minimo); cpuset(), cpuset_setid(), cpuset_getid(): creano, settano e ritornano il cpuset associato al thread o al processo specificato; cpuset_getaffinity(), cpuset_setaffinity(): ritorna o modifica la maschera delle CPU da associare al thread, al processo o al cpuset specificato; Inoltre, abilitando l estensione real-time secondo lo standard POSIX (P1003_1B), si hanno a disposizione le relative chiamate di sistema: sched_setparam(), sched_getparam(): modifica o recupera la priorità di un thread; sched_setscheduler(), sched_getscheduler(): modifica o recupera la classe di scheduling e la relativa priorità di un thread; sched_yield(): forza una prelazione del thread utente in esecuzione (senza modificare la relativa priorità); sched_get_priority_max(), sched_get_priority_min(): ritorna priorità massima e minima delle classe di scheduling; sched_rr_get_interval(): ritorna il time slice del thread (totale e non effettivo); Oltre alle chiamate di sistema esistono anche alcune sysctl relative allo scheduling di processi: kern.sched.slice: time slice per i thread utente (in unità di tick (stathz), con valore di default pari a stathz/100); kern.sched.interact: soglia dello score di interattività sotto la quale un thread viene considerato interattivo (default a 30); kern.sched.preempt_thresh: soglia del valore di priorità sotto la quale può essere prelazionato il thread in esecuzione in spazio kernel (default a 0; con la caratteristica PREEMPTION abilita assume il valore di PRI_MIN_KERN pari a 64, mentre con FULL_PREEMPTION abilitato assume il valore di PRI_MAX_IDLE, pari a 255; kern.sched.static_boost: valore di priorità che viene assegnato quando un thread, con una priorità minore rispetto a questo valore, va in sleep (default a 160, PRI_MIN_TIMESHARE); kern.sched.idlespinthresh: soglia (default a 4, e il controllo viene fatto nel thread di idle di ogni CPU) relativa alla somma dei numeri di switch avvenuti nel tick precedente e in quello corrente oltre la quale si mette in pausa una CPU (solo se diversa da SMT o HTT) per il numero di cicli di pausa contenuto in kern.sched.idlespins; kern.sched.idlespins: numero di cicli di pausa (default a 10000) che una CPU deve effettuare se supera la soglia di switch kern.sched.idlespinthresh; kern.sched.affinity: numero di tick (default a hz/1000) che, moltiplicati al livello di cache (0, 1 per L1, 2 per L2, 3 per L3), determina l affinità tra thread e CPU, nella scelta della coda di CPU da associare ad un thread passato in stato di pronto;

115 CAPITOLO 4. SCHEDULING IN FREEBSD 104 kern.sched.rebalance: abilita o disabilita il bilanciamento di carico periodico del sistema (politica di push cpu migration ); kern.sched.balance_interval: valore (default a stathz) utilizzato per determinare la frequenza di bilanciamento di carico periodico del sistema ( push cpu migration ); kern.sched.steal_idle: abilita o disabilita la politica di bilanciamento di carico pull cpu migration ; kern.sched.steal_thresh: numero minimo di thread che una coda di CPU deve mantenere affinché possa cedere qualche thread ad altre CPU (default log(mp_ncpu) con un valore massimo di 3 e con mp_ncpu numero totale di CPU); kern.sched.topology_spec: scrive su standard output la topologia delle CPU del sistema in formato XML. Considerazioni Il calcolo delle priorità dinamiche per i task utente permette di considerare i task I/O bound prioritari rispetto a quelli CPU bound. Poichè, in genere, i task interattivi sono I/O bound, questa politica migliora l interattività del sistema. Inoltre se un task viene considerato interattivo viene messo nella coda della classe real-time, prioritaria rispetto alla classe timeshare (classe di default per i thread utente). In 4BSD il calcolo delle priorità dinamiche dei task nella classe timeshare avviene in modo periodico e globale (per tutti i task), con una complessità temporale di O(n), con n numero dei thread totali nel sistema. In ULE la priorità dinamica viene calcolata solo per il task in esecuzione o quando un task passa nello stato di pronto. In questo caso la complessità temporale è di O(1). Avendo una complessità temporale costante, indipendente dal numero di task sul sistema, ULE risulta efficiente anche con alti carichi di lavoro. L equità sul tempo di esecuzione viene garantita solo a livello thread e non a livello di processo, come avviene in 4BSD. A differenza di 4BSD però dove un thread eredita la priorità del processo padre, in ULE il forking viene penalizzato (anche in modo ricorsivo), assegnando basse priorità sia al processo padre che ai relativi figli: se ci sono due processi e uno dei due crea 100 thread allora a questo (e ai suoi thread) verrà assegnata una bassa priorità di esecuzione rispetto al processo che non forka. Tuttavia non esiste un meccanismo che garantisca equità tra diversi utenti. Lo scheduler ULE scala bene rispetto al numero di CPU del sistema: ogni risorsa di calcolo ha un proprio set di code di task che può mandare in esecuzione e quindi l accesso a queste code non è in competizione tra le risorse di calcolo. Il supporto SMP risulta più efficiente rispetto a quello di 4BSD: le politiche di bilanciamento di carico in ULE, nella scelta della CPU alla quale assegnare un thread, considerano sia la distanza di cache sia l affinità del thread stesso; inoltre il bilanciamento di carico avviene in modo periodico. Ulteriori dettagli sullo scheduler ULE possono essere trovati nel libro The Design and Implementation of the FreeBSD Operating System [92] e visionando i sorgenti di FreeBSD [89], in particolare: sched_ule.c: contiene le funzioni per lo scheduling di processi dello scheduler ULE; kern_synch.c e kern_switch.c: contengono altre funzioni per lo scheduling di processi; proc.h: contiene le principali strutture dati per lo scheduling di processi;

116 CAPITOLO 4. SCHEDULING IN FREEBSD 105 priority.h: contiene le principali define delle classi di priorità; p1003_1b.c: contiene le chiamate di sistema POSIX per l estensione real-time dello scheduling di processi; 4.2 Confronto con Linux Il lavoro di analisi e sperimentazione di nuove proposte per le attività di scheduling di processi in FreeBSD risulta molto inferiore a quello fatto per il kernel Linux. Questo probabilmente è dovuto ad una maggior diffusione di Linux, sia in ambito aziendale/professionale sia in quello comunitario. Confrontando le politiche di scheduling di processi in FreeBSD (versione 8.2) rispetto a quelle del kernel Linux (versione 3.1), notiamo le seguenti differenze: mancanza in FreeBSD di un meccanismo di scheduling modulare, che permetta di implementare nuove politiche di scheduling in modo flessibile e pulito, evitando di alterare il codice delle politiche esistenti (si veda la sezione Classi di scheduling in 3.1.5); mancanza in FreeBSD di una politica di scheduling che tenga conto del tempo effettivamente utilizzato dai task utente, con l obiettivo di migliore l equità nella ripartizione del tempo di esecuzione in CPU (si veda la sezione Modulo CFS in 3.1.5); mancanza in FreeBSD di un meccanismo che permetta di creare gruppi di task con diversi criteri di aggregazione, con l intento di poter distribuire il tempo di esecuzione in CPU a gruppi di task (si veda la sezione Group scheduling in 3.1.5); mancanza in FreeBSD di un meccanismo che permetta di definire della banda di utilizzo per task kernel e/o per quelli utente, (si veda la sezione Real-time group scheduling in 3.1.5); a differenza del kernel Linux ufficiale, in FreeBSD i gestori degli interrupt (bottom e top half) sono gestiti in contesto di processo e quindi sono prelazionabili: questo aumenta la reattività del sistema operativo nel gestire eventi di maggiore priorità Supporto al real-time Per quanto riguarda il supporto al real-time in FreeBSD, inteso come un kernel il più possibile prelazionabile supportato da un algoritmo di scheduling real-time, non sono ad oggi conosciute proposte. Al contrario, in Linux, il tema del real-time viene affrontato da anni, correlato da diverse proposte (sezione 3.2). Possiamo individuare il mancato supporto al real-time in FreeBSD in tre aspetti principali: la mancanza di una politica di scheduling basata su un algoritmo di scheduling real-time, che tenga conto dei vincoli temporali di esecuzione dei task; un kernel maggiormente prelazionabile, per evitare lunghe latenze prima che un task real-time possa essere mandato in esecuzione; la mancanza di un supporto a timer ad alta risoluzione, indipendenti dal clock di sistema, per aumentare la precisione nella gestione dei vincoli temporali dei task real-time. 10 Per Linux, la patch RT-Preempt converte i gestori di interrupt da un contesto non prelazionabile in uno a processo, prelazionabile

117 CAPITOLO 4. SCHEDULING IN FREEBSD 106 In Linux, per ognuno di questi aspetti, sono state proposte delle soluzioni: SCHED_DEADLINE che introduce una nuova politica di scheduling, basata sull algoritmo di scheduling real-time EDF (sezione 3.2.2), RT-Preempt che aumenta notevolmente la possibilità di prelazionare tutte le attività che convivono nel kernel (sezione 3.2.1), e gli htimer per supportare timer ad alta risoluzione [32].

118 Capitolo 5 Real-time EDF per FreeBSD In questo capitolo si presenta una politica di scheduling real-time per FreeBSD, DEADLINE-BSD, mutuata dal lavoro fatto per il kernel Linux in SCHED_DEADLINE (sezione 3.2.2). A causa delle sostanziali differenze tra il kernel Linux e FreeBSD, come ad esempio la gestione dei timer, dei lock, il framework di scheduling, le strutture dati, per citarne alcune, questo lavoro non può essere considerato come un semplice porting della classe di scheduling fatta per Linux, ma bensì una re-implementazione della stessa. La prima sezione riporta l implementazione di DEADLINE-BSD nello scheduler ULE di FreeBSD, con i relativi dettagli tecnici, come le strutture dati, le politiche, l algoritmo e relative feature. Nella seconda sezione i risultati sperimentali mostrano come questa classe di scheduling riesca a soddisfare requisiti real-time nel kernel FreeBSD. Infine, alcune considerazioni sull implementazione chiudono questo capitolo. Per chiarezza di esposizione, in questo capitolo i termini task e thread sono utilizzati in modo intercambiabile (anche se di fatto un task può avere più thread). Inoltre per task real-time si intendo task con vincoli temporali di esecuzione e non task della classe real-time POSIX (sezione E). 5.1 Dettagli implementativi di DEADLINE-BSD Le politiche di scheduling che ritroviamo nel kernel ufficiale di FreeBSD, implementate nelle classi di scheduling ithd, real-time, timeshare e idle, funzionano egregiamente nel loro contesto applicativo (e.g. server/workstation). Tuttavia queste politiche non possono fornire garanzie su vincoli temporali di esecuzione richieste dalle applicazione real-time. Infatti, le decisioni di scheduling di queste classi non si basano su vincoli temporali di esecuzione dei task ma bensì sulla relativa priorità e, per alcune, sul relativo quanto di esecuzione a disposizione. Con queste politiche non è possibile garantire un budget temporale di esecuzione (vincolo - budget di esecuzione) di un task rispettando una deadline temporale (vincolo - deadline relativa), ad esempio specificare che un task possa essere eseguito per 20 millisecondi con una deadline relativa di 100 (calcolata nel momento in cui il task va in stato di pronto). Inoltre, il tempo trascorso tra due burst di esecuzione consecutivi di un task (vincolo - periodo relativo) non è deterministico e non può essere fissato a priori. Il rispetto di questi vincoli è fondamentale nelle applicazioni real-time per ottenere risultati corretti. Senza uno scheduling che tenga conto di questi vincoli, di fatto, non è possibile fare uno studio di fattibilità del sistema, e non ci sono garanzie che siano rispettati i vincoli temporali di esecuzione dei task. 107

119 CAPITOLO 5. REAL-TIME EDF PER FREEBSD 108 DEADLINE-BSD è una nuova politica di scheduling per lo scheduler ULE di FreeBSD 1 che implementa l algoritmo di scheduling EDF (rif ), con l aggiunta di un meccanismo che limita il tempo di esecuzione dei task, garantendo un isolamento temporale tra essi. In questa implementazione, un task real-time viene rappresentato dalle seguenti informazioni 2 : dl_runtime: massimo tempo di esecuzione (relativo) di ogni istanza del task, anche chiamato budget di esecuzione; dl_deadline: deadline (relativa) entro la quale deve essere consumato il budget a disposizione dell istanza corrente del task; dl_period: periodo di tempo (relativo) all interno del quale può essere attivata un istanza del task; nella nostra implementazione coincide con la deadline relativa. dl_bw: banda di utilizzo del task, ottenuta dal rapporto tra il dl_runtime e dl_period, ed utilizzata per validare il workload real-time nel test di ammissione. runtime: tempo di esecuzione rimanente per l istanza corrente del task (inizialmente pari a dl_runtime); exec_start: tempo di sistema in cui è stato aggiornato il runtime dell istanza del task; deadline: deadline assoluta (in tempo di sistema) dell istanza corrente del task; Nella Figura 5.1 viene riportata la rappresentazione grafica di un task real-time. Figura 5.1: Rappresentazione di un task real-time in DEADLINE-BSD Classe di scheduling La classe di scheduling deadline è stata progettata in ULE in modo tale da lasciare inalterate le caratteristiche di funzionamento delle altre classi, ithd, real-time, timeshare e idle. Per quanto riguarda la priorità esecutiva della classe deadline, è stato deciso di dare una priorità maggiore rispetto alle classi utente real-time, timeshare e idle, ma minore rispetto alla classe ithd. Questo significa che un task della classe deadline in stato di pronto vincerà la competizione per l esecuzione in CPU rispetto a un task delle classi real-time, timeshare e idle, ma la perderà rispetto a un task della classe ithd 3. Questa scelta è stata motivata dalla seguenti ragioni: 1 Applicabile anche allo scheduler 4BSD. 2 L unità di misura di queste informazioni è il microsecondo. 3 La scelta effettiva del prossimo thread da mandare in esecuzione si basa sulle priorità dei thread e non sulle relative classi di scheduling: per via della tecnica della priorità eredita può succedere che un thread della classe timeshare prelazioni uno di classe deadline.

120 CAPITOLO 5. REAL-TIME EDF PER FREEBSD 109 i gestori degli interrupt top e bottom half (e.g. I/O ) sono gestiti da task della classe kernel ithd, ed è fondamentale eseguirli appena passano in stato di pronto, evitando di introdurre instabilità nel sistema o perdite di dati. Anche l eventuale I/O dei task deadline viene gestito da questi task. Per garantire i vincoli temporali di un task deadline che richiede I/O, è fondamentale eseguire il prima possibile il relativo gestore di interrupt, in modo tale che il task deadline possa andare in stato di pronto e quindi essere mandato in esecuzione. per rispettare i vincoli temporali dei task deadline è importante mandarli in esecuzione appena passano in stato di pronto, considerandoli prioritari rispetto ad altri task utente; tramite una gestione della capacità di calcolo dei task deadline è possibile definire, all interno di una finestra temporale, il tempo di esecuzione globale da dedicare a questi task. In questo modo è possibile riservare del tempo di esecuzione certo (quello restante nella finestra temporale) per task delle altre classi utente di scheduling. Strutture dati Nella struttura informativa dei task di ULE (td_sched), le informazioni dei task deadline sono mantenute dalla struttura ts_dl (di tipo dl_entity): rb_node: nodo associato al task dell albero red-black; dl_runtime: massimo tempo di esecuzione per ogni istanza del task (budget, in us); dl_deadline: deadline relativa di ogni istanza del task (in us); dl_period: periodo di tempo tra due istanze del task (in us); dl_bw: rapporto tra dl_runtime e dl_period (ossia banda di esecuzione del task); runtime: tempo rimanente per l istanza corrente del task (in us); exec_start: tempo di sistema in cui è stato aggiornato il runtime dell istanza del task (in us); deadline: deadline assoluta per l istanza corrente del task (in us); flags: flags di scheduling; dl_throttled: indica se il task ha esaurito il suo budget di esecuzione; dl_new: indica se è stata attivata una nuova istanza del task; dl_timer: timer utilizzato per attivare nuove istanze del task; stats: informazioni statistiche sul task; debug: informazioni di debug sul task; In ULE, ogni risorsa di calcolo mantiene il proprio set di task da eseguire in una struttura dati dedicata (di tipo tdq). Questo set è organizzato da tre code di task runq. Per la gestione della lista dei task deadline viene utilizzato un albero red-black (sezione 3.1.5), uno per ogni CPU, ordinato in base al campo deadline associato ai task. L albero è mantenuto nelle struttura tdq_dl. Le informazioni contenute in questa struttura (di tipo dl_rq) sono le seguenti: rb_root: radice dell albero red-black; rb_leftmost: punta al nodo più a sinistra dell albero (il task con deadline più prossima alla scadenza); dl_nr_running: numero di task deadline (in stato di pronto) mantenuti nell albero; dl_nr_total: numero totale di task deadline (in stato di pronto e non) a cui è stata riservata della banda esecutiva su questa CPU; exec_clock: totale tempo di esecuzione utilizzato da task deadline su questa CPU (in us); Inoltre, per ogni CPU, una struttura dati (dl_bw) mantiene la banda di utilizzo dei task deadline:

121 CAPITOLO 5. REAL-TIME EDF PER FREEBSD 110 bw: banda allocata su questa CPU per l esecuzione di task deadline; total_bw: banda utilizzata dalla CPU per l esecuzione di task di classe deadline; L organizzazione delle strutture dati utilizzate per le attività di scheduling in ULE con DEADLINE-BSD viene sintetizzata nella Figura 5.2.

122 CAPITOLO 5. REAL-TIME EDF PER FREEBSD 111 Figura 5.2: Organizzazione delle strutture dati nello scheduler ULE di FreeBSD con DEADLINE-BSD

123 CAPITOLO 5. REAL-TIME EDF PER FREEBSD 112 Politica e algoritmo Un task con deadline ottiene una fase di computazione in CPU (chiamata istanza o anche burst di esecuzione), che può essere attivata in modo periodico, sporadico, o aperiodico, a seconda dell implementazione del task stesso. La durata (massima) di computazione dell istanza di un task è contenuta nel campo dl_runtime della struttura informativa del task, mentre il lasso di tempo entro il quale questa computazione deve avvenire, chiamata deadline relativa, è contenuto nel campo dl_deadline. La deadline assoluta, campo deadline, viene calcolata di volta in volta ed è pari al tempo assoluto del sistema nel quale viene attivata l istanza del task, sommato alla sua deadline relativa. Il periodo di tempo entro il quale può essere attivata una nuova istanza del task è contenuto nel campo dl_period. Per attivazione di un istanza del task si intende il primo passaggio in stato di pronto del task all interno del periodo. La banda di utilizzo dei task, campo dl_bw, è determinata dal rapporto tra il budget assegnato e il relativo periodo, ossia dl_runtime/dl_period. In DEADLINE-BSD il periodo dl_period coincide con la deadline relativa dl_deadline. Questo significa che il budget di ogni istanza del task (dl_runtime) deve essere consumato all interno di tutto il periodo (dl_period). Questa assunzione ci permette di utilizzare un semplice test di ammissione per validare il workload real-time, come vedremo in seguito. In una decisione di scheduling l algoritmo EDF seleziona il task da mandare in esecuzione sulla base dei valori delle deadline assolute (campo deadline) dei task: viene mandato in esecuzione il task con il minor valore di deadline, ossia la deadline più prossima alla scadenza. Isolamento temporale Ogni istanza di un task può stare in esecuzione per, al massimo, un tempo pari a dl_runtime. Durante l esecuzione di un task di classe deadline il valore di runtime (che inizialmente è pari a dl_runtime) viene decrementato ad ogni tick del clock dedicato allo scheduling (di un valore pari al tempo corrente del sistema sottratto al tempo di sistema contenuto in exec_start). Inoltre il valore di runtime viene aggiornato in altri punti critici, come prima di uno switch di contesto. Solo il tempo di effettiva esecuzione del task viene considerato in runtime: l eventuale tempo che il task passa in stato di wait o di sleep non viene conteggiato. Quando runtime raggiunge il valore zero, il task viene messo in stato di attesa e viene tolto dal relativo albero, per poi essere riaccodato quando sarà attivata una nuova istanza (tramite l uso di un timer di wakeup, sulla base di dl_period). Questa caratteristica viene chiamata isolamento temporale, poiché l esecuzione di un task non può essere influenzata dal comportamento di esecuzione di altri task: un task non può rimanere in esecuzione oltre il suo budget. Notare che all interno del periodo l istanza di un task può passare in stato di pronto più volte, per via degli switch di contesto che possono avvenire in modo volontario, come ad esempio per I/O del task stesso, o involontario, come ad esempio l esecuzione di un task con una deadline prioritaria oppure di un task di maggiore priorità (come quelli della classe ith). Comunque, all interno del periodo, l istanza del task può andare in esecuzione per al massimo dl_runtime, indipendentemente dagli switch di contesto. Gestione della capacità di calcolo e test di ammissione Per cercare di rispettare i vincoli temporali di esecuzione, e quindi effettuare uno scheduling con deadline, è importate mandare in esecuzione i task deadline prima possibile. Inoltre è importante poter verificare la schedulabilita del workload real-time, ossia utilizzare un controllo di ammissione per i task real-time da sottomettere. Senza questi due meccanismi il rispetto dei vincoli temporali esecutivi dei task non potrebbero essere garantiti.

124 CAPITOLO 5. REAL-TIME EDF PER FREEBSD 113 La gestione della capacità esecutiva dei task deadline è regolata dai seguenti parametri di sistema (sysctl): kern.sched.dl_period_us (default a usec o 1 sec); kern.sched.dl_runtime_us (default a usec o 0.6 sec); Questi parametri restituiscono (se letti) o settano (se scritti) il budget totale di esecuzione per i task deadline all interno del relativo periodo, per ogni CPU del sistema. I valori di default permettono di riservare un tempo di CPU pari a 400 millisecondi ogni secondo per eseguire task appartenente alle altri classi utente. Questa gestione della capacità di calcolo per i task deadline, insieme all assunzione che l informazione di periodo e di deadline dei task coincidono, ci permettono di definire il seguente controllo di ammissione : n dl_bw <= C (dl_runtime_us/dl_period_us) i=0 con C numero di CPU del sistema e n numero di task di classe deadline in stato di pronto. Questo controllo garantisce una capacità di calcolo sufficiente per eseguire il workload real-time. Ogni task deadline sottomesso nel sistema, viene validato dal controllo di ammissione: se il controllo fallisce, il task non può essere inserito. Settando il parametro kern.sched.dl_runtime_us a 1 si disabilita il controllo di ammissione. In questo caso però non ci sono garanzie che ci sia una capacità di calcolo sufficiente per rispettare le deadline esecutive dei task sottomessi. Questa gestione non prevede l utilizzo di un timer e di un contatore di tick globale per limitare la capacità esecutiva dei task real-time in modo periodico. Infatti, l implementazione della classe deadline prevede già un meccanismo di limitazione periodica della capacità di esecuzione per ogni task (isolamento temporale). Budget e deadline di esecuzione La deadline di un task real-time spesso dipende da un evento esterno 4. Ad esempio per un player video la deadline di esecuzione potrebbe dipendere dalla qualità (in termini di fluidità) del video che si vuole ottenere. Per avere un video di 25 frame per secondo, l applicazione dovrà avere una deadline relativa (uguale al periodo) di 1 secondo, con un tempo di esecuzione pari (almeno) al tempo impiegato dall applicazione per fornire 25 frame. Se, durante la riproduzione del video, l applicativo non rispetta (direttamente o non) una delle sue deadline di esecuzione periodiche, l utente avvertirà degli scatti nel video. Noto l algoritmo e la dimensione dell input di un task real-time, il tempo massimo di esecuzione della relativa istanza può essere determinato a priori. Comunque, per alcune dimensioni di input, potremmo non essere in grado di rispettare certe deadline di esecuzione. In questi casi è fondamentale l impiego di algoritmi di approssimazione iterativi, in grado di fornire sempre dei risultati, anche se approssimati, indipendentemente dal massimo tempo di esecuzione a disposizione. Inoltre la precisione dei risultati dovrebbe crescere all aumentare del tempo di esecuzione a disposizione. Granularità In FreeBSD la gestione dei timer in kernel (callout) è legata alla frequenza del clock principale di sistema, definita dalla variabile di configurazione del kernel HZ (appendice F). 4 Oppure può essere utilizzata per limitare la capacità di esecuzione in CPU su base task, in modo periodico.

125 CAPITOLO 5. REAL-TIME EDF PER FREEBSD 114 Questo significa che non è possibile armare timer con una risoluzione inferiore al periodo di clock di sistema. Con valore di HZ pari a 1000 riusciamo ad armare timer con un timeout di, al massimo, un millisecondo, e quindi riusciamo a gestire vincoli temporali non inferiori al millisecondo. La granularità dei timer influisce sulla risoluzione e precisione che possiamo avere nei vincoli temporali dei task real-time. Infatti i timer sono utilizzati per controllare la banda di utilizzo dei task (tecnica di isolamento temporale) e per attivare nuove istanze. Per migliorare la precisione e la granularità sui vincoli temporali dei task in FreeBSD, bisognerebbe utilizzare timer ad alta risoluzione, che non dipendono dalla frequenza del clock del sistema, ma utilizzano direttamente altri clock messi a disposizione dall architettura hardware. Un esempio sono gli htimer implementati per il kernel Linux [32]. Supporto SMP La gestione del bilanciamento di carico per i thread di classe deadline è stata separata da quella principale di ULE (sezione 4.1.2). Il motivo è che parte delle decisioni di bilanciamento di carico si basano sulle priorità dei thread; i thread di classe deadline non hanno range di priorità 5. Per i thread di classe deadline è stato previsto un bilanciamento di carico statico per configurazioni SMP. Quando bisogna decidere (nella funzione sched_add()) su quale CPU allocare un nuovo thread di classe deadline, si determina (tramite la funzione sched_pickcpu_dl()) la CPU con minor carico di thread di classe deadline e sufficiente banda. Questa ricerca viene effettuata solo per nuovi thread, ossia thread a cui non è stata ancora riservata banda di utilizzo su qualche CPU, altrimenti la CPU selezionata è quella sulla quale è stata allocata la banda di utilizzo. Integrazione nello scheduler ULE L integrazione nello scheduler ULE della nuova classe di scheduling deadline ha richiesto le seguenti modifiche principali. Nuove strutture dati La gestione della nuova classe di scheduling deadline ha richiesto l aggiunta di nuove strutture dati (sezione 5.1 per maggiori dettagli). Inizializzazione delle nuove strutture dati Le funzioni tdq_setup() ed sched_setup() sono state modificate per inizializzare le nuove strutture dati. Nuove funzioni deadline-bsd: dlrq_init(), init_dl_bandwidth(), init_dl_bw(). Scelta del prossimo thread da mandare in esecuzione Le funzioni sched_choose() e tdq_choose() sono utilizzate per determinare il prossimo thread da mandare in esecuzione; sono state apportate delle modifiche per ricercare il primo thread di maggiore priorità o di minore deadline tra le diverse classi di scheduling, nel seguente ordine: ith, deadline, realtime, timeshare e idle. Inoltre, nel caso in cui il thread ritornato sia di tipo deadline, bisogna togliere il thread dal relativo albero. Nuove funzioni deadline-bsd: dlrq_choose(), tdq_dl_rem(). Switch di contesto La funzione sched_switch() viene invocata su uno switch di contesto. Se il thread che sta per essere prelazionato è di classe deadline, allora bisogna aggiornare le sue informazioni temporali (come il tempo di esecuzione) e, nel caso si trovi ancora in stato di running, inserirlo nel relativo albero red-black. Inoltre, se il nuovo thread è di classe deadline, bisogna inizializzare il tempo di inizio esecuzione (variabile exec_start) al tempo corrente di sistema. 5 Il valore di priorità dei thread di classe deadline è impostato di default al valore 127, ossia la priorità minima per la classe ith. In questo modo la tecnica a priorità eredita può essere applicata anche ai thread di classe deadline.

126 CAPITOLO 5. REAL-TIME EDF PER FREEBSD 115 Nuove funzioni deadline-bsd: actions_before_switch_dl(), tdq_rbt_add(), actions_before_run_dl(). Fork di thread La funzione sched_fork_thread() viene invocata su un fork di thread. Se il thread (padre) è di classe deadline il figlio non eredita le informazioni proprie della classe deadline del padre. Affinché il thread figlio possa essere schedulato con classe deadline è necessario un inizializzazione tramite apposita syscall. Nuove funzioni deadline-bsd: sched_fork_dl_thread(). Tick di stathz La funzione sched_clock() viene invocata ad ogni tick del clock stathz. Nel caso in cui il thread in esecuzione sia di classe deadline bisogna evitare la gestione del time slice, in quanto non è prevista la tecnica a timesharing per questa classe (e che porterebbe a degli switch di contesto inattesi). Tick di hz La funzione sched_tick() viene invocata ad ogni tick del clock di HZ. Nel caso in cui il thread in esecuzione sia di classe deadline bisogna: aggiornare il suo tempo di esecuzione (campo runtime); se il thread ha esaurito il suo budget di esecuzione dl_runtime, allora: se la sua deadline assoluta non è nel passato, bisogna armare il timer per attivare una nuova istanza del thread (al tempo t, con t pari alla differenza tra la deadline assoluta e il tempo corrente di sistema); altrimenti bisogna inserire il thread nel relativo albero, resettando la deadline assoluta (al tempo t, con t pari alla somma tra deadline relativa e tempo corrente di sistema) e il relativo budget di esecuzione runtime al valore di dl_runtime; Nuove funzioni deadline-bsd: update_curr_dl(). Aggiunta di un thread La funzione sched_add() viene utilizzata per aggiungere un thread passato in stato di pronto al set di thread di una CPU. Nel caso di thread deadline, in configurazione SMP viene ricercata la CPU con minor carico di thread deadline e sufficiente banda. Questa ricerca viene effettuata solo per nuovi thread deadline, ossia thread a cui non è stata ancora riservata banda di utilizzo su qualche CPU, altrimenti la CPU selezionata è quella dove è stata allocata la banda di utilizzo. Una volta aggiunto un thread deadline a un albero di una CPU bisogna determinare se questo nuovo thread può prelazionare quello attualmente in esecuzione sulla CPU selezionata. Nuove funzioni deadline-bsd: sched_pickcpu_dl(), tdq_rbt_add(), check_preempt_curr_dl(). Yielding La funzione sched_relinquish() viene invocata quando un thread rilascia spontaneamente l esecuzione in CPU. In caso di thread deadline bisogna aggiornare le sue informazioni temporali (come il tempo totale di esecuzione) e: se la sua deadline assoluta non è nel passato, bisogna armare il timer per attivare una nuova istanza del thread (al tempo t, con t pari alla differenza tra la deadline assoluta e il tempo corrente di sistema); altrimenti bisogna inserire il thread nel relativo albero, resettando la deadline assoluta (al tempo t, con t pari al risultato della somma tra deadline relativa e tempo corrente di sistema) e il relativo budget di esecuzione runtime al valore di dl_runtime;

127 CAPITOLO 5. REAL-TIME EDF PER FREEBSD 116 Nuove funzioni deadline-bsd: yield_thread_dl(). Uscita di un thread La funzione sched_throw() viene invocata quando un thread termina la sua esecuzione (e.g. va in exit o viene killato). Nel caso di thread di classe deadline è necessario liberare la relativa banda di utilizzo sulla CPU dove era allocato, cancellare l eventuale timer pendente e aggiornare il contatore del numero totale di thread di classe deadline mantenuti sulla CPU. Nuove funzioni deadline-bsd: sched_dead_dl_thread(). Chiamate di sistema Per la gestione in userspace dei thread di classe deadline sono state aggiunte quattro nuove syscall: sched_getparam_ex(): ritorna i parametri di scheduling della struttura sched_param_ex per il thread specificato; sched_getscheduler_ex(): ritorna la classe di scheduling per il thread specificato; sched_setparam_ex(): inizializza i parametri di scheduling contenuti nella struttura sched_param_ex per il thread specificato; sched_setscheduler_ex(): inizializza la classe di scheduling e i parametri della struttura sched_param_ex per il thread specificato; Seguono la definizione della struttura sched_param_ex e i prototipi delle nuove syscall. struct sched_param_ex { int sched_priority; struct timespec sched_runtime; struct timespec sched_deadline; struct timespec sched_period; unsigned int sched_flags; struct timespec curr_runtime; struct timespec used_runtime; struct timespec curr_deadline; }; int sched_getparam_ex(pid_t, struct sched_param_ex *); int sched_getscheduler_ex(pid_t); int sched_setparam_ex(pid_t, const struct sched_param_ex *); int sched_setscheduler_ex(pid_t, int, const struct sched_param_ex *); 5.2 Risultati sperimentali Per verificare l implementazione di DEADLINE-BSD sono state condotte alcune prove sperimentali. L ambiente di test è composto da una macchina reale, con la seguenti caratteristiche hardware: AMD Athlon(tm) 64bit X2 Dual Core Processor ( MHz K8- class CPU). La versione di FreeBSD installata è la Il kernel è stato ricompilato, aggiungendo alla configurazione GENERIC (di amd64), le seguenti opzioni: options HZ=10000 # tick period on 100us # HZ defines also max deadline options CLASS_DL # deadline class options DEBUG_DL # debug deadline thread options SCHED_ULE # ULE scheduler options SCHED_STATS # Make an SMP-capable kernel by default #options SMP # Symmetric MultiProcessor Kernel

128 CAPITOLO 5. REAL-TIME EDF PER FREEBSD 117 Il supporto SMP è stato disabilitato, in quanto non necessario per queste prove. Per tracciare le informazioni temporali dei thread real-time è stato modificata la struttura sched_param_ex aggiungendo le seguenti informazioni di debugging: /* * statistics for deadline thread */ struct stats_dl { int dmiss; /* tells if the istance missed last deadline */ int rorun; /* tells if the istance run over last its dl_runtime */ uint64_t last_dmiss; /* delta of last missed deadline (usec) */ uint64_t last_rorun; /* delta of last overflowed dl_runtime (usec) */ uint64_t dmiss_max; /* max delta of last missed deadline (usec) */ uint64_t rorun_max; /* max delta of last overflowed dl_runtime (usec) */ uint64_t tot_rtime; /* total time spent in execution (usec) */ }; /* * debuggin information for deadline thread */ struct debug_dl { int cnt_new; /* keep count of td s dl_new = 1 setting */ int cnt_throttled; /* keep count of td s dl_throttled = 1 setting */ int cnt_dmiss; /* keep count of td s underflow deadline */ int cnt_rorun; /* keep count of td s overflow runtime */ int cnt_timerfail; /* keep count of td s timer reset failure */ int cnt_timerfire; /* keep count of td s timer firing */ int cnt_resched; /* keep count of td s resched */ int cnt_updatecurr; /* keep count of td s updating */ int cnt_beforerun; /* keep count of td s beforerun call */ int cnt_beforeswitch; /* keep count of td s beforeswitch call */ int cnt_enqueue; /* keep count of td s enqueue */ int cnt_dequeue; /* keep count of td s dequeue */ int cnt_resetdr; /* keep count of td s deadline and runtime reset */ }; struct sched_param_ex { int sched_priority; struct timespec sched_runtime; struct timespec sched_deadline; struct timespec sched_period; unsigned int sched_flags; struct timespec curr_runtime; struct timespec used_runtime; struct timespec curr_deadline; #ifdef DEBUG_DL struct debug_dl debug; struct stats_dl stats; #endif }; In questo modo è possibile portare in userspace, tramite la syscall sched_getparam_ex, le informazioni di scheduling dei thread real-time. Inoltre, per tracciare graficamente gli eventi di scheduling su un asse temporale, come gli switch di contesto e le transizioni degli stati dei thread, è stata utilizzata l infrastruttura KTR di FreeBSD. I dati raccolti con ktr sono stati convertiti in formato VCD tramite l utility sched_switch [31], e visualizzati in modalità grafica con GtkWave 6. I test sono stati organizzati nei seguenti gruppi: (a) rispetto dei vincoli temporali di esecuzione della classe deadline; 6 L utility sched_switch interpreta dati raccolti con ftrace di Linux. Sono state apportate delle patch sia in KTR che in sched_switch per ottenere dei tracciati VCD validi per FreeBSD

129 CAPITOLO 5. REAL-TIME EDF PER FREEBSD 118 (b) confronto con le altre classi di scheduling utente; Seguono dettagli e risultati dei test. (a) Rispetto dei vincoli temporali di esecuzione della classe deadline Questa suite di test verifica il rispetto del budget di esecuzione e della deadline/periodo di un task real-time, eseguito più volte con diversi vincoli temporali. Il codice del thread consiste in un while(1), con un timer di uscita configurabile. Segue il codice del thread. #include <sys/cdefs.h> #include <sys/param.h> #define DEBUG_DL #include <sys/sched.h> #include <sys/priority.h> #include <sys/errno.h> #include <sys/time.h> #include <signal.h> #include <ctype.h> #include <err.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> struct sched_param_ex param; int error=0,class; int exp_runtime; void debug_dl_print(const struct sched_param_ex *param) { printf("## stat info\n"); printf("tot_rtime_usec: %lu\n",param->stats.tot_rtime); printf("rtime_over_usec: %lu\n",param->stats.tot_rtime - exp_runtime); printf("last_dmiss_usec: %lu\n",param->stats.last_dmiss); printf("last_rorun_usec: %lu\n",param->stats.last_rorun); printf("dmiss_max_usec: %lu\n",param->stats.dmiss_max); printf("rorun_max_usec: %lu\n",param->stats.rorun_max); printf("## debug info\n"); printf("cnt_new: %d\n",param->debug.cnt_new); printf("cnt_throttled: %d\n",param->debug.cnt_throttled); printf("cnt_dmiss: %d\n",param->debug.cnt_dmiss); printf("cnt_rorun: %d\n",param->debug.cnt_rorun); printf("cnt_timerfail: %d\n",param->debug.cnt_timerfail); printf("cnt_timerfire: %d\n",param->debug.cnt_timerfire); printf("cnt_resched: %d\n",param->debug.cnt_resched); printf("cnt_updatecurr: %d\n",param->debug.cnt_updatecurr); printf("cnt_beforerun: %d\n",param->debug.cnt_beforerun); printf("cnt_beforeswitch: %d\n",param->debug.cnt_beforeswitch); printf("cnt_enqueue: %d\n",param->debug.cnt_enqueue); printf("cnt_dequeue: %d\n",param->debug.cnt_dequeue); printf("cnt_resetdr: %d\n",param->debug.cnt_resetdr); } void myexit(int a){ error=sched_getparam_ex(0, &param); if(error) exit(1); param.sched_priority=153; error=sched_setscheduler_ex(0, PRI_TIMESHARE, &param); if(error) exit(1); if(class==pri_deadline) debug_dl_print(&param); exit (0);

130 CAPITOLO 5. REAL-TIME EDF PER FREEBSD 119 } static void usage() { (void) fprintf(stderr, "%s\n", "usage: test_while class prio runtime(us) period(us) timeout(sec)"); exit(1); } int main(argc, argv) int argc; char **argv; { struct timeval now; struct itimerval timer1; int runtime,period,sbe,prio; if(argc!=6) usage(); class = abs(atoi(argv[1])); prio = abs(atoi(argv[2])); runtime = abs(atoi(argv[3])); period = abs(atoi(argv[4])); sbe = abs(atoi(argv[5])); signal(sigalrm, myexit); memset(&param,0,sizeof(param)); param.sched_priority=prio; param.sched_runtime.tv_sec=0; param.sched_runtime.tv_nsec=runtime*1000; param.sched_deadline.tv_sec=0; param.sched_deadline.tv_nsec=period*1000; param.sched_period.tv_sec=0; param.sched_period.tv_nsec=period*1000; now.tv_sec=sbe; now.tv_usec=0; memset(&timer1,0,sizeof(timer1)); timer1.it_value=now; switch(class){ case PRI_DEADLINE: printf("change to class PRI_DEADLINE, %d us runtime every %d us \ for %d seconds\n",param.sched_runtime.tv_nsec /1000,\ param.sched_deadline.tv_nsec /1000,now.tv_sec); exp_runtime=(param.sched_runtime.tv_nsec /1000) *\ ((now.tv_sec * ) / ( param.sched_deadline.tv_nsec /1000)); printf("we expect to run for almost %d us\n",exp_runtime); break; case PRI_REALTIME: printf("change to class PRI_REALTIME with priority %d\ for %d seconds\n",param.sched_priority,now.tv_sec); break; case PRI_TIMESHARE: printf("change to class PRI_TIMESHARE with priority %d for\ %d seconds\n",param.sched_priority,now.tv_sec); break; } error=sched_setscheduler_ex(0, class, &param); if(error) err(1, "%s", argv[0]); error=setitimer(0, &timer1, NULL); if(error) err(1, "%s", argv[0]);

131 CAPITOLO 5. REAL-TIME EDF PER FREEBSD 120 } while(1); Le prove consistono nell eseguire il thread (con timer a 3 secondi) con i seguenti vincoli temporali: (a) runtime = 100ms; deadline/periodo = 500ms; (b) runtime = 10ms; deadline/periodo = 500ms; (c) runtime = 10ms; deadline/periodo = 100ms; (d) runtime = 1ms; deadline/periodo = 500ms; (e) runtime = 1ms; deadline/periodo = 100ms; (f) runtime = 1ms; deadline/periodo = 10ms; (g) runtime = 200us; deadline/periodo = 500ms; (h) runtime = 200us; deadline/periodo = 100ms; (i) runtime = 200us; deadline/periodo = 10ms; (l) runtime = 200us; deadline/periodo = 500us; Nella Tabella 5.1 sono riportati i risultati dei test. Seguono inoltre alcune visualizzazioni grafiche degli eventi di scheduling traciati durante le prove. Il colore giallo significa che il task non è in stato di pronto, il verde ed il blu che è in esecuzione in CPU (il blu viene utilizzato se il task ha una variazione di priorità durante il tracciamento) ed il rosso significa che il task è in stato di pronto, in attesa di essere mandato in esecuzione. Figura 5.3: Esecuzione di un task in while(1) di classe deadline con runtime 100ms e deadline/periodo 500ms (zoom out).

132 CAPITOLO 5. REAL-TIME EDF PER FREEBSD 121 Tabella 5.1: Esecuzione di un while(1) per 3 secondi con diversi vincoli temporali (runtime/periodo) 100ms/500ms 10ms/500ms 10ms/100ms 1ms/500ms 1ms/100ms 1ms/10ms 200us/500ms 200us/100ms 200us/10ms 200us/500us tot_rtime_usec rtime_over_usec last_dmiss_usec last_rorun_usec dmiss_max_usec rorun_max_usec cnt_new cnt_throttled cnt_dmiss cnt_rorun cnt_timerfail cnt_timerfire cnt_resched cnt_updatecurr cnt_beforerun cnt_beforeswitch cnt_enqueue cnt_dequeue cnt_resetdr

133 CAPITOLO 5. REAL-TIME EDF PER FREEBSD 122 Figura 5.4: Esecuzione di un task in while(1) di classe deadline con runtime 100ms e deadline/periodo 500ms (zoom in). Figura 5.5: Esecuzione di un task in while(1) di classe deadline con runtime 1ms e deadline/periodo 10ms (zoom out).

134 CAPITOLO 5. REAL-TIME EDF PER FREEBSD 123 Figura 5.6: Esecuzione di un task in while(1) di classe deadline con runtime 1ms e deadline/periodo 10ms (zoom in). Figura 5.7: Esecuzione di un task in while(1) di classe deadline con runtime 200us e deadline/periodo 500us (zoom out).

135 CAPITOLO 5. REAL-TIME EDF PER FREEBSD 124 Figura 5.8: Esecuzione di un task in while(1) di classe deadline con runtime 200us e deadline/periodo 500us (zoom in).

136 CAPITOLO 5. REAL-TIME EDF PER FREEBSD 125 (b) Confronto con le altre classi di scheduling utente Questa suite di test mette a confronto le tre classi scheduling utente: deadline, real-time e timesharing. Un task in while(1) (timeout di 3 secondi) viene mandato in esecuzione in un workload con un alto carico di lavoro. Si vuole verificare la schedulazione del task a seconda della classe di scheduling assegnata. Un workload con un alto carico di lavoro può essere rappresentato da un task in while(1) con la priorità maggiore per le classi utente, quindi classe di scheduling real-time Posix con valore di priorità 128 (ricordiamo che in ULE tutte le classi di scheduling utente funzionano in timesharing e quindi il while(1) non può monopolizzare l esecuzione in CPU). Nelle figure successive sono riportati i risultati dei test. Figura 5.9: Esecuzione di un task in while(1) di classe timesharing in un workload con alto carico (zoom in).

137 CAPITOLO 5. REAL-TIME EDF PER FREEBSD 126 Figura 5.10: Esecuzione di un task in while(1) di classe real-time in un workload con alto carico (zoom in). Figura 5.11: Esecuzione di un task in while(1) di classe deadline con runtime 20ms e deadline/periodo 100ms in un workload con alto carico (zoom in).

138 CAPITOLO 5. REAL-TIME EDF PER FREEBSD 127 Figura 5.12: Esecuzione di due task in while(1) di classe real-time in un workload con alto carico (zoom in). Figura 5.13: Esecuzione di due task in while(1) di classe deadline con runtime 20ms e deadline/periodo 100ms in un workload con alto carico (zoom in).

139 CAPITOLO 5. REAL-TIME EDF PER FREEBSD Analisi dei risultati I risultati ottenuti nei test dimostrano quanto segue: la banda di utilizzo (runtime/periodo) assegnata ad un task di classe deadline viene sempre rispettata, anche nel caso in cui il task tenti di restare in esecuzione oltre il suo budget (tecnica dell isolamento temporale), come riportato nella Tabella 5.1 (valori di tot_rtime_usec) e nelle figure la deadline periodica viene garantita (cnt_dmiss a 0, Tabella 5.1): il thread di classe deadline riesce a utilizzare tutto il suo budget di esecuzione prima della relativa deadline. nella tabella Tabella 5.1, rorun_max_use contiene il massimo delta di tempo per il quale il thread deadline ha ecceduto il suo budget di esecuzione. Questo valore non va mai oltre la risoluzione del tick di clock di sistema (in questo caso pari a 100us), poichè il controllo del budget di esecuzione avviene ad ogni tick di clock. Con timer ad alta risoluzione si può attivare un controllo di budget più preciso, attivando un timer ogni qual volta il thread viene mandato in esecuzione (come avviene nell implementazione per Linux). In questo modo riusciremmo a far rispettare meglio il budget di esecuzione dei thread di classe deadline, riducendo quindi i valori di rtime_over_usec (che in parte dipendono anche dalla precisione dei timer utilizzati in userspace) e migliorando di conseguenza la precisione della tecnica dell isolamento temporale. le figure mostrano come le classi di scheduling utente real-time e timesharing non sono adatte a garantire deadline di esecuzione. Utilizzando la classe di scheduling real-time, e considerando task di massima priorità, si riesce a garantire un budget di esecuzione pari al valore della time-slice utente (rif ) (costante per ogni task) in un periodo/deadline pari a timeslice n, dove n è il numero di task di classe real-time (di pari priorità) che competono per l esecuzione. Ad esempio, con 3 task di classe real-time e con una time-slice di 100ms si riesce a garantire un budget di esecuzione di 100ms su un periodo/deadline di 300 (su base task ma costante per tutti), come mostrato nella figura La classe di scheduling deadline, invece, riesce a garantire un budget di esecuzione e un periodo/deadline con risoluzione pari al tick di clock del sistema, con la possibilità di avere vincoli diversi su base task (a condizione di rispettare il test di ammissione), come mostrato nella figura 5.13, dove vengono mandati in esecuzione 2 task di classe deadline con un budget di esecuzione di 10ms e un periodo/deadline di 100ms, in competizione con un task in while(1) di classe real-time e di massima priorità. 5.3 Considerazioni finali Il test di ammissione, in combinazione con l algoritmo EDF, garantiscono la schedulabilità del workload real-time, ossia garantiscono che ci sia una capacità di calcolo di CPU sufficiente per rispettare i vincoli temporali di esecuzione dei task real-time. La capacità di calcolo di CPU è garantita in condizioni ideali, ossia supponendo che la la banda di utilizzo dei thread di interrupt sia sempre inferiore (o al più uguale) alla banda di utilizzo di CPU non allocata ai thread della classe deadline. Questo significa che se vogliamo raggiungere un certo grado di certezza affinché il sistema possa garantire i vincoli temporali dei task real-time è necessario uno studio sui burst di esecuzione di tutti i gestori di interrupt che possono essere scatenati nel sistema. Notare che l effettivo rispetto delle deadline di esecuzione dipende anche dall algoritmo e dalla dimensione di input delle applicazioni real-time. Questa implementazione prevede lo stesso valore per il periodo e la deadline relativa del thread. Con questa assunzione, il controllo di ammissione che verifica la schedulabilità

140 CAPITOLO 5. REAL-TIME EDF PER FREEBSD 129 del workload, consiste nel controllare che la somma delle bande di utilizzo di tutti i thread (rapporto tra budget e periodo) sia inferiore o al più uguale alla capacità di calcolo del sistema allocata per questa classe di thread. A volte è utile poter diversificare il periodo dalla deadline relativa di un thread (e.g. budget di 10ms in un periodo di 500ms e con una deadline relativa di 100ms.). In questo caso per verificare la schedulabilita del sistema non è più possibile utilizzare il test di ammissione precedente, in quanto fallirebbe per alcuni casi [74]. Anche sostituendo nella formula il periodo con la deadline relativa, potremmo avere una condizione solo sufficiente per verificare la schedulabilita del sistema, ma non necessaria. Infatti potremmo dire con certezza solo se il sistema è schedulabile (somma delle bande di calcolo dei thread minore della capacità di calcolo globale del sistema) ma non se non lo è. Ulteriori considerazioni su questo problema sono disponibili in [74]. La politica di scheduling deadline-bsd scala bene rispetto al numero di processi del sistema: per le caratteristiche dell albero red-black (sezione 3.1.5) la complessità computazionale di ricerca del prossimo task da mandare in esecuzione è, nel caso peggiore, di O(log(n), con n numero di thread real-time in stato di pronto. Inoltre l algoritmo scala bene anche rispetto al numero di risorse di calcolo nel sistema: ogni risorsa di calcolo ha il proprio albero red-black di thread di classe deadline che può mandare in esecuzione e quindi l accesso all albero non è in competizione con altre risorse di calcolo. Si potrebbe considerare l idea di spostare tutta la politica di deadline-bsd in userspace (e.g. in una libreria) e utilizzare il meccanismo delle priorità per mandare in esecuzione i thread (cosiddetta separazione del meccanismo dalla politica). Benché sia una buona idea, non sarebbe attuabile per almeno i seguenti motivi: (a) anche supponendo di assegnare il valore di priorità massimo (nel range di valori di priorità delle classi utente) al task di classe deadline che deve essere mandato in esecuzione, non ci sono garanzie che quest ultimo non possa entrare in competizione di esecuzione in CPU con altri task della medesima priorità, che porterebbe, per le politiche di scheduling implementate in FreeBSD, all esecuzione di questi task in timesharing e in round robin, in conflitto con le politiche della classe deadline; (b) le transizioni kernel-userspace introducono overhead non sempre deterministico che diminuirebbe la precisione sui vincoli temporali di esecuzione; (c) bisognerebbe risolvere in userspace problematiche come la gestione dell SMP e il conteggio del tempo di esecuzione dei task. Ulteriori dettagli sull implementazione deadline-bsd in ULE possono essere trovati visionando i sorgenti di FreeBSD [89] (con la patch per deadline-bsd), in particolare: sched_ule.c: contiene le funzioni per lo scheduling di processi dello scheduler ULE; class_dl.c: contiene le funzioni di deadline-bsd; class_dl.h: strutture dati, prototipi e define di deadline-bsd; kern_synch.c e kern_switch.c: contengono altre funzioni per lo scheduling di processi; proc.h: contiene le principali strutture dati per lo scheduling di processi; priority.h: contiene le principali define delle classi di priorità; p1003_1b.c: contiene le chiamate di sistema POSIX per l estensione real-time dello scheduling di processi;

141 Conclusioni e direzioni future In questa tesi è stata presentata l implementazione per FreeBSD di un framework di scheduling real-time originariamente sviluppato per Linux, SCHED_DEADLINE, basato sull algoritmo di scheduling EDF, con l aggiunta di un meccanismo che limita il tempo di esecuzione dei task, garantendo un isolamento temporale tra essi. I risultati ottenuti mostrano come un sistema FreeBSD dotato di questa classe di scheduling possa gestire workload real-time periodici e aperiodici rispettando i vincoli di tempo di esecuzione, deadline e periodo, anche in presenza di un elevato carico di sistema. Questo lavoro apre la possibilità di impiegare FreeBSD anche in ambiti applicativi con requisiti real-time, dove Linux è il sistema operativo Unix-like dominante. Inoltre bisogna considerare che la licenza BSD-style di FreeBSD presenta meno restrizioni di quella GPL del kernel Linux e questo rende FreeBSD più appetibile per le aziende nella scelta del sistema operativo da adottare. La tesi ha richiesto 2 anni di tempo, tenendo in considerazione una situazione lavorativa. L acquisizione di tutta la parte teorica ha preso la maggior parte del tempo, mentre l implementazione, il debugging e i risultati sperimentali in FreeBSD hanno richiesto 3 mesi circa. Le maggiori difficoltà le ho riscontrate nell acquisire la teoria delle politiche di scheduling in FreeBSD, in quanto in letteratura, come su Internet, le fonti che trattano questi argomenti sono limitate e spesso riportano informazioni poco dettagliate e/o non allineate alle ultime modifiche. L unico modo di analizzare e capire le politiche di scheduling in FreeBSD allo stato dell arte è stato quello di interpretare direttamente il relativo codice scritto in linguaggio C. Abbiamo visto come nell implementazione di DEADLINE-BSD in FreeBSD, la precisione e granularità dei vincoli temporali dei task dipenda dalla frequenza del clock di sistema, ed in particolare dal parametro di configurazione del kernel HZ. Durante le prove, con una configurazione del kernel che prevedeva un periodo del clock di sistema pari a 10 us, sono stati stati riscontrati problemi di stabilità del sistema. In particolare abilitando il sistema di debugging KTR, occorrono, durante l esecuzione di un task di classe deadline, dei page fault del kernel, ossia situazioni in cui il kernel richiede una pagina di memoria non valida (il kernel non è paginabile, e un page fault in kernel è un errore grave). Benché questo problema meriti un analisi più approfondita, portare la frequenza dei tick di clock di sistema a valori così alti non è consigliabile, sia perché i frequenti interrupt di clock aumentano la possibilità di interrompere codice che non dovrebbe essere interrotto, sia per l inutile overhead introdotto nel sistema, che di fatto consuma tempo di esecuzione di CPU, che potrebbe essere prezioso per rispettare qualche deadline di esecuzione. L implementazione ed uso di timer ad alta risoluzione, indipendenti dal clock del sistema, diventa quindi indispensabile per aumentare granularità, risoluzione e precisione sui vincoli temporali dei task. Seguono altre migliori che meritano degli approfondimenti, e che potrebbero essere argomenti per tesi di primo e secondo livello: possibilità di avere una deadline minore del periodo, con conseguente rivisitazione del test di ammissione; migliorare il supporto SMP introducendo un bilanciamento dei thread deadline periodico e rendendo più efficiente quello statico; analisi e porting in FreeBSD della patch Preempt-RT fatta su Linux per aumentare la prelazione del kernel. 130

142 Appendice A Nozioni di base Questo appendice fornisce le nozioni e termini di base necessarie per capire il funzionamento delle attività di scheduling dei processi nei sistemi operativi e di ciò che ci sta intorno. La prima sezione introduce il ruolo del sistema operativo e del kernel. La seconda sezione riassume le fasi di esecuzione di un programma, le modalità di esecuzione kernel ed utente, il ruolo delle system call e l organizzazione della memoria di un elaboratore elettronico. Il ruolo degli interrupt è introdotto nella terza sezione. Nella quarta sezione sono presentati i concetti di multiprogrammazione, switch di contesto, nozioni di processo, thread e i diversi metodi di sincronizzazione. La quinta sezione introduce i sistemi paralleli, le architetture multiprocessori UMA/NUMA, le CPU muticore e hyperthreading, e il modello SMP. A.1 Il ruolo del sistema operativo Un elaboratore elettronico è composto da diverse risorse hardware: uno o più processori, memoria principale e secondaria e diversi dispositivi di I/O. Queste unità sono interconnesse con uno o più bus di sistema. L accesso e l utilizzo di queste risorse è spesso un compito complicato. Il sistema operativo (abbreviato in OS, che sta per Operating System) è uno strato di software che gestisce le risorse hardware, dando la possibilità agli utenti (e ai loro programmi) di interagire con esse in modo semplice, efficace ed efficiente. Gli utenti possono interagire con l elaboratore tramite interfacce testuali, chiamate shell, o grafiche, chiamate GUI (graphical user interface). Il cuore di un sistema operativo è il kernel: il primo strato di software sopra l hardware, il cui compito è gestire le risorse fondamentali come processori e memoria, fornendo anche un interfaccia comune di accesso all hardware ai vari programmi utenti. Le tipiche componenti di un kernel sono i gestori degli interrupt per servire le richieste di interrupt, device driver per la gestione dei dispositivi di I/O, uno scheduler per gestire l esecuzione in CPU tra più processi, un gestore di memoria per gestire lo spazio di indirizzamento dei processi e altri servizi di sistema (messi a disposizione tramite system call) come la gestione del file system, la gestione del networking e l IPC per la comunicazione tra processi. Questi componenti interagiscono tra loro in modo sincrono tramite chiamate a funzioni e/o in modo asincrono tramite l uso di segnali. Oltre al kernel un sistema operativo fornisce anche dei programmi utente di base come shell, GUI, un gestore per installare applicazioni aggiuntive, e molti altri. Possiamo quindi vedere il sistema operativo sia come l entità software che fornisce ai programmi applicativi un astrazione semplice del complicato accesso e utilizzo dell hardware sia come l entità software che gestisce le risorse come processori, memoria e dispositivi di I/O, fornendo un ordinata e controllata allocazione di queste risorse ai vari programmi che competono per esse. La gestione delle risorse include la condivisione delle stesse nel tempo e nello spazio. Un esempio di risorsa condivisa nel tempo è il processore: solitamente più programmi utente (e kernel) competono per l esecuzione in CPU. Decidere quale programma mandare in 131

143 APPENDICE A. NOZIONI DI BASE 132 esecuzione, per quanto tempo e su quale processore, è un compito del sistema operativo, ed in particolare di una parte del kernel chiamata scheduler. Un esempio di risorsa condivisa nello spazio è la memoria: prima di mandare in esecuzione un programma, lo stesso deve essere caricato in memoria, solitamente dal disco fisso. La decisione di quali programmi tenere in memoria e il controllo di eventuali violazioni di memoria è un altro compito specifico del sistema operativo, ed in particolare di una parte del kernel chiamata gestore di memoria. Per un introduzione più completa e dettagliata ai sistemi operativi si rimanda alla sezione 1.1 di [1]. A.2 Esecuzione di un programma In una CPU, l esecuzione di un programma prevede tre fasi che si ripetono in modo ciclico: fetch-decode-execute, ossia l istruzione viene presa dalla memoria, si determina il suo tipo e gli operandi e quindi viene eseguita dal processore. Ogni CPU ha un set di istruzioni ben definito che può eseguire (e.g. set di istruzioni per SPARC, i386, ARM, MIPS, ecc.). In genere la CPU ha due modalità di esecuzione di un programma: utente e kernel. Nella modalità kernel la CPU può eseguire tutto il proprio set di istruzioni e ha il completo accesso all hardware. Nella modalità utente la CPU può eseguire solo un sottoinsieme del proprio set di istruzioni con un limitato accesso all hardware. I diversi servizi del sistema operativo come la gestione dei processi, del file system e della memoria, sono messi a disposizione dei programmi utente tramite un set di funzioni denominate chiamate di sistema (system call o anche syscall). Per la loro natura queste funzioni devono essere eseguite in modalità kernel: quando un programma utente invoca una chiamata di sistema, si passa in modalità kernel, per poi ritornare in modalità utente al termite dell esecuzione della chiamata di sistema. Ogni cambio di modalità (chiamata contex switch) comporta un overhead temporale, dovuto al salvataggio e recupero delle informazioni (registri di cpu e pagine di memoria) dei task coinvolti (in questo caso programma utente e kernel). La figura Figura A.1 mostra le relazioni tra le applicazioni, il kernel e l hardware.

144 APPENDICE A. NOZIONI DI BASE 133 Figura A.1: Relazione tra applicazioni, il kernel e l hardware. L organizzazione della memoria all interno di un elaboratore elettronico è gerarchia e può essere rappresentata da una piramide e questo per una questioni di costi, velocità e dimensioni. In particolare, dal basso verso l alto velocità di accesso e dimensioni diminuiscono mentre aumentano i costi, come è possibile vedere nella Figura A.2. In cima alla piramide Figura A.2: Organizzazione della memoria all interno di un elaboratore elettronico abbiamo i registri di CPU. La loro funzione è quella di mantenere i dati e informazioni del programma in esecuzione. Esempi di questi registri sono il program counter (PC), che contiene l indirizzo di memoria della prossima istruzione che sarà eseguita; lo stack pointer (SP) che punta all inizio dello stack corrente in memoria e che contiene variabili e parametri delle funzioni che non sono mantenuti nei registri. Un altro registro è il program status word (PSW) che contiene diversi bit di controllo tra i quali la modalità di esecuzione del programma (modalità utente o kernel). Il tempo di accesso ai registri è nell ordine del nanosecondo e le dimensioni sono inferiori ad 1 kilobyte (nelle CPU a 32 bit troviamo registri di 32 bit, mentre nelle CPU a 64 bit i registri sono a 64 bit). Dopo i registri di CPU troviamo la memoria cache che può essere organizzata su più livelli (sempre in modo piramidale). La cache contiene principalmente istruzioni e dati dei

145 APPENDICE A. NOZIONI DI BASE 134 programmi che sono stati in esecuzione recentemente. Può contenere altre informazioni utili come parti di file, conversioni di nomi di dominio in indirizzi ip, conversioni di path name in indirizzi disco, e altre ancora. Tutte le richieste di CPU che non sono presenti in cache vengono prelevate dalla memoria principale. I dati presenti in memoria principale, a loro volta, vengono presi dai dischi fissi o da altri dispositivi di I/O (come una scheda di rete). In genere il sistema operativo, per l accesso in memoria principale, si appoggia all MMU (memory management unit) che mappa lo spazio di indirizzamento virtuale, utilizzato dai processi, in quello fisico; inoltre fornisce dei meccanismi di protezione della memoria. Quando un programma è in esecuzione, i registri e la cache della CPU conterranno dati e istruzioni utilizzati nell imminente dal programma e in memoria centrale ci saranno il restante codice e dati del programma (e probabilmente anche dati di altri programmi). L output del programma può essere riversato su dischi o su altri dispositivi di I/O. Parte del disco può essere utilizzato come memoria virtuale (anche chiamata memoria di swap), ad esempio quando un programma necessita di una dimensione maggiore della memoria totale disponibile. Ulteriori dettagli sull organizzazione di un elaboratore elettronico e sui concetti base di un sistema operativo sono disponibili nelle sezioni 1.3 e 1.5 di [1]. A.3 Interrupt Il kernel, e quindi il sistema operativo, interagisce spesso con i dispositivi di I/O, come l hard disk o una scheda di rete. Dato che il processore può essere ordini di grandezza più veloce rispetto ai dispositivi di I/O, bloccare l esecuzione del kernel in attesa che una sua richiesta venga soddisfatta da un dispositivo di I/O comporta un notevole spreco di CPU. L ideale sarebbe lasciare che il kernel esegua altre operazioni per poi occuparsi della risposta del dispositivo solo quando questa è realmente disponibile. Esistono due tecniche per determinare la disponibilità dei dati da parte di un dispositivo: il polling e la tecnica ad interrupt. Nel polling il kernel interroga in modo periodico lo stato del dispositivo. Questa tecnica non solo introduce overhead di sistema, inoltre può portare a dei ritardi (dovuti alle interrogazioni periodiche) nel determinare la disponibilità dei dati da parte del dispositivo. Nella tecnica ad interrupt l hardware segnala al processore un cambio di stato, ad esempio il fatto che il dato richiesto sia pronto per essere recuperato. Un interrupt è prodotto fisicamente da un segnale elettrico originato dal dispositivo hardware e diretto al processore, il quale interrompe la sua esecuzione per gestire l interrupt. Il processore può notificare al sistema operativo il verificarsi di un interrupt. E compito di una parte del kernel, chiamata gestore di interrupt, invocare la giusta routine (handler) per gestire un determinato interrupt. Il processore ha un pin di ingresso per ogni interrupt che può gestire. In questo modo ogni interrupt può essere identificato da un valore specifico, che viene utilizzato dal gestore di interrupt per invocare la corrispondente routine. Notare che gli interrupt hardware sono asincroni rispetto al clock del processore. Questo significa che un interrupt hardware può interrompere l esecuzione del kernel in qualsiasi momento. Nella Figura A.3 viene schematizzata la gestione di un interrupt hardware da parte del sistema operativo. In genere, l esecuzione dei gestori di interrupt avviene in un contesto speciale chiamato interrupt-contex. Questo contesto è anche chiamato contesto atomico poiché il codice eseguito in questo contesto non può essere interrotto/bloccato. Proprio per questo motivo è molto importate che il gestore di interrupt venga eseguito il più velocemente possibile, in modo tale da restituire il controllo al kernel (che potrebbe essere in attesa di effettuare operazioni importanti, come la gestione di un altro interrupt). La gestione di un interrupt può richiedere diverso lavoro, e quindi tempo, in conflitto con la prerogativa di terminare il lavoro il più rapidamente possibile. Per questo motivo spesso la gestione di un interrupt è divisa in due parti: top half e bottom half. La prima, che viene eseguita in contesto atomico, realizza tutte quelle operazioni che sono critiche nel tempo (come restituire un

146 APPENDICE A. NOZIONI DI BASE 135 Figura A.3: Gestione di un interrupt hardware. (a) Un interrupt occorre. (b) Dopo la gestione dell interrupt, il controllo passa allo scheduler. (c) Lo scheduler manda in esecuzione un task. ACK all hardware, resettare il dispositivo o copiare i dati provenienti da un scheda di rete in memoria di sistema); mentre la seconda, che viene eseguita in process-context (interrompibile) realizza tutte quelle operazioni che possono essere eseguite in futuro in tutta sicurezza, come l elaborazione dei dati in memoria recuperati dal relativo top half. Alcuni sistemi operativi come Linux e FreeBSD gestiscono anche interrupt software: come quelli hardware hanno lo scopo di segnalare al kernel il verificarsi di un certo evento. A differenza di quelli hardware che sono asincroni con il clock del processore, quelli software sono sincroni e solitamente sono generati tramite l invio di segnali software. Gli interrupt software sono in genere utilizzati per gestire eccezioni come la divisione per zero, un page fault o l invocazione di una system call. Una descrizione dettagliata sull implementazione degli interrupt nel kernel Linux (filone 2.6.X) si trova nel capitolo 7 di [23]. A.4 Processo, thread e multiprogrammazione Con la tecnica della multiprogrammazione e di suddivisione del tempo (vedere appendice D per i dettagli) è possibile intervallare l esecuzione in CPU di più programmi: un solo programma è in esecuzione in un dato momento, come mostrato nella Figura A.4. Per far questo è necessario interrompere il programma attualmente in esecuzione e mandare in esecuzione un altro programma. Per farlo è necessario salvare tutte le informazioni (e.g. valori dei registri di CPU) del programma interrotto per permettere di farlo ripartire esattamente dal punto in cui è stato interrotto, quando verrà nuovamente schedulato per l esecuzione. Inoltre devono essere caricate, nei registri di CPU, in cache e in memoria di sistema, tutte le informazioni necessarie per mandare in esecuzione il nuovo programma. Il salvataggio e caricamento dei registri di CPU avviene tramite una procedura in assembler (specifica per ogni architettura hardware). Tutta questa procedura viene chiamata switch di contesto (contex-switch). Uno switch di contesto può essere scatenato da diversi eventi, ad esempio quando il processo in esecuzione richiede delle operazioni di I/O, tramite interrupt hardware/software, quando

147 APPENDICE A. NOZIONI DI BASE 136 Figura A.4: Multiprogrammazione un programma in user-space invoca una system call ( trap in kernel ) e di sicuro sulla base delle politiche di scheduling. Processo Con il termine processo indichiamo non solo un programma in esecuzione ma anche tutte le relative informazioni ad esso associate (come i valori dei registri di CPU). E grazie all astrazione dei processi che una CPU può essere condivisa tra più programmi, creando un sistema multiprogrammato con un illusione di un pseudoparallelismo. Per far questo la CPU cambia il processo in esecuzione, secondo le politiche di scheduling. Di fatto, in un sistema monoprocessore, un solo processo alla volta viene eseguito in un dato istante. Su sistemi multiprocessore abbiamo un parallelismo effettivo: in un dato istante abbiamo più processi in esecuzione (pari al numero di processori disponibili). Un set di chiamate di sistema è dedicato alla gestione dei processi (e.g. fork in Unix per creare un processo). Una volta che il processo viene creato, questo può terminare in modo volontario (e.g. uscita dal programma per errore o per fine esecuzione) o involontario (e.g. killato da un altro processo o per errore fatale come una divisione per zero). In alcuni sistemi operativi, e.g. Unix-like, esiste una gerarchia di processi: i figli di un processo padre (e gli altri eventuali discendenti) appartengono a uno stesso gruppo di processi. Spesso diversi processi interagiscono tra loro (come in una pipe di comandi) e un processo può rimanere in attesa di dati di un altro processo. La struttura informativa di ogni processo contiene l informazione del suo stato di esecuzione, che può essere in esecuzione (running), può essere pronto per essere eseguito (ready) o può essere in stato di attesa di qualche evento (blocked). Figura A.5: Transizioni di stato dei processi

Sistemi Operativi. Scheduling della CPU SCHEDULING DELLA CPU. Concetti di Base Criteri di Scheduling Algoritmi di Scheduling

Sistemi Operativi. Scheduling della CPU SCHEDULING DELLA CPU. Concetti di Base Criteri di Scheduling Algoritmi di Scheduling SCHEDULING DELLA CPU 5.1 Scheduling della CPU Concetti di Base Criteri di Scheduling Algoritmi di Scheduling FCFS, SJF, Round-Robin, A code multiple Scheduling in Multi-Processori Scheduling Real-Time

Dettagli

Sistemi Operativi SCHEDULING DELLA CPU. Sistemi Operativi. D. Talia - UNICAL 5.1

Sistemi Operativi SCHEDULING DELLA CPU. Sistemi Operativi. D. Talia - UNICAL 5.1 SCHEDULING DELLA CPU 5.1 Scheduling della CPU Concetti di Base Criteri di Scheduling Algoritmi di Scheduling FCFS, SJF, Round-Robin, A code multiple Scheduling in Multi-Processori Scheduling Real-Time

Dettagli

Scheduling. Scheduling 14/12/2003 1/7

Scheduling. Scheduling 14/12/2003 1/7 Scheduling In un computer multiprogrammato più processi competono per l'uso della CPU. La parte di sistema operativo che decide quale processo mandare in esecuzione è lo scheduler. Batch OS: scheduling

Dettagli

Scheduling. Sistemi Operativi e Distribuiti A.A. 2004-2005 Bellettini - Maggiorini. Concetti di base

Scheduling. Sistemi Operativi e Distribuiti A.A. 2004-2005 Bellettini - Maggiorini. Concetti di base Scheduling Sistemi Operativi e Distribuiti A.A. 2-25 Bellettini - Maggiorini Concetti di base Il massimo utilizzo della CPU si ottiene mediante la multiprogrammazione Ogni processo si alterna su due fasi

Dettagli

Un sistema operativo è un insieme di programmi che consentono ad un utente di

Un sistema operativo è un insieme di programmi che consentono ad un utente di INTRODUZIONE AI SISTEMI OPERATIVI 1 Alcune definizioni 1 Sistema dedicato: 1 Sistema batch o a lotti: 2 Sistemi time sharing: 2 Sistema multiprogrammato: 3 Processo e programma 3 Risorse: 3 Spazio degli

Dettagli

Sistemi Operativi SCHEDULING DELLA CPU

Sistemi Operativi SCHEDULING DELLA CPU Sistemi Operativi SCHEDULING DELLA CPU Scheduling della CPU Concetti di Base Criteri di Scheduling Algoritmi di Scheduling FCFS, SJF, Round-Robin, A code multiple Scheduling in Multi-Processori Scheduling

Dettagli

Lo scheduling. Tipici schedulatori

Lo scheduling. Tipici schedulatori Lo scheduling Un processo durante la sua evoluzione è o running o in attesa di un evento. Nel secondo caso trattasi della disponibilità di una risorsa (CPU, I/O, struttura dati, ecc.) di cui il processo

Dettagli

Università degli Studi di Milano Bicocca Facoltà di Scienze Matematiche Fisiche e Naturali Corso di Laurea Specialistica in Informatica

Università degli Studi di Milano Bicocca Facoltà di Scienze Matematiche Fisiche e Naturali Corso di Laurea Specialistica in Informatica Università degli Studi di Milano Bicocca Facoltà di Scienze Matematiche Fisiche e Naturali Corso di Laurea Specialistica in Informatica Real-time EDF scheduling per il kernel FreeBSD: analisi, implementazione

Dettagli

Scheduling della CPU. Sistemi multiprocessori e real time Metodi di valutazione Esempi: Solaris 2 Windows 2000 Linux

Scheduling della CPU. Sistemi multiprocessori e real time Metodi di valutazione Esempi: Solaris 2 Windows 2000 Linux Scheduling della CPU Sistemi multiprocessori e real time Metodi di valutazione Esempi: Solaris 2 Windows 2000 Linux Sistemi multiprocessori Fin qui si sono trattati i problemi di scheduling su singola

Dettagli

Processi e Thread. Scheduling (Schedulazione)

Processi e Thread. Scheduling (Schedulazione) Processi e Thread Scheduling (Schedulazione) 1 Scheduling Introduzione al problema dello Scheduling (1) Lo scheduler si occupa di decidere quale fra i processi pronti può essere mandato in esecuzione L

Dettagli

Scheduling della CPU:

Scheduling della CPU: Coda dei processi pronti (ready( queue): Scheduling della CPU primo ultimo PCB i PCB j PCB k contiene i descrittori ( process control block, PCB) dei processi pronti. la strategia di gestione della ready

Dettagli

Scheduling. Lo scheduler è la parte del SO che si occupa di

Scheduling. Lo scheduler è la parte del SO che si occupa di Scheduling Lo scheduler è la parte del SO che si occupa di decidere quale fra i processi pronti può essere mandato in esecuzione L algoritmo di scheduling (la politica utilizzata dallo scheduler) ha impatto

Dettagli

Scheduling della CPU

Scheduling della CPU Scheduling della CPU Sistemi multiprocessori e real time Metodi di valutazione Esempi: Solaris 2 Windows 2000 Linux 6.1 Sistemi multiprocessori simmetrici Fin qui si sono trattati i problemi di scheduling

Dettagli

Con il termine Sistema operativo si fa riferimento all insieme dei moduli software di un sistema di elaborazione dati dedicati alla sua gestione.

Con il termine Sistema operativo si fa riferimento all insieme dei moduli software di un sistema di elaborazione dati dedicati alla sua gestione. Con il termine Sistema operativo si fa riferimento all insieme dei moduli software di un sistema di elaborazione dati dedicati alla sua gestione. Compito fondamentale di un S.O. è infatti la gestione dell

Dettagli

Il Sistema Operativo. C. Marrocco. Università degli Studi di Cassino

Il Sistema Operativo. C. Marrocco. Università degli Studi di Cassino Il Sistema Operativo Il Sistema Operativo è uno strato software che: opera direttamente sull hardware; isola dai dettagli dell architettura hardware; fornisce un insieme di funzionalità di alto livello.

Dettagli

Sistemi Operativi Kernel

Sistemi Operativi Kernel Approfondimento Sistemi Operativi Kernel Kernel del Sistema Operativo Kernel (nocciolo, nucleo) Contiene i programmi per la gestione delle funzioni base del calcolatore Kernel suddiviso in moduli. Ogni

Dettagli

Pag. 1. Introduzione allo scheduling. Concetti fondamentali. Scheduling della CPU. Concetti fondamentali. Concetti fondamentali. Algoritmi.

Pag. 1. Introduzione allo scheduling. Concetti fondamentali. Scheduling della CPU. Concetti fondamentali. Concetti fondamentali. Algoritmi. Concetti fondamentali Scheduling della CU Introduzione allo scheduling Uno degli obbiettivi della multiprogrammazione è quello di massimizzare l utilizzo delle risorse e in particolare della CU er raggiungere

Dettagli

Corso di Informatica

Corso di Informatica Corso di Informatica Modulo T3 3-Schedulazione 1 Prerequisiti Concetto di media Concetto di varianza 2 1 Introduzione Come sappiamo, l assegnazione della CPU ai processi viene gestita dal nucleo, attraverso

Dettagli

scheduling Riedizione modifi cata delle slide della Prof. DI Stefano

scheduling Riedizione modifi cata delle slide della Prof. DI Stefano scheduling Riedizione modifi cata delle slide della Prof. DI Stefano 1 Scheduling Alternanza di CPU burst e periodi di I/O wait a) processo CPU-bound b) processo I/O bound 2 CPU Scheduler Seleziona uno

Dettagli

I Thread. I Thread. I due processi dovrebbero lavorare sullo stesso testo

I Thread. I Thread. I due processi dovrebbero lavorare sullo stesso testo I Thread 1 Consideriamo due processi che devono lavorare sugli stessi dati. Come possono fare, se ogni processo ha la propria area dati (ossia, gli spazi di indirizzamento dei due processi sono separati)?

Dettagli

Scheduling della CPU

Scheduling della CPU Scheduling della CPU Scheduling della CPU Obiettivo della multiprogrammazione: massimizzazione dell utilizzo della CPU. Scheduling della CPU: attivita` di allocazione della risorsa CPU ai processi. Scheduler

Dettagli

SCHEDULATORI DI PROCESSO

SCHEDULATORI DI PROCESSO Indice 5 SCHEDULATORI DI PROCESSO...1 5.1 Schedulatore Round Robin...1 5.2 Schedulatore a priorità...2 5.2.1 Schedulatore a code multiple...3 5.3 Schedulatore Shortest Job First...3 i 5 SCHEDULATORI DI

Dettagli

Pronto Esecuzione Attesa Terminazione

Pronto Esecuzione Attesa Terminazione Definizione Con il termine processo si indica una sequenza di azioni che il processore esegue Il programma invece, è una sequenza di azioni che il processore dovrà eseguire Il processo è quindi un programma

Dettagli

ASPETTI GENERALI DI LINUX. Parte 2 Struttura interna del sistema LINUX

ASPETTI GENERALI DI LINUX. Parte 2 Struttura interna del sistema LINUX Parte 2 Struttura interna del sistema LINUX 76 4. ASPETTI GENERALI DEL SISTEMA OPERATIVO LINUX La funzione generale svolta da un Sistema Operativo può essere definita come la gestione dell Hardware orientata

Dettagli

La schedulazione. E.Mumolo mumolo@units.it

La schedulazione. E.Mumolo mumolo@units.it La schedulazione E.Mumolo mumolo@units.it Concetti fondamentali Multiprogrammazione: esecuzione simultanea di più sequenze di esecuzione Pseudo-parallelismo su una sola CPU Esecuzione parallela su più

Dettagli

Algoritmi di scheduling

Algoritmi di scheduling Capitolo 3 Algoritmi di scheduling Come caso particolare di studio, di seguito è discussa in dettaglio la politica di scheduling del sistema operativo LINUX (kernel precedente alla versione 2.6). Sono

Dettagli

Il software di base comprende l insieme dei programmi predisposti per un uso efficace ed efficiente del computer.

Il software di base comprende l insieme dei programmi predisposti per un uso efficace ed efficiente del computer. I Sistemi Operativi Il Software di Base Il software di base comprende l insieme dei programmi predisposti per un uso efficace ed efficiente del computer. Il sistema operativo è il gestore di tutte le risorse

Dettagli

INFORMATICA. Il Sistema Operativo. di Roberta Molinari

INFORMATICA. Il Sistema Operativo. di Roberta Molinari INFORMATICA Il Sistema Operativo di Roberta Molinari Il Sistema Operativo un po di definizioni Elaborazione: trattamento di di informazioni acquisite dall esterno per per restituire un un risultato Processore:

Dettagli

Real-time EDF scheduling per il kernel FreeBSD: analisi, implementazione e risultati sperimentali

Real-time EDF scheduling per il kernel FreeBSD: analisi, implementazione e risultati sperimentali Real-time EDF scheduling per il kernel FreeBSD: analisi, implementazione e risultati sperimentali Marco Trentini m.trentini@campus.unimib.it Relatore: Dott. Sergio Ruocco Correlatore: Prof. Francesco Tisato

Dettagli

Architetture Applicative

Architetture Applicative Alessandro Martinelli alessandro.martinelli@unipv.it 6 Marzo 2012 Architetture Architetture Applicative Introduzione Alcuni esempi di Architetture Applicative Architetture con più Applicazioni Architetture

Dettagli

Calcolatori Elettronici A a.a. 2008/2009

Calcolatori Elettronici A a.a. 2008/2009 Calcolatori Elettronici A a.a. 2008/2009 PRESTAZIONI DEL CALCOLATORE Massimiliano Giacomin Due dimensioni Tempo di risposta (o tempo di esecuzione): il tempo totale impiegato per eseguire un task (include

Dettagli

Scheduling della CPU. Concetti fondamentali. Concetti fondamentali. Concetti fondamentali. Dispatcher. Scheduler della CPU

Scheduling della CPU. Concetti fondamentali. Concetti fondamentali. Concetti fondamentali. Dispatcher. Scheduler della CPU Scheduling della CPU Concetti fondamentali Criteri di scheduling Algoritmi di scheduling Concetti fondamentali L obiettivo della multiprogrammazione è di avere processi sempre in esecuzione al fine di

Dettagli

1. Che cos è la multiprogrammazione? Si può realizzare su un sistema monoprocessore? 2. Quali sono i servizi offerti dai sistemi operativi?

1. Che cos è la multiprogrammazione? Si può realizzare su un sistema monoprocessore? 2. Quali sono i servizi offerti dai sistemi operativi? 1. Che cos è la multiprogrammazione? Si può realizzare su un sistema monoprocessore? 2. Quali sono i servizi offerti dai sistemi operativi? 1. La nozione di multiprogrammazione prevede la possibilità di

Dettagli

La Gestione delle risorse Renato Agati

La Gestione delle risorse Renato Agati Renato Agati delle risorse La Gestione Schedulazione dei processi Gestione delle periferiche File system Schedulazione dei processi Mono programmazione Multi programmazione Gestione delle periferiche File

Dettagli

Scheduling della CPU Introduzione ai Sistemi Operativi Corso di Abilità Informatiche Laurea in Fisica

Scheduling della CPU Introduzione ai Sistemi Operativi Corso di Abilità Informatiche Laurea in Fisica Scheduling della CPU Introduzione ai Sistemi Operativi Corso di Abilità Informatiche Laurea in Fisica prof. Ing. Corrado Santoro A.A. 2010-11 Architettura di un sistema operativo Progr 1 Progr 2 Progr

Dettagli

Università degli Studi di Salerno

Università degli Studi di Salerno Università degli Studi di Salerno Facoltà di Scienze Matematiche Fisiche e Naturali Corso di Laurea in Informatica Tesi di Laurea Algoritmi basati su formule di quadratura interpolatorie per GPU ABSTRACT

Dettagli

1 Processo, risorsa, richiesta, assegnazione 2 Concorrenza 3 Grafo di Holt 4 Thread 5 Sincronizzazione tra processi

1 Processo, risorsa, richiesta, assegnazione 2 Concorrenza 3 Grafo di Holt 4 Thread 5 Sincronizzazione tra processi 1 Processo, risorsa, richiesta, assegnazione 2 Concorrenza 3 Grafo di Holt 4 Thread 5 Sincronizzazione tra processi Il processo E' un programma in esecuzione Tipi di processo Stati di un processo 1 indipendenti

Dettagli

Lez. 4 Lo scheduling dei processi. Corso: Sistemi Operativi Danilo Bruschi

Lez. 4 Lo scheduling dei processi. Corso: Sistemi Operativi Danilo Bruschi Sistemi Operativi Lez. 4 Lo scheduling dei processi 1 Cicli d'elaborazione In ogni processo i burst di CPU si alternano con i tempi di I/O 2 Uso tipico di un calcolatore 3 CPU-bound e I/O-bound Processi

Dettagli

Sistemi Operativi. 5 Gestione della memoria

Sistemi Operativi. 5 Gestione della memoria Gestione della memoria Compiti del gestore della memoria: Tenere traccia di quali parti della memoria sono libere e quali occupate. Allocare memoria ai processi che ne hanno bisogno. Deallocare la memoria

Dettagli

Sistema Operativo. Fondamenti di Informatica 1. Il Sistema Operativo

Sistema Operativo. Fondamenti di Informatica 1. Il Sistema Operativo Sistema Operativo Fondamenti di Informatica 1 Il Sistema Operativo Il Sistema Operativo (S.O.) è un insieme di programmi interagenti che consente agli utenti e ai programmi applicativi di utilizzare al

Dettagli

Capitolo 5: Scheduling della CPU! Scheduling della CPU! Concetti di Base! Alternanza di Sequenze di CPU- e I/O-Burst!

Capitolo 5: Scheduling della CPU! Scheduling della CPU! Concetti di Base! Alternanza di Sequenze di CPU- e I/O-Burst! Capitolo 5: Scheduling della CPU Criteri di Scheduling Algoritmi di Scheduling Cenni Scheduling per sistemi multprocessore Modelli Asimmetrico e Simmetrico Scheduling della CPU 5.2 Concetti di Base Alternanza

Dettagli

Concetti di base. Scheduling della CPU. Diagramma della durata dei CPU-burst. Sequenza Alternata di CPU Burst e I/O Burst

Concetti di base. Scheduling della CPU. Diagramma della durata dei CPU-burst. Sequenza Alternata di CPU Burst e I/O Burst Impossibile visualizzare l'immagine. Scheduling della CPU Concetti di base La multiprogrammazione cerca di ottenere la massima utilizzazione della CPU. L esecuzione di un processo consiste in cicli d esecuzione

Dettagli

Prestazioni CPU Corso di Calcolatori Elettronici A 2007/2008 Sito Web:http://prometeo.ing.unibs.it/quarella Prof. G. Quarella prof@quarella.

Prestazioni CPU Corso di Calcolatori Elettronici A 2007/2008 Sito Web:http://prometeo.ing.unibs.it/quarella Prof. G. Quarella prof@quarella. Prestazioni CPU Corso di Calcolatori Elettronici A 2007/2008 Sito Web:http://prometeo.ing.unibs.it/quarella Prof. G. Quarella prof@quarella.net Prestazioni Si valutano in maniera diversa a seconda dell

Dettagli

Approccio stratificato

Approccio stratificato Approccio stratificato Il sistema operativo è suddiviso in strati (livelli), ciascuno costruito sopra quelli inferiori. Il livello più basso (strato 0) è l hardware, il più alto (strato N) è l interfaccia

Dettagli

Lezione 6. Sistemi operativi. Marco Cesati System Programming Research Group Università degli Studi di Roma Tor Vergata.

Lezione 6. Sistemi operativi. Marco Cesati System Programming Research Group Università degli Studi di Roma Tor Vergata. Lezione 6 Sistemi operativi 31 marzo 2015 System Programming Research Group Università degli Studi di Roma Tor Vergata SO 15 6.1 Di cosa parliamo in questa lezione? La schedulazione 1 e caratteristiche

Dettagli

Schedulazione in RTAI

Schedulazione in RTAI Schedulazione in RTAI RTAI: modulo kernel rt_hello_km.c #include #include Thread real-time... Ciclo infinito RT_TASK task; Periodico... void task_routine() { while(1) { /* Codice

Dettagli

Criteri di Scheduling Algoritmi di Scheduling Multiple-Processor Scheduling Asymmetric/Symmetric multiprocessing Processori Multicore

Criteri di Scheduling Algoritmi di Scheduling Multiple-Processor Scheduling Asymmetric/Symmetric multiprocessing Processori Multicore Criteri di Scheduling Algoritmi di Scheduling Multiple-Processor Scheduling Asymmetric/Symmetric multiprocessing Processori Multicore 5.2 Silberschatz, Galvin and Gagne 2005 1 Massimizzare lʼutilizzo della

Dettagli

Sistemi operativi e reti A.A. 2013-14. Lezione 2

Sistemi operativi e reti A.A. 2013-14. Lezione 2 Università di Roma Tor Vergata Corso di Laurea triennale in Informatica Sistemi operativi e reti A.A. 2013-14 Pietro Frasca Lezione 2 Giovedì 10-10-2013 1 Sistemi a partizione di tempo (time-sharing) I

Dettagli

Lo scheduler di UNIX (1)

Lo scheduler di UNIX (1) Lo scheduler di UNIX (1) Lo scheduling a basso livello è basato su una coda a più livelli di priorità 1 Lo scheduler di UNIX (2) Si esegue il primo processo della prima coda non vuota per massimo 1 quanto

Dettagli

Algoritmi di scheduling

Algoritmi di scheduling Capitolo 2 Algoritmi di scheduling 2.1 Sistemi Real Time In un sistema in tempo reale (real time) il tempo gioca un ruolo essenziale. Le applicazioni di tali sistemi sono molteplici e di larga diffusione.

Dettagli

Definizione Parte del software che gestisce I programmi applicativi L interfaccia tra il calcolatore e i programmi applicativi Le funzionalità di base

Definizione Parte del software che gestisce I programmi applicativi L interfaccia tra il calcolatore e i programmi applicativi Le funzionalità di base Sistema operativo Definizione Parte del software che gestisce I programmi applicativi L interfaccia tra il calcolatore e i programmi applicativi Le funzionalità di base Architettura a strati di un calcolatore

Dettagli

Università di Roma Tor Vergata Corso di Laurea triennale in Informatica Sistemi operativi e reti A.A. 2014-15. Pietro Frasca.

Università di Roma Tor Vergata Corso di Laurea triennale in Informatica Sistemi operativi e reti A.A. 2014-15. Pietro Frasca. Università di Roma Tor Vergata Corso di Laurea triennale in Informatica Sistemi operativi e reti A.A. 2014-15 Pietro Frasca Lezione 5 Martedì 21-10-2014 Thread Come abbiamo detto, un processo è composto

Dettagli

Le fattispecie di riuso

Le fattispecie di riuso Le fattispecie di riuso Indice 1. PREMESSA...3 2. RIUSO IN CESSIONE SEMPLICE...4 3. RIUSO CON GESTIONE A CARICO DEL CEDENTE...5 4. RIUSO IN FACILITY MANAGEMENT...6 5. RIUSO IN ASP...7 1. Premessa Poiché

Dettagli

Esempio: aggiungere j

Esempio: aggiungere j Esempio: aggiungere j Eccezioni e interruzioni Il progetto del controllo del processore si complica a causa della necessità di considerare, durante l esecuzione delle istruzioni, il verificarsi di eventi

Dettagli

La gestione di un calcolatore. Sistemi Operativi primo modulo Introduzione. Sistema operativo (2) Sistema operativo (1)

La gestione di un calcolatore. Sistemi Operativi primo modulo Introduzione. Sistema operativo (2) Sistema operativo (1) La gestione di un calcolatore Sistemi Operativi primo modulo Introduzione Augusto Celentano Università Ca Foscari Venezia Corso di Laurea in Informatica Un calcolatore (sistema di elaborazione) è un sistema

Dettagli

Il Sistema Operativo

Il Sistema Operativo Il Sistema Operativo Il Sistema Operativo Il Sistema Operativo (S.O.) è un insieme di programmi interagenti che consente agli utenti e ai programmi applicativi di utilizzare al meglio le risorse del Sistema

Dettagli

J. Assfalg Appunti di Sistemi Operativi

J. Assfalg Appunti di Sistemi Operativi Lo scheduler di Linux (kernel 2.4) La politica di scheduling di Linux si propone il raggiungimento dei seguenti obiettivi (molti dei quali sono in contrasto): timesharing gestione di priorità dinamiche

Dettagli

Sistemi Operativi. Scheduling dei processi

Sistemi Operativi. Scheduling dei processi Sistemi Operativi Scheduling dei processi Scheduling dei processi Se più processi sono eseguibili in un certo istante il sistema deve decidere quale eseguire per primo La parte del sistema operativo che

Dettagli

Sistema operativo: Gestione dei processi

Sistema operativo: Gestione dei processi Dipartimento di Elettronica ed Informazione Politecnico di Milano Informatica - ICA Prof. Pierluigi Plebani A.A. 2011/2012 Sistema operativo: Gestione dei processi La presente dispensa e da utilizzarsi

Dettagli

Introduzione alla Virtualizzazione

Introduzione alla Virtualizzazione Introduzione alla Virtualizzazione Dott. Luca Tasquier E-mail: luca.tasquier@unina2.it Virtualizzazione - 1 La virtualizzazione è una tecnologia software che sta cambiando il metodo d utilizzo delle risorse

Dettagli

Ambienti di calcolo a griglia Parte 2. Risorse (e loro gestione) Job di griglia e applicazioni di griglia Riservare le risorse ai job

Ambienti di calcolo a griglia Parte 2. Risorse (e loro gestione) Job di griglia e applicazioni di griglia Riservare le risorse ai job Ambienti di calcolo a griglia Parte 2 Risorse (e loro gestione) Job di griglia e applicazioni di griglia Riservare le risorse ai job Docente: Marcello CASTELLANO La vera rivoluzione non è più la capacità

Dettagli

Automazione Industriale (scheduling+mms) scheduling+mms. adacher@dia.uniroma3.it

Automazione Industriale (scheduling+mms) scheduling+mms. adacher@dia.uniroma3.it Automazione Industriale (scheduling+mms) scheduling+mms adacher@dia.uniroma3.it Introduzione Sistemi e Modelli Lo studio e l analisi di sistemi tramite una rappresentazione astratta o una sua formalizzazione

Dettagli

Dispensa di Informatica I.1

Dispensa di Informatica I.1 IL COMPUTER: CONCETTI GENERALI Il Computer (o elaboratore) è un insieme di dispositivi di diversa natura in grado di acquisire dall'esterno dati e algoritmi e produrre in uscita i risultati dell'elaborazione.

Dettagli

Tesina per l esame di Sistemi Operativi a cura di Giuseppe Montano. Prof. Aldo Franco Dragoni

Tesina per l esame di Sistemi Operativi a cura di Giuseppe Montano. Prof. Aldo Franco Dragoni Sistemi operativi real time basati su Linux: gestione delle risorse e dei processi. Tesina per l esame di Sistemi Operativi a cura di. Prof. Aldo Franco Dragoni Corso di laurea in Ingegneria Informatica

Dettagli

A intervalli regolari ogni router manda la sua tabella a tutti i vicini, e riceve quelle dei vicini.

A intervalli regolari ogni router manda la sua tabella a tutti i vicini, e riceve quelle dei vicini. Algoritmi di routing dinamici (pag.89) UdA2_L5 Nelle moderne reti si usano algoritmi dinamici, che si adattano automaticamente ai cambiamenti della rete. Questi algoritmi non sono eseguiti solo all'avvio

Dettagli

Architettura di un sistema operativo

Architettura di un sistema operativo Architettura di un sistema operativo Dipartimento di Informatica Università di Verona, Italy Struttura di un S.O. Sistemi monolitici Sistemi a struttura semplice Sistemi a livelli Virtual Machine Sistemi

Dettagli

Il Sistema Operativo. Di cosa parleremo? Come si esegue un programma. La nozione di processo. Il sistema operativo

Il Sistema Operativo. Di cosa parleremo? Come si esegue un programma. La nozione di processo. Il sistema operativo Il Sistema Operativo Di cosa parleremo? Come si esegue un programma. La nozione di processo. Il sistema operativo ... ma Cos'è un S.O.? un PROGRAMMA!... ma Cos'è un programma? PROGRAMMA: 1. algoritmo sequenza

Dettagli

Software di sistema e software applicativo. I programmi che fanno funzionare il computer e quelli che gli permettono di svolgere attività specifiche

Software di sistema e software applicativo. I programmi che fanno funzionare il computer e quelli che gli permettono di svolgere attività specifiche Software di sistema e software applicativo I programmi che fanno funzionare il computer e quelli che gli permettono di svolgere attività specifiche Software soft ware soffice componente è la parte logica

Dettagli

Il sistema operativo TinyOS

Il sistema operativo TinyOS tesi di laurea Anno Accademico 2005/2006 relatore Ch.mo prof. Domenico Cotroneo candidato Giovanni Chierchia Matr. 534 / 804 ::. Obiettivi del lavoro di tesi Studio del sistema operativo TinyOS Studio

Dettagli

I processi. Un processo è una attività, controllata da un programma, che si svolge su un processore.

I processi. Un processo è una attività, controllata da un programma, che si svolge su un processore. I processi Cos è un processo? Un processo è una attività, controllata da un programma, che si svolge su un processore. Il programma è una entità statica che descrive la sequenza di istruzioni che devono

Dettagli

Lo schedulatore del kernel

Lo schedulatore del kernel Lo schedulatore del kernel Lo schedulatore Lo schedulatore è un programma del kernel che si occupa di scegliere un processo/lavoro che ha richiesto una risorsa. Lo schedulatore a lungo termine LTS è quello

Dettagli

SISTEMI OPERATIVI. Prof. Enrico Terrone A. S: 2008/09

SISTEMI OPERATIVI. Prof. Enrico Terrone A. S: 2008/09 SISTEMI OPERATIVI Prof. Enrico Terrone A. S: 2008/09 Che cos è il sistema operativo Il sistema operativo (SO) è il software che gestisce e rende accessibili (sia ai programmatori e ai programmi, sia agli

Dettagli

Architettura di un calcolatore

Architettura di un calcolatore 2009-2010 Ingegneria Aerospaziale Prof. A. Palomba - Elementi di Informatica (E-Z) 7 Architettura di un calcolatore Lez. 7 1 Modello di Von Neumann Il termine modello di Von Neumann (o macchina di Von

Dettagli

Gestione della Memoria

Gestione della Memoria Gestione della Memoria Idealmente la memoria dovrebbe essere grande veloce non volatile Gerarchia di memorie Disco: capiente, lento, non volatile ed economico Memoria principale: volatile, mediamente grande,

Dettagli

Sistemi Operativi (modulo di Informatica II) I processi

Sistemi Operativi (modulo di Informatica II) I processi Sistemi Operativi (modulo di Informatica II) I processi Patrizia Scandurra Università degli Studi di Bergamo a.a. 2009-10 Sommario Il concetto di processo Schedulazione dei processi e cambio di contesto

Dettagli

Il Sistema Operativo (1)

Il Sistema Operativo (1) E il software fondamentale del computer, gestisce tutto il suo funzionamento e crea un interfaccia con l utente. Le sue funzioni principali sono: Il Sistema Operativo (1) La gestione dell unità centrale

Dettagli

Definizione di processo. Un processo è un programma (o una parte di una programma) in corso di esecuzione

Definizione di processo. Un processo è un programma (o una parte di una programma) in corso di esecuzione SISTEMI OPERATIVI (parte prima - gestione dei processi) Tra i compiti di un sistema operativo sicuramente troviamo i seguenti: Gestione dei processi Gestione della memoria Gestione del file-system Ci occuperemo

Dettagli

Generazione Automatica di Asserzioni da Modelli di Specifica

Generazione Automatica di Asserzioni da Modelli di Specifica UNIVERSITÀ DEGLI STUDI DI MILANO BICOCCA FACOLTÀ DI SCIENZE MATEMATICHE FISICHE E NATURALI Corso di Laurea Magistrale in Informatica Generazione Automatica di Asserzioni da Modelli di Specifica Relatore:

Dettagli

STRUTTURE DEI SISTEMI DI CALCOLO

STRUTTURE DEI SISTEMI DI CALCOLO STRUTTURE DEI SISTEMI DI CALCOLO 2.1 Strutture dei sistemi di calcolo Funzionamento Struttura dell I/O Struttura della memoria Gerarchia delle memorie Protezione Hardware Architettura di un generico sistema

Dettagli

Software per Helpdesk

Software per Helpdesk Software per Helpdesk Padova - maggio 2010 Antonio Dalvit - www.antoniodalvit.com Cosa è un helpdesk? Un help desk è un servizio che fornisce informazioni e assistenza ad utenti che hanno problemi nella

Dettagli

Gestione del processore e dei processi

Gestione del processore e dei processi Il processore è la componente più importante di un sistema di elaborazione e pertanto la sua corretta ed efficiente gestione è uno dei compiti principali di un sistema operativo Il ruolo del processore

Dettagli

Introduzione ai Sistemi Operativi

Introduzione ai Sistemi Operativi Introduzione ai Sistemi Operativi Sistema Operativo Software! Applicazioni! Sistema Operativo! È il livello di SW con cui! interagisce l utente! e comprende! programmi quali :! Compilatori! Editori di

Dettagli

Scheduling della CPU. Contenuti delle lezioni del 23 e del 26 Marzo 2009. Sequenza alternata di CPU burst e di I/O burst.

Scheduling della CPU. Contenuti delle lezioni del 23 e del 26 Marzo 2009. Sequenza alternata di CPU burst e di I/O burst. Contenuti delle lezioni del 23 e del 26 Marzo 2009 Scheduling della CPU Introduzione allo scheduling della CPU Descrizione di vari algoritmi di scheduling della CPU Analisi dei criteri di valutazione nella

Dettagli

Introduzione. Classificazione di Flynn... 2 Macchine a pipeline... 3 Macchine vettoriali e Array Processor... 4 Macchine MIMD... 6

Introduzione. Classificazione di Flynn... 2 Macchine a pipeline... 3 Macchine vettoriali e Array Processor... 4 Macchine MIMD... 6 Appunti di Calcolatori Elettronici Esecuzione di istruzioni in parallelo Introduzione... 1 Classificazione di Flynn... 2 Macchine a pipeline... 3 Macchine vettoriali e Array Processor... 4 Macchine MIMD...

Dettagli

GESTIONE DEI PROCESSI

GESTIONE DEI PROCESSI Sistemi Operativi GESTIONE DEI PROCESSI Processi Concetto di Processo Scheduling di Processi Operazioni su Processi Processi Cooperanti Concetto di Thread Modelli Multithread I thread in Java Concetto

Dettagli

Sistemi Operativi GESTIONE DELLA MEMORIA CENTRALE. D. Talia - UNICAL. Sistemi Operativi 6.1

Sistemi Operativi GESTIONE DELLA MEMORIA CENTRALE. D. Talia - UNICAL. Sistemi Operativi 6.1 GESTIONE DELLA MEMORIA CENTRALE 6.1 Gestione della Memoria Background Spazio di indirizzi Swapping Allocazione Contigua Paginazione 6.2 Background Per essere eseguito un programma deve trovarsi (almeno

Dettagli

TITLE Sistemi Operativi 1

TITLE Sistemi Operativi 1 TITLE Sistemi Operativi 1 Cos'è un sistema operativo Definizione: Un sistema operativo è un programma che controlla l'esecuzione di programmi applicativi e agisce come interfaccia tra le applicazioni e

Dettagli

Consiglio regionale della Toscana. Regole per il corretto funzionamento della posta elettronica

Consiglio regionale della Toscana. Regole per il corretto funzionamento della posta elettronica Consiglio regionale della Toscana Regole per il corretto funzionamento della posta elettronica A cura dell Ufficio Informatica Maggio 2006 Indice 1. Regole di utilizzo della posta elettronica... 3 2. Controllo

Dettagli

Obiettivo della multiprogrammazione: massimizzazione dell utilizzo CPU. Scheduling della CPU: commuta l uso della CPU tra i vari processi

Obiettivo della multiprogrammazione: massimizzazione dell utilizzo CPU. Scheduling della CPU: commuta l uso della CPU tra i vari processi Scheduling della CPU Scheduling della CPU Obiettivo della multiprogrammazione: massimizzazione dell utilizzo CPU Scheduling della CPU: commuta l uso della CPU tra i vari processi Scheduler della CPU (a

Dettagli

Valutazione delle Prestazioni. Valutazione delle Prestazioni. Architetture dei Calcolatori (Lettere. Tempo di risposta e throughput

Valutazione delle Prestazioni. Valutazione delle Prestazioni. Architetture dei Calcolatori (Lettere. Tempo di risposta e throughput Valutazione delle Prestazioni Architetture dei Calcolatori (Lettere A-I) Valutazione delle Prestazioni Prof. Francesco Lo Presti Misura/valutazione di un insieme di parametri quantitativi per caratterizzare

Dettagli

Lezione 4 La Struttura dei Sistemi Operativi. Introduzione

Lezione 4 La Struttura dei Sistemi Operativi. Introduzione Lezione 4 La Struttura dei Sistemi Operativi Introduzione Funzionamento di un SO La Struttura di un SO Sistemi Operativi con Struttura Monolitica Progettazione a Livelli di un SO 4.2 1 Introduzione (cont.)

Dettagli

Calcolatori Elettronici. La memoria gerarchica La memoria virtuale

Calcolatori Elettronici. La memoria gerarchica La memoria virtuale Calcolatori Elettronici La memoria gerarchica La memoria virtuale Come usare la memoria secondaria oltre che per conservare permanentemente dati e programmi Idea Tenere parte del codice in mem princ e

Dettagli

Guida Compilazione Piani di Studio on-line

Guida Compilazione Piani di Studio on-line Guida Compilazione Piani di Studio on-line SIA (Sistemi Informativi d Ateneo) Visualizzazione e presentazione piani di studio ordinamento 509 e 270 Università della Calabria (Unità organizzativa complessa-

Dettagli

Sistemi Operativi GESTIONE DELLA MEMORIA SECONDARIA. D. Talia - UNICAL. Sistemi Operativi 11.1

Sistemi Operativi GESTIONE DELLA MEMORIA SECONDARIA. D. Talia - UNICAL. Sistemi Operativi 11.1 GESTIONE DELLA MEMORIA SECONDARIA 11.1 Memoria Secondaria Struttura del disco Scheduling del disco Gestione del disco Gestione dello spazio di swap Struttura RAID Affidabilità Implementazione della memoria

Dettagli

Sistemi Operativi. Memoria Secondaria GESTIONE DELLA MEMORIA SECONDARIA. Struttura del disco. Scheduling del disco. Gestione del disco

Sistemi Operativi. Memoria Secondaria GESTIONE DELLA MEMORIA SECONDARIA. Struttura del disco. Scheduling del disco. Gestione del disco GESTIONE DELLA MEMORIA SECONDARIA 11.1 Memoria Secondaria Struttura del disco Scheduling del disco Gestione del disco Gestione dello spazio di swap Struttura RAID Affidabilità Implementazione della memoria

Dettagli

Strutturazione logica dei dati: i file

Strutturazione logica dei dati: i file Strutturazione logica dei dati: i file Informazioni più complesse possono essere composte a partire da informazioni elementari Esempio di una banca: supponiamo di voler mantenere all'interno di un computer

Dettagli

Corso di Informatica

Corso di Informatica Corso di Informatica Modulo T1 4-Panoramica delle generazioni 1 Prerequisiti Monoprogrammazione e multiprogrammazione Multielaborazione Linguaggio macchina Linguaggi di programmazione e compilatori Struttura

Dettagli

Scheduling Introduzione Tipi di scheduler Scheduler di lungo termine (SLT) Scheduler di medio termine (SMT) Scheduler di breve termine (SBT)

Scheduling Introduzione Tipi di scheduler Scheduler di lungo termine (SLT) Scheduler di medio termine (SMT) Scheduler di breve termine (SBT) Scheduling Introduzione Con scheduling si intende un insieme di tecniche e di meccanismi interni del sistema operativo che amministrano l ordine in cui il lavoro viene svolto. Lo Scheduler è il modulo

Dettagli

Sistema operativo: Gestione della memoria

Sistema operativo: Gestione della memoria Dipartimento di Elettronica ed Informazione Politecnico di Milano Informatica e CAD (c.i.) - ICA Prof. Pierluigi Plebani A.A. 2008/2009 Sistema operativo: Gestione della memoria La presente dispensa e

Dettagli

Sommario. Analysis & design delle applicazioni parallele. Misura delle prestazioni parallele. Tecniche di partizionamento.

Sommario. Analysis & design delle applicazioni parallele. Misura delle prestazioni parallele. Tecniche di partizionamento. Sommario Analysis & design delle applicazioni parallele Misura delle prestazioni parallele Tecniche di partizionamento Comunicazioni Load balancing 2 Primi passi: analizzare il problema Prima di iniziare

Dettagli