Sistema operativo - ruolo, struttura e system call
In questa pagina 8
Questa nota apre la parte di sistemi operativi: dice che cos'è un sistema operativo, come è fatto il calcolatore su cui gira, come un programma gli chiede un servizio (le system call) e come si organizza il codice del sistema stesso. Le note successive entrano nei singoli compiti: Processi - stati, PCB, creazione con fork ed execUn processo è un programma in esecuzione (entità attiva) con codice (text), dati globali, heap, stack e lo stato dell'esecuzione; il kernel lo descrive con il PCB (Process Control Block: stato, program counter, registri, informazioni di scheduling, memoria, accounting, file aperti, in Linux la struct task_struct) e passa da un processo all'altro con il context switch (puro overhead); gli stati sono new, ready, running, waiting, terminated, con una ready queue e varie wait queue; in UNIX fork() crea un figlio copia del padre (restituisce 0 al figlio e il pid del figlio al padre), exec() sostituisce il programma del figlio, wait()/waitpid() raccoglie lo stato di uscita: senza di essa un figlio terminato è uno zombie, un figlio il cui padre è morto è un orfano adottato da init/systemd; i processi cooperanti comunicano con memoria condivisa (POSIX shm) o scambio di messaggi (diretto o indiretto, sincrono o asincrono, buffer a capacità zero, limitata, illimitata) e con pipe ordinarie (padre-figlio, unidirezionali) e named pipe (FIFO).Processi - stati, PCB, creazione con fork ed exec →, Thread e concorrenza - modelli, Pthreads e goroutineUn thread è un flusso di esecuzione dentro un processo: ha program counter, registri e stack propri ma condivide codice, dati, heap e file aperti del processo, quindi costa meno di un processo (creazione e commutazione) e comunica più facilmente, ma i dati condivisi producono race condition; concorrenza (più compiti che avanzano, anche su un solo core) non è parallelismo (più compiti eseguiti nello stesso istante, serve più di un core); si distinguono parallelismo sui dati e sui compiti e la legge di Amdahl dà lo speedup massimo 1/(S+(1-S)/N) con S frazione seriale; i thread utente sono gestiti da una libreria, quelli kernel dal sistema, e i modelli many-to-one, one-to-one (Linux, Windows) e many-to-many li collegano; in C si usa Pthreads (pthread_create, pthread_join, pthread_exit), in Go le goroutine (parola chiave go, sync.WaitGroup, canali); questioni aperte: fork/exec con i thread, segnali, cancellazione (asincrona o differita), thread-local storage, contention scope (PCS e SCS).Thread e concorrenza - modelli, Pthreads e goroutine →, Scheduling della CPU - FCFS, SJF, priorità e round robinLo scheduler della CPU sceglie tra i processi pronti quello a cui dare il core (il dispatcher esegue il cambio, con latenza di dispatch); lo scheduling è non preemptive (il processo tiene la CPU finché termina o si blocca) o preemptive (la CPU gli può essere tolta, con rischio di race condition); i criteri sono utilizzo della CPU e throughput (da massimizzare), tempo di attesa, di turnaround (completamento meno arrivo) e di risposta (da minimizzare); FCFS è semplice ma soffre dell'effetto convoglio (esempio 24, 3, 3: attesa media 17 contro 3); SJF è ottimo per l'attesa media ma richiede la lunghezza del prossimo burst, stimata con la media esponenziale tau(n+1) = alfa t(n) + (1-alfa) tau(n); SRTF è la versione preemptive (esempio con attesa media 6,5 contro 7,75); round robin dà a ciascuno un quanto q e nessuno attende più di (n-1)q; la priorità causa starvation, risolta con l'aging; code multilivello e a feedback (MLFQ) combinano le politiche.Scheduling della CPU - FCFS, SJF, priorità e round robin →, Sincronizzazione - sezione critica, race condition e mutexQuando più processi o thread accedono agli stessi dati e il risultato dipende dall'ordine degli accessi si ha una race condition (esempio: counter++ e counter-- sono tre istruzioni macchina ciascuna, e interleaving diversi danno 4, 5 o 6 partendo da 5); il problema della sezione critica chiede un protocollo (entry section, sezione critica, exit section, remainder section) che garantisca mutua esclusione, progresso e attesa limitata; disabilitare gli interrupt non scala sui multiprocessori, l'alternanza stretta con turn viola il progresso, la soluzione di Peterson (flag[2] e turn) soddisfa i tre requisiti ma può fallire su hardware moderno per il riordino delle istruzioni, per cui servono barriere di memoria; l'hardware offre test_and_set e compare_and_swap (atomiche) e variabili atomiche; il mutex lock (acquire/release, in POSIX pthread_mutex_t) è lo strumento di alto livello, con attesa attiva se è uno spinlock oppure con sospensione del thread se è un mutex di sistema; bloccare due volte lo stesso mutex senza sbloccarlo causa deadlock.Sincronizzazione - sezione critica, race condition e mutex →, Deadlock - condizioni, prevenzione, evitamento e rilevamentoUn deadlock (stallo) è una situazione in cui un insieme di processi attende all'infinito risorse possedute da altri processi dello stesso insieme; può verificarsi solo se valgono insieme quattro condizioni (mutua esclusione, hold and wait, assenza di prelazione, attesa circolare); il grafo di allocazione delle risorse (archi di richiesta P->R e di assegnazione R->P) senza cicli esclude lo stallo, con un ciclo lo implica se ogni risorsa ha una sola istanza, e lo rende solo possibile se ha più istanze; si può prevenire (negare una delle quattro condizioni, per esempio ordinare le risorse per negare l'attesa circolare), evitare (stato sicuro: esiste una sequenza sicura; algoritmo del grafo con archi di rivendicazione per istanza singola, algoritmo del banchiere con Available, Max, Allocation, Need = Max - Allocation e complessità O(m n^2) per istanze multiple), rilevare e ripristinare (grafo wait-for per istanza singola, algoritmo di rilevamento per istanze multiple; terminazione dei processi o prelazione delle risorse con scelta della vittima e rollback), oppure ignorare.Deadlock - condizioni, prevenzione, evitamento e rilevamento →, poi la memoria, il file system e l'I/O (Memoria centrale - allocazione contigua, paginazione e segmentazioneUn programma deve stare in memoria centrale per girare; la MMU traduce a ogni accesso l'indirizzo logico (virtuale) in indirizzo fisico, sommando un registro di rilocazione e controllando il limite; con l'allocazione contigua (partizioni fisse o variabili, first/best/worst fit) si hanno frammentazione interna ed esterna e serve la compattazione; la paginazione divide memoria logica in pagine e memoria fisica in frame della stessa dimensione (potenza di 2), elimina la frammentazione esterna, traduce (p, d) con la tabella delle pagine (PTBR, PTLR) e riduce il costo dei due accessi con il TLB (EAT = h * t + (1 - h) * 2t); le tabelle grandi si dividono in gerarchiche, hash o invertite; lo swapping sposta intere pagine o processi su disco; la segmentazione usa blocchi di lunghezza variabile con coppia (base, limite), eventualmente paginati; Intel IA-32 usa segmentazione con paginazione a due livelli (10+10+12 bit) e IA-64 quattro livelli su 48 bit.Memoria centrale - allocazione contigua, paginazione e segmentazione →, File system - interfaccia, directory e implementazioneUn file è uno spazio di indirizzi logici contigui con attributi (nome, identificatore, tipo, posizione, dimensione, protezione, date) e operazioni (create, read, write, seek, delete, truncate, open, close); il sistema tiene una tabella dei file aperti di sistema e una per processo; l'accesso è sequenziale o diretto; le directory si organizzano a livello singolo, a due livelli, ad albero, a grafo aciclico (hard link e link simbolici) o a grafo generale; i permessi UNIX rwx per proprietario, gruppo e altri (chmod 754); il file system è a strati (file system logico, organizzazione dei file, file system di base, controllo dell'I/O), con strutture su disco (boot block, superblock, directory, FCB o inode) e in memoria; i blocchi si allocano in modo contiguo (extent), concatenato (FAT) o indicizzato (inode con puntatori diretti e indiretti, ext4 con extent); lo spazio libero con bitmap, lista, raggruppamento o conteggio; mount, VFS, file system distribuiti (NFS) e semantiche di consistenza.File system - interfaccia, directory e implementazione →, Sistemi di I-O e memory-mapped I-OIl sistema operativo nasconde la varietà dei dispositivi con driver e un'interfaccia uniforme (dispositivi a blocchi, a caratteri, di rete, timer, ioctl); l'hardware è fatto di porte, bus e controller con registri di dato, stato e controllo, indirizzati con istruzioni di I/O dedicate o con il memory-mapped I/O; la CPU comunica con il dispositivo con il polling (busy wait, adatto a dispositivi veloci), con le interruzioni (linea IRQ, vettore, priorità, mascherabili) o con il DMA (trasferimenti di blocchi senza CPU, un'interruzione alla fine); le chiamate possono essere bloccanti, non bloccanti o asincrone; le istruzioni di I/O sono privilegiate e si passa dalle system call; il kernel gestisce errori, buffer e protezione; mmap() mappa un file nello spazio di indirizzi di un processo: accesso con load e store, caricamento pigro con il demand paging, meno chiamate di sistema e meno copie, condivisione con MAP_SHARED.Sistemi di I-O e memory-mapped I-O →).
Che cosa è un sistema operativo
Definizione (sistema operativo). Un sistema operativo (operating system, OS) è un programma che agisce da intermediario tra l'utente e l'hardware. Ha tre scopi: eseguire i programmi dell'utente e rendere facile risolvere i suoi problemi, rendere comodo l'uso del calcolatore, usare l'hardware in modo efficiente.
Il calcolatore si può vedere a quattro livelli: utenti (persone, macchine, altri calcolatori), programmi applicativi (browser, editor, giochi: decidono come usare le risorse), sistema operativo (controlla e coordina l'uso dell'hardware da parte dei programmi), hardware (CPU, memoria, dispositivi di I/O: le risorse di base).
Il sistema operativo ha due volti:
- allocatore di risorse (resource allocator): quando più programmi o utenti vogliono la stessa CPU, la stessa memoria o lo stesso disco, decide chi li usa e quando, cercando efficienza ed equità;
- programma di controllo (control program): impedisce che i programmi sbaglino o si danneggino a vicenda e che usino male i dispositivi.
Non esiste una definizione universale di "sistema operativo". Si distinguono:
| parte | che cosa è | esempio |
|---|---|---|
| kernel | l'unico programma sempre in esecuzione, è il cuore del sistema | il kernel Linux |
| programmi di sistema (system programs) | forniti col sistema ma fuori dal kernel | shell, ls, demoni |
| programmi applicativi | tutto il resto | browser, editor |
| middleware | librerie e framework per gli sviluppatori (database, grafica, multimedia) | SQLite, WebKit su Android |
I sistemi operativi non sono solo quelli dei PC: stanno in smartphone (pensati per batteria e usabilità), auto, televisori, navi, sistemi industriali (a volte senza interfaccia utente) e nei sistemi embedded e real-time (Sistemi operativi embedded e real-time per l'IoTUn sistema embedded è un dispositivo con elettronica e software per una funzione specifica, spesso con vincoli di tempo reale; i microcontrollori (8, 16 o 32 bit, KB di RAM, senza MMU, mW) formano i sistemi profondamente embedded dell'IoT, divisi dall'IETF in classi 0, 1 e 2; i sistemi embedded hanno kernel configurabile, boot loader e root file system costruiti su un host con cross-compilazione; si può adattare un OS esistente (embedded Linux, uClinux senza MMU, Buildroot, Yocto) o usare un OS dedicato (TinyOS a componenti ed eventi, RIOT a microkernel con scheduler a priorità fissa preemptive senza tick, FreeRTOS, Zephyr, Contiki-NG con protothread); l'IoT ha requisiti di memoria minima, hardware eterogeneo, rete a basso consumo, efficienza energetica, tempo reale e sicurezza; nel laboratorio con RIOT si crea una shell, si leggono i thread con ps, si creano thread con priorità e si sincronizzano con un mutex.Sistemi operativi embedded e real-time per l'IoT →).
Cenni storici: dal batch al time sharing
| periodo | tecnologia | novità per il sistema operativo |
|---|---|---|
| 1945-55 | valvole, pannelli a spinotti | programmi caricati a mano, nessun sistema operativo |
| 1955-65 | transistor, batch | programmi e dati su schede perforate, molto tempo perso nell'I/O |
| 1965-80 | circuiti integrati, multiprogrammazione | più programmi in memoria, la CPU passa a un altro quando uno aspetta |
| 1980-oggi | personal computer | DOS e Windows, UNIX e Linux, macOS, Android |
- Multiprogrammazione (multiprogramming, sistema batch): si tengono in memoria più lavori (job); quando uno aspetta l'I/O, la CPU passa a un altro. L'obiettivo è che la CPU non sia mai ferma. Di solito l'utente non interagisce con il job mentre gira.
- Multitasking / time sharing: estende il batch: la CPU passa da un job all'altro così spesso (decine di volte al secondo) che l'utente può interagire con ognuno; il tempo di risposta deve restare sotto il secondo. Servono lo scheduling della CPU (Scheduling della CPU - FCFS, SJF, priorità e round robinLo scheduler della CPU sceglie tra i processi pronti quello a cui dare il core (il dispatcher esegue il cambio, con latenza di dispatch); lo scheduling è non preemptive (il processo tiene la CPU finché termina o si blocca) o preemptive (la CPU gli può essere tolta, con rischio di race condition); i criteri sono utilizzo della CPU e throughput (da massimizzare), tempo di attesa, di turnaround (completamento meno arrivo) e di risposta (da minimizzare); FCFS è semplice ma soffre dell'effetto convoglio (esempio 24, 3, 3: attesa media 17 contro 3); SJF è ottimo per l'attesa media ma richiede la lunghezza del prossimo burst, stimata con la media esponenziale tau(n+1) = alfa t(n) + (1-alfa) tau(n); SRTF è la versione preemptive (esempio con attesa media 6,5 contro 7,75); round robin dà a ciascuno un quanto q e nessuno attende più di (n-1)q; la priorità causa starvation, risolta con l'aging; code multilivello e a feedback (MLFQ) combinano le politiche.Scheduling della CPU - FCFS, SJF, priorità e round robin →) e, quando i processi non stanno tutti in memoria, lo swapping e la memoria virtuale (Memoria virtuale - paginazione su richiesta e sostituzione delle pagineLa memoria virtuale separa la memoria logica da quella fisica: basta che alcune pagine siano in RAM; con la paginazione su richiesta (demand paging) una pagina si carica solo al primo riferimento, e un riferimento a una pagina non residente (bit di validità = i) genera un page fault gestito dal sistema operativo in sei passi; EAT = (1 - p) * accesso + p * servizio del fault, con p minuscolo; copy-on-write rende economica la fork; se non ci sono frame liberi si sceglie una vittima con FIFO (anomalia di Belady), OPT (ottimo, irrealizzabile), LRU (algoritmo a stack), second chance (bit di riferimento, orologio) o la versione con bit di modifica; i frame si assegnano in modo uguale o proporzionale, con sostituzione globale o locale; se un processo non ha abbastanza frame va in thrashing; il modello del working set e la frequenza dei page fault lo evitano; considerazioni su prepaging, dimensione della pagina, TLB reach e struttura dei programmi; memoria del kernel con buddy system e slab.Memoria virtuale - paginazione su richiesta e sostituzione delle pagine →).
Il calcolatore visto dal sistema operativo
Una o più CPU e i controllori di dispositivo (device controller) sono collegati da un bus alla memoria condivisa. CPU e dispositivi lavorano in parallelo e competono per i cicli di memoria. Ogni tipo di controllore ha nel sistema un driver che lo gestisce. Quando un controllore finisce un'operazione avvisa la CPU con un interrupt.
Interrupt e trap
Un interrupt interrompe la CPU per servire una richiesta urgente (maggiori dettagli in InterruptClassi di interruzioni; I/O guidato da interrupt; sequenza hardware e software di gestione con salvataggio del contesto sullo stack; identificazione della sorgente (linee multiple, polling, daisy chain, vettore); priorità e interruzioni annidate; eccezioni e modi in ARM.Interrupt →). Il meccanismo è:
- il dispositivo (o il programma) genera il segnale;
- la CPU salva l'indirizzo dell'istruzione interrotta (e i registri) e salta alla routine di servizio (interrupt service routine, ISR), cercandone l'indirizzo nel vettore delle interruzioni (interrupt vector), una tabella con un elemento per sorgente;
- finita la routine, la CPU riprende l'istruzione interrotta.
Una trap (o eccezione) è un interrupt generato dal software: per un errore (divisione per zero, accesso a memoria non valido) o per una richiesta dell'utente al sistema (una system call). Un sistema operativo è guidato dagli interrupt (interrupt driven): se non c'è nulla da fare resta inattivo, e si sveglia quando arriva un interrupt o una trap. Il timer è un dispositivo che interrompe periodicamente la CPU: così il sistema riprende il controllo anche se un programma non finisce mai.
Modalità doppia e istruzioni privilegiate
Per difendere se stesso e gli altri processi, l'hardware offre due modalità, distinte da un bit di modo (mode bit):
| modalità | bit | che cosa può fare il codice |
|---|---|---|
| utente (user mode) | 1 | solo istruzioni non privilegiate, solo la propria memoria |
| kernel (kernel mode, supervisor) | 0 | tutte le istruzioni, anche quelle privilegiate (I/O, abilitare e disabilitare interrupt, cambiare il bit di modo, configurare la memoria) |
Come si evita che un programma si metta da solo in modalità kernel? Cambiare il bit è un'istruzione privilegiata. L'unico modo per passare al kernel è una system call, che esegue una trap (interrupt software) verso un indirizzo deciso dal sistema; il ritorno dalla chiamata rimette il bit su "utente". Se un programma prova un'istruzione privilegiata in modalità utente, l'hardware genera una trap e il sistema lo termina. Nell'esercizio del laboratorio 1 (Esercizio - Modalità utente e modalità kernel con un modulo del kernel (laboratorio 1)) si legge questa modalità sul processore x86.
Questa è la forma più elementare di protezione: istruzioni, dispositivi e controllo degli interrupt sono riservati al kernel, e la CPU in modalità utente ha poche possibilità di danneggiare risorse critiche.
Più processori
I sistemi con più CPU (multiprocessor, paralleli, strettamente accoppiati) danno maggiore throughput, economia di scala e affidabilità (la rottura di una CPU degrada il servizio senza fermarlo). Due modelli:
- asimmetrico (asymmetric multiprocessing): a ogni processore è assegnato un compito;
- simmetrico (symmetric multiprocessing, SMP): ogni processore può fare qualunque compito; condividono la memoria ma ognuno ha le proprie cache. Nei chip multicore ogni core ha la cache L1 e condivide con gli altri una L2 più grande (Processori superscalari e multicoreParallelismo a livello di istruzione: processori superscalari, esecuzione fuori ordine, ridenominazione dei registri contro WAR e WAW, speculazione; limiti dell'ILP e della frequenza (consumo); multithreading, multicore e coerenza delle cache; acceleratori.Processori superscalari e multicore →, Memoria cacheBlocchi, linee ed etichette; scomposizione dell'indirizzo; associazione diretta, completamente associativa e associativa a insiemi con calcolo dei campi; politiche di rimpiazzo (LRU, FIFO, casuale); politiche di scrittura (write-through, write-back con bit sporco, write-allocate); dimensione del blocco; cache multilivello e separate; come ridurre i miss; quesiti sui campi dell'indirizzo.Memoria cache →).
Nei sistemi NUMA (non-uniform memory access) ogni CPU accede in fretta alla memoria vicina e più lentamente a quella remota; il sistema operativo può ridurre il problema assegnando bene CPU e memoria (Scheduling multiprocessore e real-timeNei sistemi real-time conta la scadenza: si misurano latenza di interrupt e latenza di dispatch; i sistemi soft real-time danno priorità ai compiti critici senza garanzie, quelli hard devono rispettare le scadenze; per compiti periodici (burst t, scadenza d, periodo p, con 0 <= t <= d <= p) l'utilizzo è la somma di t/p: Rate Monotonic (priorità fissa, minore il periodo maggiore la priorità) garantisce le scadenze se l'utilizzo non supera N(2^(1/N)-1) (83% per due compiti), Earliest Deadline First (priorità dinamica, scadenza più vicina) le garantisce se l'utilizzo è sotto il 100%; la ripartizione proporzionale assegna T quote con controllo di ammissione; con più processori si hanno code comuni o private (SMP), core con più thread hardware, bilanciamento del carico (push e pull migration), affinità al processore (soft e hard), scheduling NUMA-aware; Linux usa CFS/EEVDF con domini di scheduling, Windows 32 livelli di priorità.Scheduling multiprocessore e real-time →).
Gerarchia di memorie
La memoria centrale (DRAM, volatile) è l'unico grande supporto a cui la CPU accede direttamente. La memoria secondaria (disco, SSD) la estende con grande capacità non volatile; la memoria terziaria (nastri, ottici) è più grande e più lenta. L'ordine per velocità e costo è lo stesso discusso in Gerarchia di memoria e principio di localitàCaratteristiche delle memorie (posizione, capacità, unità di trasferimento, metodo di accesso, prestazioni, volatilità); compromesso costo-capacità-velocità e gerarchia a livelli; località temporale e spaziale con esempi; hit, miss e tempo medio di accesso con calcolo svolto.Gerarchia di memoria e principio di località →. Il sistema operativo gestisce memoria centrale e di massa; i registri e le cache sono gestiti dall'hardware (le cache) o dal compilatore (i registri).
Che cosa fa il sistema operativo
All'avvio un piccolo programma di bootstrap inizializza il sistema e carica il kernel; il kernel parte, avvia i demoni (daemons, servizi fuori dal kernel) e poi attende interrupt. Le sue aree di responsabilità:
| area | compiti |
|---|---|
| gestione dei processi | creare e terminare processi, sospenderli e riprenderli, fornire sincronizzazione, comunicazione e gestione dello stallo |
| gestione della memoria | sapere quali parti sono in uso e da chi; decidere cosa caricare o togliere; allocare e liberare |
| gestione dell'archiviazione | montare e smontare, spazio libero, allocazione, scheduling del disco, partizioni, protezione |
| file system | vista logica uniforme (il file), directory, controllo degli accessi, creare/cancellare, mappare i file sulla memoria di massa, backup |
| sistema di I/O | nascondere le particolarità dei dispositivi: buffering (dati temporanei durante un trasferimento), caching (copia in una memoria più veloce), spooling (sovrapposizione dell'uscita di un lavoro con l'ingresso di altri), driver con interfaccia generale |
| protezione e sicurezza | protezione: controllare l'accesso di processi e utenti alle risorse; sicurezza: difendere dagli attacchi interni ed esterni (identificatori di utente e di gruppo associati a file e processi) |
Un programma è un'entità passiva (il file eseguibile sul disco), un processo è un'entità attiva: il programma in esecuzione, con le risorse che gli servono. Quando il processo termina il sistema recupera le risorse riutilizzabili.
Servizi offerti ai programmi
Un sistema operativo fornisce un ambiente in cui programmi e utenti possono lavorare. Servizi utili all'utente:
- interfaccia utente: a riga di comando (CLI), grafica (GUI), touch, batch;
- esecuzione dei programmi: caricare in memoria, avviare, terminare (normalmente o per errore);
- operazioni di I/O su file o dispositivi;
- gestione del file system: leggere, scrivere, creare, cancellare, cercare, elencare, permessi;
- comunicazione tra processi, sulla stessa macchina o in rete, con memoria condivisa o scambio di messaggi;
- rilevazione degli errori (CPU, memoria, dispositivi, programmi) con la reazione appropriata.
Servizi che servono al sistema stesso per funzionare bene: allocazione delle risorse (CPU, memoria, file, dispositivi), accounting (chi usa quanto) e protezione e sicurezza (autenticazione, controllo degli accessi).
La shell è l'interprete di comandi: legge un comando e lo esegue (a volte è un comando interno, di solito è il nome di un programma). Esistono più shell (Bourne shell, bash); una GUI è l'altra faccia dello stesso servizio.
System call e API
Definizione (system call). Una system call (chiamata di sistema) è l'interfaccia di programmazione verso i servizi del sistema operativo. È eseguita dal kernel; il programma passa dalla modalità utente alla modalità kernel con un interrupt software (trap) e ritorna con il risultato.
Normalmente un programma non invoca le system call direttamente, ma tramite una API (Application Programming Interface): per esempio la libreria C standard, la POSIX API (UNIX, Linux, macOS) o la Windows API. La libreria prepara gli argomenti, esegue la trap e restituisce al programma il valore che il kernel ha prodotto. Chi scrive il programma non deve sapere come la system call è realizzata: conosce solo l'API. Un esempio con printf:
programma C printf("ciao\n"); (libreria C: formatta in un buffer)
|
v
libreria C write(1, buf, 5); (wrapper della system call)
| trap: il bit di modo passa a "kernel"
v
kernel tabella delle system call[numero di write] -> sys_write
| esegue l'operazione, poi ritorno e bit di modo = "utente"
v
programma C riprende con il valore restituito (byte scritti oppure -1)Le funzioni di libreria come printf o fopen chiamano system call (write, open) quando serve; la differenza e i valori di ritorno (-1 e errno) sono nella nota System call POSIX, file descriptor e API delle socketLe system call sono le funzioni con cui un programma in user space chiede servizi al kernel (open, read, write, close, fork, pipe, dup2, socket, bind, listen, accept, connect); restituiscono -1 e impostano errno in caso di errore; un file descriptor e' un intero che indicizza la tabella dei file aperti del processo (0 stdin, 1 stdout, 2 stderr) e vale per file, pipe e socket; read e write possono trasferire MENO byte del richiesto (e sui socket TCP non c'e' nessun confine fra i messaggi), quindi servono cicli write_all e read_exact; l'API delle socket crea un punto finale di comunicazione (socket), lo lega a indirizzo e porta (bind), lo rende passivo (listen), accetta connessioni (accept) o si connette (connect); getaddrinfo traduce nomi e porte in indirizzi (IPv4 e IPv6); UDP usa sendto e recvfrom e conserva i confini dei datagrammi, TCP e' uno stream affidabile.System call POSIX, file descriptor e API delle socket →. Il comando strace mostra le system call effettuate da un programma.
Realizzazione e passaggio dei parametri
A ogni system call è associato un numero. L'interfaccia mantiene una tabella indicizzata da questi numeri: la trap porta nel kernel, che usa il numero per trovare e chiamare la routine giusta. Per passare i parametri si usano tre metodi:
- nei registri (il più semplice, ma i registri possono non bastare);
- sullo stack: il programma li mette con
push, il sistema li toglie conpop(Stack e chiamata di funzioni in ARMChiamata con BL e ritorno con BX LR; convenzione di chiamata AAPCS (argomenti in r0-r3, risultato in r0, registri da preservare r4-r11); stack discendente pieno con PUSH e POP; record di attivazione; funzioni foglia e non foglia; esempio ricorsivo del fattoriale con evoluzione dello stack.Stack e chiamata di funzioni in ARM → mostra la stessa idea per le funzioni); - in un blocco di memoria (una tabella), di cui si passa l'indirizzo in un registro: è il metodo di Linux e Solaris, non ha limiti sul numero o sulla lunghezza dei parametri.
Tipi di system call
| categoria | esempi di operazioni |
|---|---|
| controllo dei processi | creare e terminare, caricare ed eseguire, attributi, attendere un tempo o un evento, allocare e liberare memoria, debugger, lock per dati condivisi |
| gestione dei file | creare, cancellare, aprire, chiudere, leggere, scrivere, riposizionare, attributi |
| gestione dei dispositivi | richiedere e rilasciare, leggere, scrivere, attributi, collegare e scollegare |
| manutenzione delle informazioni | data e ora, dati di sistema, attributi di processi, file e dispositivi |
| comunicazione | creare e chiudere una connessione, inviare e ricevere messaggi, memoria condivisa, trasferire informazioni di stato |
| protezione | controllare gli accessi, impostare permessi, consentire o negare l'accesso |
In UNIX/Linux le system call sono funzioni C della libreria quasi una per una (da meno di cento a diverse centinaia, secondo la versione). In Windows le funzioni della Windows API sono scollegate dalle system call: la stessa API vale per versioni diverse del sistema.
Esempio di una sequenza di system call: copiare un file in un altro richiede di leggere i due nomi, aprire l'ingresso (errore se non esiste), creare l'uscita (errore se esiste già), ripetere lettura e scrittura fino alla fine, chiudere i due file, scrivere un messaggio e terminare, ognuno con una system call.
Dal sorgente al processo: linker e loader
Il sorgente viene compilato in file oggetto rilocabili (relocatable object file), pensati per essere caricati in qualunque posizione della memoria fisica. Il linker li combina in un unico eseguibile: assegna indirizzi relativi a codice e dati, dice al sistema da dove partire e inserisce le librerie. Il loader porta l'eseguibile in memoria e ne rilocata gli indirizzi, cioè li aggiusta in base a dove è stato caricato. I sistemi moderni non incollano le librerie nell'eseguibile: le librerie a collegamento dinamico (DLL su Windows, .so su Linux) si caricano all'occorrenza e sono condivise da tutti i programmi (si caricano una volta sola). I file eseguibili hanno un formato standard (ELF su Linux e UNIX) così il sistema sa come caricarli e avviarli. Il rapporto con Dal programma C all'eseguibile ARMCatena di traduzione compilatore, assemblatore, linker e loader; file oggetto, tabella dei simboli e rilocazione; collegamento statico e dinamico; organizzazione della memoria di un processo; chiamare funzioni assembly da C e viceversa, con esempio.Dal programma C all'eseguibile ARM → è diretto: lì si vedono compilatore e assembler, qui il caricamento.
I programmi sono specifici di un sistema operativo: ognuno ha le sue system call e i suoi formati di file. Si può rendere un'applicazione portabile se è scritta in un linguaggio interpretato (Python, con un interprete per ogni sistema), se gira su una macchina virtuale (Java), o se è scritta in un linguaggio standard (C) e compilata separatamente su ogni sistema. L'ABI (Application Binary Interface) è l'equivalente dell'API a livello di binario: dice come le parti di codice macchina si collegano su una data combinazione di sistema e architettura.
Strutture dei sistemi operativi
Un sistema operativo generale è un programma molto grande. Le architetture principali:
| struttura | idea | esempi | pregi e difetti |
|---|---|---|---|
| monolitica semplice | tutto in un unico blocco, senza strati | MS-DOS | veloce e semplice, ma fragile e difficile da modificare |
| monolitica complessa | un solo grande kernel con tutto ciò che sta sotto le system call: file system, scheduling, memoria, driver | UNIX originale, Linux | veloce (nessuna comunicazione tra moduli), ma un errore nel kernel affonda tutto |
| a microkernel | nel kernel resta il minimo; gli altri servizi stanno in spazio utente, comunicano con scambio di messaggi | Mach, QNX | facile da estendere e portare, più affidabile e sicuro (meno codice in modalità kernel); ma costa più tempo il passaggio fra spazio utente e kernel |
| a moduli | il kernel ha componenti separati e caricabili al volo (LKM, loadable kernel module), che lavorano nel kernel e comunicano direttamente | Linux, Solaris | flessibile come il microkernel ma più veloce (nessun messaggio) |
| ibrida | mescola gli approcci per prestazioni, sicurezza e usabilità | Windows, Darwin, Android | pratica |
- Il kernel Linux è monolitico (tutto in modalità kernel) ma modulare: si caricano e scaricano moduli mentre gira. Per esempio un driver nuovo si aggiunge con
insmodsenza ricompilare il kernel (si vede in Esercizio - Modalità utente e modalità kernel con un modulo del kernel (laboratorio 1)). - Windows è soprattutto monolitico, con funzioni da microkernel per i diversi "sottosistemi" (per esempio per eseguire applicazioni di altri sistemi).
- Darwin (macOS e iOS): a strati, con il microkernel Mach (memoria, scheduling, comunicazione tra processi con messaggi) e un kernel BSD UNIX; offre due interfacce di system call, quelle Mach (le "trap") e quelle BSD (POSIX), e un kit di I/O per i driver.
- Android: stessa pila di iOS ma su un kernel Linux modificato (con la gestione dell'energia); le applicazioni Java sono tradotte per la macchina virtuale ART; le librerie includono WebKit, SQLite e la libreria C
Bionic, più piccola.
La famosa discussione tra Tanenbaum e Torvalds riguarda proprio questa scelta: microkernel contro kernel monolitico.
Domande d'esame
1. Spiegare la differenza tra modalità utente e modalità kernel e come un programma passa dall'una all'altra. Traccia. Il bit di modo, fornito dall'hardware, distingue il codice utente da quello del kernel. Le istruzioni privilegiate (I/O, interrupt, cambio del bit) funzionano solo in modalità kernel; in modalità utente una di esse genera una trap e il processo è terminato. Un programma entra nel kernel solo con una system call, cioè una trap (interrupt software) che il kernel serve a un indirizzo scelto da lui; il ritorno rimette la modalità utente. È una forma elementare di protezione.
2. Che cosa succede quando un programma C chiama printf? Qual è la differenza tra API e system call?
Traccia. printf è una funzione di libreria che formatta il testo e, quando serve, chiama la system call write. L'API è l'interfaccia di programmazione visibile al programmatore (libc, POSIX); la system call è il punto di ingresso nel kernel, identificato da un numero in una tabella. La libreria prepara i parametri (registri, stack o blocco di memoria), esegue la trap e restituisce il valore ottenuto (byte scritti, oppure -1 con errno).
3. Confrontare kernel monolitico, microkernel e kernel a moduli. Traccia. Il monolitico mette tutto in modalità kernel: veloce, ma un guasto in un componente può abbattere il sistema. Il microkernel lascia nel kernel solo il minimo e sposta il resto in spazio utente con messaggi: più sicuro e portabile, ma più lento per i passaggi tra spazi. I moduli caricabili mantengono i componenti separati ma nel kernel, che comunicano direttamente: flessibili e senza costo di messaggi (Linux).
Versione ripasso
- Sistema operativo = intermediario tra utente e hardware; allocatore di risorse e programma di controllo. Kernel = parte sempre in esecuzione; fuori dal kernel: programmi di sistema, applicazioni, middleware.
- Multiprogrammazione: più job in memoria, la CPU passa a un altro quando uno attende l'I/O (CPU mai ferma). Time sharing: commutazioni così frequenti da permettere l'interazione (risposta sotto 1 s), servono scheduling e memoria virtuale.
- Interrupt: salva l'indirizzo interrotto, salta alla routine tramite il vettore delle interruzioni; trap = interrupt software (errore o system call). Il kernel è guidato dagli interrupt; il timer ridà il controllo al sistema. Rimandi: InterruptClassi di interruzioni; I/O guidato da interrupt; sequenza hardware e software di gestione con salvataggio del contesto sullo stack; identificazione della sorgente (linee multiple, polling, daisy chain, vettore); priorità e interruzioni annidate; eccezioni e modi in ARM.Interrupt →.
- Modalità doppia: bit di modo (utente/kernel); istruzioni privilegiate solo in modalità kernel; si entra nel kernel solo con una system call (trap), il ritorno rimette "utente". Istruzione privilegiata in modo utente -> trap -> processo terminato.
- SMP (ogni CPU fa tutto) contro asimmetrico; multicore (L1 per core, L2 condivisa); NUMA (memoria locale veloce, remota lenta). Rimandi: Processori superscalari e multicoreParallelismo a livello di istruzione: processori superscalari, esecuzione fuori ordine, ridenominazione dei registri contro WAR e WAW, speculazione; limiti dell'ILP e della frequenza (consumo); multithreading, multicore e coerenza delle cache; acceleratori.Processori superscalari e multicore →, Memoria cacheBlocchi, linee ed etichette; scomposizione dell'indirizzo; associazione diretta, completamente associativa e associativa a insiemi con calcolo dei campi; politiche di rimpiazzo (LRU, FIFO, casuale); politiche di scrittura (write-through, write-back con bit sporco, write-allocate); dimensione del blocco; cache multilivello e separate; come ridurre i miss; quesiti sui campi dell'indirizzo.Memoria cache →.
- Compiti: processi, memoria, archiviazione, file system, I/O (buffering, caching, spooling), protezione e sicurezza. Programma = passivo, processo = attivo.
- System call: interfaccia ai servizi del kernel, di solito usata tramite API (libc, POSIX, Windows API);
printf->write. Numero + tabella; parametri in registri, stack o blocco in memoria (Linux). Categorie: processi, file, dispositivi, informazioni, comunicazione, protezione. Rimandi: System call POSIX, file descriptor e API delle socketLe system call sono le funzioni con cui un programma in user space chiede servizi al kernel (open, read, write, close, fork, pipe, dup2, socket, bind, listen, accept, connect); restituiscono -1 e impostano errno in caso di errore; un file descriptor e' un intero che indicizza la tabella dei file aperti del processo (0 stdin, 1 stdout, 2 stderr) e vale per file, pipe e socket; read e write possono trasferire MENO byte del richiesto (e sui socket TCP non c'e' nessun confine fra i messaggi), quindi servono cicli write_all e read_exact; l'API delle socket crea un punto finale di comunicazione (socket), lo lega a indirizzo e porta (bind), lo rende passivo (listen), accetta connessioni (accept) o si connette (connect); getaddrinfo traduce nomi e porte in indirizzi (IPv4 e IPv6); UDP usa sendto e recvfrom e conserva i confini dei datagrammi, TCP e' uno stream affidabile.System call POSIX, file descriptor e API delle socket →. - Linker (file oggetto rilocabili -> eseguibile), loader (carica e rilocata), librerie dinamiche (
.so, DLL), formato ELF, ABI = API a livello binario. I programmi sono specifici del sistema. - Strutture: monolitica (UNIX, Linux), microkernel (Mach, QNX: messaggi, sicuro ma più lento), moduli (LKM in modalità kernel, Linux), ibride (Windows, Darwin = Mach + BSD, Android = Linux modificato + ART).
- Storia: 1945-55 valvole e pannelli (nessun SO); 1955-65 transistor e batch (schede perforate, molto tempo perso nell'I/O); 1965-80 circuiti integrati e multiprogrammazione; dal 1980 personal computer (DOS/Windows, UNIX/Linux, macOS, Android).
- Servizi per l'utente: interfaccia (CLI, GUI, touch), esecuzione dei programmi, I/O, file system, comunicazione (memoria condivisa o messaggi), rilevazione degli errori. Servizi per il sistema: allocazione delle risorse, accounting, protezione e sicurezza.
- Aree gestite: processi (creare, terminare, sospendere, sincronizzare, comunicare, deadlock); memoria (chi usa cosa, cosa caricare, allocare); archiviazione (montaggio, spazio libero, scheduling del disco); file system (vista logica, directory, permessi, backup); I/O (buffering = dati temporanei, caching = copia veloce, spooling = sovrapposizione di uscita e ingresso); protezione (controllo degli accessi) e sicurezza (difesa da attacchi).
- Percorso di una system call (
printf): libreria C formatta nel buffer ->write(1, buf, n)-> trap, bit di modo = kernel -> tabella delle system call indicizzata dal numero -> routine del kernel -> ritorno, bit di modo = utente, valore di ritorno (byte scritti o conerrno).stracemostra le system call di un programma. - Categorie di system call: controllo dei processi, gestione dei file, dei dispositivi, manutenzione delle informazioni, comunicazione, protezione. UNIX/Linux: funzioni C quasi 1:1 con le system call (da meno di cento a centinaia); Windows: la Windows API è scollegata dalle system call.
- Architetture: SMP (ogni CPU fa tutto, memoria condivisa), asimmetrico (compiti assegnati), multicore (L1 per core, L2 condivisa), NUMA (memoria locale veloce, remota lenta, lo scheduler può mitigare). Rimandi: Scheduling multiprocessore e real-timeNei sistemi real-time conta la scadenza: si misurano latenza di interrupt e latenza di dispatch; i sistemi soft real-time danno priorità ai compiti critici senza garanzie, quelli hard devono rispettare le scadenze; per compiti periodici (burst t, scadenza d, periodo p, con 0 <= t <= d <= p) l'utilizzo è la somma di t/p: Rate Monotonic (priorità fissa, minore il periodo maggiore la priorità) garantisce le scadenze se l'utilizzo non supera N(2^(1/N)-1) (83% per due compiti), Earliest Deadline First (priorità dinamica, scadenza più vicina) le garantisce se l'utilizzo è sotto il 100%; la ripartizione proporzionale assegna T quote con controllo di ammissione; con più processori si hanno code comuni o private (SMP), core con più thread hardware, bilanciamento del carico (push e pull migration), affinità al processore (soft e hard), scheduling NUMA-aware; Linux usa CFS/EEVDF con domini di scheduling, Windows 32 livelli di priorità.Scheduling multiprocessore e real-time →, Memoria cacheBlocchi, linee ed etichette; scomposizione dell'indirizzo; associazione diretta, completamente associativa e associativa a insiemi con calcolo dei campi; politiche di rimpiazzo (LRU, FIFO, casuale); politiche di scrittura (write-through, write-back con bit sporco, write-allocate); dimensione del blocco; cache multilivello e separate; come ridurre i miss; quesiti sui campi dell'indirizzo.Memoria cache →, Processori superscalari e multicoreParallelismo a livello di istruzione: processori superscalari, esecuzione fuori ordine, ridenominazione dei registri contro WAR e WAW, speculazione; limiti dell'ILP e della frequenza (consumo); multithreading, multicore e coerenza delle cache; acceleratori.Processori superscalari e multicore →.
- Strutture a confronto: monolitica (tutto in kernel mode, veloce, un guasto affonda tutto); microkernel (messaggi tra spazio utente e kernel: più sicuro e portabile, più lento); moduli caricabili (LKM: separati ma in kernel mode, comunicano direttamente); ibride (Windows soprattutto monolitico, Darwin = Mach + BSD con due interfacce di system call, Android = Linux modificato + ART).
- Dal sorgente al processo: compilatore -> file oggetto rilocabili -> linker (eseguibile, librerie) -> loader (carica e rilocata); librerie dinamiche condivise (
.so, DLL); ELF; ABI = API a livello binario; i programmi sono specifici di un SO a meno di interpreti, macchine virtuali o ricompilazione. - Risposte d'esame in una riga: modalità doppia = istruzioni privilegiate solo in kernel mode e ingresso solo con trap; multiprogrammazione = CPU mai ferma (job in memoria); time sharing = commutazione frequente per l'interazione (risposta sotto 1 s); kernel = programma sempre in esecuzione.
- Errori tipici: confondere API e system call; dire che
printfè una system call; credere che il microkernel sia più veloce; pensare che i moduli girino in spazio utente; dimenticare che la trap è l'unico modo per entrare nel kernel.