Sei sulla pagina 1di 11

2.

1 Servizi di un sistema operativo


sabato 27 luglio 2019 12:51
Il sistema operativo fornisce non solo un ambiente di esecuzione per i programmi ma fornisce anche dei servizi. Analizziamo tali servizi, che
sono in parte utili all'utente e in parte al sistema stesso.

Il primo insieme di servizi che analizziamo sono quelli utili agli utenti:
▪ Interfaccia con l'utente. Quasi tutti i sistemi operativi si interfacciano con l'utente e dunque forniscono un'interfaccia che può essere di
varie tipologie; alcuni hanno una interfaccia a riga di comando CLI che consiste appunto in stringhe che codificano i comandi, e
chiaramente un metodo per inserirle; altri hanno un'interfaccia batch che prevede invece che i comandi siano codificati in file che
vengono poi eseguiti; infine la maggior parte dei sistemi mettono a disposizione un'interfaccia grafica GUI che appunto prevede
finestre e un puntatore per comandare le operazioni. Spesso alcuni sistemi danno la possibilità di avere anche più tipologie di
interfacce, lasciando scegliere talvolta all'utente quale utilizzare.
▪ Esecuzione di un programma. Il sistema svolge un servizio fondamentale, che come abbiamo già ampiamente visto è quello di
permettere l'esecuzione di un programma, caricandolo in memoria e interrompendolo in modo normale o anomalo.
▪ Operazioni di I/O. Il sistema operativo offre mezzi adeguati per gestire i dispositivi, senza permettere, per motivi di efficienza e
protezione, il controllo di questi da parte dell'utente.
▪ Gestione del file system. Il sistema permette l'utilizzo e la gestione di file e directory, del loro accesso, della loro creazione e
cancellazione, della ricerca e talvolta permette all'utente persino di scegliere il file system per avere funzionalità e prestazioni
specifiche.
▪ Comunicazioni. Molti sono i casi in cui i processi necessitano di comunicare tra di loro. La comunicazione può avvenire in due modalità:
attraverso lo scambio di messaggi, che implica appunto la creazione di un canale di comunicazione in cui i processi si scambiano i
messaggi, oppure attraverso la memoria condivisa, che permette invece la comunicazione tramite lettura e scrittura di informazioni in
un’area di memoria condivisa ai processi che comunicano.
▪ Rilevamento degli errori. Il sistema fornisce un servizio, come abbiamo potuto vedere, di protezione e sicurezza che necessita di un
meccanismo per la rilevazione degli errori. Deve infatti saper poi intraprendere l'azione giusta, dopo aver rilevato l'errore; talvolta
l'unica azione possibile è l'arresto di sistema, altre volte invece si riesce a provocare solo la terminazione di un processo o restituirgli un
messaggio di errore in modo tale che possa cercare di risolvere l'errore.

Il secondo gruppo di servizi che analizziamo sono invece quelli utili al sistema:
▪ Allocazione delle risorse. Il sistema si occupa della distribuzione delle risorse necessarie ai processi e agli utenti, che possono essere
molteplici ed essere attivi contemporaneamente.
▪ Accounting delle risorse. Si tiene meticolosamente traccia degli utenti che utilizzano il calcolatore, e di quali risorse stanno
impiegando, allo scopo di contabilizzare le risorse o anche per redigere statistiche.
▪ Protezione e sicurezza. Come abbiamo già detto, la protezione assicura che l'accesso alle risorse del calcolatore sia controllato, mentre
la sicurezza di assicura che non accedano al sistema utenti non riconosciuti, assicura i dispositivi, assicura che non ci siano accessi
illegali.
2.2 User Interface del sistema operativo
sabato 27 luglio 2019 13:14
Abbiamo visto che la maggior parte dei sistemi operativi sono interattivi con un utente, hanno bisogno spesso dell'intervento utente e
dunque forniscono un'interfaccia di comunicazione con esso. In generale le due tipologie di interfacce più diffuse e famose sono proprio la CLI
e la GUI che adesso analizzeremo in dettaglio.

❖ Interprete dei comandi


In alcuni sistemi l'interprete dei comandi è una funzionalità comprensiva del kernel, mentre in altri (come in Windows o Unix) è un
programma speciale.
Spesso si può scegliere tra più interpreti dei comandi e in tal caso questi assumono il nome di Shell; basti pensare che in Unix e Linux è
possibile scegliere tra svariate Shell tra cui la Bourne, la Bourne-again, la C ecc. e la scelta è spesso solo legata a preferenze personali, visto
che bene o male forniscono tutte le stesse funzionalità.
Analizziamone ora il funzionamento. La funzione della CLI è quella di raccogliere ed eseguire il prossimo comando impartito dall'utente. I
comandi che l'utente impartisce si possono implementare in due modi:
• Lo stesso interprete dei comandi contiene il codice di esecuzione del comando. Questo vuol dire che un comando praticamente non
fa altro che far fare un salto dell'interprete ad una sezione del suo stesso codice, dove ci sono le istruzioni per quel comando. Il
numero di comandi che si possono impartire all'interprete è strettamente legato, quindi, alla sua dimensione!
• La maggior parte dei comandi si implementa per mezzo di programmi di sistema. Questo significa che un comando non è
"compreso" dall'interprete, nel senso che l'interprete si limita a impiegarne il nome per identificare il file da caricare in memoria
durante l'esecuzione. È come se si avessero delle "librerie esterne". In tal caso quindi il codice dell'interprete è sicuramente di
dimensioni minori, e i programmatori possono aggiungere comandi creando semplicemente nuovi file con nomi appropriati, senza
alcuna modifica al codice dell'interprete.

❖ Interfaccia grafica con l'utente


L'interfaccia grafica è sicuramente una modalità di interazione con il sistema molto più user-friendly. Invece di obbligare l'utente a digitare
direttamente i comandi, la GUI nasconde il tutto tramite finestre, icone, menu cliccabili con il puntatore. Si utilizza la metafora della scrivania,
il desktop, su cui si selezionano icone, finestre e quant'altro, mentre le directory in tale contesto sono note come cartelle.
La prima interfaccia grafica della storia, anche se molti attribuiscono tale invenzione ad Apple, comparve nel 1973 con lo Xerox Alto, nei
laboratori di ricerca Xerox PARC. La PARC infatti al tempo era una azienda specializzata nella produzione di stampanti, e aveva un gruppo di
ricercatori proprio per trovare nuove tecnologie. L'interfaccia grafica dello Xerox Alto però rimase solo ristretta ai laboratori di ricerca
dell'azienda stessa e a qualche università, ma non fu mai messa in commercio per l'uso su larga scala. Steve Jobs, che vide tale tecnologia,
prese spunto e migliorò l'interfaccia grafica dello Xerox, e la usò sui suoi Apple Macintosh che spopolarono negli anni '80; seguendo la stessa
onda, in casa Microsoft, dopo anni di commercio di MS-DOS, uscì nello stesso periodo Windows 1.0 che fu il primo sistema operativo
dell'azienda ad avere un’interfaccia grafica, il precursore degli attuali sistemi in uso.
La GUI è un’interfaccia che si ritrova anche nei dispositivi mobili, sostituendo però il mouse dei sistemi pc con un'interfaccia touch-screen.
Nei sistemi Unix, visto che abbiamo parlato sia di Mac che di Windows, l'interfaccia grafica ha tardato ad arrivare, visto che tali sistemi hanno
dato sempre un ruolo preponderante alle CLI; nella creazione delle interfacce grafiche ebbero un impulso predominante le versioni di Unix
open-source come KDE e il desktop GNOME del progetto GNU.
2.3 Chiamate di sistema
sabato 27 luglio 2019 13:44
Le chiamate di sistema possiamo vederle come un'interfaccia per i servizi che rende disponibili il sistema operativo. Consistono in routine
(funzioni, procedure) che sono scritte nella maggior parte dei casi in C o C++, in casi particolari invece, come per compiti di più basso livello,
sono scritte addirittura in linguaggio Assembly.
Per capire maggiormente l'importanza primaria delle chiamate di sistema, consideriamo un esempio del loro uso durante la scrittura di un
programma che legga i dati da un file e li scriva in un altro. Innanzitutto è necessario individuare il file da cui effettuare la lettura e il file in cui
fare la scrittura, dunque servono i nomi dei due file; se si struttura il programma richiedendo i nomi all'utente ci vorranno una chiamata per la
richiesta all'utente a schermo e una chiamata per la lettura da tastiera dei dati che l'utente ha inserito, mentre nel caso in cui si faccia
comparire una finestra con i nomi del file, in cui l'utente deve scegliere, le chiamate di sistema aumentano. Successivamente il programma
deve aprire il file dove deve leggere e creare effettivamente il file di destinazione, e tali operazioni implicano più di una chiamata di sistema,
soprattutto nel caso in cui ci sono delle condizioni di errore che portano ad ulteriori azioni e ad ulteriori chiamate. Una volta poi predisposti i
due file inizia il ciclo in cui si ha la lettura dal primo file e la scrittura nel secondo, con altre numerose chiamate di sistema, che proseguono per
la fine dell'operazione se si vuole creare un programma con la chiusura di entrambe i file, oppure con una richiesta all'utente a schermo o una
normale terminazione.
È chiaro quindi che anche per un'operazione semplice sono necessarie numerose chiamate di sistema e quindi un intenso uso del sistema
operativo.
Nonostante il fatto che il sistema operativo possa svolgere migliaia di chiamate di sistema, questo non è un problema del programmatore, nel
senso che tutto ciò al programmatore è nascosto grazie alle API, interfacce per la programmazione delle applicazioni, che mettono i
programmatori ad un livello di astrazione più alto, fornendo sostanzialmente un insieme di funzioni e protocolli che "dietro le quinte" invocano
le chiamate di sistema per conto del programmatore. Tra le API più diffuse abbiamo l'API POSIX (per i sistemi con standard POSIX ovvero Unix,
Linux e Mac OSX), l'API di Windows e l'API di Java per le funzioni che svolge la sua JVM. I vantaggi di utilizzo delle API sono molteplici, abbiamo
infatti innanzitutto una maggiore portabilità delle applicazioni ma soprattutto vi è una maggiore facilità di utilizzo rispetto all'utilizzo delle
chiamate di sistema stesse.
Le API non sono le uniche interfacce ad essere presenti, infatti nella maggior parte dei linguaggi di programmazione il sistema di supporto
all'esecuzione (Runtime-support-system), che non è altro che un insieme di procedure strutturate in librerie nel compilatore, fornisce
l'interfaccia alle chiamate di sistema che collega il linguaggio stesso alle chiamate di sistema del SO. Tale interfaccia infatti intercetta le
chiamate a funzione nella API e invoca lei stessa le system call relative. Non solo, infatti mantiene anche la tabella delle chiamate, visto che le
chiamate di sistema sono spesso identificate da un numero, e invoca quindi di volta in volta la chiamata richiesta restituendo al chiamante lo
stato della chiamata e i valori di ritorno; il chiamante come sempre non ha minimo interesse a sapere com'è fatta la chiamata, gli basta sapere
che è conforme all'API e che ha un certo determinato effetto. Quindi riassumendo le API nascondono al chiamante il meccanismo delle
chiamate di sistema e tramite le chiamate a funzione dell'API l'interfaccia di chiamate di sistema se le gestisce e le fa in modo effettivo.
Le chiamate di sistema sono diverse a seconda del sistema operativo che si utilizza e spesso sono necessarie più informazioni, più "parametri",
rispetto al semplice ID della chiamata, e la richiesta di più informazioni varia anch'essa da sistema a sistema. Il passaggio dei parametri per la
system call al sistema operativo può avvenire in tre modalità:
1. Tramite registri. Ci possono essere casi però in cui i parametri sono più dei registri.
2. Tramite blocchi o tabelle di memoria. I parametri infatti vengono memorizzati in blocco e si passa direttamente l'indirizzo del blocco,
come parametro in un registro. Tale metodo lo utilizzano i sistemi Linux
3. Tramite lo stack. I parametri infatti possono anche essere collocati nello stack (con un push) e prelevati dal sistema tramite la pop.
2.4 Categorie di chiamate di sistema
lunedì 29 luglio 2019 12:16

❖ Controllo dei processi


Iniziamo l'analisi delle chiamate di sistema partendo con quelle relative al controllo dei processi.
Un programma in esecuzione deve potersi fermare sia in modo normale che anormale, e per queste due operazioni sono necessarie
chiamate di sistema come end() e abort(); in caso di terminazione anomala o comunque nel caso di un lancio di un'eccezione, il sistema crea
un'immagine del contenuto della memoria e lo salva in un file dump, generando poi un messaggio di errore; il programmatore può utilizzare
tale file esaminandolo con quello che viene chiamato debugger, alla ricerca di errori e problemi. Molti sistemi inoltre ad ogni tipologia di
errore fanno corrispondere una determinata azione di recupero; creano spesso inoltre una sorta di gerarchia degli errori, tramite un
parametro d'errore, dove se il parametro ha un valore alto allora l'errore è grave e viceversa.
Per quanto riguarda il caricamento ed esecuzione, un processo che esegue un programma può richiedere la load() e la execute() di un altro
programma. È interessante chiedersi in tal caso il controllo poi a chi si restituisce, se al sistema o al programma principale che ha caricato il
secondario: dipende. Se il controllo rientra al programma principale è necessario dunque salvare in memoria un’immagine del programma
principale prima del suo arresto per poter riprendere l'esecuzione dal punto giusto; se invece entrambe i programmi continuano l'esecuzione
concorrentemente, si è creato allora un nuovo processo da eseguire in multiprogrammazione.
Rimanendo in questo contesto la creazione di un nuovo processo o il suo arrestarsi, e il modo in cui tale viene creato, necessita di chiamate
come Create_process() oppure submit_job(); tali chiamate implicano anche un settaggio o una richiesta di informazioni riguardanti tali
processi, dunque nascono le chiamate get_process_attributes() e set_process_attributes(), ed infine per la terminazione una chiamata
terminate_process().
Una volta creati può succedere che si debba impostare un certo periodo d'attesa che i processi terminino la loro esecuzione; ciò è possibile
tramite wait_time(), mentre nel caso in cui si debba impostare l'attesa di un evento allora si ha wait_signal() , ed è chiaro che se si ha l'attesa
di un evento è necessaria anche la sua segnalazione, tramite signal_event().
Infine, per quanto concerne il controllo dei processi, spesso tali condividono dati e spesso, per regolare l'accesso a memorie condivise e
dunque per assicurare l'integrità dei dati, si utilizzano chiamate di sistema che consentono di bloccare l'accesso alla memoria mentre un
processo vi ha già avuto accesso. Il blocco e lo sblocco si hanno tramite le chiamate aquire_lock() e release_lock(), che approfondiremo nei
più avanti.
❖ Gestione dei file
Analizziamo ora diverse chiamate di sistema riguardanti invece i file.
Le chiamate permettono di creare e cancellare i file, dunque vi sono chiamate del tipo create () e delete(); dopo la creazione si deve poter
utilizzare il file, con open() per aprire, read() per leggerlo, write() e reposition() per scrivere sul file o riposizionarlo (riavvolgendo e saltando
alla fine del file per esempio) e infine chiuderlo con close(). Nel caso in cui si vuole definire gli attributi di un file (nome, tipo, ecc.) oppure nel
caso in cui si vogliano conoscere tali attributi si avranno le chiamate di sistema set_file_attributes() e get_file_attributes(). Le chiamate poi
variano da sistema a sistema, ce ne sono alcuni infatti che ne hanno qualcuna in più; ve ne sono poi alcuni che forniscono API che eseguono
operazioni per spostare o copiare i file tramite codice appropriato che combina più chiamate di sistema

❖ Gestione dei dispositivi


Sappiamo bene ormai che un processo, durante la sua esecuzione, richiede parecchie risorse, e se tali sono disponibili allora gli vengono
concesse altrimenti il processo deve attendere. Alla fine, si possono considerare tali risorse dei dispositivi, o fisici o virtuali. In presenza di
utenti multipli ci sarà sicuramente una request() ovvero una richiesta del dispositivo per l'uso esclusivo da parte di un utente e la
conseguente release() per il rilascio; possiamo vedere tali chiamate di sistema una "copia" relativa però ai dispositivi delle chiamate open e
close dei file ed esattamente come nei file si hanno le stesse chiamate per la lettura, la scrittura e il riposizionamento. Non a caso sistemi
come Unix non fanno alcuna distinzione tra file e dispositivi.

❖ Gestione delle informazioni


Molte chiamate di sistema che vengono fornite servono semplicemente per avere uno scambio di informazioni tra il programma utente e il
sistema operativo. Si può ottenere l'ora tramite la chiamata time() oppure la data attuale con la chiamata date(); altre chiamate possono
servire per ottenere informazioni strettamente legate al sistema (utenti collegati, versione del sistema operativo ecc.).
Per quanto riguarda operazioni di debugging abbiamo chiamate come dump() per ottenere l'immagine della memoria, chiamate per ottenere
la trace di un programma (ovvero delle chiamate di sistema da esso effettuate) oppure ancora chiamate per esaminare il tempo utilizzato da
un programma. Infine anche i processi hanno degli attributi, come i file, ed è per questo che ci saranno sicuramente chiamate come
get_process_attributes() e set_process_attributes().

❖ Comunicazione
Restano da analizzare le chiamate di sistema relative alla comunicazione dei processi. Esistono due tipologie di comunicazione che adesso
analizzeremo.
Si definisce comunicazione a scambio di messaggi quel modello di comunicazione per cui, tramite una mailbox, i processi si scambiano i
messaggi in modo diretto o indiretto. È necessario innanzitutto creare la connessione e chiuderla alla fine della comunicazione e ciò è
possibile prima identificando i processi in gioco con get_host_id() per sapere il nome macchina e get_process_id() per sapere invece
l'identificatore di processo; fatto ciò tali identificatori vengono passati alle chiamate open() e close() oppure a open_connection() e
close_connection() a seconda del sistema. Generalmente un processo deve anche accettare la comunicazione tramite la chiamata
accept_connection(). Lo scambio di messaggi, dunque l'invio e la ricezione avvengono tramite le chiamate di sistema write_message() e
read_message().
Si definisce comunicazione a memoria condivisa quel modello in cui i processi condividono appunto aree di memoria. Si creano e si accede
alla memoria tramite le chiamate shared_memory_create() e shared_memory_attach(). Il sistema operativo deve poi avere dei meccanismi
per far sì che i processi non abbiano accesso in contemporanea alla memoria condivisa, scrivendo e leggendo contemporaneamente.
Questi modelli di memoria sono assai diffusi e spesso sono addirittura presenti contemporaneamente; lo scambio di messaggi è utile in fatti
per lo scambio di piccole quantità di dati, mentre nel caso di grosse quantità è sicuramente più conveniente il modello a memoria condivisa;
il primo modello però è più semplice, il secondo è complesso perché deve assicurare protezione e sincronizzazione dei processi.
2.5 Programmi di sistema
lunedì 29 luglio 2019 15:10
Abbiamo visto che, nella gerarchia di un calcolatore, abbiamo alla base l'hardware, poi salendo abbiamo il sistema operativo e poi al di sopra i
programmi di sistema insieme ai programmi applicativi.
I programmi di sistema o utilità di sistema sono sostanzialmente programmi forniti dal sistema stesso che sono talvolta utili ad esso stesso;
alcuni sono semplici chiamate di sistema mentre altri sono più complessi. Possiamo classificare i programmi di sistema nelle seguenti
categorie:
• Gestione dei file. Sono programmi che effettuano operazioni (copia, crea, cancella ecc.) su file e directory.
• Informazioni di stato. Sono programmi che richiedono di indicare data, ora, spazio disponibile in memoria, numero utenti ecc. al
sistema.
• Modifica dei file. Sono programmi che vengono chiamati editor che permettono di creare e modificare il contenuto dei file
memorizzati su dischi o altri dispositivi.
• Ambienti di supporto alla programmazione. Comprendono compilatori, assemblatori, debugger, interpreti, talvolta forniti con il
sistema stesso, talvolta scaricabili.
• Caricamento ed esecuzione di programmi. Comprendono caricatori (loader) e linker.
• Comunicazioni. Offrono meccanismi per cui si possono creare collegamenti virtuali tra processi, utenti e calcolatori diversi, tipo
browsers ecc.
• Servizi di background. Alcuni programmi, una volta che vengono lanciati, restano in esecuzione lungo tutta l'esecuzione del sistema
operativo e terminano con lui, anziché terminare in modo classico; tali processi sono chiamati servizi, sottosistemi o demoni. Un
sistema ha tipicamente decine di demoni.
Tipicamente un utente si fa un’immagine del sistema influenzata dalle applicazioni e dai programmi di sistema, non di certo dalle chiamate di
sistema messe a disposizione; molto spesso però all'apparenza due sistemi operativi sono diversi ma in realtà mettono a disposizione le stesse
chiamate di sistema.
2.6 Progettazione e realizzazione di un sistema operativo
lunedì 29 luglio 2019 15:23
❖ Scopi della progettazione
Nel momento in cui inizia la progettazione di un sistema operativo, la prima domanda che ci si pone riguarda il capire gli obiettivi e le
specifiche del sistema stesso. Da una parte infatti, al livello più alto, la progettazione è influenzata dalla scelta dell'architettura fisica e dal tipo
di sistema. Ad un altro livello gli obiettivi sono un po' più blandi da specificare, ma in generale si dividono in due gruppi, ovvero in obiettivi
utenti e obiettivi sistema.
Per quanto riguarda gli obiettivi per gli utenti ci sono alcuni obiettivi ovvi, come la facilità di utilizzo, la facilità nell'imparare a usarlo, la
sicurezza, la velocità e l'affidabilità, eppure non sono obiettivi facili da identificare perché tali caratteristiche sono soggettive da utente ad
utente. Bene o male le stesse caratteristiche sono richieste anche da chi utilizza per progettare, creare e operare con il sistema, quindi gli
obiettivi di sistema, ed anche qui purtroppo non sono obiettivi precisi perché sono vaghi ed interpretabili in vari modi. Non esiste dunque
una soluzione unica alla progettazione del sistema per quanto riguarda gli utilizzatori e non a caso esistono numerosi sistemi operativi in
commercio per coprire i più svariati usi e utenti.

❖ Meccanismi e politiche
Creare un sistema operativo richiede molta creatività e non esistono linee guida precise per capire come fare; in compenso esistono però dei
principi generali forniti nel campo del software engineering.
Fondamentale è la distinzione tra due concetti che sono meccanismi e politiche: i meccanismi determinano come eseguire qualcosa, mentre
le politiche indicano e stabiliscono che cosa si debba fare. Analizziamo ad esempio il timer di sistema: è un meccanismo che assicura la
protezione del sistema e il tempo da impostare nel timer riguarda invece le politiche.
La differenza ulteriore tra meccanismi e politiche sta nel fatto che le politiche sono soggette a cambiamenti nel tempo e di luogo, e nel caso
peggiore tali cambiamenti portano anche al dover cambiare i meccanismi. In generale sarebbe ottimale disporre di meccanismi generali, in
modo tale che il dover cambiare politica consiste nel dover modificare solo qualcosa, ridefinendo solo alcuni parametri.
I sistemi a microkernel portano all'esasperazione la divisione tra meccanismi e politiche, dal momento che forniscono un insieme di
funzionalità di base (quindi meccanismi di base) che sono quasi completamente indipendenti dalle politiche, e tali consentono poi l'aggiunta
di moduli ulteriori aggiungendo allo stesso tempo meccanismi nuovi e criteri più complessi.
I sistemi come Windows invece si hanno meccanismi e criteri fissati a priori e propri al sistema.

❖ Realizzazione
È difficile fare affermazioni su come un sistema operativo venga realizzato, dal momento che la sua realizzazione implica tempo e soprattutto
implica la scrittura di moltissimi programmi da parte di moltissime persone.
Soffermiamoci quindi sulla sua programmazione che ovviamente è il modo in cui viene realizzato.
Inizialmente i sistemi operativi venivano scritti in linguaggio assembler; al giorno d'oggi vi sono ancora sistemi scritti in tale linguaggio ma la
maggior parte è scritta in linguaggi di alto livello come il C o il C++. Un sistema operativo attuale però non è scritto in realtà in un solo
linguaggio, quindi si utilizza per la maggior parte linguaggi di alto livello, ma ci sono ancora piccole parti di codice, relative magari a procedure
critiche o a più stretto contatto con l'hardware, che sono scritte in assembler.
I vantaggi dell'uso di linguaggi di alto livello sono ovvi, infatti basta considerare che innanzitutto linguaggi più comprensibili e vicini al
programmatore, sono più veloci, più compatti, più facili da capire e da mettere a punto anche in un secondo momento; consente un
maggiore porting da una macchina ad un'altra, infatti se consideriamo l'MS-DOS che fu scritto in assembler, per l'Intel 8088, tale è disponibile
quindi solo per la famiglia di CPU Intel; il sistema operativo Linux invece, scritto in C, è disponibile invece su più CPU.
Gli svantaggi dell'uso di linguaggi di alto livello invece sono una minore velocità di esecuzione e una maggiore occupazione di spazio in
memoria, ma tali svantaggi, grazie agli hardware attuali, sono stati ormai superati; ormai gli algoritmi migliori, le strutture dati che forniscono
i linguaggi di alto livello sono sicuramente più vantaggiosi di una buona scrittura in assembler.
Una volta scritto il sistema e verificato il funzionamento, è possibile procedere poi al debugging del sistema, ovvero all'individuazione di
procedure che possono portare a colli di bottiglia, sostituendole magari con procedure più precise scritte in basso livello.
2.7 Struttura del sistema operativo
lunedì 29 luglio 2019 16:29
❖ Struttura monolitica
Molti sistemi non hanno una struttura ben precisa; sono nati come sistemi piccoli, semplici e solo dopo poi si sono accresciuti, superando lo
scopo originale.
Un primo sistema che possiamo considerare è sempre MS-DOS, che fu progettato da poche persone che non si sarebbero mai immaginate la
sua grandissima diffusione. Tale sistema non fu suddiviso in moduli, è appunto un unico blocco, perché il suo scopo era quello di occupare il
minimo spazio in memoria. Non vi è infatti alcuna separazione tra interfacce e livelli di funzionalità, le applicazioni infatti accedono
direttamente alle routine di sistema, scrivendo magari direttamente su disco e I/O, rendendo, come abbiamo già visto, il sistema vulnerabile
a errori e attacchi sia interni che esterni. Da un certo punto di vista, visto che fu progettato sull'Intel 8088, non ci si poteva aspettare altro,
infatti come abbiamo già visto l'hardware sottostante e disponibile è chiaramente rilevante nella struttura del sistema operativo; tale
processore non aveva infatti la distinzione tra modalità kernel e modalità utente, e dunque non lasciava altra scelta ai progettisti di lasciar
libero accesso, incondizionato, all'hardware.
Anche il primo sistema Unix non era strutturato, sempre a causa delle limitazioni hardware. Consisteva in due parti separate, ovvero il kernel
e i programmi di sistema, e a sua volta il kernel era suddiviso in interfacce e driver, aggiunti ed espansi poi con il corso della sua evoluzione;
sembrerebbe un sistema parzialmente stratificato, ma tutto sommato in realtà è un ammasso di funzionalità combinate comunque su un solo
livello.
Tale struttura rendeva inevitabilmente non solo difficile la sua realizzazione ma anche la sua manutenzione; l'unico vantaggio che poteva
apportare era nelle prestazioni.

❖ Metodo stratificato
Grazie alla presenza poi di hardware appropriato, fu possibile suddividere il sistema in più moduli, più facilmente gestibili e più piccoli, ancor
di più del primo sistema Unix. Stratificare ha permesso di avere un controllo più stretto sul calcolatore e sulle applicazioni che lo utilizzano.
Seguendo un approccio top-down si parte, in tali sistemi, dall'individuare le funzionalità complessive e le sue caratteristiche, e si procede
dividendole in componenti distinte, facendo anche attenzione all'incapsulamento.
Vi sono vari modi per modulare un sistema operativo e quello che analizzeremo ora è il metodo stratificato; secondo tale metodo il sistema è
suddiviso in un certo numero di livelli o strati, dove il più basso è quello relativo all'hardware fino ad arrivare all'n-esimo livello che è
l'interfaccia con l'utente.

Lo strato del sistema è chiaramente un oggetto astratto, che incapsula dati e operazioni che trattano tali dati. Uno strato del sistema ha
strutture dati e ha routine che possono essere richiamate solo dagli strati di livello più alto; dunque a sua volta tale strato può richiamare solo
le routine degli strati inferiori.
Il vantaggio principale che fornisce tale struttura è che è di facile progettazione e debugging, infatti per come sono composti gli strati e per il
fatto che si possono utilizzare solo funzioni degli strati inferiori, fa sì che ogni qual volta si progetti uno strato si può dare per certo che gli altri
siano corretti e ci si può concentrare solo sullo strato che si sta progettando; questo vuol dire anche che se si incontra un errore questo non
può essere che relativo allo strato che si sta considerando. Ogni strato si realizza soltanto partendo da quello che è messo a disposizione dagli
strati inferiori, senza entrare nel merito di come ciò è stato realizzato, per questo vi è incapsulamento poiché ogni strato nasconde agli altri
ciò che contiene.
Lo svantaggio di tale struttura invece risiede nel fatto che bisogna ben capire come fare tali strati, cosa comprendere in tali strati, come
dividere insomma il sistema. Un ulteriore problema sta nel fatto che tale modello tende ad essere meno efficiente rispetto agli altri, perché
magari per effettuare una particolare operazione sono necessarie chiamate di sistema dal livello sottostante, che richiama a sua volta
funzioni, e quindi chiamate di sistema, del livello ancora più sottostante e così via, facendo risultare una chiamata di sistema che richiede
molto più tempo di una di un sistema non stratificato. Tali problemi hanno arrestato abbastanza lo sviluppo di sistemi con tale struttura,
infatti attualmente si progettano sistemi basati su pochi strati con più funzioni

❖ Microkernel
Il primo sistema operativo Unix, abbiamo visto, è cresciuto sempre più, allontanandosi un po' dalla sua struttura tipicamente monolitica.
Negli anni '80 un gruppo di ricercatori universitari progettò un sistema operativo chiamato Mach, il quale aveva un kernel strutturato in
moduli, a microkernel. L'orientamento a microkernel consiste nel rimuovere dal kernel tutti i componenti non essenziali, realizzandoli come
programmi di sistema o utente. In questo modo il kernel risulta di dimensioni molto ridotte.
Non c'è un'opinione comune su quali servizi debbano rimanere nel kernel e quali no. Tuttavia, in generale un microkernel fornisce i servizi
minimi di gestione dei processi, della memoria e di comunicazione.
Il microkernel deve quindi poi fornire un meccanismo di comunicazione tra i programmi client e i vari servizi in esecuzione nello spazio
utente. La comunicazione avviene tramite lo scambio di messaggi; in questo modo se si vuole accedere a un file, un programma client deve
interagire con un programma server e ciò non avviene mai direttamente ma sempre tramite il microkernel.
Il vantaggio principale dei sistemi a microkernel è che rendono il sistema operativo facilmente estensibile, infatti i nuovi sistemi si
aggiungono allo spazio utente e non modificano in alcun modo il kernel. Quest'ultimo se deve essere modificato subirà cambiamenti molto
ridotti e dunque ciò rende il sistema anche più portabile su altre piattaforme. Offre maggiore sicurezza ed affidabilità, perché se un servizio è
compromesso il kernel ne resta immune.
Lo svantaggio di tali sistemi è che possono incappare in cali di prestazione dovuti al sovraccarico dell'esecuzione di troppi processi sistema in
modalità utente. Per questo motivo al tempo della progettazione di Windows XP si era ormai passati ad un'architettura più monolitica che a
microkernel.

❖ Moduli
Il miglior approccio attualmente disponibile è quello a moduli, che consiste in moduli kernel caricabili dinamicamente. Il kernel è infatti
costituito da componenti di base alla quale si integrano dinamicamente altre funzionalità, o all'avvio o all'occorrenza. Questa strategia è
ormai utilizzata dalle implementazioni moderne di Unix, quali Linux, Solaris, Mac OSX e anche da Windows.

Il kernel deve fornire solo i servizi principali mentre poi gli altri servizi si implementano in modo dinamico, durante l'esecuzione del kernel.
L'aggiunta infatti di nuove funzionalità nel kernel è molto più dispendiosa dell'aggiunta di moduli, in quanto la prima richiederebbe di
compilare il kernel ogni volta che si fa un cambiamento.
Si ha quindi tutto sommato un sistema che ricorda quello stratificato, perché ogni sezione del kernel ha interfacce ben protette e nascoste
agli altri, ma è sicuramente più flessibile in quanto ogni modulo può richiamare qualsiasi altro modulo; si ha anche la somiglianza con un
sistema microkernel visto che si ha un modulo principale dove ci sono solo funzionalità di base e poi le altre sono al di fuori, ma è
sicuramente più efficiente perché i moduli non devono invocare funzioni di trasmissione di messaggi per la comunicazione.

❖ Sistemi ibridi
Abbiamo infine un'ultima modalità che sostanzialmente è quella che adottano quasi tutti i sistemi operativi, ovvero possiamo considerarli
quasi tutti dei sistemi ibridi, e questo perché quasi nessun sistema operativo ha una struttura ben precisa e definita; vanno infatti a
combinare strutture diverse.
I sistemi come Linux e Solaris ad esempio sono monolitici (perché il sistema operativo ha un unico spazio di indirizzamento per aumentare le
prestazioni) ma allo stesso tempo sono modulari.
I sistemi come Windows invece sono monolitici in gran parte, sempre per le prestazioni, ma conservano comunque un orientamento a
microkernel e addirittura a moduli, come accennato prima.
Soffermiamoci adesso su tre esempi, approfondendoli nel dettaglio, ovvero Mac OS X e, per quanto riguarda i dispositivi mobili, iOS e
Android.
Mac OS X ha una struttura ibrida; è sicuramente un sistema stratificato
dove abbiamo, allo strato più alto, Aqua che è l'interfaccia grafica con
l'utente, poi proseguendo a scendere abbiamo un insieme di ambienti
applicativi e servizi, tra cui Cocoa che è un ambiente che definisce un API
per l'Objective-C che si utilizza nella programmazione di applicazioni per
tale sistema.
Proseguendo ancora c'è il kernel che è costituito dal microkernel Mach e
dal kernel BSD; il primo si occupa della gestione della memoria, delle
chiamate a procedure remote, dalla comunicazione dei processi e allo
scheduling dei thread, mentre il secondo mette a disposizione
un'interfaccia a riga di comando con gli ambienti applicativi, fornisce servizi
legati al file system e alla rete e un'implementazione dell'API POSIX.
Infine, come si può vedere il kernel fornisce anche un kit di strumenti per lo sviluppo dei driver dei dispositivi, l'I/O kit, e moduli caricabili
dinamicamente che nel gergo del Mac vengono chiamate estensioni del kernel.
In sostanza quindi in Mac OS X abbiamo una struttura stratificata, a microkernel e a moduli.

iOS è il sistema operativo per smartphone e tablet della casa


Apple, ovvero per iPhone e iPad. Anch'esso ha una struttura
ibrida.
È strutturato sostanzialmente sul sistema operativo Mac OS X,
aggiungendo chiaramente funzionalità per i dispositivi mobili e
ovviamente non eseguendo direttamente le applicazioni per il
Mac. Si parte da Cocoa Touch che fa la stessa cosa che fa Cocoa
in Mac OSX ma con la differenza che vengono aggiunti diversi
framework per lo sviluppo relativo smartphone, aggiungendo
ovvero il supporto all'hardware come il supporto al touchscreen.
Lo strato dei servizi multimediali fornisce servizi per la grafica, l'audio e il video; lo strato dei servizi di base si occupa di fornire varie
funzionalità come i database e i servizi per il cloud computing.
Infine, c'è l'ultimo strato che è il nucleo, basato sull'ambiente kernel del Mac OS X.

Android è un sistema operativo praticamente sviluppato da Google,


sviluppato per smartphone e tablet. A differenza di IOS che è stato
sviluppato strettamente per i dispositivi prodotti dalla stessa Apple,
Android gira su una grande quantità di piattaforme mobili ed è
soprattutto open-source. Anche Android è sostanzialmente una pila di
strati software, dove in fondo, alla base di tutto, c'è il kernel Linux, ovvero
il kernel di Linux modificato però da Google e attualmente fuori dalla
normale distribuzione delle versioni di Linux. Quest'ultimo è utilizzato
principalmente per la gestione dei processi, della memoria e dei
dispositivi. È stato poi anche ampliato per includere la gestione dei
consumi energetici.
Poiché, come già detto, deve funzionare su una miriade di piattaforme
mobili dunque con differenti hardware sottostanti, l'ambiente di runtime
comprende librerie di base e soprattutto la macchina virtuale Dalvik.
Gli sviluppatori scrivono applicazioni in Java e non utilizzano le API proprie
di Java bensì usano le API Android, sempre definite da Google. Le classi
java sono prima compilate in bytecode, come di norma, e poi vengono tradotte in un eseguibile per la macchina virtuale. Dalvik inoltre è stata
ottimizzata per poter lavorare e funzionare su hardware con memorie limitate e poca capacità di elaborazione della CPU.
2.9 Generazione dei sistemi operativi e avvio
lunedì 29 luglio 2019 18:03
❖ SYSGEN
Un sistema operativo può essere ovviamente progettato e realizzato per lavorare su una singola macchina, ma chiaramente non è una pratica
che ha molto senso, infatti la pratica più diffusa è quella di progettare e creare bensì per più macchine di una stessa classe, con configurazioni
diverse, in vari siti.
Ciò implica quindi che il sistema si deve configurare o generare per qualsiasi situazione specifica, e tale processo prende il nome di
generazione del sistema operativo SYSGEN.
I sistemi operativi vengono distribuiti su dischi, CD-ROM o come file ISO. Per generare un sistema è necessario un programma specifico che
legge da un file, oppure che richiede ad un operatore, la specifica configurazione del sistema hardware sottostante; tale programma quindi
raccoglie informazioni specifiche riguardo la CPU (e nel caso dei sistemi multiprocessore va descritta ogni singola CPU), riguardo la
formattazione del disco, riguardo la memoria e i dispositivi disponibili.
Tali informazioni possono essere utilizzate sostanzialmente in tre modi:
• In un primo approccio, l'amministratore potrebbe utilizzare tali informazioni per modificare una copia del codice sorgente del
sistema operativo, che andrebbe poi ricompilato. Chiaramente è un caso limite, anche se tale soluzione creerebbe un sistema
operativo ad hoc per la macchina su cui viene utilizzato.
• Seguendo un approccio meno specifico, la descrizione del sistema potrebbe determinare la creazione di tabelle e la selezione di
moduli da una libreria precompilata, che si collegano poi per formare il sistema. La selezione permette alla libreria di contenere tutti
i driver di tutti i dispositivi di I/O previsti, e solo quelli che sono effettivamente necessari sarebbero poi inclusi nel sistema operativo.
Il sistema in tal modo non va ricompilato, la sua generazione è più rapida, ma comunque il sistema risultante potrebbe essere non
idoneo.
• All'opposto si può costruire un sistema operativo completamente controllato da tabelle, dove tutto il codice è parte del sistema
stesso e la selezione avviene al momento della sua esecuzione, e non nella fase di compilazione o collegamento. La generazione
implica così solo la creazione di tabelle idonee a descrivere il sistema.
La differenza tra tali metodi è solo dunque legata alla dimensione e alle generalità del sistema ottenuto, inoltre anche alla facilità di modifica
successiva di esso.

❖ Avvio del sistema


Abbiamo visto come si progetta un sistema operativo, abbiamo visto poi come si realizza, con i suoi linguaggi e con le sue strutture, infine
abbiamo visto come si genera in base alla macchina sottostante. La domanda che ora sorge spontanea è: l'hardware dell'elaboratore come
fa a sapere dove è locato il kernel del sistema operativo? Cerchiamo di capire quindi cosa succede all'avvio del nostro sistema.
La procedura di avviamento del sistema, cioè il caricamento del kernel, è una fase che si chiama booting. Nella maggior parte dei calcolatori,
è una piccola parte di codice, chiamata bootstrap program o bootstrap loader che si occupa dell'individuazione del kernel e dunque, come
qualsiasi altro programma da eseguire, si occupa del suo caricamento in memoria e quindi della sua esecuzione; in alcuni invece tale fase è
divisa in due, dove prima c'è un piccolo caricatore d'avvio che preleva dal disco il bootstrap program più complesso, che poi a sua volta carica
il kernel.
Quando la CPU "si accende", il registro delle istruzioni è caricato con una locazione di memoria predefinita, da cui poi parte l'esecuzione.
Anche il bootstrap program dunque inizia da questa locazione; è contenuto come abbiamo già visto in una ROM, perché non si conosce lo
stato della RAM all'avvio del sistema e perché come già abbiamo letto non deve essere inizializzata ed è immune ai virus.
Il bootstrap program effettua operazioni di vario genere, sottopone, ad esempio, la macchina a diagnosi, per ottenere informazioni sullo
stato; inizializza poi il sistema in ogni sua parte e presto o tardi farà partire il sistema operativo.
Per quanto riguarda i dispositivi mobili abbiamo tutto il sistema operativo interamente caricato nella ROM, e tale scelta non deve
sorprenderci perché i sistemi per tali dispositivi sono più piccoli. Nel momento in cui può esserci la necessità di modificare il codice del
programma di avvio, si scelgono EEPROM anziché ROM perché i circuiti sono modificabili e cancellabili elettronicamente.
Per i sistemi di grandi dimensioni o per i sistemi che cambiano di frequente, il bootstrap program è contenuto nel firmware (nella ROM) e
invece il sistema è su disco. Quest'ultimo utilizza una parte di codice per la lettura di un blocco singolo che occupa una locazione fissa del
disco (ad esempio il blocco zero), lo trasferisce quindi in memoria per eseguire poi il codice da quel blocco di avvio. Il programma che risiede
nel blocco d'avvio può essere un programma molto complesso ma nella maggior parte dei casi è semplice, conosce infatti unicamente la
lunghezza e l'indirizzo sul disco del codice residuo di cui è composto l'intero programma d'avvio.
Nei sistemi Linux un esempio di bootstrap program è GRUB; tutto il codice di avviamento è sul disco, e anche il sistema operativo stesso, e tali
possono essere sostituiti facilmente, scrivendone nuove versioni sempre sul disco. Un disco che contiene una partizione di avvio è chiamato
boot disk o disco di sistema. Caricato il bootstrap program completo, questo può percorrere l'intero file system per localizzare il kernel ed
avviarlo. Il sistema così diventa finalmente nello stato di running.
In altre parole, all'avvio del calcolatore, la CPU deve eseguire il programma di avvio che risiede nel firmware. Se l'intero sistema è sul
firmware (come nei dispositivi mobili) allora all'accensione l'intero sistema è direttamente eseguibile, altrimenti il ciclo di avvio procede
per fasi, in ognuna delle quali si caricano in memoria, dal firmware e dal disco, porzioni sempre più consistenti di sistema operativo, fino a
caricarlo ed eseguirlo tutto.