Introduzione
Panoramica di un sistema operativo
Senza il software, un computer è solo un inutile cumulo di metallo, con il software un computer può riprodurre audio/video, mandare e-mail, ricercare in internet, processare e manipolare informazioni. Il software di un computer può essere diviso in due tipi: il software di sistema ed il software applicativo. Il più importante software di sistema è il sistema operativo.
Un sistema operativo (OS = Operative System) è un software che non opera al livello applicativo ma ad un livello più basso, infatti l’OS comunica direttamente con l’hardware della macchina su cui viene eseguito e per cui è progettato. Il sistema operativo coesiste con l’applicazione e ne soddisfa le necessità quando per esempio quest’ultima chiede accesso a moduli con accesso controllato. È infatti una risorsa a disposizione del programmatore, poiché mette a disposizione delle utilities, cioè oggetti software esterni al S.O. e che possono sfruttare le sue potenzialità; queste utilities sono quindi usate dal programmatore applicativo per interagire con il S.O. Un esempio di utilities "stdio.h" possono essere le librerie standard C, tipo quella per l’interazione con l’I/O.
Abbiamo usato il termine di programmatore applicativo perché è ben differente dal concetto di programmatore di sistema: il primo infatti fa utilizzo delle suddette utilities per interagire con il livello hardware del sistema in quanto egli non deve conoscere per forza il tipo e le funzionalità dell’hardware in questione, si dice che il programmatore applicativo utilizza lo standard di linguaggio; invece il secondo dialoga direttamente con il S.O. prescindendo quindi da quelle che sono le interfacce apposite per un tipo di programmazione a più alto livello e conoscendo l’hardware e il suo comportamento è in grado di cambiarne lo stato a suo piacimento, si dice che il programmatore di sistema utilizza uno standard di sistema.
Come vediamo dall’immagine, ci sono due tipi di programmatore, uno che punta alle utilities (programmatore applicativo) e uno che punta direttamente al S.O. (programmatore di sistema). Le utilities sono quindi una esemplificazione delle funzioni del sistema operativo. È ovvio che il programmatore di sistema potendo comunicare direttamente con il S.O. riesce ad ottimizzare al massimo il proprio software.
Una controindicazione, o per meglio dire, una conseguenza dell’usare funzioni che chiamano direttamente il S.O. è certamente la scarsa o inesistente portabilità. Infatti, usando routine di sistema non posso utilizzare il mio software su più macchine, perché appunto quel software è stato ottimizzato per il sistema operativo X e non può girare su un S.O. Y in quanto quest’ultimo non sarà dotato delle stesse funzioni di sistema del primo.
Obiettivi di un S.O.
I principali obiettivi di un sistema operativo sono:
- Flessibilità: Il sistema operativo offre trasparenza al livello applicativo mascherando quelle che sono le vere interazioni con l’hardware, quindi rende il software portabile.
- Efficienza: Il sistema operativo gestisce in maniera efficiente gli accessi alle risorse di sistema ed ottimizza quindi le operazioni che il livello applicativo vuole fare.
- Semplicità: Il sistema operativo svolge un'operazione di monitoraggio ed esecuzione di task che al livello applicativo risultano in questo modo più semplici.
Esiste una corrispondenza fra risorse reali (fisiche) e risorse virtuali, il sistema operativo maschera anche questo aspetto gestendo i dispositivi in maniera virtuale: cioè il programmatore applicativo accede a risorse che in realtà vengono poi mappate in risorse fisiche. Il programmatore di sistema invece ha bene in mente anche le risorse fisiche mentre sviluppa software. Siccome poi l’oggetto virtuale rappresentante un determinato dispositivo può essere invocato da più di un processo attivo per volta, il S.O. deve ottimizzare l’accesso alla risorsa reale rendendo sequenziali gli accessi.
Esecuzione seriale delle applicazioni (Anni 40-50)
Un sistema di calcolo veniva riservato per l’esecuzione di un job (programma) alla volta, e l’utente stesso doveva caricare il programma nella memoria del sistema, salvare, compilare e collegare con moduli esterni il sorgente del programma ed infine avviarlo. Ovviamente quest’approccio non fu ottimale in termini di tempo e costi, perciò si apportarono delle migliorie, cioè si aggiunsero librerie di funzioni, linker, loader e debugger che svolgevano operazioni macchinose al posto dell’utente. Non esisteva ancora il sistema operativo!
Sistemi operativi batch (Anni 50-60)
In questi anni le cose cambiano, vengono aggiunte nuove componenti di sistema. La RAM viene partizionata in due parti:
- Monitor: Un primo embrione di S.O, insieme di moduli software che vengono caricati in RAM e vi rimangono fino all’arresto del sistema. Chiamato sistema operativo BATCH perché ha la capacità di eseguire un job per volta.
- Area User Space: Area riservata a caricare i programmi applicativi in esecuzione.
In questi sistemi BATCH il monitor era quindi ciò che noi chiamiamo sistema operativo, questo permetteva infatti di interagire con i dispositivi di I/O, caricare un programma da eseguire in memoria, invocare altri processi come subroutine del monitor stesso. Dopo aver chiamato un processo, il monitor passa il controllo della CPU al programma e resta in attesa della chiusura di quest’ultimo o di un eventuale sua chiamata al monitor (ad es. perché deve interagire con un dispositivo di I/O). Quando il controllo dell’esecuzione è in mano al monitor, la CPU presenta un certo stato, con massimo livello di privilegio, cioè è nella modalità cosiddetta ADMIN; in questa modalità sono permesse alcune istruzioni che nella modalità USER non si possono eseguire, quando in modalità user si cerca di eseguire queste istruzioni critiche o di accedere alla memoria riservata al monitor il controllo viene immediatamente ripassato a quest’ultimo.
Spooling: introduzione del disco rigido, una memoria tampone rispetto ai dispositivi di I/O, che di per sé sono lenti rispetto alla CPU. Questo non era veloce, ma era preferibile al prendere i dati da caricare in RAM direttamente dal dispositivo. In questo modo l’input veniva anticipato nel disco prima di caricarlo in RAM (viceversa l’output), riducendo il tempo di attesa della CPU.
Batch multitasking
Nei sistemi batch succedeva che fra una richiesta di accesso al mezzo e l’effettiva risposta la CPU restava inattiva, in attesa che il dispositivo di I/O rispondesse; questo fa sì che vengano sviluppati sistemi batch multitasking. Questi sistemi possono mantenere attivi due o più job per volta. Mantenere attivi più job sta a significare caricarne diversi in RAM e gestirli simultaneamente. I vantaggi di un sistema multitasking è che potevano essere utilizzati più dispositivi diversi per volta, così da parallelizzare le richieste di I/O. Grazie allo spooling poi venivano memorizzati i job sul disco rigido e si liberava spazio in RAM. Comunque però la CPU veniva occupata da un solo job per volta, infatti lo svantaggio principale di questo modello di sistema era lo scenario in cui un job prendeva il controllo della CPU e non lo restituiva mai al monitor, perché in attesa di una risposta da un dispositivo di input. (cosa che penalizza le applicazioni interattive)
Nel 1962 infatti venne progettato il primo sistema operativo time-sharing, per contrastare la monopolizzazione dell’uso della CPU da parte dei job venne integrato nel monitor un algoritmo di scheduling, che veniva usato per assegnare ai job intervalli di tempo nei quali potevano eseguire le loro istruzioni. Questa modalità non penalizza più le applicazioni interattive, perché se il job A aspetta un input dal dispositivo ha comunque un certo intervallo di tempo per verificarlo, altrimenti il controllo della CPU passa al job B (passando per il monitor che esegue l’algoritmo di scheduling).
Un esempio di algoritmo time-sharing è il Round-Robin, letteralmente “carosello”, che può supportare l’esecuzione di 4 job attivi in un certo intervallo di tempo, a turno questi job prendono il controllo della CPU. Con l’implementazione del time-sharing nei sistemi batch multitasking si ha il primo esemplare di sistema operativo, non più monitor.
Ciò che fa quindi il S.O. nel modello time-sharing è, basandosi su un intervallo di tempo stabilito, togliere il controllo al programma in esecuzione indipendentemente dal suo stato e dal suo set di istruzioni in esecuzione. Quest’attività si denomina con il termine di pre-emption, il concetto fondamentale per una pre-emption è quello di interrupt (o trap), che cambia lo stato della CPU bloccando il flusso d’esecuzione del job e passando il controllo di nuovo all’algoritmo di scheduling, a seguito della gestione dell’interrupt stesso. Un interrupt può essere causato da un dispositivo (interrupt hardware) o da un comando (interrupt software); infatti esistono proprio istruzioni di trap, usate per richiedere l’intervento del sistema operativo, una di queste è l’istruzione exit(0) la quale invoca il sistema operativo per bloccare definitivamente il flusso di esecuzione corrente e dismettere il job dalla memoria.
Il sistema operativo reagisce all’interrupt invocando un comando appropriato (routine), che è associato a quell’interrupt specifico tramite un interrupt vector, contenente gli indirizzi di memoria che identificano le routine che gestiscono l’interrupt che si è scatenato. Nei sistemi x86 l’interrupt vector si chiama Interrupt Descriptor Table (IDT).
Un’architettura Hw/Sw che usa gli interrupt deve essere in grado di ripristinare il flusso originale una volta che il job in questione riprende il controllo, cioè il job deve tornare nello stato in cui si trovava prima di essere interrotto; questo significa salvare e ripristinare informazioni di esecuzione, come il Program Counter e i registri della CPU in aree di memoria note; notare che il gestore dell’interrupt, cioè la routine chiamata secondo l’interrupt vector, può salvare informazioni aggiuntive a seconda della criticità dell’interrupt scatenato. Ogni S.O. odierno è interrupt-driven, cioè è basato sulla gestione degli interrupt.
Esempio di flusso d’esecuzione con gestione di interrupt da dispositivo.
Sistemi real-time
Lo scopo principale dei sistemi real-time è quello di completare l’esecuzione dei job in un certo intervallo di tempo chiamato dead-line. A seconda di quanto importante è rispettare queste dead-line, si distingue fra sistemi HARD real-time e sistemi SOFT real-time.
Nei sistemi HARD real-time le deadline devono essere rispettate assolutamente, altrimenti potrebbe succedere qualcosa di pericoloso; un esempio di sistemi hard real-time sono sistemi che controllano complessi processi industriali. I sistemi time-sharing non supportano l’hard real-time.
Nei sistemi SOFT real-time le deadline dovrebbero essere rispettate, altrimenti non succede nulla di grave. Un esempio di questi sistemi è un sistema che controlla processi multimediali, in questo caso infatti non c’è qualcosa da controllare con precisione massima.
Processi
Un’applicazione (job) attiva su un sistema time-sharing può essere identificata con il nome di processo, ed è un programma in esecuzione sotto il controllo del sistema operativo; ogni processo attualmente attivo nel sistema ha bisogno di un bagaglio di memoria composto da moduli software e strutture dati su cui opera il processo e un contesto di esecuzione, cioè l’insieme di dati o metadati che indicano lo stato del processo (come l’identificatore del processo) mantenuti dal software di sistema ed utile alla schedulazione del processo. Anche il sistema operativo stesso è composto da processi, infatti quando si avvia un computer vengono lanciate determinate applicazioni, queste vengono mappate su dei processi attivi in memoria, processi che poi possono chiamare altri processi andando così a costituire una struttura gerarchica fra processi: se un processo invoca un nuovo processo quest’ultimo viene detto un child del primo; ogni applicazione è figlia di un’altra, tranne il primo processo attivato in assoluto durante l’accensione, questo viene attivato direttamente dal sistema operativo.
Compiti classici di un sistema operativo
Elenchiamo le funzionalità fondamentali di un sistema operativo:
- Gestione dei processi e la loro corretta esecuzione
- Gestione della memoria (primaria e secondaria), essere quindi a conoscenza dello stato della memoria in qualsiasi momento.
- Gestione dei file, gestire quindi le informazioni contenute nelle memorie di massa
- Gestione dei dispositivi di I/O, supervisionare dunque lettura e scrittura verso i dispositivi di I/O
- Protezione delle risorse, gestire in maniera ottimizzata il time-sharing di CPU effettuando scheduling sui processi, proteggere la memoria riservata di un’applicazione.
System call
La chiamata ad una routine del sistema operativo tramite il supporto delle trap (interrupt) viene chiamata system call, questo meccanismo costituisce un’interfaccia al software di sistema, quindi in realtà si tratta di istruzioni macchina, o linguaggio assembly; Il linguaggio C però permette di inserire istruzioni macchina all’interno del codice tramite il costrutto ASM(“codice assembly”); i S.O. ed in generale tutto il software di basso livello sono scritti in C/ASM, quando il flusso d’esecuzione incontra una system call esso entra nel livello stub.
Il livello stub è il livello intermedio fra il sistema operativo e il codice applicativo e vi si entra appunto tramite queste system call; all’interno dello stub poi è implicita la necessità di istruzioni di trap per far eseguire al sistema operativo un certo codice. Lo stub è ritagliato per la macchina dove viene eseguito, infatti è machine dependent.
Uno stub è diviso in preambolo e coda (preamble and tail): tutti e due in C/ASM. Servono per far acquisire al sistema tutte le informazioni necessarie e ritirare il risultato tramite registri della CPU, oppure far capire che c’è stato un errore il cui codice è registrato nel registro EAX.
Una system call è vista dal codice applicativo come una funzione di libreria ma al suo interno vengono usati costrutti ASM per chiamare istruzioni di trap al sistema operativo e modificare valori di registri; essa può avere come input parametri o puntatori. I puntatori vengono resi noti al software di sistema, quindi indicano le aree di memoria coinvolte, questi vengono usati per fare del “side-effect”, cioè rilasciare altri valori di output rispetto al parametro di ritorno e al codice dell’errore in EAX; ecco perché per prendere informazioni articolate viene passato il puntatore const (cioè sola lettura).
Esempio di esecuzione di system call, con passaggio a stub e trap al sistema.
Aree memoria
L’area di memoria user space di ogni processo attivo in sistema è divisa in diverse sezioni:
- Text, area per la memorizzazione del codice del programma in esecuzione
- Data, cioè area per lo storage dei dati inizializzati
- Bss (Block Started by Simbol), area dedicata ai dati non inizializzati
- Heap, area di memoria espandibile su chiamata (es. funzione malloc)
- Stack, istanze delle variabili locali, controllo di ritorno e chiamate a routine
Ogni processo ha poi una system memory, che generalmente consiste nella system stack, una stack globale che memorizza valori di ritorno e indirizzi di istruzioni comprendente anche software di sistema. Ovviamente questa stack è accessibile solo in modalità kernel, ed il processo modalità utente non può assolutamente accedere allo stato della memoria livello sistema. Appena una trap al kernel viene accettata il firmware cambia la stack di esecuzione da quella user a quella di sistema. Per ogni processo attivo è presente una stack di sistema! Se così non fosse infatti il kernel non ha un’immagine di istruzioni su cui riprendere l’esecuzione quando un processo viene rischedulato.
Standard di sistema e standard di linguaggio
Quando si sviluppa software si possono usare standard di sistema o standard di linguaggio; per standard di sistema si intende l’insieme di servizi offerti da uno specifico sistema per una specifica tecnologia. Ad esempio lo standard WINAPI per i sistemi operativi della famiglia Windows e lo standard POSIX per i sistemi operativi basati su UNIX; lo standard di sistema definisce il set di system call che il sistema operativo deve implementare. Per standard di linguaggio invece si intende l’insieme di istruzioni di libreria che, al variare di sistema su cui si sviluppa, rimane invariato dal punto di vista dell’interfaccia di utilizzo ma viene implementato in maniera differente a seconda di quale sistema operativo si ha (ad esempio la funzione di libreria C printf può essere usata sia in UNIX che in Windows, ma la sua reale implementazione dipende da che sistema operativo deve interrogare, perché le system call cambiano da un sistema all’altro).
Kernel
Un S.O. è diviso in 2 sezioni, Kernel e User-space. Il kernel è la collezione dei moduli software e strutture dati che sono sempre residenti in RAM durante il funzionamento del sistema, esse sono il cuore del sistema. La sezione user space contiene programmi applicativi che useranno il kernel ma...
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.