Lezione n° 1 (De Carlini) - 26/2/97
Introduzione
Un sistema operativo (SO) è un software (spesso di grande complessità) che può essere considerato da vari punti di vista. Si può dire ad esempio che esso consente di creare un ambiente di lavoro su di una macchina, ponendosi tra la ‘macchina nuda’ (l’hardware) ed il mondo delle applicazioni. Le applicazioni che vengono solitamente usate dall’utente possono ‘prendere vita’ soltanto all’interno dell’ambiente generato dal sistema operativo. Esso ha il compito fondamentale di controllare e coordinare l’utilizzo dell’hardware della macchina da parte delle applicazioni.
Quando l’utente (ovvero l’applicazione di cui sta facendo uso) desidera ‘mettere le mani’ direttamente sulla macchina, interviene il sistema operativo. È quanto accade, ad esempio, quando i programmi già compilati, linkati e caricati in memoria effettuano delle supervisor call (il che avviene continuamente) per poter fare uso delle risorse hardware del sistema, quali l’I/O. In questo senso si dice pure che il SO è un allocatore di risorse, in base alle richieste che ne vengono fatte e alla loro effettiva disponibilità.
Le due parti fondamentali nelle quali possiamo vedere suddiviso un SO sono il kernel e la shell. Il kernel è il nucleo del SO, ed è responsabile della creazione dell’ambiente nel quale vengono calate le applicazioni utente. L’utente non comunica mai direttamente con il kernel, ma esegue le necessarie operazioni facendo uso di un apposito linguaggio di comando che viene interpretato dallo shell (l’interprete dei comandi di un SO). L’esempio più immediato che possiamo fare è quello dei PC caricati con il SO MS-DOS. Una volta che il PC è acceso, compare il ben noto prompt dei comandi. Questo è il segno che il processo del SO attualmente in funzione è la shell, e che quest’ultima si trova in attesa di un comando (per processo intendiamo un programma in esecuzione).
La shell del DOS (da non confondersi con l’omonimo programma, inserito nelle edizioni più recenti, che ricorda graficamente l’interfaccia del File Manager del Windows 3) è piuttosto spartana. La shell di un SO può avere anche un aspetto più gradevole e fare uso di icone, finestre, menù a tendina etc. come avviene ad esempio proprio nelle varie versioni di WINDOWS. Digitando un comando, ad esempio COPY, viene attivata dalla shell una utility, operante anch’essa all’interno del kernel, che esegue l’operazione di copia.
Benché sia indubbiamente riduttivo, si può affermare che il vero SO è in realtà ciò che abbiamo definito come suo kernel, ovvero l’insieme di tutto ciò che può essere eseguito in stato supervisore, e che è interdetto all’utilizzo diretto da parte dell’utente. Il resto (shell e utility) viene detto nell’insieme software di base. Si noti in proposito lo schema che segue.
| Applicazioni | Shell (utente) | Utility |
| Sistema operativo (kernel) | ||
| Hardware | ||
Qualche cenno storico
I SO non sono affatto nati contemporaneamente alle macchine per l’elaborazione delle informazioni. Quando si cominciarono a costruire i primi sistemi di elaborazione, di fatto l’unico tipo previsto di programmazione era quello in linguaggio macchina. Il programmatore era tenuto a effettuare in prima persona anche tutte le operazioni di I/O, il che rendeva il suo lavoro piuttosto oneroso, dato che ogni dispositivo aveva le sue caratteristiche peculiari (il proprio buffer, i propri bit di stato e di controllo, i propri flag etc.). Quando si sentì l’esigenza di sollevare il programmatore da queste responsabilità, nacque il primo modello di SO. Esso aveva fondamentalmente il compito di facilitare il colloquio con le unità esterne, utilizzando per ogni diverso dispositivo un’appropriata routine di I/O; possiamo dire che i primi SO altro non erano che insiemi di driver (i programmi che funzionano da interfaccia fra l’unità centrale e i dispositivi periferici: per quanto oggi la loro importanza sembri ovvia, rispetto alla costruzione dei computer originari essi furono ideati solo in un secondo momento).
Successivamente fu ideato e realizzato il primo esempio di caricatore: un programma residente in memoria, avente il compito di ‘caricare’ nella memoria stessa programmi e dati, che venivano inseriti entrambi attraverso schede perforate e supporti di altra natura. Il caricatore funzionava sulla base di comandi che gli venivano impartiti dallo shell del SO. Ad esempio, se il comando era di eseguire un programma scritto in un certo linguaggio simbolico, il caricatore prelevava il compilatore del linguaggio da un nastro magnetico e lo caricava in memoria, poi caricava il programma e i dati, e attendeva che il compilatore svolgesse il suo lavoro (il cui risultato era un listato in Assembly). Occorreva quindi caricare, sempre generalmente da un nastro magnetico, l’assemblatore, che produceva la forma binaria del programma o programma oggetto e infine caricare quest’ultimo in memoria, pronto per l’esecuzione. In assenza di un caricatore, colui che scriveva i programmi sarebbe stato costretto ad effettuare a mano tutte queste operazioni.
A quei tempi - stiamo parlando dei primi anni 1960 - la shell di un SO solitamente non contava più di 4 o 5 comandi. Per caricare in memoria un compilatore poteva bastare anche un solo comando, e questo poteva essere dispensato da un operatore del tutto ignorante in materia di programmazione, invece che dallo stesso programmatore.
Col tempo si trovò conveniente limitare l’impiego delle schede perforate, e ci si pose l’obbiettivo di arrivare ad effettuare qualunque operazione di I/O mediante i nastri magnetici, i supporti di memorizzazione più veloci che si conoscevano allora. Le schede venivano portate ad un operatore appositamente incaricato di riversare, mediante un calcolatore dedicato, tutte le informazioni contenute sulle schede su dei nastri magnetici.
Questo diede il via al concetto di batch o lotto. Si comprese subito che era meglio attendere di possedere un numero sufficiente di schede prima di cominciare a riversarle, invece di sprecare tempo ed energia per trasferire su nastro le informazioni magari di una sola scheda o di pochissime. A breve scadenza si cominciò a parlare di multiprogrammazione e di processo. Si capì che l’unità centrale di elaborazione di una macchina o CPU (che per il momento era unica) poteva essere sfruttata alternativamente da più processi attivi, purché si prestasse la giusta attenzione ai problemi che una simile gestione poteva comprensibilmente comportare.
Il risultato fu che si poté riassumere in un unico elaboratore anche molte funzioni fino a quel momento demandate ad unità esterne, incluso il trasferimento appena citato di informazioni a partire da schede perforate. La procedura adottata venne denominata di spooling. Essa si avvaleva della disponibilità di unità di memorizzazione a disco, le quali presentavano il notevole vantaggio, rispetto ai nastri, di essere ad accesso casuale: la testina di lettura/scrittura può spostarsi rapidamente da una zona all’altra del disco leggendo o scrivendo informazioni.
Le schede potevano dunque essere memorizzate sul disco anziché sui nastri. Se la CPU aveva bisogno del contenuto di una particolare scheda, perché un processo o job ne aveva fatto richiesta, ne ricercava la posizione, mediante un’apposita tabella, sul disco. Negli intervalli di tempo in cui il disco non era impegnato a soddisfare richieste da parte della CPU poteva dedicarsi ad assorbire altre schede, compatibilmente con lo spazio disponibile. La struttura fisica dell’unità a disco consentiva di passare rapidamente dall’operazione di memorizzazione di schede a quella di lettura da parte della CPU.
Il disco poteva essere impiegato anche in qualità di buffer di uscita, oltre che di ingresso, per la CPU. Ad esempio la richiesta di stampa di una riga da parte di un job poteva sortire la memorizzazione di quella riga sul disco, anziché la sua stampa immediata. La stampa effettiva si aveva solo quando il lavoro era stato completato. Ciò consentiva di sovrapporre la computazione di un job con le operazioni di I/O. Infatti era ad esempio possibile che la CPU eseguisse un job ‘leggendo le sue schede’ dal disco mentre era in corso la stampa dei risultati di un altro job.
Lezione n° 2 (De Carlini) - 27/2/97
Cenni storici - Parte 2
Come abbiamo accennato la scorsa volta, fu con la tecnica dello spooling che si poté parlare per la prima volta di multiprogrammazione, della quale abbiamo anche dato un semplice esempio: l’operazione basilare, detta di spool, permetteva di sovrapporre la stampa dei risultati di precedenti job all’inizio dell’esecuzione di nuovi job.
Più in generale, si procedeva così: sull’unità a disco si memorizzava un certo un insieme di job (pool di job). Un sottinsieme del pool era a sua volta spostato dal disco alla memoria centrale, che mediamente era (ed è tuttora) di capacità inferiore ma di velocità di accesso superiore rispetto ai dischi. A partire da tale sottoinsieme si poteva scegliere di volta in volta il particolare job da eseguire. Man mano che la memoria si liberava, era possibile selezionare altri job dal disco (in base a criteri di spazio di occupazione) per spostarli nella memoria stessa. Ciò sarebbe stato impossibile con le unità a nastro o a schede perforate, dalle quali i job potevano essere prelevati solo in modalità sequenziale. Il batch multiprogrammato consisteva nell’alternare l’uso dell’unità centrale da parte dei vari job presenti in memoria in questa maniera: ogni volta che un job costringeva la CPU a rimanere inattiva (ad es. per attendere il montaggio di un nastro) quest’ultima veniva assegnata ad un altro job.
Una diversa specie di gestione dei job si ha nel cosiddetto time sharing. Supponiamo che più utenti utilizzino, magari attraverso terminali (computer privi di unità di elaborazione proprie), uno stesso sistema hardware centrale. Con il time sharing si vuole dare l’impressione ad ogni utente di avere a disposizione l’intera macchina. Questo naturalmente non avviene nella realtà, dato che il sistema centrale deve in qualche modo distribuire le proprie risorse sui vari terminali.
Indicato come processo ready un processo che per poter funzionare necessita soltanto della disponibilità della CPU, il modo di ragionare è in sostanza il seguente: ogni processo ready ha a disposizione la CPU solo per un tot di tempo, dopo di che viene interrotto per consentire ad un altro processo ready di usare a sua volta la CPU. Un processo ready, una volta interrotto, deve attendere un certo intervallo di tempo prima di ricevere nuovamente l’attenzione della CPU.
I sistemi time sharing vengono combinati con il batch multiprogrammato nel modo seguente (si parla di batch multiprogrammato con partizione time sharing). I job utente sono gestiti in modalità time sharing nel modo che si è indicato, e sono prioritari rispetto alle altre attività del sistema. Questi processi vengono detti conversazionali e devono necessariamente avere la priorità e funzionare in time sharing, in quanto soggetti a stringenti limiti di tempo di risposta (esempio: uno degli utenti potrebbe richiedere la compilazione di un programma entro la fine della giornata di lavoro). Ciò significa che solo quando non ci sono processi ready in attesa il sistema si dedica ad effettuare altri lavori batch, come le tipiche attività di spooling (stampe lasciate in sospeso, etc.), le quali - si suppone - non devono essere eseguite entro limiti di tempo inderogabili e che quindi non rientrano nel ciclo time-sharing. I lavori “secondari” vengono detti job di background, o di sottofondo.
Questo sistema di attribuzione delle risorse è detto scheduling a lungo termine dei job. Lo schedulatore dei processi prioritari, gestiti mediante il time-sharing, si chiama invece scheduling a breve termine. Nella realtà dei fatti, si può considerare non opportuno che la CPU sia monopolizzata sempre e comunque dagli utenti, e pertanto si può pensare di attribuire almeno una minima parte del tempo di lavoro del sistema ai job di background.
Le esigenze temporali dei job possono essere, è ovvio, le più disparate, a seconda della serietà e della natura del contesto in cui si opera. In particolare si distingue comunemente fra sistemi hard real time che sono costruiti in modo da rispettare dei tempi di risposta categoricamente assegnati, e sistemi real time (non ‘hard’), costruiti in modo da fornire dei tempi di risposta ‘medi’.
Quindi mentre nel batch multiprogrammato bisogna attendere che un job si interrompa perché la CPU sia trasferita ad un altro job, nel time sharing (più noto con il nome multitasking) il trasferimento avviene in ogni caso a determinati istanti di tempo. Il primo sistema di solito non permette una gestione dinamica (cioè interattiva, manipolabile a run-time) dei job. Invece nel multitasking la CPU commuta rapidamente tra i vari job in modo da consentire ai vari utenti di interagire con ciascun programma durante la sua esecuzione. L’immediata commutazione si deve al fatto che, tipicamente, in un sistema time-sharing le azioni e i comandi eseguiti sono molto brevi. Classico esempio è quello dell’input fornito dagli utenti mediante tastiera: la velocità di digitazione dell’utente non può sicuramente competere con i velocissimi tempi di elaborazione del sistema, per cui fra un carattere e l’altro si vengono a creare dei ‘tempi morti’ che vengono impiegati trasferendo la CPU (con impercettibile rapidità) ad altri programmi utente.
Purtroppo questo accostamento non risulta nel Silberschatz-Galvin, il quale tende a distinguere nettamente tra time sharing e real time (pag.28), affermando che nel time-sharing “è desiderabile, ma non indispensabile, avere una risposta immediata”. Quest’ultimo e l’hard real time poi vengono definiti addirittura “in conflitto”. Le ragioni addotte sono due: nell’hard real time è quasi inesistente il ricorso a memorie secondarie (ossia di massa, come i dischi) il ruolo prevalente essendo assunto dalla memoria ROM; inoltre le attese nell’esecuzione dei programmi in un sistema time sharing, dovute alla possibilità offerta agli utenti di inserire degli input interattivi, sono inconcepibili nell’hard real time, che richiede dei tempi di esecuzione ben delimitati.
Il time-sharing, ancora oggi largamente usato, rappresentò un’ovvia semplificazione tecnologica per la realtà di quegli anni, che in pratica contemplava come massimo risultato l’uso contemporaneo di grosse apparecchiature da parte di un notevole - per l’epoca - numero di operatori. La vera e propria rivoluzione scatenata dall’avvento dei microprocessori portò invece in auge le macchine personal, ovvero dedicate a singoli utenti.
I SO realizzati per queste macchine presentavano un ambiente di utilizzo più “amichevole” e inizialmente non prevedevano la multiprogrammazione, in quanto si riteneva che una singola persona non dovesse avere a che fare con più di un processo per volta. Il concetto di multiprogrammazione era infatti strettamente legato a quello di pluralità di utenti. D’altra parte, ciò era dovuto anche al fatto che la multiprogrammazione richiedeva delle capacità di calcolo elettronico che a quei tempi non potevano essere pretese da un PC.
I notevoli progressi fatti da allora hanno messo in grado i PC di poter eseguire ben più di un processo per volta e i moderni SO sono ormai tutti concepiti tenendo conto di tale potenzialità. Inoltre negli ultimi 20 anni si è resa possibile (e si è sempre più diffusa) l’aggregazione di PC in sistemi distribuiti, comunemente detti reti, geograficamente dislocate anche a livello planetario.
Struttura di un sistema operativo
Un SO è schematizzabile come una ‘cipolla’, ossia è formato da più strati o “livelli” concentrici. Cerchiamo di capire in che senso si può affermare questo.
Abbiamo detto che ogni qual volta che un processo in corso di esecuzione necessita del SO (in quanto dev’essere fatta un’operazione non direttamente accessibile) si ha una supervisor call (SRC). Ad esempio, se stiamo eseguendo un programma in C e si incontra un’istruzione di read da una periferica di input, questo comporta, come si sa, il richiamo della giusta subroutine dall’interno di una libreria fornita insieme al linguaggio. La subroutine non fa altro che richiedere una SRC che effettua ‘a basso livello’ (ossia a stretto contatto con l’hardware) l’operazione di I/O.
Ci si potrebbe domandare perché mai non può essere direttamente il programma utente ad effettuare la SRC e in questo caso la risposta sarebbe duplice:
- Una SRC non può essere ottenuta se non con un’istruzione in linguaggio macchina; questa è la risposta più ovvia ma è anche poco soddisfacente;
- Un’operazione di ingresso/uscita necessita della conoscenza di tutta una serie di informazioni non banali che riguardano il ‘come operare’ e il ‘su che cosa operare’, al di fuori della portata dell’utilizzatore medio e che dipendono solo in piccola parte dal particolare programma in esecuzione. Queste informazioni sono note al kernel del sistema operativo, che le può quindi mettere a disposizione della procedura richiamata (in questo caso una read) per il buon funzionamento dell’operazione.
È bene che il programma utente...
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.
-
C - Thread - Concorrenza - Sistemi Operativi - Monitor
-
Sistemi Operativi: monitor, Gestione thread livello utente e nucleo, Deadlock
-
Lezione 8 Sistemi operativi
-
Sistemi operativi