Che materia stai cercando?

riassunto sistemi operativi

Appunti di sistemi operativi basati su appunti personali del publisher presi alle lezioni del prof. Spalazzi dell’università degli Studi del Politecnico delle Marche - Univpm, facoltà di Ingegneria, Corso di laurea in ingegneria informatica e dell'automazione . Scarica il file in formato PDF!

Esame di Sistemi operativi docente Prof. G. Spalazzi

Anteprima

ESTRATTO DOCUMENTO

6. Algoritmo Lettori e Scrittori

Prevede che ci sia una struttura dati in cui possono andarci a leggere e a scrivere N processi. Se un

processo sta leggendo non devo precludere la lettura di questa struttura dati ad altri processi, cioè, devo

dare la possibilità a più processi di leggere (e non modificare) contemporaneamente le informazioni. Quando

un processo invece sta scrivendo è conveniente non permettere di leggere le informazioni in quanto è

possibile che siano non veritiere e a maggior ragione non devo poter permettere ad altri di modificare la

stessa struttura altrimenti potremmo incombere in errori. Quindi abbiamo individuato due tipologie di

processi:

Processo scrittore, che quando lavora deve essere l'unico a lavorare.

• Processo lettore, che può lavorare contemporaneamente ad altri.

Si utilizzano due semafori e una variabile per risolvere tale problema:

WRT, semaforo binario che gestisce il processo scrittore. Serve per garantire la mutua esclusione.

• Mutex, semaforo binario che serve per accedere ad una variabile in particolare. (in questo caso serve per

• accedere alla variabile Readcount)

Readcount è una variabile intera inizializzata a 0 che uso per contare i processi lettori che ci sono.

Il processo scrittore, quando deve iniziare a scrivere, fa una wait su WRT e una signal sempre su WRT

quando finisce. Questo mi garantisce la mutua esclusione per i processi scrittore, solo uno di questi processi

può lavorare.

wait (wrt)

il processo sta scrivendo

signal (wrt)

Il processo lettore, invece, deve mettere il semaforo wrt a 0, ovvero deve impedire ad altri processi di

scrivere mentre sta leggendo. Tale operazione si esegue con una wait su wrt ma deve essere fatta solo dal

primo processo lettore. Perciò gli altri processi lettori devono controllare se gia wrt è a zero. Quando non ci

sono più processi lettori invece wrt deve essere rimesso a 1, quindi l'ultimo processo lettore deve fare una

signal su wrt. Per capire se il processo è l'ultimo si usa la variabile readcount che dice il numero di processi

lettori che stanno leggendo. Essa viene decrementata quando un processo lettore ha finito e incrementato

quando entra. In questo modo essendo readcount una variabile condivisa dobbiamo permettere di accedere

a tale variabile in modo mutamente esclusivo. Quindi come possiamo vedere per prima cosa viene fatta una

wait su mutex, in modo tale che accediamo a readcount in maniera mutuamente esclusiva. Se è l'unico

processo deve fare una wait su wrt perché sono il primo processo. Una volta finita la prima parte, rimette a 1

il mutex. Eventuali processi lettori ora possono rientrare. Quando il processo esce rifà la wait su mutex per

accedere a readcount e decrementa quest'ultimo. Poi vede se è uguale a 0 so che tale processo è l'ultimo e

quindi deve fare una signal su wrt, cioè deve permettere la scrittura dell’area di memoria condivisa, e poi

infine esegue una signal su mutex. Questa soluzione garantisce tutte e tre le proprietà.

% codice del processo lettore

wait(mutex);

readcount++;

if(readcount == 1)

wait(wrt);

signal(mutex);

%il processo lettore sta leggendo

wait(mutex);

readcount - -;

if(readcount == 0)

signal(wrt);

signal(mutex)

7. Produttore consumatore scambio di messaggi (codice)

Le primitive di sincronizzazione sono di due tipi:

• scrittura di un messaggio, send(message) — messaggi a dimensioni fisse o variabili.

• ascolto di messaggi, receive(message).

(send(identificativo_del_canale, indirizzo_messaggio_da_inviare)

receive(identificativo_del_canale, indirizzo_su_cui_copiare_il_messaggio_ricevuto))

I processi comunicano tra loro senza ricorrere alle variabili condivise quindi in questo caso non ci sono

problemi di corsa critica.

Esistono diversi aspetti nella comunicazione riguardanti:

• struttura del canale di comunicazione

• capacità del canale

• quanti messaggi posso contenere

Possiamo utilizzare anche qui un buffer limitato per i messaggi, cioè abbiamo un numero massimo di

messaggi che possiamo ricevere e mettere in coda di attesa, come un problema produttore/consumatore.

#define BUFFER_SIZE 10 /* dimensioni buffer */

#define MSIZE 4 /* dimensioni messaggio*/

typedef int message[MSIZE];

Posso far si che il produttore non invii messaggi se non è sicuro che ci sia spazio nel buffer: per fare ciò il

consumatore deve dirgli se ha spazio o no. Il produttore quindi produce qualcosa, aspetta dal processo

consumatore uno spazio del buffer libero dove può inserire il messaggio e a quel punto lo riempie e lo invia.

Questo messaggio contiene ciò che il produttore ha prodotto. Quindi il produttore non fa altro che ripetere un

ciclo: produce un qualcosa —> osserva se può scrivere nel buffer —> ci scrive e poi invia il tutto al

consumatore.

void producer(void)

{

int item;

message m;

while(1)

{ produce_item(&item); %produce qualcosa

receive(consumer, &m); %osserva se può scrivere nel buffer

build_message(&m,item); %ci scrive

send(consumer, &m); %invia il tutto al consumatore }

}

Il consumatore invece deve gestire la sua coda che ha un limite, quindi come prima cosa invia un numero di

messaggi pari alla dimensione del buffer al produttore, questo ci fa capire che anche il produttore ha una

coda dei messaggi e ha la stessa dimensione della dimensione del buffer. Poi il consumatore si mette in

ascolto, infatti deve ricevere i dati, estrarli e rinviarli al produttore.

void consumer(void)

{

int item, i;

message m;

for (i = 0; i<BUFFER_SIZE; i++)

send(producer, &m); %invia un numero di messaggi pari alla dimensione del buffer al

%produttore

while(1)

{ receive(producer, &m); %il consumatore si mette in ascolto e riceve dati

extract_item(&m, &item); %estrae i dati

send(producer, &m); %rinvia i dati al produttore

consume_item(item); %consuma i dati ricevuti }

}

Questa soluzione soddisfa tutte e tre le proprietà, anche se dovremmo prima vedere come funzionano le

send e le receive.

La send può essere:

• Di tipo asincrona se non resta in attesa della risposta dell’altro processo e continua l’esecuzione.

• Di tipo sincrona se invece attende la risposta che il destinatario riceve riguardo al messaggio che la send

ha inviato.

• RPC

La receive invece può essere di tipo:

• bloccante qualora aspetti un messaggio nel buffer e non vada avanti nell’esecuzione.

• non bloccante qualora non aspetti ma semplicemente non faccia niente qualora non ci siano messaggi.

Esiste un terzo tipo di messaggio, l’RPC (Remote Procedure Call) in cui il mittente si sospende fino a

quando non riceve una risposta. Qui, a differenza della send sincrona l’RPC aspetta proprio una risposta al

messaggio e non una semplice ricevuta.

Abbiamo ipotizzato che i messaggi vengano scambiati nello stesso canale, ma in realtà è possibile avere

due tipi di canali che permettono due tipi di comunicazioni:

• Comunicazione simmetrica: in questo caso il mittente conosce il destinatario e viceversa, quindi il

destinatario sa da chi riceverà il messaggio. Un esempio è la pipeline (il concetto di pipeline (in inglese,

tubatura — composta da più elementi collegati — o condotto) viene utilizzato per indicare un insieme di

componenti software collegati tra loro in cascata in modo che il risultato prodotto da uno degli elementi sia

l'ingresso di quello immediatamente successivo). Nel nostro caso sono i processi che devono comunicare,

quindi finché siamo su una stessa macchina, conoscere il nome dei processi (process ID) è molto

semplice ma risulta complesso qualora ci spostiamo in diverse macchine.

• Comunicazione asimmetrica: solo il mittente conosce il destinatario, mentre quest’ultimo non conosce il

mittente e quindi sta in ascolto genericamente. Un esempio è il server web. In questo caso la

comunicazione su macchine differenti è un pò più semplice; in quel caso infatti, basta conoscere solo

l’indirizzo del destinatario. (Tanti utenti connessi ad un solo web server; gli utenti devono necessariamente

conoscere il web server il quale invece può non conoscere tutti gli utenti)

Nel web ad esempio la comunicazione con il processo destinatario avviene semplicemente specificando un

indirizzo della macchina a cui è associato questo processo. Questi messaggi verrano ricevuti da delle

mailbox (o anche dette porte, sono specificate nell’indirizzo) che hanno un unico identificatore ai quali i

processi possono comunicare se condividono questa mailbox.

In questo modo quindi basterà guardare i messaggi all’interno della mail box per osservare i messaggi che

devono essere mandati al mio processo. L’utilizzo delle mailbox hanno degli svantaggi:

• Più processi possono condividere la stessa mailbox e quindi bisogna decidere chi deve prendere i

messaggi. Esistono diverse soluzioni:

I. Permettere ad un canale di essere associato a solo due processi alla volta

II. Permettere a un solo processo alla volta di effettuare l’operazione di ricevere messaggi

III. Permettere al sistema di selezionare arbitrariamente il ricevente. Il sistema deve poi notificare al mittente

chi ha ricevuto il messaggio.

8. Algoritmo di Peterson

L'algoritmo di Peterson o algoritmo tie-breaker è un algoritmo sviluppato nella teoria del controllo della

concorrenza per coordinare due o più processi o thread che hanno una sezione critica in cui deve esservi

mutua esclusione. L'algoritmo assicura la corretta sincronizzazione, impedendo lo stallo (deadlock) ed

assicurando che soltanto un processo alla volta possa eseguire una sezione critica (serializzazione).

Funzionamento:

Se il processo #1 viene eseguito per primo, in1 viene impostato a true prima di impostare turno a 2. Dal

momento che in2 è stato inizializzato a false, la condizione dell'iterazione while non è soddisfatta e il

processo #1 accede alla sezione critica.

Se nel frattempo viene avviato il processo #2, questo imposterà in2 a true e turno a 1. in1 è già stato

impostato a true dal processo #1, perciò la condizione dell'iterazione while del processo #2 è soddisfatta,

così che questo deve aspettare. Solo dopo che il processo #1 ha abbandonato la sezione critica in1 diventa

false, e il processo #2 può accedere alla sua sezione critica.

Se il processo #1 viene nel frattempo riavviato, imposterà turno a 2, e dovrà aspettare che il processo #2

abbia abbandonato la sua sezione critica.

•La mutua esclusione è garantita dal fatto che il controllo di in e di turno viene fatto in modo atomico.

•L'assenza di deadlock viene garantita dal fatto che solamente una delle due condizioni while può

essere vera nello stesso momento.

•Il progresso dell'elaborazione è garantito dal fatto che se uno dei due processi tenta di entrare e

l'altro non è in sezione critica può tranquillamente entrare.

•L'attesa di un processo è limitata in quanto, se i due processi tentano di entrare in sezione critica,

entrano alternamente.

Appunti:

L’algoritmo di Peterson è una soluzione alla corsa critica per due processi che soddisfa tutti e tre i requisiti

necessari per un sistema corretto (cioè in cui non si verifichi il deadlock). Esso combina le due soluzioni che

avevamo visto in precedenza, cioè fa uso sia di un vettore di flag che viene utilizzato per far esprimere ai

processo la volontà di entrare in corsa critica, sia la variabile condivisa turn per stabilire i turni di chi entra.

CODICE:

do { [i] := true;

flag

turn = j; %cede il turno all’altro processo

while (flag [j] and turn = j) ; %non fa nulla finchè è il turno dell’altro processo e quel

processo vuole entrare in sezione critica

critical section

flag [i] = false;

reminder section

} while(1);

Quando un processo vuole entrare in CS mette il proprio flag a true per indicare che vuole entrare.

Quindi all’inizio il processo pone a true il proprio flag e cede il turno all’altro processo. A questo punto, finché

il flag dell’altro processo è a true ed è il suo turno, il processo aspetta e quando una di queste due diventa

non vera, esso può entrare in CS. Perché se il flag è a 0, significa che l’altro processo non ha chiesto di

entrare in CS o ne è uscito; se il turno è del primo processo, significa che l’altro processo ha ceduto il turno e

quindi può entrare il primo processo.

Questo algoritmo soddisfa tutte e tre le proprietà: la mutua esclusione è regolata grazie alla variabile turno

e al flag: o entra uno o entra l’altro. Il progresso è soddisfatto perché grazie al turno uno dei due processi

entra e, qualora entrambi richiedano di entrare e se uno solo richiede di entrare, entra ugualmente. L’attesa

limitata pure è soddisfatta perché se entrambi hanno flag a 1 (cioè vogliono entrambi entrare in sezione

critica) e supponiamo che il secondo processo entra in CS quando esso esce pone il suo flag a false e

anche se fosse che per qualche ragione (per lo scheduler) ritorna a porre il suo flag a true prima che il primo

processo rientri in CS tale processo cede il turno all’altro, quindi non riesce ad entrare per due volte di

seguito in CS.

9. Fornaio

L'algoritmo del fornaio è uno dei metodi di mutua esclusione che trovano applicazione pratica nella

programmazione parallela per serializzare l'esecuzione delle sezioni critiche da parte di più processi o thread

concorrenti.

L'algoritmo deve il nome al suo inventore Leslie Lamport, che propose l'analogia con il modello reale di una

frequentata panetteria, dove i clienti strappano un numero prima di mettersi in fila ad aspettare il proprio

turno. I clienti del fornaio rappresentano i task in attesa del proprio turno per eseguire la sezione critica.

Funzionamento:

Al di là di ogni affinità con le situazioni reali, è possibile che più thread ricevano lo stesso numero di turno.

Per ovviare a questa circostanza si introduce l'indice del thread come secondo argomento di confronto. Se

più thread ricevono lo stesso numero di turno, si conviene di assegnare la precedenza al thread con l'indice

più basso. Va notato che l'indice di ciascun thread deve essere unico: esso può venire assegnato al

momento della creazione o passato come parametro. Nell'esempio schematico riportato sopra, la costante N

rappresenta il numero massimo di thread concorrenti. Dopo aver ricevuto il suo indice unico, ogni thread

scrive solo nelle sue slot (sceglie[i] e numero[i]). La serializzazione è assicurata dalle due iterazioni while

consecutive. Questi cicli vengono ripetuti da ogni thread per tutti i thread, incluso quello in esecuzione e i

thread non attivi. Solo quando non ci sono più altri thread con priorità più alta è possibile l'accesso alla

sezione critica.

Considerazioni:

L'algoritmo del fornaio garantisce che un solo thread alla volta possa accedere alla sezione critica,

indipendentemente dall'ordine delle commutazioni di contesto e dagli altri dettagli dello scheduling. Sussiste

tuttavia il principale inconveniente che è proprio di tutti gli algoritmi di coordinazione decentrata, cioè non

coordinata dallo scheduler: i task in attesa continuano ad utilizzare il processore in un ciclo di attesa attiva

detto busy waiting, rallentando così gli altri thread nel sistema.

Appunti

Quando invece di avere due singoli processi ne abbiamo N, si utilizza questo tipo di algoritmo. Ad ogni

processo che vuole entrare in sezione critica viene assegnato un numero che rappresenta il turno

con cui entrare in sezione critica. E’ il processo stesso che determina il valore del proprio turno,

semplicemente guardando il numero dell’ultimo processo che voleva entrare in sezione critica. Questo però

non basta perché se arrivano due processi contemporaneamente, rischiamo di assegnare lo stesso numero

per più processi (in quanto lo richiedono nello stesso momento). Per risolvere questo problema, se due

processi hanno lo stesso numero, entra il processo più vecchio: per identificare qual’è il processo più

vecchio basterà guardare il process ID.

Si utilizzano due array: (n = numero di processi)

• int number[n] — qui dentro per ogni processo abbiamo il numero d’ordine del processo.

• Boolean choosing[n] — mi serve per controllare l’accesso in modifica a number, ovvero quando un

processo sta scegliendo un numero, gli altri non devono andare a modificare il number perché è errato

scegliere il numero se ancora uno se lo sta calcolando.

Per prima cosa quindi, il processo i-esimo mette il flag a choosing[i] = true, quindi non dobbiamo chiedere il

numero di questo processo se ancora non lo ha preso. Poi prende il numero come il massimo tra tutti i

numeri presenti nel vettore number e lo incrementa di 1 (ovviamente per far si che il suo numero diventi il più

alto e quindi che il suo turno sia l’ultimo) e pone il flag di nuovo a false in quanto ha preso il numero. Poi

esegue un ciclo for, in cui per ogni processo, si mette in attesa finché il processo j-esimo sta scegliendo il

numero. Poi controlla se ha la precedenza rispetto al processo j-esimo e posso entrare in sezione critica.

Questo controllo viene fatto tramite un altro while: il processo i-esimo resta in attesa qualora il numero del

processo j-esimo è diverso da zero (perchè in caso fosse uguale a zero significa che il processo i-esimo ha

la precedenza in quanto questo processo non ha richiesto di entrare in sezione critica) e il numero dell’altro

processo è inferiore al numero del processo i-esimo oppure è uguale al mio ma il numero del suo process ID

è inferiore al mio (è più anziano e lo faccio passare). Quando per tutti i processi una di queste condizioni

diventa false il processo i-esimo puo’ entrare in sezione critica.

do {

choosing[i] = true;

number[i] = max(number[0], number[1], …,

number[n - 1])+1;

choosing[i] = false;

for(j = 0; j < n; j++) {

while (choosing[j]) ; % il processo i-esimo resta in attesa finchè il processo j-esimo

sceglie il numero

while( (number[j] != 0) &&

((number[j],j) < (number[i],i))) ; % controlla se ha la precedenza rispetto

al processo j-esimo (se le condizioni

sono entrambe vere, Pi attende e non

fa nulla)

}

critical section

number[i] = 0;

reminder section

} while (1);

Questo algoritmo soddisfa tutte e tre le proprietà. La mutua esclusione perché per assurdo se non fosse

rispettata avremo almeno due processi che sono entrambi in sezione critica: ciò significa che devono

esistere due processi che hanno stesso numero e stesso process ID, impossibile. Il progresso è rispettato

in quanto se per assurdo non fosse vero vuol dire che tutti i processi sono in attesa e nessuno entra però

questo significa che ogni processo ha trovato un processo che ha un numero più piccolo del suo oppure uno

uguale con process ID più piccolo. Ma deve esistere per forza almeno un processo con il numero più piccolo

o comunque con un process ID più piccolo che entrerà in sezione critica. L’attesa limitata è rispettata in

quanto ogni processo che esce in sezione critica deve riassegnare il numero che è pari al massimo +1

quindi sicuro sta dopo di altri processi in coda. Le operazioni però devono essere fatte in modo atomico,

come abbiamo detto sopra, perché se no non riusciamo a soddisfare le proprietà di questi algoritmi.

10. Algoritmi di scheduling del disco

Struttura del disco

Un insieme di piatti con due superfici. Ogni superficie è divisa in una serie di tracce concentriche.

Se consideriamo che le varie superfici sono una sopra l’altra, ad una determinata traccia posta in una

superficie corrispondono alla stessa posizione le tracce che sono sulle altre superfici, cioè idealmente, se

tracciamo delle linee tra le tracce poste tra le diverse superfici ricaviamo un cilindro.

Il concetto di cilindro è importante perché in realtà il braccio che si utilizza per andare a effettuare delle

operazioni su disco non ha un unica testina ma ne ha tante quante sono le superfici, quindi siccome le

posizioni delle testine sono le stesse possiamo eseguire delle operazioni di lettura e scrittura su tutti i blocchi

in quella posizione di differenti dischi.

Ogni traccia a sua volta è suddivisa in una serie di settori, la più piccola unità di contenitore di dati.

Una volta che la testina è posizionata su una certa traccia per andare a leggere o scrivere su un certo

settore basta che la testina attenda che arrivi nella posizione di quel settore per effetto della rotazione del

disco. A quel punto può leggere o scrivere. La posizione di un blocco è determinata da 3 coordinate:

• numero di superficie

• numero di cilindro

• numero di settore.

Come mappiamo il nostro disco?

Ogni blocco fisico viene assegnato ad un settore quindi, alle 3 coordinate che identificano quel settore. In

particolare prima riempiamo in maniera sequenziali i settori poi cambiamo i cilindri e infine le superfici.

Scheduling del disco

Mi interessa minimizzare il tempo di accesso o massimizzare l'ampiezza della banda. Essi dipendono da una

serie di fattori che sono caratteristici del dispositivo, ad esempio la velocità di rotazione, la quale dipende da

com'è stato costruito.

Il tempo di accesso ad una informazione dipende da due tempi di operazioni differenti:

• Tempo di ricerca (seek time), che rappresenta il tempo necessario alle testine per posizionarsi nel cilindro

contenente il settore desiderato. Qui possiamo agire.

• Latenza di rotazione: il tempo necessario affinché il disco ruoti fino a quando il settore desiderato si trova

sotto la testina. Su questo non possiamo agire proprio per le caratteristiche meccaniche e anche perché il

disco ruota sempre in un senso anche quando le testine si muovono.

Possiamo pensare di minimizzare il tempo di ricerca cercando di minimizzare la distanza percorsa

dal braccio tra i cilindri.

L'ampiezza di banda è il numero di byte trasferiti diviso il tempo intercorso fra la prima richiesta e

il completamento dell'ultimo trasferimento.

Per tutti gli algoritmi che andiamo ad analizzare prendiamo una sola superficie.

FCFS (first come first served)

Questo tipo di algoritmo è il più semplice in funzione dell'ordine di arrivo delle richieste di accesso ai blocchi

il braccio si sposta tra i vari cilindri servendoli in questo ordine.

Nell’esempio in figura la testina si muove in totale per 640 cilindri

GUARDA LA FIGURA SULLE SLIDES

Questo algoritmo è il peggiore in quanto:

• il braccio fa continui spostamenti di direzione avanti/indietro, questo provoca un usura maggiore.

• Il numero di cilindri attraversati non è minimizzato.

Shortest seek time first(SSTF)

Seleziona la richiesta con il minimo tempo di ricerca rispetto alla posizione corrente della testina, cioè, tra

tutte le richieste in coda cerchiamo quella nel cilindro più vicino a quello dove è posizionata la testina.

Nell’esempio in figura la testina si muove in totale per 236 cilindri

GUARDA LA FIGURA SULLE SLIDES

Non è detto che sia la scelta migliore prendere quello più vicino perché ogni volta devo ricalcolare la

distanza minima quindi non è assoluta la distanza minima ma ogni volta cambia in funzione da dove mi

trovo. Anche qui dal punto di vista dell'usura non abbiamo miglioramenti.

Possiamo avere dei problemi di attesa indefinita in quanto se arrivano sempre richieste vicine alla posizione

in cui sono, vengono servite queste e non quelle distanti che magari erano in coda da più tempo.

Scheduling per scansione (SCAN)

Mentre si muove in una direzione potrei esaudire tutte le richieste che magari arrivano mentre mi sposto e

che si trovano nella mia strada.

Il braccio del disco quindi parte da un estremo del disco e si muove verso l'altro estremo per poi tornare

indietro, servendo le richieste mano a mano che le incontra (algoritmo dell'ascensore).

Questo è uno degli algoritmi che sfruttano il principio dell’ascensore. Per esempio in figura la testina si

muove in totale per 208 cilindri

GUARDA LA FIGURA SULLE SLIDES

Mediamente questo dà risultati migliori. Dal punto di vista dell’usura l'abbiamo minimizzata. Questo algoritmo

non presenta starvation. Look

Si comporta in modo simile allo scan. Se non ci sono più richieste dopo a quello che ho servito nel verso in

cui sto andando non arrivo fino in fondo ma inverto in quel momento il senso di marcia. Mediamente è un

miglioramento dello scan. Circolar scan o C-Scan

Questo algoritmo è una versione simile allo scan solo che le richieste vengono servite solo in un verso, cioè

in un verso serve le richieste fino a che non arriva in fondo, poi ritorna all'inizio senza servire richieste poi

ritorna a servirle.

GUARDA LA FIGURA SULLE SLIDES Circolar look o C-Look

Uguale all'algoritmo look ma anche qui serviamo le richieste solo in un verso nell'altro si sposta la

testina e basta.

GUARDA LA FIGURA SULLE SLIDES

11. Driver

Si è cercato di standardizzare i vari tipi di controller dei dispositivi anche se comunque ogni dispositivo poi

ha dei comandi che cambiano in funzione del tipo, quindi il problema rimane. La soluzione a questo

problema è quella di utilizzare i driver: è un modulo software che ha sempre la stessa interfaccia, e quindi il

kernel ci lavora sempre allo stesso modo ma, attraverso il quale, invia al controller del dispositivo segnali in

maniera appropriata. Il kernel invia questa richiesta al driver, tale richiesta viene servita da un modulo del

driver che traduce la richiesta che vuole fare il kernel al dispositivo nell’operazione opportuna per il

controller.

Passi eseguiti dai driver dei dispositivi:

• Controllare i parametri passati all’invocazione del driver: serve per capire come eseguire l’operazione

richiesta

• Accodare le richieste in una coda di operazioni (soggette a scheduling!).

• Eseguire le operazioni, accedendo al controller.

• Passare il processo in modo wait (I/O interrupt-driven), o attendere la fine dell’operazione in busy-wait..

• Controllare lo stato dell’operazione nel controller. Deve sapere quando ha finito

• Restituire il risultato

Questo significa che il driver deve interagire con il controller. Per non tenere in attesa il processo e quindi

poter assegnare la cpu ad un altro processo è il dispositivo che invia interruzioni hardware per segnalare che

ha finito. Tale interruzione provoca l’esecuzione di una routine per gestire questa interruzione, questa routine

sarà parte del driver. Quindi il driver sarà composto da due differenti funzioni:

• Quelle invocate mediante interruzioni software dagli strati superiori del kernel.

• Quelle invocate mediante interruzioni hardware inviate dal dispositivo. Tra queste interruzioni possono

esserci quelle dedicate all’inizializzazione del dispositivo (quando lo colleghiamo).

12. Principio di località e paginazione degenere

Il principio di località enuncia che, se la CPU sta eseguendo una data istruzione presente in memoria, vuol

dire che con molta probabilità le prossime istruzioni da eseguire saranno ubicate nelle vicinanze di quella in

corso; vale a dire che, durante la normale esecuzione dei programmi, la CPU passa molto tempo accedendo

a zone di memoria ristrette e solo occasionalmente accede a locazioni molto lontane. Inoltre, in genere gran

parte del codice di cui sono costituiti i programmi viene eseguito solo raramente, al verificarsi di errori o

condizioni anomale: quindi succede spesso che di tutto il codice che un programma carica in memoria ne

venga realmente eseguita solo una piccola parte.

È indispensabile che la quantità di memoria fisica presente sia almeno sufficiente a mantenere la località del

sistema, ovvero quella parte di dati ed informazioni che sono utilizzate nell'immediato da ogni processo. Se

così non fosse, infatti, il sistema dovrebbe continuamente provvedere ad eseguire delle operazioni di

swapping per far sì che ogni processo abbia i dati di cui necessita.

Ad esempio supponiamo che in un dato momento la memoria fisica sia satura e contenga esattamente la

località del sistema (vale a dire, la somma di tutti i "working set" dei processi in elaborazione è esattamente

uguale alla RAM fisica presente), e che in questa situazione venga avviato un nuovo programma. Il processo

che viene creato ha bisogno di allocare della memoria. Dato però che la memoria principale è piena il

sistema operativo provvede a liberare una parte dello spazio memorizzando facente parte delle informazioni

nella memoria secondaria. Successivamente, quando il controllo torna al processo i cui dati sono stati

appena spostati, viene richiesta un'operazione di swap-in per ricaricare in memoria principale gli stessi. Dato

che tutte le informazioni contenute nella memoria principale sono indispensabili questo fenomeno avviene

molto spesso. Essendo la memoria secondaria molto più lenta (centinaia o migliaia di volte) rispetto alla

memoria principale, questo causa un considerevole rallentamento del sistema, che è impegnato quasi

esclusivamente in operazioni di I/O, e diventa presto inutilizzabile e poco o per nulla reattivo ai comandi

dell'utente. Tale fenomeno è chiamato trashing.

Tecnicamente, quando la memoria fisica libera (e quindi il numero di frame liberi) è insufficiente a contenere

il working set corrente di un processo, quest'ultimo comincerà presto a generare parecchi page fault,

rallentando considerevolmente la propria velocità d'esecuzione. Quando parecchi processi cominciano ad

andare in trashing, ovvero a spendere più tempo per la paginazione che per l'esecuzione, il sistema

operativo potrebbe erroneamente essere indotto a dedurre che sia necessario aumentare il grado di

multiprogrammazione (dato che la CPU rimane per la maggior parte del tempo inattiva a causa dell'intensa

attività di I/O). In questo modo vengono avviati nuovi processi che però, a causa della mancanza di frame

liberi, cominceranno a loro volta ad andare in trashing: in breve le prestazioni del sistema collassano fino ad

indurre l'operatore a dover terminare forzatamente alcuni processi. Un modo per limitare questo fenomeno

consiste nell’ utilizzare una procedura di rimpiazzamento locale, ovvero dare la possibilità al gestore della

memoria virtuale di sostituire le pagine associate al solo processo che ne fa richiesta. In questo modo si

impedisce che l'intero sistema vada in trashing.

13. Algoritmi di scheduling cpu multiprocessore

Quando abbiamo più processori abbiamo dei grossi vantaggi in quanto più processi possono essere in

esecuzione contemporaneamente. Facciamo una distinzione, possiamo avere 2 tipi di SO:

• Asimmetrici. Uno dei processori viene utilizzato solo dal SO mentre gli altri processi applicativi girano su

gli altri processori. Ha il vantaggio che il SO non deve attendere la terminazione dei processi, vengono

sempre eseguite subito le sue operazioni mentre lo svantaggio è che abbiamo un unico processore per

eseguire anche tutte le operazioni privilegiate che vengono richiamate da system call, quindi si ha un

rallentamento (il problema è tanto maggiore quanto è maggiore il numero di processori). Abbiamo anche

un altro vantaggio le strutture dati che deve gestire il SO le gestisce tramite un unico processore, non ci

sono problemi di corsa critica in quanto c'è un unica istanza di SO che accede all'area di memoria

riservata al SO.

• Simmetrici (SMP). Il SO può girare su qualsiasi CPU, non appena un processo deve richiamarlo, il SO

verrà richiamato sul processore di quel processo. Lo svantaggio è che l'area di memoria riservata al SO

diventa un area condivisa quindi abbiamo problemi di corsa critica.

Osserviamo quali sono i criteri per valutare la bontà degli algoritmi di scheduling multiprocessori:

Gli stessi obiettivi dello scheduling monoprocessore. (Utilizzo della CPU, ovvero tenere la CPU

• occupata il più possibile —> da massimizzare. Produttività (Throughput) – numero di processi

che completano la loro esecuzione per unità di tempo —> da massimizzare. Tempo di Completamento

(Turnaround Time), ovvero il tempo di esecuzione di uno specifico processo —> da minimizzare. Tempo di

attesa, ovvero quanto tempo in totale un processo è rimasto nella coda di ready —> da minimizzare.

Tempo di risposta – quanto tempo passa dal momento in cui la richiesta è stata sottomessa al momento in

cui viene prodotta la prima risposta (non il suo output) – per i sistemi time-sharing da minimizzare)

Parallelismo dei job: massimizzare il parallelismo per sfruttare la concorrenza dell'applicazione —> da

• massimizzare. In alcuni casi per esempio non è utile minimizzare il tempo di esecuzione di un processo nel

gruppo se non si minimizza anche quello di tutti gli altri (se il lavoro complessivo risulta terminato solo dopo

la terminazione di tutti i processi nel gruppo).

Affinità con il processore: relazione di un processo con un particolare processore, la sua memoria locale

• e la sua cache ---> da massimizzare. In alcuni casi nella cache potrebbero esserci ancora suoi dati (e nel

TLB potrebbero esserci ancora elementi della sua tabella delle pagine) perché magari era stato assegnato

ad un processore e poi è stato interrotto. Quindi conviene riassegnare lo stesso processore in quanto già

abbiamo dati caricati e quindi non perdiamo tempo a ricaricarli.

Sostanzialmente possiamo trovare due tipologie di algoritmi:

Coda multipla: cioè una coda per ogni processore. In questo caso ogni processore ha la sua coda e

• quindi quel processo rimane a lavorare sempre nello stesso processore associato alla stessa coda in cui il

processo è stato messo. Vantaggi: affinità con il processore buona. Svantaggi: potrei avere uno

sbilanciamento del carico del lavoro quindi dovremmo utilizzare un algoritmo opportuno per ribilanciarlo

(linux è a coda multipla).

Coda unica: tutti i processi sono in una stessa coda, appena si libera un processore prendo un processo

• dalla coda e lo assegno al processore qualsiasi esso sia. Questa categoria si suddivide ancora in altre due

tipologie:

Job-blind (job è un insieme di thread), questo algoritmo non è consapevole che l'insieme di thread

• appartengono tutti quanti ad uno stesso thread

Job-aware (job è un insieme di thread), questo algoritmo è consapevole che l'insieme di thread

• appartengono tutti quanti ad uno stesso thread. Quindi questi algoritmi sono capaci di ottimizzare il

parallelismo e/o l'affinità.

Se abbiamo un algoritmo a coda multipla ricadiamo nel caso di scheduling per monoprocessori.

Job-blind scheduling:

Io ho una coda unica, quando si libera un processore lo scheduler gestisce la coda: seleziona un processo e

lo assegna alla cpu. Non tengono conto di parallelismo dei job ne dell'affinità. Gli algoritmi di scheduling che

possiamo utilizzare sono anche quelli per monoprocessore (FCFS, RR, …). Unico algoritmo aggiunto è lo

smart scheduling: è di fatto il round robin solo che viene migliorato, si introduce un flag che permetta di

riconoscere un processo in sezione critica. Quando un processo è in sezione critica e il quanto di tempo è

scaduto, lasciare la CPU al processo fino al punto in cui rilascia la sua sezione critica.

Ora vediamo gli algoritmi di tipo job-aware.

Shortest-number-of-thread-first(SNTF):

Usa una coda globale di job a priorità, diamo la priorità a quei processi che hanno il minor numero di thread.

Può essere sia senza prelazione che con prelazione. La priorità di un job è inversamente proporzionale al

numero di thread appartenenti a quel job. Quando un processore diventa disponibile, lo scheduler sceglie un

thread dal job in testa alla coda e gli permette di andare in esecuzione fino al termine del suo CPU burst -

Utilizzo CPU, viene massimizzata.

Tempo di Attesa viene ridotto.

Parallelismo dei Job, buono.

Affinità con il Processore, non prende in considerazione questa proprietà.

Round-Robin- Job (RRjob):

Ogni volta che viene selezionato un job, gli vengono assegnate tante CPU quanti sono i thread che lo

compongono. Il job occupa i processori fino all'esaurimento del quanto di tempo assegnato al job oppure tutti

i thread hanno terminato il loro CPU burst. Ogni volta le CPU assegnate al job possono essere diverse.

Vediamo le caratteristi di questo algoritmo:

• Utilizzo CPU, non è buona possono esistere delle cpu che non vengono utilizzate.

Tempo di Attesa, proprio per questi motivi i tempi di attesa possono essere non brevi

• Parallelismo dei Job, il parallelismo viene massimizzato.

• Affinità con il Processore, l'affinità è molto negativa.

Coscheduling o gang Scheduling:

Tutti i thread di un job sono posti in elementi adiacenti della ready queue, ogni job adiacente all'altro. Si

cerca di aumentare l'utilizzo della cpu. - Lo scheduler mantiene una finestra di dimensioni pari al numero di

processori. - I thread interni alla finestra sono eseguiti in parallelo per al massimo un quanto. - Al termine del

quanto la finestra si sposta al gruppo successivo. - Se un thread di una finestra passa a waiting, la finestra

viene estesa di un thread a destra.

Vediamo le caratteristiche:

- Utilizzo CPU ,viene massimizzata

- Tempo di Attesa, non buoni

- Parallelismo dei Job, buoni

- Affinità con il Processore, non buono

Space sharing

Si assegnano ad ogni job tante CPU (in uso esclusivo) quanti sono i thread che lo compongono (si può

prevedere una versione "collaborativa" di questo algoritmo, in cui il processo può modificare il numero di

threads sulla base della disponibilità di CPU e di richieste pendenti-> Dynamic Partitioning). Dividiamo quindi

i processori ai vari job.

- Non fa time sharing, non c'è il quanto di tempo. (è un RRjob ma senza il quanto di tempo)

- Utilizzo CPU, è molto basso, alcune cpu non utilizzate.

- Tempo di Attesa, sono pessimi devo aspettare che finiscano i job.

- Parallelismo dei Job, massimizzata

- Affinità con il Processore, massimizzata

Partizionamento Dinamico

Lo scheduler distribuisce equamente i job sui processori del sistema in modo proporzionale al numero di

thread. Il numero di processori allocati ad un job è sempre minore o uguale al numero di thread eseguibili del

job. Quando entra un nuovo job, il sistema aggiorna dinamicamente l'allocazione dei processori.

Caratteristiche:

Ottimizzazione CPU è massimizzata

• Ottimizzazione tempo di attesa, è buono in quanto riassegniamo le cpu.

• Parallelismo dei job è buono.

• Affinità con il processore è massimizzato.

14. Scheduling dispositivi I/O linux

Linux classifica i dispositivi in 3 categorie:

Dispositivi a blocchi, permettono l’accesso diretto a blocchi di dati di dimensione fissa.

• Dispositivi a caratteri. Comprendono gran parte dei dispositivi, non sono in gradi di fornire funzionaoità dei

• file ordinari. C’è un’astrazione dal TTY, è un dispositivo unico sia per lettura che per scrittura.

Dispositivi di rete, sono interfacciati con il sottosistema di gestione delle reti del kernel, di fatto sono come

• quelli a caratteri solo che hanno delle proprie peculiarità.

Dispositivi a Blocchi

Linux accede ai dischi attraverso due cache:

• I dati sono memorizzati nella page cache, che è unita alla memoria virtuale del sistema;

• I metadati sono memorizzati nella buffer cache, una cache separata indicizzata dal blocco fisico del disco

Il block buffer cache serve principalmente a due cose:

• come pool di buffer per I/O attivo, e

• come cache per l'I/O completato.

Il request manager gestisce la scrittura e la lettura del buffer con dati provenienti da/diretti verso un driver di

un dispositivo a blocchi. Dispositivi a Caratteri

Un driver di un dispositivo a caratteri deve avere un insieme di funzioni che realizzano le varie operazioni sui

file. Il kernel non esegue quasi nessuna operazione alla richiesta di leggere o scrivere su un dispositivo a

caratteri, semplicemente passa la richiesta al dispositivo. La sola eccezione è rappresentata dai terminali,

nei confronti dei quali il kernel mantiene una interfaccia standard.

Dispositivi di Rete

I dispositivi di rete in Linux permettono di gestire:

Il protocollo standard di Internet (TCP/IP) per la comunicazione tra macchine Unix,

• Protocolli per macchine con SO non Unix, come Appletalk e IPX.

• E’ realizzato con tre strati di software:

L'interfaccia delle socket (permette la gestione della rete come se fosse un file)

• I driver dei protocolli

• I driver dei dispositivi di rete

Vediamo ora gli algoritmi di scheduling del disco adottati da Linux

Linus elevator scheduler (simile a SCAN)

Inizialmente si usava questo fino alla versione 2.4 del kernel. Questo algoritmo aveva una sola coda in cui

vengono inserite tutte le richieste e sono ordinate per numero di blocco (non lavorava sul numero di cilindro

ma direttamente sul blocco), questo permetteva di lavorare con memorie di massa differenti. Si muove in

una sola direzione, man mano che scorre il disco serve le richieste, poi arrivato in fondo ritorna all'inizio

come un algoritmo circolare. Poi linux ha iniziato a prendere in considerazioni algoritmi per il sistema real-

time, per il soddisfacimento dei tempi real-time.

Deadline scheduler

Dal kernel 2.6 linus elevator fu sostituito da questo algoritmo. Deadline non ha una sola coda ma 3 code:

Incoming request —> Qui vengono messe tutte le richieste indipendentemente dalla loro tipologia ordinate

• per il numero di blocco.

Read request --> Qui vengono inserite solo le richieste di lettura.

• Write request ---> Qui vengono inserite solo le richieste di scrittura.

• Le ultime due vengono gestite in modalità FIFO. Di default vengono servite le incoming request con una

differenza, quando tali richieste vengono servite vengono tolte non solo da questa coda ma anche dalle

altre. Ogni richiesta ha una scadenza (expiration time), allo scadere di questo parametro questa richiesta

ha la priorità sugli altri e quindi il controllo passa alla coda di competenza cioè se la richiesta è di read o di

write va a alla coda opportuna. Questo permette di evitare che alcune richieste abbiamo tempi di attesa

troppo lunghi. Una volta esaudita questa richiesta bisogna vedere se sono scadute altre richieste allora

bisogna servire queste altrimenti ritorna in modalità circolare. Tecniche di questo tipo sono appunto per

sistemi real-time in modo da dare un'importanza a quelle richieste.

Anticipatory I/O scheduler

Questo algoritmo fu affiancato al deadline. Magari la testina si è spostata in un blocco e arrivano alcune

richieste vicine che però l'algoritmo circolare servirebbe alla fine, con questo algoritmo invece viene servita

subito). Questa scelta fu fatta perché si osservò che la probabilità che una nuova richiesta venisse fatta sui

blocchi vicini era alta. Quindi in questo algoritmo cerca di servire le richieste nei settori vicini.

Completely Fair Queueing — CFQ:

L’anticipatory fu sostituito dal CFQ. In questo algoritmo si divide la coda delle richieste in due

Coda richieste sincrone. Devono essere servite in quell'ordine per la loro natura non ha senso servirle in

• ordine diverso.

Coda richieste asincrone, queste vengono gestite in funzione della priorità.

• Le due code vengono gestite con un algoritmo FIFO. Quelle asincrone in realtà hanno anche una loro

priorità. Come algoritmo di gestione di scelta delle diverse code si usa il RR, quindi lo scheduler assegna

un certo quanto di tempo ad ogni coda. Il quanto di tempo non è statico ma viene calcolato sulla base di

certi parametri. Questo algoritmo è importante perché è stato introdotto il concetto di equità anche nello

scheduling del disco. Per alcuni dispositivi il FCFS è il migliore infatti esiste in linux un algoritmo che

utilizza il principio del FCFS, tale algoritmo si chiama Noop. Si può in linux decidere che algoritmo

scheduling utilizzare per un determinato dispositivo.

15. Meccanismi di coerenza della cache, i vari protocolli e diagramma di stato del

protocollo MSI. Codice del problema Produttore-Consumatore con i semafori

Meccanismi di coerenza della cache:

Il problema di coerenza della cache consiste nel mantenere aggiornati e consistenti i dati memorizzati nelle

varie cache dei nodi: ovvero significa avere dei dati che, anche se modificati o letti da altri nodi, devono

sempre rispecchiare l’andamento delle operazioni subite. (vedi esempio su slides)

I meccanismi di coerenza utilizzati si dividono in due categorie:

Meccanismi basati sull’invalidazione

• Meccanismi basati sull’aggiornamento

I meccanismi basati sull’invalidazione fanno uso dei protocolli MSI e MESI, mentre i protocolli basati

sull’aggiornamento fanno uso del protocollo DRAGON.

Per la spiegazione di questi diagrammi si rimanda alla domanda 17.

Codice del problema Produttore-consumatore con i semafori: DOMANDA 28

16. Algoritmi per lo scheduling del disco in Linux

VEDI DOMANDA 14 (SCHEDULING DISPOSITIVI I/O LINUX)

17. Protocollo MESI

Il protocollo MESI è un protocollo molto usato per la gestione della coerenza delle cache in sistemi a

multiprocessore.

Anzitutto spieghiamo il protocollo MSI:

Il protocollo MSI è un protocollo che fa si che per ogni nodo/core/processore un determinato blocco possa

essere marcato come M, S oppure I.

M sta per Modified e indica che quel blocco di memoria è stato modificato, per cui il processo che l’ha

modificato ce l’ha aggiornato mentre gli altri processi no. Se il processore richiede una PrWr a partire da

qualsiasi stato, il blocco di memoria finirà per essere sempre M.

I sta per Invalid e indica che la copia di un determinato processo non è valida perchè qualcun altro ha

modificato il suo dato.

S sta per Shared, cioè i dati che ha un processo sono uguali a quelli che hanno tutti gli altri (se gli altri

processi hanno quel dato, allora è uguale a quello del processo in questione).

Un digramma di questo tipo si chiama “Diagramma di stato”.

Per gestire i singoli blocchi di memoria dobbiamo rappresentare in che stato si trovano i blocchi di memoria.

Ogni blocco di memoria presente nella memoria di cache di un certo nodo si può trovare negli stati

precedentemente descritti.

M, S e I sono gli stati.

Le frecce indicano le transizioni di stato: sono etichettate in qualche modo. Sono state etichettate con una

prima stringa di caratteri, una barra e un’altra stringa.

La prima stringa è un evento: ovvero il controller osserva che il suo processore ha fatto una certa

operazione, oppure che nel bus sta passando un certo comando.

L’action invece è un’azione fatta proprio dal controller.

Come si legge il grafico?

esempio da I a S:

Il controller osserva che il suo processore ha fatto richiesta di leggere quel dato (PrRd). Il controller invia

quindi una richiesta di lettura di quel dato (BusRd). Quel dato dunque va a finire nello stato S in quanto le

copie di quel dato diventano almeno 2.

Da I a M:

Il controller osserva che il suo processore ha chiesto un blocco di memoria per farci una modifica (PrWr).

Siccome il processore non ha una copia aggiornata di quel dato richiedere un’azione di scrittura.

Allora il controller effettua una operazione di read esclusiva (BusRdX).

In questo caso il processore sa che è l’unico ad avere una copia di quel dato.

Un blocco di memoria rimane Invalido fino a quando non mi serve. Una volta che eseguo un’operazione e il

blocco è nello stato di Invalido, il suo stato finale non potrà mai ricadere nello stato di Invalido.

Qualora il processore richieda un dato che non è presente nella sua cache, lo carica in memoria e il suo

stato iniziale è S: il comando sarà un BusRd. Se si vuole modificare quel dato, esso viene letto con un

diverso comando, ovvero BusRdX e il blocco viene posto in stato di Modified.

In base allo stato in cui un blocco di memoria si trova e in base agli eventi si potranno eseguire determinate

azioni. ===> vedi slides

Il protocollo MESI presenta invece un ulteriore stato:

- I (invalid): come per MSI;

- S (shared o valid): due o più processori hanno questo blocco nella loro cache in uno stato non modificato

- E (exclusive): solo una cache ha una copia del blocco ed essa non è stata modificata. La memoria

centrale è aggiornata, quindi se il controllore della cache riceve una richiesta di scrittura dal proprio

processore (PrWr) non deve invalidare nessuno (non deve eseguire BusRdX) risparmiando tempo.

- M (modified o dirty): come per MSI.

In questo caso lo stato S dell’MSI è stato suddiviso in due stati, S e E, perchè prima quando lo stato era solo

S e il processore chiedeva di modificare il valore del blocco S, si doveva inviare un segnale sul bus per

invalidare i blocchi degli altri. Però tale azione ha senso solo se qualcun altro ha quel blocco. Perciò se gli

altri non hanno quel blocco, non si deve inviare nulla (e viene appunto individuato dallo stato E) così da

ridurre il traffico nel Bus.

Lo stato in cui si può trovare un blocco di memoria shared nell’MSI non teneva conto se effettivamente quel

blocco di memoria era effettivamente presente in altre memorie di cache, infatti Shared significa che il bocco

di memoria è stato caricato sulla memoria cache di un nodo senza modificarlo o senza alcuna richiesta di

modifica.

Questa variante suddivide lo shared in 2 stati: uno si chiama ancora shared, solo che significa che il blocco

di memoria è sicuramente posseduto da qualche altro nodo.

L’altro è l’exclusive, ovvero quel blocco di memoria è posseduto solo da quel nodo.

Possiamo quindi avere delle ottimizzazioni nella gestione dei blocchi di memoria.

Supponiamo che un nodo carichi nella sua cache un blocco: siamo nello stato di exclusive.

Se il controller della cache osserva una richiesta di lettura da parte del proprio processore, cosa deve fare?

Nulla, in quanto gia possiede quel blocco! Rimane nello stato exclusive.

Ipotizziamo che il controller osservi un’operazione di richiesta di scrittura, allora il blocco diventa modified,

ma non fa comunque nulla perchè non deve avvertire gli altri nodi che quel blocco deve essere invalidato in

quanto è l’unico ad avere quel blocco.

Supponiamo che osservi una richiesta da parte di qualche altro nodo di andare a leggere quel blocco: a quel

punto non è piu l’unico ad avere quel blocco, dato che c’è un altro nodo ad averlo: il blocco diventa shared.

Supponiamo che il controller osservi una richiesta di modifica da parte di un altro nodo: non solo lui non è

piu il proprietario esclusivo di quel blocco, ma a quel punto la sua copia non è piu valida perchè un altro

nodo l’ha letto e l’ha modificato.

Traduzione pratica per leggere i diagrammi:


PAGINE

37

PESO

1.73 MB

PUBBLICATO

4 mesi fa


DETTAGLI
Corso di laurea: Corso di laurea in ingegneria informatica e dell'automazione
SSD:
A.A.: 2018-2019

I contenuti di questa pagina costituiscono rielaborazioni personali del Publisher luckylucianooo di informazioni apprese con la frequenza delle lezioni di Sistemi operativi e studio autonomo di eventuali libri di riferimento in preparazione dell'esame finale o della tesi. Non devono intendersi come materiale ufficiale dell'università Politecnico delle Marche - Univpm o del prof Spalazzi Gianluca.

Acquista con carta o conto PayPal

Scarica il file tutte le volte che vuoi

Paga con un conto PayPal per usufruire della garanzia Soddisfatto o rimborsato

Recensioni
Ti è piaciuto questo appunto? Valutalo!

Altri appunti di Sistemi operativi

Appunti Completi Sistemi Operativi
Appunto
Informatica - Introduzione
Appunto
Algebra lineare e geometria - spazi vettoriali
Appunto
Riassunto esame Fondamenti di Informatica: Manuale di C/C++, prof. Dragoni
Appunto