Che materia stai cercando?

Sistemi operativi - il costrutto monitor Appunti scolastici Premium

Appunti di Sistemi operativi per l'esame del professor De Carlini. Gli argomenti trattati sono i seguenti: il costrutto monitor, la gestione della memoria, la memoria virtuale, la paginazione e le partizioni variabili multiple.

Esame di Sistemi operativi docente Prof. U. De Carlini

Anteprima

ESTRATTO DOCUMENTO

end ;

end.

Il generico processo effettua una richiesta di risorse; se tale richiesta è superiore alla

necessità residua si ha un errore, mentre se è inferiore si passa a confrontare la richiesta con l’effetti-

va disponibilità. Se le risorse disponibili non sono sufficienti a soddisfare la richiesta, il processo

dovrà mettersi in attesa. Contrariamente, si vaglia la possibilità di concedere le risorse: allo scopo si

esamina la condizione di stato sicuro (relativamente a ciò che succederebbe se le risorse fossero ef-

fettivamente assegnate), richiamando la procedura che segue. Se la condizione di stato sicuro risulta

contraddetta il processo dovrà ancora mettersi in attesa, altrimenti si concede l’allocazione. Si noti

che in caso di attesa occorre risistemare la struttura dati, che è stata modificata dall’esecuzione della

procedure verifica_stato _sicuro.

Nel testo seguente compaiono relazioni di ‘minoranza fra due vettori’. Affermando

che un vettore è minore o uguale a un altro intendiamo dire che la relazione di minoranza è verifica-

ta componente per componente (ad es.: (2,0,1) < = (5,0,2)).

procedura verifica_stato_sicuro (S = boolean) ;

i

var j : integer ;

flag : boolean ;

begin

S := true ;

i /* viene riprodotta la situazione che si verrebbe a creare

in caso di effettiva assegnazione */

available : = available - request ;

i

allocation : = allocation + request ;

i i i

need : = need - request ;

i i i

/* lo stato di arrivo sarebbe sicuro? Le istruzioni seguenti lo verificano */

work : = available ;

for j : = 1 to n do finish [j] : = false ;

repeat

flag : = true ;

for j : = 1 to n do

if finish[j] = false and need < = work then

j

152

begin

work : = work + allocation ;

j

finish[j] : = true ;

flag : = false

end

until flag ;

j : = 0;

repeat

j : = j + 1

until not finish[j] or j = n ;

S := finish[j] ;

i

end ;

Affinché le risorse vengano allocate deve risultare ‘S = true’. L’assegnazione del va-

i

lore logico ad S avviene nell’ultima istruzione di questa procedura, e, perché il valore assegnato sia

i

true, il vettore finish deve essere composto interamente da true (si deve cioè giungere alla conclu-

sione che ogni processo possa finire la propria esecuzione). Si consideri allora il primo ciclo Re-

peat... until. Affinché tutti gli n elementi di finish (che vengono inizializzati a false) siano posti a

 1..n

true, devono essere eseguite per ogni j le istruzioni contenute nella sezione begin.. end. Se

si prospetta l’eventualità che, in seguito all’assegnazione, il fabbisogno degli n processi non possa

essere più appagato (need > work), l’ingresso a tali istruzioni viene precluso. È sufficiente che ciò

i

accada anche per un solo degli indici j perché ne consegua la mancata assegnazione. Si noti per inci-

so che il ciclo repeat... until viene percorso una sola volta unicamente nel caso limite in cui la con-

dizione di fabbisogno insoddisfatto si verifichi per tutti e n i processi, altrimenti sempre due volte.

RILEVAMENTO DEL DEADLOCK E SUO SUPERAMENTO. Supponiamo ora

che il sistema non utilizzi alcuno strumento per evitare il deadlock. In tal caso, un eventuale dead-

lock può essere rilevato solo dopo la sua occorrenza e pertanto il SO deve fornire due algoritmi:

- un algoritmo per rilevare la presenza di un deadlock;

- un algoritmo per eliminare la condizione di deadlock.

L’algoritmo per la rivelazione dello stato di deadlock viene proposto di seguito.

153

Procedure Test_deadlock (var deadlock : boolean) ;

var i : integer ;

flag : boolean ;

begin

work : = available ;

for i : = 1 to n do

finish [i] : = true ;

if allocation < > 0 then finish [i] : = false ;

i

repeat

flag : = true ;

for i : = 1 to n do

if finish [i] = false and request < = work then

i

begin

work : = work + allocation ;

i

finish [i] : = true ;

flag : = false ;

end ;

until flag ;

i : = 0;

repeat

i : = i + 1

until not finish [i] or i = n ;

deadlock : = finish [i] ;

end ;

Vengono usati i vettori available, work e finish e la matrice allocation della struttura

dati del caso precedente, con identico significato; request è invece una matrice n x m che indica la

i,j

richiesta attuale dei processi: ‘ request = k ’ significa che il processo i sta richiedendo altre k

istanze della risorsa j.

Si noti la notevole somiglianza esistente fra questo algoritmo e quello subito prece-

dente. Cambiano, oltre all’istruzione finale:

- la parte iniziale. In questo caso non c’è evidentemente una situazione da simulare,

ma occorre analizzare una situazione già esistente. Quindi, la struttura dati non viene modificata.

154

Inoltre il vettore finish viene posto a false solo per i processi non ancora terminati, ossia laddove al-

location <> 0.

i - la condizione need < = work, che qui diventa request < = work. Ai fini dell’ipote-

j i

si di terminazione di un processo i viene considerata attendibile la richiesta di risorse piuttosto che

l’effettivo bisogno. È evidente che se request < = work è garantito il fatto che il processo P non è

i i

attualmente coinvolto in un deadlock. Naturalmente P potrebbe stare effettuando per qualche moti-

i

vo una richiesta inferiore al bisogno reale, e in un successivo momento potrebbe sovrapporre ulte-

riori richieste, magari fino al punto di causare un deadlock, ma non c’è problema: ciò sarebbe certa-

mente rilevato alla successiva esecuzione dell’algoritmo.

Quando è consigliabile attivare l’algoritmo di rilevamento del deadlock? Osserviamo

che il deadlock può verificarsi solo se accade che un processo fa una richiesta che non può essere

immediatamente soddisfatta. Si può pensare quindi di attivare l’algoritmo ogni volta che si presenta

questa eventualità, ma tale scelta sarebbe palesemente inopportuna poiché aumenterebbe in maniera

intollerabile i tempi di calcolo. Alternative più plausibili consistono nell’attivare l’algoritmo perio-

dicamente (ad es. ogni ora), oppure ogni volta che la CPU risulta sottoutilizzata, ad esempio ogni

volta che l’aliquota di utilizzo della CPU scende al di sotto del 40%. Infatti, un deadlock ha general-

mente l’effetto di rendere inefficienti le prestazioni del sistema e in particolare di causare una caduta

nell’uso del processore.

Veniamo ora brevemente alla questione dell’eliminazione dello stato di deadlock. Un

deadlock può essere eliminato in due modi:

1) mediante l’abort di uno o più dei processi che si trovano in attesa circolare. Si può

pensare di abortire simultaneamente tutti i processi che si trovano in deadlock, oppure di abortirne

uno per volta, verificando dopo ogni abort se esistono ancora processi in deadlock. Entrambe le

strategie possono risultare molto dispendiose: la seconda per ovvie ragioni; la prima perché i pro-

cessi abortiti potrebbero avere effettuato già parecchi calcoli, e i loro risultati vengono così irrime-

diabilmente persi.

2) mediante prerilascio delle risorse. Si tolgono con la forza le risorse ad uno dei pro-

cessi bloccati, mettendole a disposizione di altri; se ciò non basta si fa la stessa cosa via via con tutti

gli altri. I processi che subiscono il prerilascio vanno posizionati in uno stato dal quale poi possano

essere riavviati (ROLLBACK). 155

LA GESTIONE DELLA MEMORIA

Le tecniche che sono state studiate fino ad oggi per la gestione della memoria posso-

no essere raggruppate in due grandi classi:

1) Tecniche che richiedono che il codice e i dati di un processo siano integralmente

contenuti nella memoria RAM;

2)Tecniche che richiedono che il codice e i dati di un processo siano solo in parte

contenuti nella RAM.

Quando il processo è running, è necessario in ogni caso che tutti i sui dati e tutte le

sue istruzioni siano presenti in uno spazio di memoria ad esso visibile indicato come memoria cen-

trale. Nel caso 1, quindi, la memoria centrale coincide con la RAM; quando invece è sufficiente che

solo parte dei dati e delle istruzioni siano in RAM, la memoria centrale non coincide interamente

con la RAM, ma il suo ruolo è rivestito dall’insieme di memoria RAM e memoria di massa e si par-

la in questo caso anche di MEMORIA VIRTUALE.

Alcuni dei concetti fondamentali riguardanti la gestione della memoria dovrebbero

esserci già noti. Ricordiamo in particolare che, al momento dell’esecuzione di un programma, se

viene richiesta la lettura o la scrittura di un dato o di una istruzione contenuti nella RAM, è necessa-

rio inviare a quest’ultima un indirizzo assoluto. Nei fatti, l’indirizzo fisico assoluto mandato verso

la RAM può essere generato in fase di:

1) compilazione;

2) collegamento;

3) caricamento in memoria;

4) esecuzione.

L’indirizzo delle istruzioni e dei dati del processo (ovvero la sua posizione nella me-

moria centrale) possono essere conosciuti o determinati in una delle prime tre fasi, e in tal caso vie-

ne generato quello che si chiama codice assoluto del processo. Qualora gli indirizzi vengano invece

generati in tutto o in parte in fase di esecuzione, nel codice del processo figureranno indirizzi di rife-

rimento ad uno spazio logico, anziché fisico, di memoria.

Si intende con il nome di BINDING l’operazione di trasformazione dell’indirizzo (di

dati e istruzioni) dalla forma logica alla forma fisica. Essa può essere effettuata in una qualunque

delle quattro fasi succitate: ciò dipende dalla risposta alla domanda: in quale fase si può conoscere

156

l’indirizzo esatto di istruzioni e dati? La situazione limite è quella in cui l’associazione indirizzo lo-

gico - indirizzo fisico sia possibile solo in fase di esecuzione e in tal caso il BINDING è eseguito di-

namicamente. Si rifletta sulle conseguenze che questo comporta: se un programma (e quindi la sua

area codice, la sua area dati etc.) risiede in memoria ad indirizzi assoluti, non è possibile modificare

la sua posizione, ovvero far ‘migrare’ i codici da un segmento all’altro della memoria. Questo è in-

vece possibile se si fa riferimento ad uno spazio logico. Se ad esempio la regola di binding utilizzata

47

è quella che indicheremo qui di seguito, basterà modificare a run time la base .

Il sistema più banale e conosciuto di binding (trasformazione da indirizzi logici a fi-

sici) è quello della base e della costante di spiazzamento, illustrato nella figura 1.

registro di base 14000 M

E

M

ind. logico ind. fisico

CPU + O

R

348 14348 I

A

figura 1

Per ricavare l’indirizzo fisico, ovvero quello di memoria, occorre sommare al conte-

nuto del registro BASE (o REGISTRO DI RILOCAZIONE) quello del registro SPIAZZAMENTO

(che rappresenta l’indirizzo logico). Questa procedura si chiama associazione dinamica degli indi-

rizzi e può essere realizzata solo su di una macchina che abbia una speciale, apposita struttura HW;

in particolare essa deve provvedere uno o più registri contenenti la o le basi (non è detto infatti che

un programma faccia riferimento ad un unico spazio logico).

Immaginando ad esempio di avere in memoria, a valle del caricamento, un program-

ma che presenti la situazione descritta in figura 2.

47 Negli appunti presi in classe al posto della parola ‘base’ compare qui e nel seguito ‘costante di spiazzamento’, il che è

chiaramente e notevolmente errato (ad esempio: che senso avrebbe un processore con un solo registro base e più registri

di spiazzamento?!?). 157

0 0 0

cod op | add Dati Stack

20000 10000 Z

figura 2

0 AI

AD

X AS

figura 3

Vi sarà cioè uno spazio logico di istruzioni (che va dall’indirizzo logico 0 all’indiriz-

zo 20000), uno spazio logico di dati (indirizzi da 0 a 10000) e un’area stack (indirizzi da 0 a z). Nel

programma utente gli indirizzi di istruzioni e dati sono espressi sempre in forma logica, dato che

questo semplifica non poco la programmazione. Quindi in questo caso abbiamo bisogno di almeno

tre registri base. Nulla vieta che all’atto di caricare il programma in memoria il caricatore provveda

a raccogliere i tre spazi in un solo spazio logico, come illustrato in figura 3. In questo modo il pro-

cessore nell’andare a ricercare in memoria dati e istruzioni da eseguire può fare riferimento ad un

unico spazio logico, delimitato dagli indirizzi 0 e X, nel quale trovano posto, nell’ordine, l’area

istruzioni, quindi l’area dati e infine l’area stack. Di conseguenza saranno necessari solo un registro

- base ed un registro destinato a contenere la costante di spiazzamento.

Per fare un esempio pratico, l’ormai vetusto processore UNIVAC 1100 era progettato

in modo da ‘vedere’ due spazi logici: uno per le istruzioni e uno per i dati. L’appartenenza di un in-

dirizzo logico all’uno o all’altro spazio era determinata in base al suo primo bit. Se il bit maggiore

dell’indirizzo logico era zero, esso faceva riferimento allo spazio logico delle istruzioni; se era uno,

allo spazio dei dati. Di conseguenza a seconda del valore del primo bit veniva selezionato uno fra

due diversi registri base, a cui veniva poi sommato l’indirizzo logico.

In genere si presenta la necessità di gestire un certo numero di registri base (è questo

il caso del processore INTEL, in cui i registri base sono quattro). Ogniqualvolta si verifica un cam-

bio di contesto (si salta da un processo all’altro) il contenuto di tali registri viene generalmente ride-

158

finito. Infatti questi registri di macchina contengono informazioni relative al processo attualmente in

fase di running, e quando quest’ultimo viene rimpiazzato da un altro processo essi vanno reinizia-

lizzati. Affinché allora non si perda memoria di quella che era situazione della CPU al momento del

cambio di contesto, è indispensabile che il contenuto dei registri macchina venga salvati da qualche

parte, e come ormai dovremmo sapere, tale salvataggio viene fatto all’interno del descrittore del

processo sospeso.

Il BINDING è uno dei due compiti fondamentali di cui deve occuparsi il GESTORE

DELLA MEMORIA. L’altro è costituito dall’evitare che i processi in memoria si sovrappongano

l’uno all’altro.

Limite dello spazio logico BASE

LIMITE M

E

SI M

Indirizzo O

+

CPU < fisico R

Indirizzo logico I

A

NO

Trap: errore di indirizzamento

figura 4

Il meccanismo classico di prevenzione di questo problema consiste nell’effettuare,

prima di utilizzare l’indirizzo logico, una comparazione tra l’indirizzo logico ed un limite logico

precedentemente stabilito (cfr. figura 4) (si tratta naturalmente di un limite superiore, essendo sem-

pre zero il limite inferiore per un indirizzo logico). Se l’indirizzo logico risulta maggiore, si verifica

un trap. Ad ogni cambiamento di contesto vengono definiti, fra i vari registri di macchina, anche i

registri limiti di memoria relativi al nuovo processo running.

L’OVERLAY

159

Ci sarà utile ora esaminare una tecnica di gestione della memoria che veniva usata

molti anni fa nei piccoli sistemi di elaborazione. All’epoca uno dei problemi più ostici era costituito

dall’impossibilità di disporre di banchi di memoria indirizzabile sufficientemente grandi, anche lad-

dove si doveva far girare un solo programma alla volta. Questo problema era aggirato mediante un

trasferimento di informazioni fra memoria di massa e memoria centrale, come del resto avviene

oggi nei sistemi a memoria virtuale. Ma la differenza in termini pratici è enorme. In un moderno si-

stema a memoria virtuale, infatti, di questo ‘travaso’ si occupa esclusivamente il SO, in modo da

rendere la cosa assolutamente trasparente all’utente.

In quel caso accadeva l’esatto contrario: il SO era in grado di ‘vedere’ la sola memo-

ria centrale del sistema e del trasferimento informazioni fra memoria di massa e memoria centrale

doveva farsi carico l’utente, mediante una tecnica nota come OVERLAY. Essa si basava sulla pos-

sibilità di conservare in memoria solo le istruzioni e i dati utilizzati con maggiore frequenza. Quan-

do sono necessarie altre istruzioni, queste vengono caricate nello spazio che precedentemente era

occupato dalle istruzioni che ora non vengono più utilizzate.

Un esempio può essere dato dall’assembler a due passi. Durante il passo 1, l’assem-

bler costruisce una tabella di simboli; durante il passo 2, genera il codice in linguaggio macchina.

Supponiamo che le dimensioni delle componenti del processo siano queste:

- Passo 1 : 70 K ;

- Passo 2 : 80 K ;

- Tabella dei simboli : 20 K ;

- Routine comuni : 30 K.

Quindi per caricare in memoria l’intero programma servono 200 K di memoria. Sup-

poniamo che ne siamo disponibili solo 150. In tal caso, si può notare che non è necessario che il

passo 1 e il passo 2 si trovino contemporaneamente in memoria. Si possono allora definire due over-

lay: l’overlay A costituito dalla tabella dei simboli, dalle routine comuni e dal passo 1; l’overlay B

costituito dalla tabella dei simboli, dalle routine comuni e dal passo 2. Deve essere presente in en-

trambi un gestore (o driver) dell’overlay, che poniamo occupi 10 K, indispensabile per passare da

un overlay all’altro.

Si inizia il processo con in memoria l’overlay A. Terminato il passo 1, subentra il

driver dell’overlay che sovrascrive in memoria l’overlay B (prelevandolo dal disco) all’overlay A e

trasferisce il controllo al passo 2. Si noti come dunque il programma assembler a due passi possa

essere eseguito nei 150 K. Il caricamento sarà più rapido, perché coinvolge meno dati; l’esecuzione

160

invece, naturalmente, risulterà rallentata dalla necessità di effettuare l’operazione di I/O delegata a

sovrascrivere il codice dell’overlay B a quello di A.

SIMBOL 20k

TABLE

ROUTINE 30k

COMUNI

70k PASSO 1 GESTIONE 10k PASSO 2 80k

OVERLAY

LO SWAPPING

Questa tecnica è in effetti stata già incontrata quando abbiamo parlato dello schedu-

latore a medio termine. Il suo scopo è quello di lavorare con un quantitativo di processi attivi mag-

giore di quello che potrebbe essere consentito con le tradizionali tecniche di gestione della memo-

ria.. Se il kernel ha un certo numero di processi aperti, ma non c’è abbastanza spazio per

contenerli tutti nella memoria centrale, alcuni di essi vengono spostati nella memoria di massa (deve

trattarsi di un’unita di memorizzazione con una buona velocità di trasferimento e una sufficiente ca-

pienza, come un disco fisso). Un processo può essere ad esempio ‘swappato’ nella memoria di mas-

sa quando si mette in attesa del completamento di un’operazione di I/O (con le dovute precauzioni,

come si dirà fra poco): questa è l’operazione di swap out. I processi ready dovrebbero trovarsi tutti

in memoria centrale, per velocizzare il più possibile la transizione degli stati ready - running. Se ciò

non è possibile, si deve cercare comunque di fare in modo che lo swap in di un processo sia effet-

tuato prima che lo schedulatore a breve termine lo faccia divenire running. Il processo di volta in

volta scelto dallo schedulatore della CPU dovrebbe cioè essere sempre presente in memoria centra-

le. Se deve essere eseguito un processo che si trova residente nella memoria di massa occorre effet-

tuarne lo swap in e contemporaneamente fare lo swap out di uno dei processi attualmente presenti in

memoria, il che costituisce un’evidente inefficienza. In altri termini, se ben gestito, lo swapping

161

consente, a parità di memoria disponibile, un livello di multiprogrammazione più alto; contraria-

mente può finire con l’aumentare l’overhead del sistema.

La tecnica di swapping trova riscontro un po’ in tutte le tecniche di gestione della

memoria: memoria a partizioni variabili, memoria paginata, memoria virtuale etc.. Ne esistono inol-

tre varie interpretazioni, a seconda della tecnica di schedulazione dei processi adottata dal SO. Se

vige una disciplina round-robin, ad esempio, trascorso un quanto di tempo il gestore della memoria

esegue lo swap out del processo appena terminato e lo swap in di un altro processo nello spazio di

memoria centrale che si è così liberato. Nel frattempo, lo schedulatore a breve termine avrà dato la

parola ad un altro processo ready, ed è auspicabile che si sia trattato di un processo residente nella

memoria centrale. In memoria, cioè, dovrebbero sempre esserci dei processi pronti per essere ese-

guiti. In presenza di un sistema a priorità lo swapping prende anche il nome di rolling: In tal caso, se

giunge un processo a maggiore priorità occorre prerilasciare la memoria occupata dal processo at-

tualmente in esecuzione, eseguendo su di esso uno swap (o roll) out, attendere la terminazione del

nuovo arrivato e poi richiamare il processo ‘swappato’ e continuare con la sua esecuzione.

Si noti che in quest’ultima circostanza lo swap out viene effettuato su di un processo

ready. Precedentemente abbiamo accennato allo swap out fatto per un processo che si trova sull’at-

tesa di un’operazione di I/0 (processo wait). Questo però non può essere fatto sempre e comunque,

perché potrebbe dar luogo a malfunzionamenti. In particolare, ciò non è possibile se la situazione è

tale che l’I/O accede continuamente e in modo asincrono alla memoria utente per leggervi i dati di

un processo che ha richiesto un servizio. Se quest’ultimo fosse swappato e se un altro processo oc-

cupasse il suo spazio di memoria, l’operazione di I/O potrebbe tentare di usare la memoria che ora

appartiene ad una altro processo.

Un processo che è in attesa di un’operazione di I/O può essere swappato solo se le

operazioni di ingresso uscita vengono fatte attraverso un’apposita area di memoria del SO, anziché

direttamente nell’area di memoria del processo. Il trasferimento dati tra il buffer del SO e la memo-

ria del processo potrà avvenire in un momento posteriore, dopo che il processo sarà stato sottoposto

a swap in. GESTIONE DELLA MEMORIA A PARTIZIONI

162

Nella memoria centrale sono presenti più processi contemporaneamente, e ogni pro-

cesso occupa una propria partizione di memoria. Generalmente viene effettuata la seguente distin-

zione: 1) partizioni fisse: il numero delle possibili partizioni è costante (cioè è un numero

ben preciso); 2) partizioni variabili: il numero delle partizioni dipende dalle esigenze dell’utente, e

viene quindi determinato dinamicamente. In tal caso questo numero sarà legato alla quantità di pro-

cessi che è necessario mantenere nello stesso tempo in memoria e dalle esigenze in termini di spazio

di memoria di ciascun processo. L’effettiva occupazione di memoria cambierà nel tempo, con la

conseguente necessità di gestire opportunamente lo spazio che si viene a liberare o che viene ad es-

sere occupato. Il sistema delle partizioni fisse si usa solo nei piccoli sistemi, e normalmente in que-

sto caso il programma in memoria è sempre a indirizzi assoluti, anziché relativi; i suoi indirizzi ven-

gono inizializzati al massimo in fase di caricamento. Ben più interessante è la questione della ge-

stione della memoria a partizioni variabili, della quale ci occuperemo nel seguito.

0 K SISTEMA OPERATIVO

UTENTE

SPAZIO LIBERO

512 K

Facciamo almeno un cenno a quella che è la tecnica di gestione a partizioni più sem-

plice possibile e immaginabile: allocazione con partizione singola. In questo modello, il SO risiede

nella ‘memoria bassa’ ed un singolo processo utente è in esecuzione, all’interno della propria parti-

zione, nella ‘memoria alta’, come risulta dalla figura precedente (in realtà non ha importanza l’ordi-

ne dei segmenti di memoria). Il problema più immediato che questa banale struttura comporta è la

possibilità che si abbia una sovrapposizione dell’area di memoria occupata dal SO da parte della

partizione utente, ma questo problema può essere agevolmente superato mediante uso opportuno di

un registro base e di un registro limite di memoria, come detto in precedenza.

163

LEZIONE N° 16 (DE CARLINI) - 16/4/1997

Proseguiamo il discorso sulla gestione della memoria, che abbiamo introdotto in oc-

casione della scorsa lezione. In particolare, abbiamo introdotto la tecnica dell’OVERLAY che aveva

lo scopo di consentire che programmi di grosse dimensioni potessero essere eseguiti in memorie di

capacità ridotta.

Una strategia simile a questa consisteva nel conservare in memoria un unico pro-

gramma ed uno speciale buffer destinato a contenere delle routine di errore, realizzate con lo scopo

di mandare il programma in abort. Si trattava di programmi di tipo commerciale dei quali l'80% cir-

ca era costituito di routine d'errore. La probabilità di generare un errore dipendeva dalla poca espe-

rienza dell’utente; non appena l’utente imparava a conoscere bene il programma, quest’ultimo ‘si

stabilizzava’. Le routine d’errore perdevano dunque la loro ragione di esistere, continuando però a

occupare inopportunamente spazio in memoria. Oggi una tecnica come questa sarebbe considerata

obsoleta. Abbiamo visto infine la tecnica dello SWAPPING, usata per mantenere più processi

in vita contemporaneamente all'interno del sistema, e abbiamo considerato un semplice schema di

gestione della memoria: quello dell’allocazione con partizione singola.

Entriamo ora nel merito delle più importanti tecniche di gestione della memoria.

GESTIONE DELLA MEMORIA: PARTIZIONI VARIABILI MULTIPLE

In questa tecnica (ormai abbandonata; ad esempio dalla famiglia INTEL, che ora

adotta la tecnica della paginazione) a ciascun processo viene associata una partizione, ovvero una

‘fetta’ di memoria a indirizzi consecutivi, la cui lunghezza è dimensionata in modo che vi possano

essere contenuti il codice, lo stack e i dati del processo stesso. L’illustrazione seguente permette di

comprendere come evolve la gestione di memoria usando questa tecnica.

164

0 Sistema

operativo

400K CODA DEI PROCESSI

processo memoria tempo

P 600K 10

1

P 1000K 5

2

P 300K 20

3

2160 P 700K 8

4

P 500K 15

5

2560K

Inizialmente in memoria c’è la parte ‘vivente’ del SO (cioè quella che è necessario

che rimanga sempre residente in memoria) mentre risultano liberi i restanti 2160K. Devono essere

caricati i 5 processi della tabella di destra; di conseguenza, vengono allocate varie partizioni in

modo dinamico. Il problema sorge nel momento in cui alcuni processi terminano liberando così il

proprio spazio di memoria, o anche, qualora sia lecito, nel momento in cui alcuni processi già allo-

cati richiedono una estensione della memoria ad esso assegnata. Nel primo caso si creano dei ‘bu-

chi’ di memoria liberata; lo spazio libero non è più ad indirizzi consecutivi, ma la memoria risulta

frammentariamente ripartita tra segmenti occupati e segmenti liberi. Naturalmente se due o più bu-

chi di memoria sono adiacenti, è possibile collegarli per formare un unico buco più grande. Quando

un nuovo processo deve essere allocato, occorre assegnargli uno fra i ‘buchi di memoria’ disponibili

che sia idoneo a contenerlo. Vediamo come ci si comporta se sono disponibili uno o più buchi di

memoria adatti. È necessario in tal caso individuare un criterio d'assegnazione; le strategia fra le

quali è possibile scegliere sono tre: FIRST-FIT, BEST-FIT e WORST-FIT.

FIRST-FIT alloca il primo buco sufficientemente grande. La ricerca può cominciare

dall'inizio dell'insieme di buchi, oppure dal punto in cui era terminata la ricerca precedente, e può

essere fermata non appena viene individuato un buco libero di dimensioni sufficientemente grandi.

BEST-FIT alloca il più piccolo buco in grado di contenere il processo. La ricerca va

effettuata su tutta la lista, a meno che questa sia ordinata per dimensione. Questa strategia produce

le più piccole parti di buco inutilizzate. 165

WORST-FIT: alloca il buco più grande. Anche in questo caso è necessario esaminare

tutta la lista, a meno che questa non sia ordinata per dimensione. Questa strategia produce le più

grandi parti di buco inutilizzate, che possono essere più utili delle parti più piccole ottenute con la

strategia best-fit.

Il primo criterio tende a far perdere meno tempo in quanto non sono effettuate ricer-

che. Il secondo tende a ridurre gli spazi vuoti, mentre l'ultimo tende a lasciare più spazio libero per

altri eventuali processi. Non esiste un criterio ottimale per ciò che riguarda l’utilizzo di memoria,

mentre si può affermare che firsi-fit è il più veloce. Tutte e tre le tecniche comunque presentano il

problema della frammentazione esterna, ovvero della frantumazione dello spazio libero di memoria

in piccole fette inutilizzabili. Può succedere cioè che non sia possibile allocare memoria ad un pro-

cesso, dato che non c’è al momento un ‘buco’ abbastanza grande, nonostante il fatto che il totale

della memoria libera (‘somma’ di tutti i buchi) sia sufficiente per le esigenze del processo in que-

stione. È questo il motivo fondamentale per cui questa tecnica ha finito con il cedere il posto ad al-

tre più idonee, in primis quella della paginazione.

Per risolvere la frammentazione esterna continuando a rimanere nel contesto delle

partizioni multiple variabili si applica un intuitivo sistema di compattazione: lo spazio viene recupe-

rato andando a ricompattare periodicamente la memoria, in pratica ‘accorpando’ lo spazio libero in

un unico ‘buco’. Questo si può fare però solo se il programma sia rilocabile, ovvero espresso me-

diante indirizzi relativi; in altre parole, è impossibile applicare la compattazione se l’allocazione del

programma viene effettuata in fase di compilazione, assemblaggio o caricamento ed è quindi stati-

ca. Ad esempio nel sistema base + spiazzamento, la compattazione può essere realizzata facendo

‘migrare’ le partizioni occupate verso un’estremità della memoria e modificando il contenuto del

registro di rilocazione (base) per i processi che sono stati spostati (esistono anche altre possibilità,

sulle quali non ci soffermiamo). Tale swap non viene fatto direttamente all’interno della memoria,

ma utilizzando un apposito canale, allo scopo di sollevare il processore da questo compito ed avere

quindi un minore overhead.

Esiste poi una problematica di frammentazione interna, dovuta al fatto che un proces-

so potrebbe andare ad occupare un buco che è di poco più grande rispetto al processo stesso, e lo

spazio che rimane è troppo piccolo per essere gestito, in quanto la sua gestione (ovvero la registra-

zione della duplice informazione indirizzo di memoria - dimensione del buco, solitamente effettuata

in un’apposita tabella) richiederebbe più spazio del buco stesso. Nella tecnica di gestione a partizio-

ni multiple, il problema della frammentazione interna risulta trascurabile, mentre si fa sentire (talora

166

pesantemente) quello della frammentazione esterna; nella tecnica della paginazione accade l’esatto

contrario. Un’alternativa alla compattazione consiste nel disporre di più registri base, in modo

che uno stesso programma possa essere suddiviso in memoria in più parti, ad esempio allocando l’a-

rea programma, l’area stack e l’area dati in punti differenti della RAM (cioè ad indirizzi non conse-

cutivi). In questo caso bisogna però fare uso anche di altri registri speciali, detti registri FENCE (=

recinto, steccato) che servono a impedire la fuoriuscita degli indirizzi logici dalle aree assegnata al

processo. Poiché occorre inibire l’invasione verso altre aree di memoria per ogni distinta sezione del

processo, il numero dei registri fence deve aumentare con l’aumentare del numero dei registri di ri-

locazione. Questa modifica fatta alla tecnica a partizioni variabili consente di introdurre il con-

cetto di codice rientrante (ovvero di CODICE CONDIVISO) fra due processi. Ad esempio, consi-

deriamo il caso di due utenti che lavorano su di un word processor scrivendo due lettere differenti.

In pratica si tratta di due processi che lavorano sulla stessa area codice (il codice in LM del word

processor) ma su aree stack e aree dati distinte (contenenti i testi delle due lettere). Ciò naturalmente

è possibile solo se si hanno segmenti di memoria separati per istruzioni e dati.

GESTIONE DELLA MEMORIA: PAGINAZIONE

Questa tecnica consiste nel suddividere la memoria fisica in blocchi di dimensioni

fisse, detti FRAME, e la memoria logica in blocchi di uguale dimensione chiamati appunto PAGI-

NE. Quando un processo deve essere eseguito le sue pagine vengono caricate nei frame della memo-

ria. frame occupato

Indirizzo logico frame libero

.

Indirizzo fisico .

CPU P d F d MEMORIA

FISICA

.

.

.

F

Tabella delle pagine 167

Ogni indirizzo generato dalla CPU è diviso in due parti: un numero di pagina (p), e

un offset di pagina (d). Il primo serve da indice per la tabella delle pagine, che contiene l’indirizzo

di ogni pagina nella memoria fisica. L’insieme di questo indirizzo e dell’offset di pagina permette di

definire l’indirizzo dell’informazione desiderata nella memoria fisica, mediante il supporto hard-

ware che viene schematizzato nella figura precedente. L’offset di pagina rappresenta lo spostamento

rispetto alla prima locazione del frame che corrisponde a quella particolare pagina.

La dimensione di una pagina, uguale a quella di un frame, è definita dall’hardware

(dato che essa coinvolge la memoria RAM) ed è in genere una potenza di 2. Il vantaggio di ciò è che

in questo modo è immediato trarre a partire da un indirizzo logico l’insieme di un numero di pagina

e di un offset. La parte di maggior peso dell’indirizzo logico rappresentano il numero di pagina,

quella meno significativa rappresenta l’offset di pagina. Per facilitare le cose, facciamo un esempio

con la base decimale: se l’indirizzo logico fosse 37273, e se ogni pagina logica fosse di 1000 K, al-

lora l’indirizzo corrisponde alla pagina logica 37 e alla locazione di indice 273 (che è ad esempio

una word) del corrispondente frame della memoria centrale (nel nostro esempio il frame di indirizzo

25). Memoria

0 1000 K

37 273 fisica

1

. .

. . offset 273

37 25 .

38 26 25

. .

. .

Tabella delle pagine

Il fatto che K sia piccolo o grande comporta, in riferimento alla gestione e alla fram-

mentazione delle pagine, vantaggi e svantaggi, come vedremo in seguito. Si noti come per la pre-

senza della doppia coordinata frame - offset lo spazio fisico in questo modello sia bidimensionale,

mentre lo spazio logico è lineare.

Dal punto di vista pratico, il mapping viene eseguito a tempo di esecuzione (dinami-

camente) mediante un dispositivo hardware simile a quello sopra descritto che considera i bit di

maggior peso dell’indirizzo logico, sfruttandoli come entry per la tabella delle pagine, e ricava la

168

pagina fisica corrispondente. L’indirizzo cosi ricavato non va sommato all’offset, ma giustapposto a

quest’ultimo; nel nostro caso, si effettua dapprima una semplice sostituzione fisica di bit tra 25 (in-

dirizzo frame) e 37 (indirizzo pagina logica); il numero 25 costituisce la parte alta dell’indirizzo che

viene mandato sul bus che va verso la memoria, la parte bassa di tale indirizzo è 273.

Non è quindi necessario scomodare l’ALU per ricavare gli indirizzi di memoria e si

evitano le conseguenti perdite di tempo. Questo però vale solo nell’ipotesi, che abbiamo fatto impli-

citamente, che le pagine fisiche siano disgiunte; diverso è ciò che avviene ad esempio nel processo-

re INTEL 8086 che contempla invece la possibilità di sovrapporre le pagine fisiche, e quindi ad

esempio di accedere ad uno stesso indirizzo fisico da due diverse pagine. Questa peculiarità, che in-

dubbiamente complica non poco il mapping della memoria, fu realizzata dalla INTEL in previsione

dei microprocessori più completi e veloci che sarebbero stati costruiti negli anni avvenire (famiglia

80X86). MMU MEM

CPU

Il componente hardware incaricato di sostituire l’indice di pagina logica con l’indiriz-

zo di pagina fisica può essere realizzato in vari modi. Nei sistemi meno recenti (anni ’80) prendeva

il nome di MMU (‘Memory Management Unit’). Non è altro che una batteria di registri contenenti

le corrispondenze fra pagine logiche e fisiche; naturalmente deve essere caricato con nuovi valori ad

ogni context switch. I registri della batteria sono tanti quanto deve essere lunga la tabella delle pagi-

ne. Si osservi che L’MMU può essere collegato quale componente esterno a processori che non

sono progettati internamente per gestire una memoria a pagine. Il discorso non è più fattibile se vie-

ne meno l’ipotesi di disgiunzione delle pagine fisiche: il problema va allora risolto all’interno della

CPU e non esternamente ad essa.

Altra soluzione è quella in cui la tabella delle pagine è contenuta nella memoria stes-

sa, e l’hardware è ridotto ad un solo registro speciale del processore (anziché un’intera batteria) che

169

punta in memoria alla posizione di tale tabella. Per ottenere quindi l’indirizzo di un frame occorre

sommare al contenuto di questo registro l’indirizzo della pagina richiesta (37 nell’esempio di cui so-

pra) e accedere in memoria all’indirizzo che ne consegue. Ogni processo possiede una propria tabel-

la delle pagine, quindi al cambiare del processo running cambia dinamicamente il contenuto del re-

gistro, il quale punterà così ad un'altra tabella. Lo svantaggio evidente di questa alternativa sta nel

suo richiedere degli accessi supplementari in memoria, la quale, come è noto, comporta tempi di ac-

cesso più lunghi rispetto ai registri hardware dedicati. D’altro canto si ha il vantaggio di eliminare

ogni limite fisico per quanto riguarda il numero di pagine occupabili, eccetto ovviamente quello del-

la dimensione fisica della memoria. Inoltre, i tempi di context switch diminuiscono sensibilmente.

Una soluzione intermedia si ottiene con processori al cui interno non sono presenti né

registri speciali né batterie di registri, ma che possiedono una MEMORIA ASSOCIATIVA: una me-

moria cioè nella quale l'accesso avviene per chiave e non per indirizzo.

CHIAVE DATO

0 20 Alla chiave 20 corrisponde la parola di memoria 20

21 7

37 25

44 30

Quando ai registri associativi si presenta un elemento, esso viene confrontato con-

temporaneamente con tutte le chiavi. Evidentemente nella colonna ‘chiave’ figureranno le pagine

logiche e nella colonna ‘dato’ quelle fisiche. La ricerca è estremamente veloce, ma il costo di realiz-

zazione di un simile componente è elevato. Tuttavia la memoria associativa può essere utilizzata per

contenere non l’intera tabella delle pagine, ma solo una sua porzione. Se si è ‘fortunati’ la chiave ri-

chiesta sarà presente in uno dei registri associativi e verrà immediatamente restituito l’indirizzo del

corrispondente frame. In caso contrario non resta che rivolgersi alla tabella completa, che sarà regi-

strata nella memoria centrale del sistema.

Il massimo della velocità media si ottiene costruendo un dispositivo hardware che

prevede sia la memoria associativa sia il registro speciale che punta alla tabella completa delle pagi-

ne in memoria; l’accesso viene eseguito in parallelo: da un lato inviamo il numero 37 come chiave

170

per la memoria associativa, dall’altro lato lo sommiamo all'indirizzo contenuto nel registro speciale

per accedere alla giusta riga della tabella delle pagine residente in memoria. Si percorrono cioè en-

trambe le strade, quella veloce ma incerta della memoria associativa e quella sicura ma lenta della

memoria RAM. Se la strada veloce ha successo, mandiamo un segnale di abort alla memoria RAM;

in caso contrario lasciamo che l’operazione di accesso alla RAM prosegua, permettendoci di ottene-

re a tempo debito l’indirizzo della pagina fisica. 

Posto t = tempo di accesso in memoria e = tempo di accesso alla memoria associati-

t),

va (si può porre ad esempio con questo marchingegno otteniamo un tempo medio di ac-



cesso che è (t + nella migliore delle ipotesi (l’indice di pagina fisica si trova nella memoria asso-

ciativa, per cui occorre poi un solo accesso alla memoria centrale) e 2t nella peggiore (l’indice pre-

sentato si trova nella sola tabella delle pagine; per cui occorrono in totale due accessi alla memoria,

uno per ricavare l’indirizzo del frame, l’altro per accedere alla locazione desiderata). Il tempo di ac-

cesso medio risulta essere: p

(t + +2t * (1-p)

Dove p è la probabilità di avere successo con la memoria associativa. Dunque, il noc-

ciolo della questione sta nel valore assunto da p. Se p è molto prossimo a 1 il tempo di accesso è

.

dato da quello del caso migliore, t + Nella realtà si verifica che per la quasi totalità dei programmi

risulta essere p ~ 0,8 ÷ 0,9, per cui il tempo di accesso T normalizzato su t vale all’incirca:

 48

T ~ (t + 0,1 * t) * 0,9 + 2t * 0,1= 1,19t T / t ~ 1,19

Una probabilità p così alta di può realizzare nella maggior parte dei casi tenendo nel-

la memoria associativa le pagine utilizzate più di recente. In tal caso il tempo di accesso medio ri-

sulta essere di poco superiore a t + e in particolare uguale all’incirca a t + 0,2 * t.

La protezione della memoria è garantita dalla stessa logica del metodo utilizzato.

Non è possibile che si verifichi un’invasione verso altre pagine, poiché avendo imposto che le di-

mensioni di pagina logica e fisica siano uguali non c’è modo di raggiungere indebite locazioni di

memoria con l’insieme degli indirizzi frame e offset. L'unico registro fence di protezione occorrente

48 Totalmente sballati apparivano i calcoli sugli appunti presi in classe: T t + 0,1 * t * 0,9 + 2t * 0,1 = 1,29 t (??) da

cui T/t 1,29, e per di più c’era scritto che questo tempo di accesso medio poteva essere espresso come t + 3 t (ossia 4t

che è ben maggiore di 2t, per cui questo ‘marchingegno’ dovrebbe essere certamente scartato!).

171

è quello che impedisce all’indice di pagina logica di essere tanto grande da fuoriuscire dalla tabella

delle pagine residente in memoria.

In più, questa tecnica non presenta alcun problema di frammentazione esterna ma

può denunciare una forte situazione di frammentazione interna, in quanto mediamente l’ultima pagi-

na di ogni processo è occupata solo per metà spazio. Si rifletta allora sul ruolo che riveste la dimen-

sione K di una singola pagina: se K è piccolo diminuisce il tasso di frammentazione interna, ma la

gestione diviene più onerosa, poiché aumenta il totale delle pagine da gestire.

172

LEZIONE N° 17 (DE CARLINI) - 17/4/97

Nella ultima lezione abbiamo visto la tecnica della paginazione, un’evoluzione della

tecnica di gestione a partizione variabili, che presenta come proprietà fondamentale la non consecu-

tività della spazio fisico (ovvero degli indirizzi fisici), mentre lo spazio logico che su di esso viene

mappato continua ad essere ad indirizzi logici consecutivi. Lo spazio fisico viene ad essere costitui-

to da blocchi o frame non necessariamente contigui in memoria, ma che possono essere dislocati a

macchia di leopardo. Il meccanismo di mapping della memoria dello spazio logico su quello fisico

funziona particolarmente bene nell’ipotesi che la lunghezza delle pagine sia una potenza di 2, e nel-

l’ipotesi che non vi sia sovrapposizione di pagine fisiche, poiché in tale caso per effettuare il map-

ping non occorre nessuna elaborazione, se non una semplice sostituzione di parte dell'indirizzo logi-

co con i corrispondenti bit esprimenti la pagina fisica.

Elemento fondamentale della paginazione è la tabella delle pagine, che fissa la corri-

spondenza pagina logica - frame. Un modo di implementarla è quello di conservare nella memoria

centrale una tabella delle pagine per ogni processo, e di farla puntare da un apposito indirizzo me-

morizzato in uno dei campi del suo descrittore. Quando un processo diviene running, ovvero al veri-

ficarsi di un cambio di contesto, si copia il valore di quel puntatore dal descrittore di processi in un

registro speciale. Tale operazione può essere fatta all'interno della CPU. In alternativa si potrebbe

49

disporre di una batteria di registri speciali (MMU) in cui memorizzare la tabella . La gestione mi-

gliore si realizza con l’insieme di una memoria associativa che contiene parte di quella tabella, e in

parallelo un registro speciale che punta alla tabella delle pagine qualora la memoria associativa dia

esito negativo. Si è trovato che mediamente sono sufficienti 16 registri associativi per avere una

probabilità dell'80% di trovare il riferimento cercato. Tenendo la tabella in memoria si elude la limi-

tazione costituita dal massimo numero di pagine che è obbligatorio imporre affidandosi al sistema

della batteria di registri. D’altra parte, usando contemporaneamente una memoria a struttura asso-

ciativa si riesce ad ottenere un tempo di accesso relativo abbastanza buono.

A queste informazioni, che dovrebbero essere già conosciute, aggiungiamo che il SO

deve poter utilizzare un’altra struttura dati, denominata tabella dei frame, dimensionata questa volta

49 Sembra di capire che nel primo caso ad ogni processo è associata una differente tabella delle pagine, contenente gli in-

dirizzi dei vari frame di cui dispone; mentre nel secondo esiste un’unica tabella della pagine valida per l’intero spazio di

memoria logica. 173

non sul numero di pagine logiche ma sul numero di frame, che indica per ogni frame se è libero o in

caso negativo a quale processo (o a quali processi) è allocato.

Vediamo ora di riassumere i vantaggi della tecnica della paginazione.

1) Qualora il codice di un processo sia separato dai dati, esso può essere condiviso

con altri processi. Ciò, lo ripetiamo, è possibile solo se non ci sono pagine che contengono in parte

codice e in parte dati. Invero, la tendenza ad accodare i dati al codice (o viceversa) in uno stesso

frame è normalmente favorita, perché ha come effetto positivo quello di minimizzare la frammenta-

zione interna. Se infatti con la paginazione mediamente mezza pagina per ogni processo risulta non

utilizzata, separando codice e dati si perdono in media due mezze pagine e quindi una pagina intera.

Da notare inoltre che il codice fra due processi può essere condiviso solo se è non rientrante, cioè se

nessuna istruzione è in grado di modificarne un’altra, che essa vede come se fosse un dato.

2) La paginazione permette un efficace meccanismo di protezione. Non ci riferiamo

al controllo della validità degli indirizzi, perché quello viene realizzato tramite il registro FENCE

che evita di esorbitare dalla tabella delle pagine o, se si adottano una batteria di registri o una me-

50

moria associativa, tramite i bit di validità ad esse collegati. Piuttosto, è possibile definire il genere

di accesso consentito sulle pagine; ad esempio una pagina che contiene codice si può accedere solo

in esecuzione (durante la fase FETCH dell’algoritmo fondamentale del processore), ad una pagina

dati si può accedere solo in lettura (stavolta nella fase EXECUTE) o in scrittura (sempre durante la

fase EXECUTE).

Il primo vantaggio menzionato comunque è presente anche nella tecnica delle parti-

zioni variabili. Non così il secondo, che costituisce una peculiarità della paginazione.

GESTIONE DELLA MEMORIA: SEGMENTAZIONE

Nella tecnica della paginazione la memoria fisica viene suddivisa in pagine di uguale

lunghezza. Nella SEGMENTAZIONE, queste pagine, più propriamente SEGMENTI, hanno una

lunghezza che varia a seconda delle esigenze. Analogamente allo spazio fisico, anche lo spazio logi-

co viene visto come suddiviso in segmenti; inoltre esso, diversamente da quanto accade nel caso

della paginazione, lo spazio logico assume come il suo corrispondente fisico una struttura bidimen-

50 Cfr. Silberschatz-Galvin, pag. 257 § 8.5.2.2 e fig. 8.17 pag. 258.

174

sionale: l'indirizzo logico è costituito dalla coppia indirizzo del segmento - offset all'interno del seg-

mento. La segmentazione è normalmente supportata da quei compilatori che mettono in seg-

menti logici diversi le variabili globali, l’area stack, i dati e così via. La fase di mapping, che si oc-

cupa di sostituire all'indirizzo di segmento logico quello di segmento fisico, richiede necessariamen-

te un’operazione di addizione invece di una semplice sostituzione come nel caso precedente.

Nel caso della paginazione il compilatore genera gli indirizzi di uno spazio logico li-

neare, ed è ignaro della suddivisione in pagina logica (ovvero fisica) e offset di pagina che si inge-

nera subito dopo. Da questo punta di vista quanto avviene nella segmentazione è essenzialmente di-

verso. Si noti la figura che segue. Limite base

Tabella dei

segmenti

S d

CPU MEMORIA

FISICA

SI +

< NO

. TRAP : errore di indirizzamento

L'indirizzo logico è costituito esso stesso da una coppia: indice di segmento - displa-

cement (anziché da un singolo indirizzo dal quale ricavare poi la coppia). La tabella che realizza la

corrispondenza fra spazio fisico e logico prende qui il nome di TABELLA DEI SEGMENTI, ed è

necessariamente più ampia della tabella delle pagine. Intanto, in tale tabella ad ogni segmento è as-

sociata una doppia informazione: l’indirizzo base del segmento nella memoria fisica e la lunghezza

175

del segmento stesso. Durante la fase di EXECUTE viene rilevato un indirizzo logico (che può esse-

re quello di un dato, ma anche di una istruzione: si pensi ad esempio alle istruzioni di salto), in cui

l’indice di segmento serve ad accedere alla tabella dei segmenti dalla quale ricavare base e limite del

corrispondente segmento fisico; il displacement viene confrontato con quest’ultimo allo scopo di

impedire accessi illegali. Se il displacement è minore, viene sommato con la base per ottenere l’in-

dirizzo fisico. L’hardware necessario per determinare l’indirizzo fisico include dunque un compara-

tore, un addizionatore ed un array di coppie (base, limite) ed è perciò più complicato di quello visto

per la paginazione.

Nei fatti, si ricorre sempre al sistema di utilizzare l’insieme di una memoria associati-

va e di un registro speciale che punta alla tabella completa, residente in memoria. La memoria asso-

ciativa contiene solo una parte di quella tabella. Se si tenta l’accesso ad un segmento il cui indice è

assente all’interno della memoria associativa, di prosegue la ricerca in parallelo nella tabella dei

segmenti. Ad ogni segmento inoltre si associa spesso una informazione supplementare: i diritti di

accesso, che permettono di gestire i meccanismi di protezione relativi a quel processo. In questo

modo un programma, anche di breve lunghezza, può essere disposto lungo più segmenti, ognuno dei

quali con un proprio criterio di accesso: per esempio uno accessibile solo in lettura, uno anche in

scrittura, eccetera. Si noti in proposito la figura seguente.

subroutine

segmento 0 TABELLA Limite base accesso

DEI

STACK SIMBOLI 1000 1400 0

segmento 3 400 6300 . 1

segmento 4 400 4300 . 2

1100 3200 . 3

SQRT 1000 4700 4

Programma

principale

segmento 1 segmento 2 176

La tecnica della segmentazione permette uno sviluppo ulteriore del concetto di con-

divisione. Nell’ambito della paginazione è possibile dividere le pagine contenenti il codice da quelle

contenenti i dati, e condividere l’intero codice tra due o più processi; con la tecnica dei segmenti in-

vece è possibile condividere tra più processi soltanto parte del codice; poiché infatti i segmenti ven-

gono definiti già a livello logico, si possono inserire opportunamente all’interno dei vari segmenti

‘spezzoni’ di codice, routine intere (come la SQRT dell’esempio) o gruppi di routine e condividere

51

ciascun segmento fra quei processi che sono preposti ad utilizzarlo .

Affrontiamo ora il discorso della frammentazione. Con la tecnica della segmentazio-

ne sparisce la frammentazione interna, perché i segmenti possono essere definiti proprio della lun-

ghezza necessaria. Permane invece il problema della frammentazione esterna. Dopo aver allocato i

vari segmenti in memoria, potremmo ritrovarci con dei buchi di memoria così piccoli da non poter

contenere alcun segmento. Questo problema però risulta meno grave di quanto lo fosse nel contesto

delle partizioni variabili, perché è meno probabile che un buco di memoria sia di dimensioni così

contenute da essere del tutto inutilizzabile, e quindi la necessità di ricompattare la memoria si pre-

senta con una frequenza inferiore. Per eliminare del tutto la frammentazione esterna si usa una tec-

nica mista detta di SEGMENTAZIONE PAGINATA, nella quale ogni segmento è organizzato ‘a

pagine’. GESTIONE DELLA MEMORIA: LA MEMORIA VIRTUALE

Questa tecnica può essere applicata sia nell’ambito di una memoria paginata, sia nel-

l’ambito di una memoria segmentata. Noi esamineremo in particolare la tecnica della memoria vir-

tuale applicata alla paginazione.

La memoria (segmentata o paginata che sia) non coincide più con la RAM ma risulta

essere fisicamente allocata sulla memoria di massa; solo un sottoinsieme di questa memoria rimane

nella RAM. Durante l’esecuzione di un processo, può accadere che lo stesso abbia bisogno di acce-

dere ad una particolare pagina residente non nella RAM ma sul disco; in questo caso il SO deve pre-

vedere un meccanismo automatico che effettui l'arresto del processo, l'ingresso della pagina man-

51 Segue misteriosa osservazione di De Carlini, presente tra l’altro anche sul Silberschatz-Galvin (pag.268): “Ad esem-

pio, in un segmento possiamo inserire un array perché così mi rendo conto subito tramite una Trap se sono uscito dal-

l'array e quindi dal segmento (meccanismo automaticamente gestito)”.

177

cante in memoria e la ripresa del processo, il tutto in maniera completamente trasparente rispetto

all'utente. Un’area più o meno vasta del disco viene dunque ad essere la vera e propria memoria

del sistema. Tale zona è concettualmente del tutto distinta dal File System, sebbene alcuni moderni

SO la includano al suo interno. In fase di caricamento di un generico processo, esso viene caricato

in questa area del disco. Naturalmente però perché il programma possa essere eseguito è indispen-

sabile che le sue istruzioni siano caricate sempre e comunque dalla memoria RAM (ci riferiamo alla

fase FETCH); è questo il motivo fondamentale per cui un sottoinsieme della memoria deve trovarsi

sempre in RAM. In teoria, sarebbe sufficiente che nella memoria RAM fosse presente la sola istru-

zione corrente (che cambia di volta in volta) e i dati su cui essa opera. Supponendo di usare la tecni-

ca della paginazione, possiamo più ragionevolmente affermare che tutte le pagine risiedono sul di-

sco, fuorché un sottoinsieme minimo che si trova in RAM; ogni volta che occorre, una intera pagina

viene spostata dal disco alla memoria e viceversa.

Anche in questo caso avremo uno spazio logico di indirizzi, ed uno fisico. All'atto

della generazione (mapping) dell'indirizzo fisico, può accadere che la pagina logica riferita dall’i-

struzione su cui stiamo lavorando sia inesistente. In questo caso il meccanismo di traduzione falli-

sce. Il processo viene allora sospeso e parte il meccanismo di sostituzione, che porta all’interno della

memoria la pagina opportuna.

Al solito, l’indirizzo logico può essere quello di un dato oppure di una istruzione,

come nell’esempio dell’istruzione di salto (ad un’altra istruzione) jump 1000. Consideriamo que-

st’ultimo caso. Si può pensare di effettuare il mapping soltanto all’atto dell’accesso in memoria

(FETCH riferita all’istruzione memorizzata nell’indirizzo 1000). Nel PC viene ad essere caricato

l’indirizzo logico 1000 (si carica dunque nel PC un indirizzo logico, anziché fisico). In questo caso

la trap che segnala l’impossibilità di trovare la pagina richiesta viene innescata quindi in fase di

FETCH. Se invece la corrispondenza indirizzo logico - fisico viene fatto prima di caricare il PC, l’e-

ventuale trap sarà innescata preventivamente in fase di EXECUTE (EXECUTE riferita all’istruzio-

ne jump 1000).

500 jump 1000 1000: nop

TRAP : indirizzo inesistente

FETCH FETCH

OPERAND ASSEMBLY OPERAND ASSEMBLY

EXECUTE EXECUTE

TRAP : indirizzo inesistente 178

Notiamo che in questo caso la trap (detta di page fault) non provoca un abort del

processo, bensì il travaso di una pagina dalla memoria di massa alla RAM o viceversa. Naturalmen-

te è possibile che questo travaso, chiamiamolo (per pura comodità) Swap in, comporti preventiva-

mente un trasferimento inverso, ovvero uno Swap out da RAM a disco, perché potrebbe non esserci

memoria RAM a sufficienza per effettuare il solo Swap in. Il trasferimento che abbiamo denomina-

to Swap out può essere fatto subito prima dello Swap in o in un altro precedente momento. Alcune

macchina lavorano ‘a pagine libere’, altre ‘a pagine piene’. Il secondo sistema prevede che una fra-

me passi dalla RAM al disco ogni volta che le esigenze del programma richiedano che un’altra pagi-

na effettui il trasferimento opposto. Si parla allora di lazy swapping (swapping ’pigro’) o di pagina-

zione su richiesta. Nel primo caso, invece, c’è sempre una altissima probabilità di trovare dello spa-

zio libero, perché il sistema opera lo Swap out non all'atto dello Swap in ma periodicamente. Ad

esempio, UNIX costruisce un ‘serbatoio di pagine libere’ mediante un’interruzione periodica.

Indipendentemente dal criterio adottato, saranno necessari degli opportuni algoritmi

per selezionare le pagine da trasferire su disco. Da notare che lo swap out delle pagine fisiche può

anche essere evitato: ci si può limitare in certi casi a sovrascrivere un frame giù occupato da un altro

processo grazie al criterio dell’avvenuta modifica. Ad ogni frame si associa un bit di modifica che,

inizializzato a zero, viene posto a 1 nel momento in cui si scrive nella pagina una sia pur minima in-

formazione. Se il bit di modifica di un frame è zero, vuol dire che quella pagina non è stata mai mo-

dificata; essa è identica alla copia che risiede sul disco, e pertanto è inutile effettuare lo swap out.

Vediamo ora come avviene la traduzione degli indirizzi. Il procedimento è simile a

quello della paginazione: occorre una tabella delle pagine. Di nuovo, una parte di quest’ultima si

trova in memoria associativa. L’indirizzo di pagina fisica viene ricercato nella memoria associativa

e parallelamente il registro speciale effettua la sua ricerca nella tabella completa residente in memo-

ria, come indicato in precedenza. La tabella presenta però una struttura leggermente diversa. In par-

ticolare, un apposito bit di validità specifica se la corrispondente pagina logica è ‘valida’ o meno.

Che una pagina logica sia considerata non valida significa che ad essa non corrisponde alcuna pagi-

na fisica (cfr. fig. pagina seguente). Se il bit di validità non è ‘okay’ si genera una trap di page fault;

interviene il kernel che effettua il demand paging, ovvero il travaso dalla memoria di massa della

179

52

pagina richiesta . Si potrebbe scegliere di memorizzare l’indirizzo della pagina su disco nella stes-

sa tabella delle pagine.

In queste posi- .

zioni può essere .

Memoria

registrato l’in- DISCO

fisica

dirizzo del fra- 3

me su disco A 4

5

A 0 Frame bit C 6 A B

7

B 1 4 v 0 8

i 1 9 C D E

F

C 2 6 v 2 10

i 3

D 3 11

i F

4

9 v 5

E 4 .

Tabella delle .

F 5 pagine .

Ovviamente, può ancora presentarsi il caso che l’indirizzo logico sia del tutto illega-

le, nel senso che ad esso corrisponde una locazione di memoria che è al di fuori della tabella delle

pagine. (In altre parole, a quell’indirizzo logico non corrisponde alcuna pagina fisica). Di questa si-

tuazione si occupa il solito registro di controllo fence, che fissa il limite della tabella delle pagine. Si

può quindi generare una trap di indirizzo scorretto (anziché di page fault), con conseguente abort

del processo, nel caso in cui si tenti di accedere ad una pagina di indirizzo logico maggiore del con-

sentito. Il supporto hardware di un sistema a memoria virtuale deve realizzare nel modo più

semplice ed efficiente possibile tutte questi meccanismi.

Un’ultima osservazione, prima di chiudere la lezione. Abbiamo detto che il meccani-

smo di sostituzione di una pagina residente in memoria con una del disco parte per effetto del falli-

mento di un’operazione di traduzione indirizzi. Il programma deve essere temporaneamente sospe-

so, per ripartire (a sostituzione avvenuta) dall’istruzione che ha provocato quella sospensione. Si

noti per inciso che se l’interruzione si è verificata in fase di OPERAND ASSEMBLY, in occasione

52 Nel Silberschatz-Galvin (pag.283 sgg) si afferma che il bit di validità segnala anche la condizione di ‘indirizzo illeci-

to’ (non corrispondente ad una pagina fisica accessibile al processo). Pertanto, tale bit può essere ‘non okay’ per due di-

versi motivi: indirizzo illecito, pagina residente su disco (sarà poi il SO a discriminare fra i due casi). De Carlini non ha

menzionato questo aspetto durante la lezione. La figura di pagina 179 è stata modificata (rispetto a quella del libro, che

sta a pag.285) in base alla spiegazione di De Carlini. 180

della successiva ripartenza occorre ripetere anche il precedente prelievo dell’istruzione (FETCH).

Questo genera dei comprensibili problemi per quelle particolari istruzioni che modificano molte va-

riabili in memoria. Il Silberschatz-Galvin propone (pag. 288) l’esempio dell’istruzione macchina

MVC che consente di spostare fino a 256 byte per volta da un punto all’altro della memoria. Potreb-

be darsi che uno dei blocchi di origine o destinazione fuoriesca dal confine di una pagina, generando

una page fault e costringendo a ricominciare daccapo il trasferimento. Inoltre, MVC consente anche

di sovrapporre i due blocchi, e se effettivamente si verifica una sovrapposizione del blocco destina-

zione sul blocco origine evidentemente non ci si può limitare a riavviare l’istruzione. Problemi

come questo devono essere prevenuti in fase di progetto del processore. Ad esempio, si possono uti-

lizzare dei registri temporanei di memoria per conservare i valori delle locazioni sovrascritte. In

caso di page fault e subito prima che si verifichi la corrispondente trap, tutti i vecchi valori vengono

risistemati nel blocco origine. 181

18/4/97 (PROF. DE CARLINI)

CONTINUO MEMORIA VIRTUALE

Abbiamo detto che per implementare la memoria virtuale c’è bisogno di un meccanismo di

basso livello (hardware essenzialmente) che serve a controllare, nel momento in cui biso-

gna effettuare il mapping dell’indirizzo virtuale sull’indirizzo reale, se questo mapping è

realizzabile o meno. Ovviamente il mapping sarà realizzabile se all’indirizzo logico specifi-

cato corrisponde un indirizzo della memoria fisica; siccome abbiamo detto che l’organizza-

zione sottostante è una organizzazione a pagine, ciò significa dire che la pagina fisica cer-

cata, oltre a stare su memoria di massa (cioè su un blocco di disco), risulta essere stata an-

che travasata nella memoria centrale.

L’hardware di cui sopra funziona in questo modo:

Abbiamo una memoria associativa in cui viene registrata una parte della tabella delle pagi-

ne; un riferimento viene ricercato in questa memoria, se esso viene trovato, si controlla un

bit di validità che ci dice se la pagina cercata è nella memoria centrale o meno. Qualora in-

vece nella memoria associativa non fosse presente il riferimento cercato, esso viene ricerca-

to nella tabella delle pagine. In quest’ultima tabella ci sarà sicuramente il rigo corrispon-

dente al riferimento (in quanto essa contiene tutti i possibili riferimenti del processo), di

tale rigo viene controllato il bit di validità che ci dice ancora una volta se la pagina cercata

è presente o meno nella memoria fisica.

In entrambi i casi (memoria associativa o no) se il bit di validità ci dice che la pagina ricer-

cata non è in memoria centrale si genera un page fault ; tale evento ci informa che è neces-

sario caricare la pagina riferita. A questo punto se abbiamo una pagina libera nella memo-

ria centrale, la nuova pagina potrà andare in questo punto, altrimenti bisognerà sostituire

qualche altra pagina che era in memoria ; ciò è realizzato tramite un “algoritmo di sostitu-

zione”.

Tale algoritmo deve essere ovviamente ottimizzante e ciò significa dire che esso deve esse-

re in grado di individuare nel modo più opportuno la pagina vittima.

Cominciamo con il dire che un algoritmo di sostituzione può essere :

( ): la pagina vittima ,quella da sostituire, viene ricercata unicamente tra le pa-

 LOCLE ASL

gine del processo che ha causato il page fault.

( ) : ricerca la pagina vittima tra la totalità delle pagine.

 GLOBALE ASG

Esistono inoltre due strategie in cui si decide di lavorare rispettivamente a:

: si va avanti fino a quanto la memoria non è piena, ponendosi il problema

 PAGINE PIENE

della sostituzione di una pagina solo quando si è verificato un page fault e non c’è più

alcun buco libero; in tal caso può essere utilizzato sia un ASL che un ASG.

: tramite uno svuotamento periodico delle pagine faccio in modo che al

 PAGINE LIBERE

momento in cui si verifica un page fault ci sia sempre qualche pagina disponibile. In tal

caso si capisce che l’algoritmo di sostituzione entra in gioco al momento dello svuota-

mento periodico della memoria in cui non esiste il concetto dei località e quindi tale al-

goritmo non può essere che un ASG. (UNIX usa questa tecnica, cioè è presente un timer

che periodicamente fa partire un DEMON che crea un pool di pagine libere che viene

consumato fino al prossimo timer). 182

Esaminiamo adesso alcuni algoritmi di sostituzione.

ALGORITMO FIFO

Abbiamo una informazione che ci dice da quanto tempo la pagina è stata portata in memo-

ria centrale. Basandoci su tale informazione noi andremo a sostituire la pagina che è pre-

sente in memoria da più tempo.

Esempio :

Supponiamo che il processo abbia avuto (in qualche modo) a disposizione 3 pagine e che

l’algoritmo di sostituzione sia un “algoritmo di sostituzione locale”.

Viene considerata la seguente stringa dei riferimenti alle pagine (che dovrebbe rappresen-

tare i riferimenti alle pagine logiche che vengono fuori eseguendo le istruzioni del proces-

so) :

7 0 1 2 0 3 0 4 2 3 0 3 2 1 2 0 1 7 0 1

Alcune delle istanze della tabella delle pagine che si genereranno sono le seguenti :

7 0 1 2 0 3 0

7 2 2 2

7 7 0 0 3 3

0 1 1 1 0

Inizialmente si verificano tre page fault, che potremmo definire fisiologici (si parla in que-

sto caso di paginazione pura) in corrispondenza dei primi tre riferimenti. Subito dopo vie-

ne riferita la pagina 2 che non essendo in memoria causa un page fault ; l’algoritmo FIFO

sostituisce allora la pagina 7 che era stata la prima ad entrare in memoria. Successivamente

c’è un riferimento a 0 che non causa page fault, e un riferimento a 3 che

causa la sostituzione della pagina 0 che è la più vecchia ecc.

Il risultato finale è che per questa stringa di riferimento, a prescindere dai 3 page fault ini-

ziali (un’altra tecnica avrebbe potuto inizialmente prendere 3 pagine a caso e portarle den-

tro : caricamento a-priori), si sono verificati 12 page fault.

Il difetto di questo algoritmo che subito si nota è che esso non tiene conto del fatto che una

pagina cariata da molto tempo potrebbe ancora servire; ad esempio sopra, al riferimento a

3 è stata sostituita la pagina 0 che sarebbe servita subito dopo.

L’algoritmo FIFO non è allora un algoritmo ottimale (e neanche molto efficiente).

ALGORITMO OTTIMALE

Da quanto visto sopra si capisce che l’algoritmo di sostituzione ottimale, nel momento in

cui si deve effettuare una sostituzione di una pagina, tende a buttare fuori la pagina che da

quel momento in avanti non sarà referenziata per maggior tempo.

183

Che questo sia un algoritmo efficiente lo si può vedere riconsiderando la stringa dei riferi-

menti precedente, in tal caso infatti questo algoritmo causa appena 6 fault oltre ai 3 iniziali

(cioè la metà dell’algoritmo FIFO).

(Vedi libro pag. 298)

Questo algoritmo è in realtà un algoritmo teorico in quanto è impossibile prevedere la

stringa dei riferimenti di un processo (un po’ come l’algoritmo SJF nello scheduling della

CPU in cui dovevamo limitarci a fare una stima di ciò che poteva succedere) ; ci si limita

quindi a cercare di approssimarlo tenendolo come riferimento : cioè un algoritmo sarà

tanto migliore quanto più approssima l’algoritmo ottimale (è questa la sua importanza).

( )

ALGORITMO LRU LAST RECENTELY USED

Questo algoritmo fa lo stesso ragionamento dell’algoritmo ottimale, ma invece di guardare

in avanti guarda all’indietro.

Viene sostituita non la pagina che è stata portata in memoria da più tempo (FIFO), bensì

quella che non viene usata da più tempo. La filosofia è che se quella pagina mi è appena

servita, probabilmente mi servirà ancora (località temporale).

Usando la stesa sequenza di riferimento (libro pagina 299) si vede che in questo caso ven-

gono generati solo 9 fault oltre ai 3 iniziali ; quindi in questo esempio l’algoritmo risulta

avere una efficienza che è intermedia tra quello ottimale e quello FIFO.

Tale algoritmo ha bisogno di un supporto hardware che permetta di associare a ciascuna

pagina un informazione che consente di ordinare le pagine da quella non usata da più

tempo a quella usata più recentemente.

Citiamo 4 possibili realizzazioni :

1. La CPU ha un counter che viene incrementato ad ogni accesso in memoria il cui valore

viene copiato nella tabella delle pagine ; la pagina sostituita è quella con valore del

counter + basso (questo algoritmo non piace al professore).

Il modo più semplice per realizzare un tale algoritmo (da un punto di vista logico e non

2. reale) sarebbe quello di avere un campo nella tabella delle pagine in cui ogni volta che

viene fatto un riferimento ad una determinata pagina viene scaricato il valore di un ti-

mer ; nel momento della sostituzione viene scelta come pagina vittima quella che ha il

valore del timer più basso. Questo procedimento anche se lineare da un punto di vista

logico risulterebbe abbastanza costoso da realizzare. Si possono fare, invece, dei ragio-

namenti che approssimano questo algoritmo e che rendono la sua realizzazione meno

costosa.

3. C’è un hardware speciale che realizza una lista di delle pagine, ordinata in base all’ac-

cesso ; cioè ogni volta che faccio riferimento ad una pagina il riferimento a quella pagina

viene messo in testa alla lista. Ne segue che la pagina vittima, è in ogni momento, quella

in coda alla lista (anche questo sistema è molto costoso e in pratica quasi mai utilizzato).

Si può effettuare una discretizzazione del tempo in intervalli, registrando se in ognuno

4. di questi intervalli io ho fatto almeno un accesso ; questa informazione mi deve essere

fornita dall’hardware. Ad esempio un meccanismo attraverso il quale io posso realizza-

re ciò è il seguente. Nella tabella delle pagine io ho un ulteriore bit che è detto bit di rife-

rimento, nel quale l’hardware segna un uno ogni volta che viene fatto un accesso (a pre-

scindere da lettura o scrittura, non confondere con il bit di modifica che si setta solo in

184

scrittura) alla corrispondente pagina. Inoltre ho un timer tarato ad un intervallo X che e

l’intervallo di discretizzazione che ho scelto ed è quello di cui parlavamo sopra. Ho an-

cora una batteria di shift register, uno per ogni pagina della tabella delle pagine, il cui

bit + significativo è proprio il bit di riferimento. Il numero di bit di cui questi shift regi-

ster sono composti mi da il numero di periodi passati (ognuno di lunghezza X) di cui io

voglio tenere conto. Il meccanismo funzione in questo modo : ogni volta che scatta il ti-

mer viene fatto lo shift di tutti gli shift register con bit entrante pari a 0. Nel momento in

cui io devo sostituire una pagina vado a prendere quella che ha il numero binario + pic-

colo nel proprio shift register :

Direzione Shift Shift register

Bit di riferimento

Num. pagine Num. Intervalli

se ad esempio due pagine hanno questi numeri nei loro shift register :

0001011

0001001

viene sostituita la seconda perché avendo il numero binario più piccolo è stata referenziata

meno frequentemente di recente. Notiamo che questo modo di procedere non rispecchia in

pieno l’algoritmo LRU perché nel nostro esempio avendo discretizzato il tempo le due pa-

gine erano equivalenti ai fini di questo algoritmo (cioè non venivano usate dal medesimo

tempo). Si fa qui cioè l’ipotesi aggiuntiva che la seconda pagina mi serva meno della prima

perché è stata referenziata di meno negli ultimi periodi.

185

Si vede quindi che avendo a disposizione questo tipo di hardware, l’algoritmo non deve

fare altro che trovare il minimo in una tabella.

Molti sistemi operativi utilizzano proprio questo sistema ma anziché considerare n inter-

valli di tempo, ne considerano solo 1. Questo vuol dire che l’hardware si riduce ai soli bit

di riferimento. UNIX è fra questi ma applica un algoritmo un po’ particolare, simile a que-

sto, che viene detto “algoritmo della seconda chance”.

ALGORITMO DELLA SECONDA CHANCE

Ribadiamo che affinché possa realizzare questo algoritmo, l’hardware mi deve mettere a

disposizione i bit di riferimento delle pagine che vengono settati ogni volta che viene fatto

un accesso in memoria e azzerati a ogni intervallo di tempo. Aggiungiamo che in questo

caso specifico, i bit di riferimento sono organizzati come una coda circolare.

1

0 Puntatore del daemon

1

0

0

Sottolineiamo inoltre che UNIX lavora secondo la strategia a ; tale strategia è

PAGINE LIBERE

realizzata in questo modo : Le pagine libere sono viste da come un serbato che ha un

UNIX

livello massimo (prefissato) e un livello minimo al di sotto del quale non si deve scendere .

Il serbatoio viene portato a massimo livello ; man mano che il sistema funziona il livello

scende ; ad intervalli di tempo (scanditi da qualche clock) ben precisi interviene un demon

che controlla se il serbatoio e sceso sotto il livello minimo e solo in questo caso lo riporta a

livello massimo. Il livello minimo è ovviamente introdotto per ridurre il numero di volte in

cui l’algoritmo entra in funzione, e quindi per evitare dei farlo funzionare per la liberazio-

ne di poche pagine (infatti in questo caso, il numero minimo di pagine da liberare è pari a

max-min. Ricordiamo che l’I/O su disco è tanto più efficiente quanto più è consistente).

(Osserviamo che in realtà esistono due versioni di UNIX, una che fa come detto sopra e

una che assegna al serbatoio delle pagine libere solo un livello massimo. In questa versione

al clock, il demon comunque ripristina il livello massimo. Chiaramente, in questo caso non

è assolutamente prevedibile il numero di pagine che il demon dovrà liberare, dipende da

che cosa è successo in quell’ultimo intervallo di tempo, e quindi questo modo di fare

meno efficiente del precedente).

Abbiamo detto questo per dire che l’algoritmo di cui dobbiamo parlare è quello realizzato

dal demon, ed è, come abbiamo detto sopra quando abbiamo parlato di strategie di sosti-

tuzione e algoritmi di sostituzione, necessariamente un ASG.

Il demon ha un stato rappresentato dalla posizione del puntatore ai bit di riferimento. La

posizione del puntatore, il quale ovviamente è manovrato unicamente dal demon, mi dice

quale è la pagina candidata come prossima vittima. Quando il demon entra in funzione se

trova un 1 nel bit di riferimento puntato significa che quella pagina è stata utilizzata nel-

l’ultimo periodo ; allora da una “seconda possibilità” a quella pagina azzerando il suo bit

186

di riferimento e avanzando il puntatore al bit di riferimento successivo. Ciò va avanti fin-

ché non viene trovato qualche bit di riferimento a zero ; in questo caso significa che dall’ul-

tima volta che il demon è passato per quel bit mettendolo a zero la pagina non è stata uti-

lizzata ; essa allora viene sostituita. Questo procedimento va avanti finché il livello delle

pagine minime non è stato portato al massimo prestabilito.

Il nome dell’algoritmo deriva dal fatto che quando il demon trova un uno nel bit di riferi-

mento di una determinata pagina esso lo azzera e non sostituisce la pagina dandogli una

seconda possibilità di essere utilizzata. La durata di questa seconda possibilità non è deter-

minabile, dipende dal tempo che il puntatore del demon impiega a ritornare su quel bit.

Ciò dipende essenzialmente : dal intervallo di tempo scelto per il timer, dal numero di zeri

che esso trova nei bit di riferimento e dal numero delle pagine che deve sostituire. (ricor-

diamo che tale numero di pagine in una versione di UNIX è pari minimo a MAX- MIN

mentre nell’altra può essere qualsiasi). Un caso limite potrebbe essere quello in cui le pagi-

ne da sostituire sono numerose così come il numero di 1 per cui il puntatore fa un giro

completo dell’array ritornando ad una pagina a cui aveva dato una seconda possibilità so-

stituendola; in questo caso la seconda possibilità di quella pagina sarebbe durata 0 unità di

tempo. ( )

ALGORITMO LFU LAST FREQUENTELY USED

Questo algoritmo invece di sostituire la pagina che non viene utilizzata da maggior tempo,

sostituisce quella che tiene la più bassa frequenza di uso. Per realizzare ciò basta avere un

contatore che si incrementa ogni volta che si fa accesso ad una pagina (cioè si conta il nu-

mero degli accesi alla pagina). Il problema a cui si può andare incontro in questo modo e

che le pagine che stanno dentro da più tempo hanno molto probabilmente un valore ele-

vato del contatore rispetto alle pagine che sono state portate dentro da poco tempo. Per ri-

solvere questo problema, si potrebbe pensare ad esempio di dimezzare il contatore perio-

dicamente in modo da abbattere il valore delle pagine che stanno da molto tempo e che in

passato hanno avuto grande uso e poi recentemente vengono utilizzate di meno. Tuttavia

in questo caso no sarebbe più un LFU ma sarebbe una via di mezzo che tiene conto da

quanto tempo non faccio acceso alla pagina.

Una volta individuate quali sono le pagine candidate per la sostituzione attraverso uno de-

gli algoritmi sopra visti, un problema che si presenta è la scelta tra due pagine che secondo

questi algoritmi sono entrambe candidate per essere sostituite.

Ma anche a prescindere da questo problema e da questi algoritmi, si potrebbe pensare di

sostituire quelle pagine che nel periodo o nei periodi di osservazione non sono state utiliz-

zate(sarebbero quelle pagine che tengono tutti 0 nei loro uno o più bit di riferimento) e, da

quando sono state portate in memoria centrale, non sono mai state modificate. Chiaramen-

te se una pagina è in memoria significa che almeno una volta è servita e quindi noi richie-

diamo che essa non sia stata mai modificata. Si capisce che per tenere nota della modifica

delle pagine c’è bisogno di avere un bit di modifica che viene settato se la pagina viene

modificata almeno una volta dal suo arrivo in memoria centrale. Chiaramente l’utilità di

questo criterio sta nel fatto che se una pagina non è stata modificata, non c’è bisogno di

aggiornarla sulla memoria di massa nel momento in cui essa deve uscire dalla memoria

centrale. Terminate le pagine che non sono state ne modificate ne utilizzate si va a sostitui-

187

re quelle che non sono state utilizzate ma modificate, poi quelle che sono state utilizzate

ma non modificate e, se ancora non bastano, quelle utilizzate e modificate.

Chiaramente all’interno di ciascuna delle quattro classi ci si può limitare ad applicare un

algoritmo FIFO.

Ci sono poi altri accorgimenti che si possono accompagnare a questi algoritmi per renderli

più efficienti. Ad esempio UNIX tiene memoria dell’identità delle pagine (per identità si

intende : chi sono , e ciò è normale ; ed inoltre che cosa contengono, e ciò è fatto per l’otti-

mizzazione) che stanno nel pool libero (cioè nell’insieme delle pagine che il demon ha but-

tato logicamente fuori dalla memoria), in modo tale che, se si verifica un fault perché c’è

bisogno di una pagina che appartiene al pool libero, essa viene “resuscitata” senza essere

caricata dalla memoria di massa.

Nel momento in cui un processo viene creato, e quindi viene caricato in memoria, ovvia-

mente deve ricevere un certo numero di pagine. Un problema è il numero di pagine da as-

segnare ad un processo. Le possibili soluzioni sono le seguenti :

Si assegna al processo un numero minimo di pagine e poi ci si affida ai fault. Il numero

 minimo di pagine dipende in questo caso dal numero minimo di pagine che sono neces-

sarie per eseguire un’istruzione in linguaggio macchina (ciò dipendente principalmente

dal numero massimo di operandi che un’istruzione può avere).

Si da un numero di pagine costante uguale per tutti i processi.

 Si assegna ad ogni processo un numero di pagine proporzionale alla sua dimensione.

 Si assegna ad ogni processo un numero di pagine proporzionale alla sua priorità.

 Si assegna ad ogni processo un numero di pagine proporzionale alla sua dimensione e

 alla sua priorità.

TRASHING

Un problema importante è quello del trashing.

Quando in un sistema ci sono molti page fault significa che il sistema sta lavorando male

in quanto molte risorse del sistema sono spese per la sostituzione delle pagine.

Il fenomeno del trashing è appunto quel fenomeno che si verifica quando un processo o

un insieme di processi richiedono più risorse per fare lo swap-in e swap-out delle pagine

che per effettuare la normale computazione. Chiaramente un processo genera un gran nu-

mero di fault quando ha un numero di pagine assegnato che non è sufficiente alle sue esi-

genze. Ovviamente questo fenomeno può essere sia relativo :

ad un singolo processo del sistema

 ad un insieme di processi del sistema

 all’intero sistema.

Ad esempio, supponiamo di avere una strategia di gestione a “pagine piene”, ed un algo-

ritmo di sostituzione locale. In queste condizioni, sia dato un processo che sviluppa molti

fault in quanto non ha a disposizione un numero sufficiente di pagine per portare avanti

l’elaborazione. Siccome la sostituzione avviene nell’ambito delle stesse pagine del processo

esso non uscirà ma da questa situazione ; ci troviamo in presenza di un fenomeno di tra-

shing locale. 188

Se invece io ho un algoritmo di sostituzione globale, non è possibile avere un trashing lo-

cale in quanto nel momento in cui si verifica un page fault la pagina viene recuperata dal

pool libero ( strategia a pagine libere) oppure è probabile che sia presa dalle pagine di

qualche altro processo (strategia a pagine piene )mettendo così in difficoltà la paginazione

di quest’ultimo che probabilmente “ruberà” le pagine di qualche altro. Si cadrà così in un

circolo vizioso che porta ad un trashing del sistema di portata più ampia.

Un esempio in cui si può avere un trashing globale è in quelle macchine in cui il livello di

multiprogrammazione è regolato in modo automatico in base alla percentuale di utilizzo

della CPU : Supponiamo che il sistema stia portando avanti un certo numero di processi e

ad un certo punto si accorga che la percentuale di utilizzo della CPU è bassa. Attribuendo

questo fatto alla mal composizione dei processi (sproporzione tra CPU_BOUND

e I/O_BOUND), per risolvere il problema viene aumenta in modo automatico il grado di

multiprogrammazione (ciò significa portare altri processi in memoria togliendo pagine a

quelli già presenti). Se il poco utilizzo della CPU non era dovuto alla mal composizione dei

processi ma ad un fenomeno di trashing l’aumento del grado di multiprogrammazione

porterà molto probabilmente ad un trashing totale del sistema.

La cosa ideale da fare quando ci si accorge che un sistema è in trashing è prendere un certo

numero di processi che sono attivi in memoria e farne uno swap-out complete, cioè elimi-

narli completamente dalla memoria centrale.

Per evitare il trashing la cosa ideale è quella di far si che ogni processo abbia a disposizione

un numero adeguato di pagine. Tale numero di pagine varia per uno dato processo a se-

condo del punto del proprio flusso di controllo in cui si trova (ad esempi in un certo punto

ci potrebbero essere molti salti a pagine differenti o un ciclo molto ampio, mentre, in un al-

tro punto, ci potrebbe essere un ciclo molto stretto). Quindi per evitare il trashing si do-

vrebbe implementare una tecnica che assegni ad ogni processi un numero di pagine varia-

bili durante la sua evoluzione ; ciò significa applicare il concetto del working-set. Il wor-

king-set di un processo è definito come l’insieme delle pagine che vengono utilizzate alme-

no una volta in un certo numero di riferimenti più recenti. Quindi abbiamo un certo nume-

ro di riferimenti fissato che mi determina quali pagine devono fare parte del working-set.

Ciò vuol dire che se un processo, nella sua evoluzione comincia a lavorare su pagine diffe-

renti da quelle in cui lavorava precedentemente, accadrà sicuramente che qualcuna delle

pagine del working-set ( qualche vecchia pagina) non venga mai referenziata e quindi la

posso togliere ; viceversa se io mi accorgo che il processo, negli ultimi n riferimenti, ha re-

ferenziato tutte le pagine del suo working-set e la frequenza di paginazione è alta, significa

che il processo ha bisogno di molte pagine e quindi gli devo ingrandire il working-set.

Ciò può essere realizzato avendo a disposizione sempre i bit di riferimento e un timer che

fa partire una routine la quale periodicamente (cioè quando parte) prende il bit di riferi-

mento lo mette nello shift register e lo azzera........la stessa cosa dell’algoritmo di sostituzio-

ne.

Una stima meno costosa del working-set ma più grossolana può essere fatta basandosi di-

rettamente sul page fault : Conto il numero di page fault avuti nell’ultimo intervallo di

tempo e se mi accorgo che sono in aumento e hanno superato un certo livello significa che

le pagine che tiene il processo sono inadeguate e che quindi il suo working-set è insuffi-

ciente e quindi nei prossimi page fault devo sostituire le pagine prendendole da altri e non

189

da lui. Viceversa se la sua frequenza di page fault si mantiene costante vuol dire che le sue

pagine gli sono sufficienti . se la sua frequenza di paginazione tende ad abbassarsi vorrà

dire che quando la frequenza di paginazione di altri si alza le sue pagine sono candidate

ad essere sostituite. Ciò permette di ridistribuire le pagine tra i vari processi scegliendo

sempre il bit di riferimento come discriminante del fatto che una pagina debba essere sosti-

tuita o meno. /

INTERLOCK DI I O

Se, in un ASG io tolgo pagine ad un processo che in quel momento è bloccato (potrebbe es-

sere un criterio) debbo stare attento se il processo bloccato sta facendo un operazione di

I/O riguardante la pagina che io voglio sostituire ; in al caso infatti la pagina non può esse-

re sostituita. Un modo è quello dimettere un bit di interlock di I/O alle pagine(così fa

UNIX. UNIX ha anche un bit di interlock (che serve per l’algoritmo di seconda chance)

quando un processo ha fatto un fault perché gli serviva una pagina ma non ha potuto an-

cora utilizzare quella pagina in quanto non ha ottenuto ancora la CPU ; in tal caso la pagi-

na viene bloccata da questo bit).

PREPAGING

Se un processo viene portato totalmente fuori perché servivano le pagine da lui occupate,

nel momento in cui viene riportato dentro dobbiamo dargli perlomeno il numero di pagi-

ne che teneva quando lo abbiamo buttato fuori in modo da ridargli il suo set di lavoro che

aveva. Quindi anche questa è un informazione di cui va tenuto conto nel momento in cui

viene gestito lo swap-in e lo swap-out.

PROBLEMA DELLA DIMENSIONE DELLE PAGINE

in questo problema non c’è niente di nuovo rispetto a quando visto per la memoria centra-

le : una pagina piccola significa minore frammentazione e riesco a soddisfare meglio la lo-

calità del programma (in quanto tasselli più piccoli riescono a coprire meglio il flusso di

controllo del programma, cioè posso sfruttare una definizione maggiore della memoria).

D’altro canto un numero maggiore di pagine piccole sono più difficili da gestire e sono più

costose nelle operazioni di I/O (in quanto, di solito le dimensioni dei blocchi di disco coin-

cidono con quelle delle pagine e quindi piccoli blocchi mi costringono a perdere molto

tempo per i seek e le latenze). Tutti questi difetti sono ovviamente i vantaggi delle pagine

grandi a cui si aggiunge il fatto che con pagine grandi diminuisce la probabilità di page

fault. 190

24/4/97 .

GIOVEDÌ PROF FASOLINO

Le primitive per la gestione dei processi che ci rimangono da vedere sono quelle :

Per lo scambio di messaggi. Infatti, un altro mezzo che UNIX fornisce per la comunica-

 zione tra processi è una struttura dati organizzata a coda di messaggi attraverso la qua-

le i vari processi comunicano inserendo e prelevando i messaggi secondo la strategia ti-

pica della coda.

Per la gestione dei semafori utili per la sincronizzazione dei processi.

 Per la gestione delle PIPE (condotto) tra i processi, dove per PIPE si intende un canale

 attraverso cui i processi producono degli output per altri processi o prendono degli in-

put provenienti da altri processi.

Cominciamo a occuparci delle primitive per la gestione dei semafori. Prima di iniziare ri-

cordiamo che la volta scorsa abbiamo dette che per le quattro strategie che UNIX mette a

disposizione per la IPC (inter process comunication), ci sono delle regole comuni.

La prima regola riguardava l’assegnazione di una risorsa, e diceva che la system call che

richiedeva l’assegnazione della risorsa doveva sempre specificare una chiave. La chiave è

un intero che il sistema utilizza per identificare in modo univoco una risorsa.

Vedemmo che c’erano sostanzialmente due modi per specificare una chiave, di cui il primo

è praticamente il modo che viene utilizzato dai processi che hanno intenzione di mettere a

disposizione la risorsa con tutti i propri figli :

chiave=ipc_private

Quando si parla invece di processi indipendenti, cioè che non appartengono allo stesso

gruppo, un processo che vuole utilizzare una risorsa deve fornire la chiave della risorsa

che gli interessa (tale chiave sarà stata definita dal creatore di quella risorsa) cioè deve spe-

cificare il numero intero corrispondente alla risorsa.

Siccome abbiamo parlato di gruppi di processi, puntualizziamone qualche aspetto.

UNIX permette di organizzare anche i processi in gruppi (così come fa con gli utenti). Ogni

gruppo è identificato da un intero che per default è quello della shell che ad esso corri-

sponde ; cioè tutti i processi che sono stati creati a partire da una determinata shell fanno

parte tutti del gruppo di quella shell.

Se io faccio un’operazione opportuna ho la possibilità di modificare il gruppo di apparte-

nenza di un processo. L’utilità di cambiare il gruppo di appartenenza di un processo sta

nel fatto che ogni volta che si chiude una shell cioè alla fine di ogni sessione di lavoro tutti

i processi appartenenti a quella shell verrebbero automaticamente terminati ; se invece io

voglio che un gruppo di processi continui a vivere anche dopo la chiusura della sessione di

lavoro, basta che io gli cambi il gruppo di appartenenza scegliendone uno diverso da quel-

lo della shell attiva.

La seconda regola, riguardava la modalità di assegnamento di una risorsa. Questa modali-

tà serve per far capire innanzitutto se la risorsa viene creata al momento e quindi siamo

noi i creatori della risorsa dobbiamo specificare i permessi, oppure se non siamo i creatori

cioè c’è stato qualcuno che in precedenza ha chiesto il rilascio della risorsa dobbiamo sem-

plicemente specificare che la vogliamo utilizzare anche noi e quindi specifichiamo una mo-

191

dalità ipc_alloc. Quindi i permessi li specifica sempre il creatore dando la modalità

ipc_creat /permessi chi invece utilizza quella risorsa deve specificare semplicemente

ipc_alloc.

Le system call per la gestione dei semafori sono 3 :

: per la richiesta di un gruppo di semafori

SEMGET

1. : per il controllo dei semafori (ad esempio rilascio, definizione del valore del se-

SEMCTL

2. maforo, acquisizione dello stato del semaforo ecc.)

: permette di modificare lo stato del semaforo e quindi permette di risolvere i

SEMOP

3. problemi di sincronizzazione realizzando le primitive di alto livello wait e signal.

La richiesta di un gruppo di semafori si ha con :

SEMGET

Ricordiamo che UNIX gestisce i semafori in gruppi e quindi ogni volta devo specificare ne

gruppo che sto richiedendo, quanti semafori ci sono (es. 1). Questo fatto porta come conse-

guenza che le operazioni che andiamo a fare su un semaforo devono poter essere eseguite

contemporaneamente su ognuno dei semafori perché se questo non fosse possibile si

avrebbe il blocco dell’operazione.

Nella vanno specificati :

SEMGET

 CHIAVE

 NUMERO SEMAFORI

 MODALITA DI ASSEGNAMENTO

_ / (per chi crea la risorsa)

IPC CREAT PERMESSI

1. _ (senza permessi ; per chi la usa)

IPC ALLOC

2.

La restituirà :

SEMGET

un identificativo (>0) di tipo predefinito che identificherà il gruppo di semafori (tale

 identificativo servirà per referenziare il gruppo di semafori).

-1 in caso di errore :la risorsa non è stata rilasciata.

All’atto dell’allocazione di un gruppo di semafori, a ciascun semaforo del gruppo corri-

spondono alcune variabili :

semval (contiene il valore del semaforo : 0 o intero positivo ; ovviamente mi serve

per conoscere il valore del semaforo)

semzcnt e semncnt (riportano qual è il numero di processi che è in attesa sulla coda

del semaforo in attesa che semval diventi =0 o <> 0 rispettivamente per le due varia-

bili)

La sintassi della è :

SEMGET

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h> 192

int semget (chiave, num_sem, modo)

Esempio di uso di :

SEMGET

key_t chiave ;

int id_sem ;

.

.

.

chiave = ftok (“/usr/des”,’a’) ;

id_sem = semget (chiave,2,ipc_creat0664) ;

if (id_sem == -1)

perror(“semget”)

exit(2)

I figli di questo processo erediteranno questa risorsa e la potranno quindi referenziare au-

tomaticamente.

Per modificare il “colore” di uno o più semafori di un gruppo si usa la:

SEMOP

Nella vanno specificati :

SEMOP (esso è l’id_sem restituito dalla getsem)

 IDENTIFICATIVO DEL GRUPPO DI SEMAFORI

(cioè, devo indicare quale o quali semafori voglio

 INDIRIZZO DI UNO O PIÙ SEMAFORI

modificare all’interno del gruppo indicando l’indirizzo di una o più strutture

sembuf, dove sembuf è una struttura dati che viene associata ad ogni semaforo)

(quindi il numero di operazioni

 IL NUMERO DI SEMAFORI SUI QUALI SI DEVE OPERARE

da eseguire).

La struttura associata ad un semaforo è il seguente record contenente i seguenti 3 campi :

short sem_num /*numero del semaforo nel gruppo. Cioè dobbiamo immaginare

la struttura dati “gruppo di semafori” come un array di record

a 3 campi */

short sem_op /*valore da sommare al valore di partenza del semaforo per

modificarne il colore */

short sem_flag /* play di controllo che serve a stabilire il comportamento che il

sistema deve avere nei riguardi del semaforo in caso di anoma-

lie. Ad esempio se esso vale sem_undo succede che se un pro-

193

cesso ha messo un semaforo a rosso e termina senza prima di ri-

metterlo a verde, il sistema evita il blocco critico di eventuali al-

tri processi (che erano in attesa sulla coda di quel semaforo),

mettendo automaticamente il semaforo a verde. Più in generale

il processo ripristina lo stato del semaforo alterato dal processo

terminato.

In effetti non abbiamo le primitive di alto livello wait e signal, ma abbiamo il semaforo ini-

zializzato ad un certo valore attraverso una opportuna primitiva. A tale semaforo è asso-

ciata la struttura dati sopra vista per permetterci di specificare gli opportuni parametri che

permettono la giusta modifica del semaforo giusto. Usando questa struttura noi possiamo

realizzare tutte le primitive di sincronizzazione basate sull’utilizzo di semafori.

Ad esempio un wait su un semaforo viene fatta semplicemente :

indicando in sem_num su quale semaforo del gruppo la si vuole fare

 indicando -1 in sem_op ; che decrementa di 1 il semaforo

 specificando opportunamente il flag

 eseguendo infine la primitiva semop

Quindi la struttura dati di cui sopra deve essere inizializzata prima di eseguire la semop a

secondo della operazione che vogliamo eseguire sul semaforo.

La sintassi della è :

SEMOP

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

int semop (id_sem,vsops,ntops)

int id_sem vsops nsops ;

shrid sembuf *vsops[]; /*sembuf è un tipo predefinito che è un puntatore a un

array che si chiama vsops*/

Quindi la variabile vsops rappresenta l’indirizzo del vettore delle strutture dati associate

ai semafori del gruppo. La variabile ntops rappresenta invece il numero di operazioni che

si vuole eseguire.

La semop è un’operazione atomica, cioè non può essere interrotta e quindi tutte le opera-

zioni indicate vengono eseguite senza possibilità di interruzione. Ciò comporta che se una

delle operazioni che abbiamo indicato è illecita (ad esempio è stato specificato un semaforo

che non c’è nel gruppo) ci sarà un errore e nessuna delle operazioni, anche se lecita, verrà

portata a termine e il processo si sospenderà.

Una volta compilata la struttura sembuf si potrà eseguire l’operazione semop. Vediamo

adesso i vari casi che si possono avere in funzione del valore specificato nella semop e il

valore del semaforo semval :

se sem_op=-1 e semval=1 semval=0 (equivale ad una wait : il processo in questione

 avendo trovato il semaforo ad 1 passa mentre i

seguenti lo trovano a 0 e si bloccano)

194

se sem_op=-1 ma semval=0 la system call è sospesa finché il semaforo è alzato da

 qualche altro processo

se sem_op=1 e semval=0 semval=1 (equivale ad una signal)

Esempio di uso di :

SEMOP

struct sembuf vsop[3] ;

id_sem = semget (chiave,3,ipc_creat0666) ; /*chiede il rilascio di un gruppo di 3

semafori dando a tutti il diritto di leggere e scrivere sui semafori */

semctl (id_sem,0,0,setall,array) ; /*inizializza tutti i semafori del gruppo al valore

nullo (dopo analizzeremo meglio nel dettaglio la primitiva semctl)*/

/*decrementiamo il secondo semaforo del gruppo (con indice=1)*/

/*riempiamo la struttura d’appoggio */

vsops[1].sem_num=1 ;

vsops[1].sem_op=-1 ;

vsops[1].sem_flag=sem_undo ;

/*se avessi voluto operare su tutti e 3 semafori avrei dovuto fare le operazioni di

settaggio anche su vsops[0] evsops[2]*/

semop (id_sem,&vsops[1],1) ;

/*notiamo che siccome il semaforo era stato inizializzato a 0 la semop con -1 qui

sopra non viene eseguita fino a quando qualcuno non porta il semaforo ad

1*/

/*a questo punto, se voglio effettuare un incremento del semaforo basta risettare

il valore si sem_op e rieseguire senop*/

vsops[1].sem_op=1 ;

semop(id_sem,&vsops[1],1) ;

N.B . Le variabili chiave, id_sem ecc. si intendono definite altrove.

Il controllo di un gruppo di semafori si ha con :

SEMCTL

La sintassi della è :

SEMCTL

semctl (id_sem,sem_num,end,arg)

Per eseguire l’azione di controllo :

_ per leggere i dati del gruppo di semafori

 IPC STAT

_ er variare alcune caratteristiche (es. permessi)

 IPC SET P

_ per rilasciare la risorsa

 IPC RMID restituisce il valore di semval del semaforo specificato

 GETVAL setta ad un intero il semval di un semaforo

 SETVAL legge i valori attuali di tutto il gruppo

 GETALL setta il valore di tutto il gruppo

 SETALL 195

Esercizio : Nel modello a memoria comune realizzare la competizione di due processi per

l’uso di una risorsa comune :

P ,P = processo lettore e processo scrittore

1 2

A = lettura

1

A = scrittura

2

Notiamo che non vogliamo risolvere il problema lettore scrittore ma semplicemente il pro-

blema della mutua esclusione tra due processi ; cioè dobbiamo unicamente fare in modo

che l’accesso all’area di memoria condivisa avvenga in modo esclusivo tra lettore e scritto-

re.

In UNIX bisogna :

1. creare i 2 processi

2. chiedere il rilascio dell’area di memoria condivisa

3. chiedere il rilascio di un semaforo per la mutua esclusione

4. gestire il semaforo 196

Sistemi Operativi

Prof. FASOLINO 28/4/1997

Vediamo ora come risolvere l'esercizio proposto nella scorsa lezione :

#include <stdio.h>

#include <sys/types.h>

#include <sys/wait.h>

.....................................

#define DIM 16

#define NVOLTE 16

main ( ) { pid_t pid;

int status;

key_t chiave=IPC_PRIVATE , c_sem=IPC_PRIVATE;

int idshared,id_sem;

struct sembuf sem_buf;

char * prt_shared, *cursor, val=’a’;

int i,k=1;

/* Richiede un segmento di memoria condivisa */

id_shared=shwget(chiave,DIM,IPC_CREATE/0664);

prt_shared=shwat(id_shared,0,0);

/* Richiede un semaforo */

id_sem=semget(c_sem,1,IPC_CREATE/0664);

/*Setta il semaforo a verde */

sem_buf.sem_num=0;

sem_buf.sem_op=1;

sem_buf.sem_flag=SEM_UNDO;

semop(id_sem,&sem_buf,1);

*/ Genera il figlio scrittore */

pid=fork();

if ( ! pid ) { /* Codice scrittore */

printf("Sono lo scrittore : PID=%d PPID=%d \n",getpid(),getppid()

); for k=0,k<NVOLTE, K++ {

/*Setta a rosso e scrive */

/ sem_buf.sem_num=0;

WAIT | sem_buf.sem_op=-1;

MUTEX | sem_buf.sem_flag=SEM_UNDO;

\ semop(id_sem,&sem_buf,1);

printf ("scrittore %c \n",val);

REGIONE CRITICA - *prt_shared=val;

sem_buf.sem_num=1; /* verde */

SIGNAL MUTEX semop(id_sem,&sem_buf,1);

val++;

sleep (5);

} 197

exit (0);

}

else { /* Genera il lettore */

pid=fork();

if ( ! pid ) { /* Codice lettore */

printf("Sono il lettore : PID=%d PPID=%d \n",getpid(),getppid() );

sleep(3);

for k=0,k<NVOLTE, K++ { /* Ciclo di lettura

WAIT | sem_buf.sem_op=-1;

MUTEX | semop(id_sem,&sem_buf,1);

val=*prt_shared;

REGIONE CRITICA - printf ("lettore %c \n",val);;

sem_buf.sem_num=1;

SIGNAL MUTEX semop(id_sem,&sem_buf,1);

sleep (3);

}

exit (0);

}

else { /* padre

pid=wait (&status);

printf ("1^ figlio terminato PID=%d S=%d"), pid, status;

........................................................

semctl (id_sem,IPC_RMID,0);

shmctl(id_shared,IPC_RMID,0);

}

}

}

Commentiamo tale programma :

Troviamo una parte dichiarativa di tutte le variabili che ci servono per implementare il problema;

notiamo che le due chiavi che sono state dichiarate una per l'area di memoria condivisa e l'altra per

il semaforo sono IPC_PRIVATE perché se ne chiede il rilascio da parte del processo padre il quale

metterà poi a disposizione la risorsa ai processi figli ; con questa dichiarazione i figli hanno automa-

ticamente visibilità della risorsa.

Ci sono poi id_shared e id_sem che fanno da identificativi della risorsa e puntano all'area di memo-

ria condivise ed al semaforo e poi c'è una struttura di appoggio di tipo sem_buf per realizzare le

operazioni sul semaforo. Facciamo l'ipotesi di avere un buffer di dimensioni 16 byte e di scrivere su

questa area di memoria condivisa per cicli di scrittura consecutivi un carattere diverso di volta in

volta, partendo da 'a' . Poi si richiede il segmento di memoria condiviso tramite le opportune proce-

dure. A questo punto basta settare il semaforo a 1 ( si poteva fare anche con SEM_CTRL). Poi si ge-

nerano i figli: prima lo scrittore con il suo codice che stampa un messaggio e poi fa partire i cicli di

scrittura; ogni scrittura viene fatta in mutua esclusione ed si trova in una regione critica; alla fine si

incrementa val e si sospende il processo per 5 secondi per rallentare il ciclo di scrittura. Successiva-

mente c'è il codice del lettore che è del tutto analogo a quello dello scrittore; abbiamo sono introdot-

to un certo ritardo all'inizio per dare allo scrittore il tempo di cominciare a scrivere. Nell'ipotesi che

il processo scrittore sia stato schedulato prima del processo padre il quale poi crea il figlio, non ci

sono problemi perché c'è almeno un carattere nel buffer. Viceversa se ammettiamo che il processo

padre crei il figlio e poi abbia di nuovo assegnata la CPU, una volta creato direttamente il lettore,

198

questo potrebbe leggere prima che ci sia un carattere depositato dallo scrittore. Per impedire un caso

del genere bisogna aggiungere dei controlli non previsti in questo semplice programma.

Infine c'è l'else che si riferisce al corpo del padre che aspetta semplicemente la terminazione dei figli

e rilascia le risorse. Senza le ultime due istruzioni l'area di memoria rimarrebbe allocata inutilmente.

In UNIX c'è il comando IPCS che permette di conoscere lo stato delle risorse. IPCRM serve invece

ad eliminare la risorsa.

Diamo alcuni cenni su come operare con UNIX :

Per aprire una sessione di lavoro in UNIX si specifica il nome dell'utente al login e poi la password;

a questo punto si ha un messaggio che dice l'ultima volta che ci si è allogati e si ha il prompt.

Per chiudere la sessione si usa CTRL-D oppure LOGOUT.

L'interfaccia utente è a linea di comando con il seguente formato :

comando flag [argomento....]

es: ls -l -F file1 file2 file3

parametri argomenti

con le convenzioni [ ]= opzionale ....=ripetizione dell'ultimo argomento

-Vediamo ora i comandi base di UNIX :

WHO [AM I ] permette di verificare gli utenti allogati o da informazioni sul proprio login

MAN permette di richiamare il manuale in linea diviso in sezioni che possono essere specificate

o meno con -s.

PASSWD permette di cambiare la password immettendo la vecchia e la nuova due volte per evi-

tare errori

LAST user permette di conoscere l'ultimo login fatto e da quale macchina.

Vediamo ora alcuni cenni sul FILE SYSTEM di UNIX e cioè come sono organizzati i dati e quali

sono i principali comandi per gestire i File.

Il File system è quella parte del sistema operativo che provvede all'organizzazione dei dati degli

utenti e quindi alla creazione delle directory che permettono di organizzare logicamente questi dati.

Il File system di UNIX è gerarchico e assomiglia molto a quello DOS; vediamone le caratteristiche :

- è organizzato ad albero a partire dalla Root ( / )

- UNIX vede i file come sequenza di caratteri (byte); ogni strutturazione logica è vista a livello più

alto

- ogni file è identificato dal suo I-NODE che è l'accesso ad una tabella ( I-STRUCTURE )che con-

tiene le informazioni sui file del sistema; essa è organizzata a Record che contengono l'I-

NODE (numero intero associato al file),il suo nome logico, il creatore del file, la data

dell'ultimo accesso ecc.

- UNIX vede anche i device come file speciali permettendo di realizzare la DEVICE INDEPEN-

DENCE 199

UNIX permette di gestire tre tipi di file, gli ordinari, le directory e i file speciali che come abbiamo

già detto corrispondono a device. Ogni file ha il suo identificativo l'I-NUMBER che è un numero in-

tero che permette di trovare il file all'interno del file system.

Al di sotto della Root si trovano le directory di sistema e di utenti ( \USR ).

A livello più basso una directory è costituita da una serie di directory entryes ed ha una rappresenta-

zione di questo tipo : entry entry

|.......| [ ] | | [ ] | | |..........|

^ ^

| \____________ I-NUMBER 2 bytes in SV

FILENAME 14 bytes in SV

Vediamo un esempio: | a La directory è organizzata in questo modo :

(107)

| b [. | 202 ] [.. | 107 ] [ c | 253 ] [ d | 289 ] [ EOF ]

(202)

c / \ d . - > Directory stessa

(253) (289) .. - > Directory padre

UNIX permette anche di creare dei link cioè di referenziare un unico file fisico con nomi simbolici

diversi (filename sinonimi). Supponiamo di avere (vedi fig. sotto) una directory 89 e un file al di

sotto con I-node 107; questo ultimo si può referenziare con nomi logici diversi (b e c) nell'ambito di

quella directory e ancora

con un altro link ( e ) nell'ambito dell'altra directory 210. Ci saranno più directory con I-node 107

avente però nomi logici diversi.

| e 89 .....[ b | 107 ] [ c | 107 ] [ d | 210 ]

(89) 210 [ e | 107 ] [ f | 402 ]

b /c \ d 107 [ ]

(107) --- (210)

e \ f 402 [ ]

(402)

89 contiene due entry col nodo 107 ( b, c) ; 210 ha 107 col nome e ed f con I-node 402; il 107 è

quindi puntato da tre entry con diversi nomi.

Parliamo ora del Path name. A partire dalla Root ( / ) troviamo le varie directory tutte separate da '/

';

ad esempio potremmo avere / USR / MARIO /PROG per arrivare a PROG (niente di nuovo rispetto

a DOS). Tipiche directory di sistema sono : /BIN, /DEV, /ETC, /LIB, /LOST + FOUND, /TMP,

/USR.

Ogni utente ha una sua HOME DIRECTORY che gli viene assegnata e che di solito si trova sotto

USR.

Per cambiare la directory di lavoro si usa il comando CD.

E' possibile identificare i vari file con Pathname relativi a partire dalla home directory, dalla root

ecc.

Ad esempio se mi trovo nella mia home directory (diciamo che sia Roberto ) posso fare:

/USR / ROBERTO / PROG /A per riferirmi ad A nel modo assoluto a partire dalla Root oppure

/PROG /A o anche . /PROG / A tenendo presente che ' . ' identifica la directory corrente.

200

Viceversa se da ROBERTO voglio accedere ad altri file che si trovano ad esempio sotto MARIO

posso fare:

/ USR / MARIO / PROG dalla Root se voglio accedere a PROG oppure con

.. / MARIO / PROG e vi accedo tramite il padre di ROBERTO indicato con ' .. ' o anche con

. / .. / MARIO / PROG sempre partendo da ROBERTO.

Esistono anche i caratteri Jolly che sono come in DOS * e ? (con lo stesso significato).

Parliamo ora dei FILE SPECIALI.

Ogni operazione di accesso ad una periferica avviene facendo riferimento al nome del file col suo

percorso.

Richieste di lettura/scrittura su file speciali causano quindi operazioni di I/O sul device corrispon-

dente.

Si ha il vantaggio di avere programmi che fanno riferimento a device che possono essere facilmente

modificate all'insaputa del programma stesso. Si ha uno schema di questo tipo :

COMANDI UNIX ---> | UNIX <- DISCHI

| FILE <- STAMPANTI ECC.

PROG. UTENTI ---> | SYSTEM

|

INTERFACCIA

Accenniamo ora all'implementazione dei file (la rivedremo poi meglio). L'I- number del file è la en-

try in una tabella del file system che contiene le informazioni relative ai file fisici caratterizzati dal

loro I- node.

In questa tabella ci sono varie informazioni tra cui il puntatore al file fisico. Esempio :

[ PIPPO | 102 ] 101 |......................................|

102 |......................................| ---> Blocchi dati file

Gli attributi del file possono essere :

- Tipo (ordinario, speciale, directory)

- Posizione (Pathname)

- Dimensione

- Numero di Link (quanti nomi ha il file )*

- Permessi

- Data di creazione

- Ultimo accesso

- Modifica

........................................

* Questa informazione è importante perché quando cancello il file devo cancellare anche tutti i Link

Vediamo ora qualche comando di base per gestire le directory:

PWD Visualizza la directory corrente col suo Pathname

CD Cambia directory; se non ci sono argomenti torna alla Home directory

LS Lista il contenuto di una directory (è analoga alla DIR del DOS); ha le seguenti opzioni:

-l lista ogni file su una linea

-s visualizza il size dei file 201

-t ordina i file in modo che il primo sia l'ultimo che è stato modificato

-f dice se il file è eseguibile (*) o è una directory (/)

-r visualizza ricorsivamente tutte le directory interne

-i dà l'I-number

-a visualizza tutti i file (ALL) anche quelli nascosti che cominciano con '.' (Dot-files)

DU Verifica lo spazio che un file occupa

MKDIR Crea una directory nella directory corrente o nel Pathname specificato

RMDIR Cancella una directory solo però se essa è vuota, altrimenti bisogna cancellare tutti i file.

LN nome1 nome2 Crea un Link assegnando a nome1 il nome logico nome2 ( solo per i file )

Trattiamo poi i comandi di gestione dei file :

MV Sposta un file da una directory ad un'altra; ha la seguente sintassi:

MV [OPTIONS] name.... target

Muove name sotto target se target è una directory, altrimenti lo sovrappone se target è esso

stesso un file; non si possono spostare più file su un target file.

CP Copia un file; ha la stessa sintassi di MV

RM Rimuove un file; non si può cancellare una directory a meno che non si usa l'opzione -r :

RM [ -r ] name.... cancella in maniera ricorsiva tutte le directory

Si possono usare ? per indicare un carattere qualsiasi e * per indicare una sequenza di ca-

ratteri Inoltre ad esempio :

RM file [123] cancella tutti i file che si chiamano file1,file2 o file3

RM file [a-z] cancella tutti i file che si chiamano filea, fileb fino a filez

TOUCH Modifica data e ora dell'ultimo accesso al file

FIND Permette di cercare un file 202

30/04/97 (DE CARLINI)

Tratteremo l’aspetto del sistema operativo che è maggiormente a conoscenza dell’utente.

L’utente sostanzialmente sa come lanciare un programma, come si entra nel sistema e come si lavo-

ra con la memoria di massa e quindi come si lavora con i driver (cioè con dischi) facendo operazioni

di tipo particolare come p.e. la formattazione e fondamentali tipo l’operazione salva e l’operazione

crea directory.

Avendo quindi una conoscenza abbastanza buona di una macchina DOS ed una conoscenza anche

superficiale di macchine UNIX, non entreremo nel dettaglio nella descrizione di certi concetti ma

vedremo fondamentalmente gli aspetti interni del S.O., per quanto riguarda appunto il File System,

per poi vedere l’organizzazione dei file e delle directory fisicamente nelle memorie di massa.

File System

Sappiamo che un file system gestisce essenzialmente una locazione di file sulla memoria di massa e

tutte le informazione relative ai file e permette di effettuare operazioni.

Quindi abbiamo due aspetti :

1)Problema di operazione ed allocazione sui file;

2)Problema di gestione delle informazioni che sono connesse ai file;

Un file system è organizzato in directory e file, le directory sono degli oggetti che contengono infor-

mazioni riguardanti i file e i file sono oggetti che contengono i dati.

In realtà dal punto di vista astratto un file system vede sia le directory che i file come un insieme di

file, infatti se le directory sono degli oggetti che sono presenti in memoria di massa e debbono con-

tenere delle informazioni non possono che essere essi stessi dei file.

I File

Normalmente ogni S.O. ha delle sue regole per la nominazione dei file. A prescindere dal nome, ab-

biamo che alcuni S.O. permettono di classificare i file secondo una certa tipologia, altri S.O. in real-

tà non lo consentono.

I S.O. che consentono di classificare i file secondo una certa tipologia, normalmente permettono di

esprimere anche la tipologia nel nome stesso del file, facciamo un

Esempio

I S.O. permettono di distinguere i file in eseguibili e non eseguibili. P.e. nel DOS i file eseguibili,

vengono distinti dal sistema dai file non eseguibili dal nome stesso del file (in realtà dalla sua esten-

sione), in particolare sono file eseguibili quelli con estensione EXE, COM (ed anche BAT il quale è

un command file cioè una serie di comandi interpretabili in batch file).

Abbiamo S.O. che permettono di gestire altri tipi di file oltre a quelli eseguibili e non, (p.e. per lo

stesso DOS sono i file batch detti prima).

Esistono S.O. che riconoscono se un file è un file testo (p.e. il DOS non riesce a riconoscerlo, ma da

comunque un file ASCII o binario).

Abbiamo altri S.O. che riconoscono i file codici sorgente, questo permette di far ripartire automati-

camente, nel momento in cui un file codice sorgente viene scritto in un Editor, i programmi che fan-

no la rincompilazione di tutti gli oggetti del file sorgente per evitare che una volta effettuata una

modifica al livello sorgente dimentichi di riportarlo sull’oggetto stesso.

Riassumendo : sostanzialmente tutti i S.O. distinguono tra file eseguibili e non, altri S.O. permetto-

no di riconoscere qualche altro tipo di file. 203

Alcuni S.O. permettono di indicare la struttura a record del file, cioè possiamo individuare in un file

i record logici costituiti da un certo numero di byte, questi S.O. permettono di effettuare un accesso

diretto al record logico, viceversa altri S.O. non permettono all’utente di eseguire il record logico in

quanto hanno un loro interno un record fisico standard.

P.e. in UNIX tutti i file vengono visti come una sequenza di byte e quindi a livello di S.O. non pos-

siamo organizzare i vari byte in record logici divisi in certi campi di lunghezza prefissata.

Altri S.O., viceversa, permettono di fare questo ma non tanto di individuare i singoli campi del re-

cord ma permettono di definire la lunghezza in byte complessiva del record, questo significa che al

livello di applicativo possiamo fare una chiamata al file system e leggere un qualsiasi record, questo

fa un certo algoritmo che individua il particolare record desiderato che sarà reso disponibile in un

buffer, poi sarà cura dell’operatore di individuare i vari campi e prelevare le informazioni che inte-

ressano, quindi l’accesso al S.O. lo facciamo direttamente al livello di record logico.

In UNIX, invece, è permesso di fare il singolo accesso “diretto” però dobbiamo specificare a quale

byte dobbiamo effettuare l’accesso, poi è a livello applicativo che dobbiamo vedere ogni record del-

l’applicazione di quanti byte è fatto e portarci al punto desiderato.

Oss. Quando parliamo di livello applicativo non intendiamo che debba farlo l’utente ma è un com-

pito del compilatore quando genera il codice. Infatti il problema viene risolto a livello di compila-

zione non al livello di S.O.

UNIX non ha quindi il comandi read e write ad accesso diretto, ma ha solo un accesso sequenziale,

però ha due comandi di cui uno permette di posizionare il puntatore ed un altro permette di leggere,

dove si deve posizionare il puntatore è un calcolo che dobbiamo fare a parte.

Quindi, come abbiamo detto, elementi fondamentali del file system sono le :

Directory

Tutti i S.O. gestiscono un oggetto particolare che è detto directory.

Questo è un file che contiene un array di record in cui ogni record è un descrittore di file.

Quindi una directory è un qualcosa che viene implementato da un file che contiene un array, dove

gli elementi di questo array è un record e questo è un descrittore di file.

Questo record contiene tutte le informazioni che permettono di gestire un particolare file presente

nella directory del sistema.

Tipicamente la struttura dei campi di un descrittore di file cambiano dal tipo di S.O.

Supponiamo di avere un array (vedi fig.) ogni elemento dell’array è un record , un elemento di que-

sto Campo

----------- ----------- Record

Array

nome ----------- -----------

204

Had unit Array

record è un descrittore di file ed i campi dipendono dal tipo di S.O. ed esistono alcune informazioni

standard.

Oss. Quando parliamo di descrittore di file lo intendiamo nel suo complesso, quindi nulla vieta che

nel file (visto come elemento di questo array) non ci siano tutte le informazioni del descrittore di

file, ma ci siano solo alcune informazioni e poi esista un puntatore che ci mandi su di un altro array,

nel quale ci sono delle informazioni che completano il descrittore di file (infatti UNIX funziona in

questo modo), e quest’ultimo array è unico per tutte le directory i-node.

Vediamo ora quali sono le informazioni memorizzate in un campo nel descrittore di file:

1)Nome del file (nome simbolico del file);

2)Tipo di file (solo in S.O. che gestiscono il tipo, cioè essenzialmente file eseguibili e non);

3)Locazione (dispositivo e allocazione sul dispositivo);

4)Posizione corrente (puntatore alla posizione corrente di lettura/scrittura del blocco)

5)Protezione (informazioni per il controllo dell’accesso);

6)Contatore di utilizzo (# di utenti che allo stato condividono il file, poiché se p.e. vogliamo elimi-

nare il file dobbiamo vedere se tutti gli utenti sono d’accordo);

7)Ora e data (ora, data e identificatore di processo);

8)Identificatore di processo (relativamente ad operazioni quali: creazione, ultima modifica e ultimo

utilizzo del file)

Oss. Per quanto riguarda la locazione, sappiamo che nel S.O. DOS non abbiamo un solo file system

ma ne vediamo tanti( è una foresta ),viceversa nella macchina UNIX il file system è unico, questo

indipendentemente se il file lo abbiamo sull’Hard-Disk o su floppy disk.

Quindi dato che su UNIX il file system è unico, indipendentemente dai supporti fisici (cioè da i vari

file), per poter lavorare con unità esterne dobbiamo fare in modo di collegare questo all’intero File

system per poterli rendere visibili (questo perché il file system non è una foresta di alberi, ma è un

unico albero).

Oss. Per quanto riguarda la posizione corrente, questa la utilizziamo quando facciamo una lettura e

scrittura di un file, e sappiamo che prima di poter scrivere su di un file dobbiamo fare l’operazione

di aperture a di un file (peraltro questo non è previsto da tutti i S.O.) e questa operazione consiste

nel prendere il descrittore del file e portarlo in una cache in memoria RAM.

Quindi nella RAM il S.O. gestisce una tabella (vedi fig.)

Tabella dei file aperti

Descrittore di file compl.

205

che è la Tabelle Dei File Aperti in cui ciascun rigo di questa tabella contiene un descrittore di file

completo e solo da questo momento possiamo scrivere e leggere e dopo questa operazione la posi-

zione del puntatore non ha alcuna importanza.

Tra questi campi il più importante è il campo locazione, la cui articolazione può essere più o meno

articolata dipendentemente dalle varie tecniche di allocazione che il S.O. gestisce sui file.

Oss: Vedremo nelle prossime lezioni quali sono queste particolari tecniche di allocazione e per ogni

tecnica ci saranno delle particolari informazioni che dovranno essere gestite e che sono presenti nel

campo che abbiamo chiamato locazione( il quale nei fatti è anch’esso un record quindi ha anch’esso

una strutturazione interna).

Soltanto nei S.O. elementari il file directory coincide con il file della directory che è presente sul di-

sco fisico (cioè se abbiamo un supporto fisico, un dischetto p.e. probabilmente su questo, nei primi

blocchi, è presente un file in sono contenute tutte le informazioni relative ai file del dischetto

stesso).

Gestione di un file directory

Le tre operazioni fondamentali che vengono fatte a livello di directory sono la creazione e la cancel-

lazione di un file oppure l’apertura o la chiusura o visualizzazione dei dati relative ad un file.

Creare o aggiornare un file significa aggiungere un descrittore di file all’interno di un file directory,

ovviamente cancellare un file significa eliminarlo, visualizzare significa accedere o ricercare dei

dati.

Queste operazioni presuppongono ovviamente una visita dei descrittori di file, ed in particolare la

creazione deve fare una operazione di visita totale, mentre la visualizzazione e la cancellazione pre-

suppongono una visita parziale. Infatti dal momento in cui vogliamo creare un file dobbiamo essere

certi che non esiste un file con lo stesso nome, per la cancellazione e per la visualizzazione ci fer-

miamo appena lo troviamo.

Dal momento di creazione del file dobbiamo trovare lo spazio per allocare in un descrittore di file,

poiché di solito quando viene fatta la cancellazione di un file lo spazio non viene ricompattato, allo-

ra lo spazio disponibile sarà quello occupato da un precedente file e, se la tecnica lo consente, si

può allargare questo spazio se ciò e indispensabile per la sua allocazione.

Oss. Questo fa capire che la cancellazione è un fatto logico e non un fatto fisico, poiché il descritto-

re ha un puntatore fisso in memoria.

Esistono un insieme di organizzazioni che variano a seconda del S.O. i quali cercano di velocizzare

al massimo le operazioni di creazione , cancellazione dei file e di visualizzazione ossia di ricerca di

un descrittore.

Ovviamente nel momento in cui vogliamo lavorare su di un file, le operazioni fondamentali sono :

1)Apertura di un file;

2)Chiusura di un file;

3)Creazione di un file(scrittura);

4)Lettura di un file;

5)Reset di un file;

Queste operazioni equivalgono a fare l’operazione di trasferimento tra la memoria di massa e la

RAM (e viceversa) di un descrittore di file. 206

L’apertura di un file è una operazione in cui trasferiamo un descrittore del file dalla directory alla ta-

bella e quindi anche questa operazione è onerosa, poiché presuppone la ricerca in un file directory il

descrittore del file che dobbiamo aprire. Peraltro, come abbiamo detto, l’operazione di apertura è

fondamentale poiché trasferiamo tutto il descrittore nella Tabella dei File Aperti (vedi fig.) e quindi

per una successiva operazione non abbiamo più bisogno di accedere a memoria di massa.

Quindi aprire un file significa assegnare una nuova entry nella Tabella dei File Aperti e questo si-

gnifica anche dire che da questo momento in poi non abbiamo più bisogno di inizializzare il file con

il nome simbolico ma viene referenziato a livello sistema dall’entry della tabella dei file aperti.

Esempio

Una volta che abbiamo aperto il file PIPPO l’operazione successiva di scrittura e lettura su PIPPO

non viene fatto dal nome simbolico PIPPO ma vengono fatti in riferimento ad un numero (3,4,....)

che è la entry del descrittore di PIPPO nella Tabella dei File Aperti.

La Tabella dei File Aperti non è detto che sia unica, infatti possiamo avere più Tabelle dei File

Aperti e questo dipende come è organizzato il sistema ( in UNIX abbiamo più Tabelle dei file Aper-

ti ed ogni processo ha una propria tabella).

Alcuni S.O. richiedono la definizione del numero massimo di tabelle aperte contemporaneamente

(sono quelli che lavorano a tabella fissa ) e quindi bisogna conoscere qual è lo spazio totale disponi-

bile.

La chiusura del file è l’operazione opposta dell’apertura e comporta la registrazione del contenuto

dell’entry in memoria RAM della directory e l’annullamento dell’entry in RAM.

Oss. A livello di linguaggio, l’apertura e la chiusura di un file non è detto che siano presenti, cioè

esistono dei linguaggi che hanno degli stantement appositi per aprire e chiudere i file ed altri invece

in cui non sono previsti. Questo significa dire che il S.O. automaticamente apre nel momento in cui

fa la prima lettura (o scrittura) sul file e chiude nel momento in cui termina il programma e natural-

mente la chiamata al S.O. per aprire e chiudere è fatta dal compilatore.

Oltre all’operazione di apertura e chiusura di un file, altre operazioni che possiamo fare sono quella

di scrittura e lettura di un file.

Se abbiamo la Tabella dei File Aperti, nel momento in cui scriviamo non andiamo più ad accedere

al file directory ma dobbiamo accedere direttamente alla Tabella dei File Aperti dove troveremo di-

rettamente il puntatore. Se invece non c’è l’apertura dobbiamo fare un accesso al file directory e

dobbiamo eventualmente individuare il puntatore.

Oss. Stiamo parlando di scrittura e lettura in termini sequenziali di un file.

La stessa cosa vale per la lettura di un file ma con l’operazione inversa a quella di scrittura.

Abbiamo sistemi in cui il puntatore di lettura e scrittura sono distinti ed altri sistemi in cui è unico

(e questo dipende dal particolare S.O.) e in quest’ultimo caso è associato il comando di posiziona-

mento del puntatore che permette appunto di poter ridefinire la posizione del puntatore e quindi non

c’è bisogno che si abbia esplicitamente la lettura e scrittura diretta del file, poiché possiamo sempre

avere una coppia di comandi del S.O. nel quale il primo posiziona il puntatore ed il secondo legge o

scrive. Peraltro esistono S.O. che hanno anche le supervisor-call con le quali facciamo direttamente

una lettura e scrittura diretta e questo è particolarmente importante per i S.O. che hanno una struttu-

207

ra a record di un file, infatti definiamo prima qual è la struttura a record di un file e puntiamo diret-

tamente al record e quindi in questo caso non utilizziamo i puntatori(questo tipo di accesso non è

previsto da UNIX ).

Vediamo ora quali sono i metodi di accesso supportati dal S.O. ai file :

1)Accesso sequenziale (questo è offerto da tutti i S.O.);

2)Accesso diretto (questo è offerto da alcuni S.O.);

3)Accesso mediante un indice (Index Sequention Access Metod, cioè possiamo accedere ad un file

mediante un file indice (chiave) e questo è un tipo di accesso molto sofisticato usato da ISAM);

Vediamo alcuni esempi di comandi del S.O. per l’accesso sequenziale e diretto:

1)Read next (comando di una lettura sequenziale per un S.O. che prevede una struttura a record,

“leggi il prossimo record logico“);

2)Write next (comando di scrittura come sopra);

3)Read n (comando di lettura ad accesso sequenziale o diretto, è sequenziale se è inteso come ” leg-

gi i prossimi n byte”,( e questo è un comando esistente in UNIX ) e potrebbe anche essere visto

come un comando di accesso diretto in cui diciamo “leggi il record n” e quindi dipende dalla se-

mantica che diamo a questo comando);

4)Write n (comando di scrittura ad accesso sequenziale o diretto come sopra);

5)Position to n (comando che posiziona il contatore);

Oss. Questi sono comandi che evidenziano solo la semantica, poi ognuno di questi sarà espresso in

modo opportuno, p.e. in UNIX esiste una supervisor-call che è chiamata tramite una routine C.

Un’altra osservazione da fare, relativa ai file condivisi, è che dal momento in cui abbiamo la possi-

bilità di condividere dei file dobbiamo sapere se due utenti possiedono lo stesso file, vogliono aprire

lo stesso file e operare sullo stesso file oppure vogliono operare sullo stesso file senza avere la ne-

cessità di aprirlo a livello di directory

Esempio

Abbiamo un utente che vuole accedere al descrittore di un file per modificare le regole di accesso e

un altro utente per gli stessi motivi vuole accedere allo stesso descrittore di file.

Quindi abbiamo bisogno di avere dei S.O. che devono gestire dei LOCK

LOCK dei file condivisi

I Lock impediscono i conflitti che possono aversi per file condivisi. Possiamo avere dei Lock a vari

livelli, poiché la condivisione può avvenire a livello di descrittore di file qualora ognuno abbia il

proprio descrittore e viene rimandato ad un unico punto e quindi dobbiamo gestire la lettura e la

scrittura dell’accesso a questa parte del descrittore che è condivisa. Un altro tipo di Lock può essere

quello che potrebbe capitare in UNIX in cui due utenti aprono un file condiviso, cioè l’utente A ha

un descrittore della Tabella dei File Aperti visto dal processo A e il processo B vede anch’esso la

sua Tabella dei File Aperti e quindi avrà un entry che è relativa allo stesso file. UNIX permette que-

sto tipo di gestione dal momento in cui ha memoria di massa sufficiente e quindi ha una tabella de-

gli i-node dei file aperti (in RAM) Quando apriamo un file, il descrittore lo mette nella tabella in cui

208

convergono la coppia di puntatori e decide se le informazioni possono essere condivise o non (vedi

fig.). A

B Tabella Tabella i-node

Oss P.e. le tabelle A e B sono rispettivamente padre e figlio e sono quindi diverse ma hanno la ta-

bella di mezzo comune in modo tale che questi possano cooperare. Se abbiamo invece abbiamo che

A e B condividono il file ma non cooperano, cioè sono due programmi differenti che generano pro-

cessi differenti che però lavorano sullo stesso file allora ognuno di questi avrà il suo puntatore.

Se abbiamo condivisione di file abbiamo la possibilità che due processi accedano allo stesso blocco

di disco, quindi abbiamo bisogno di un altro Lock che però è gestito dal segmento del S.O. che ge-

stisce sequenziali zeri accessi sul disco. Quindi, in altre parole, non si può mai verificare che due

processi accedano fisicamente allo stesso blocco di disco, cioè comunque accederanno in sequenza

e tramite una gestione di ordinamento degli accessi ( p.e. applicando la routine dell’ascensore per

ottimizzare il tempo medio di attesa della coda).

Un altro tipo di conflittualità può nascere in una rete di computer, infatti da un PC arriva un file

condiviso da un altro PC e dal momento in cui lo apriamo può accadere che quel file venga trasferi-

to localmente e possiamo lavorarci Ovviamente quel file deve essere lockato per impedire ad altri

utenti la condivisione.

Possibilità di caricare un file in memoria virtuale.

Se abbiamo un sistema a memoria virtuale possiamo aprire un file e trasferirlo in memoria (cache).

Naturalmente il file non andrà in memoria RAM, ma se abbiamo una memoria centrale che è gestita

con il principio della memoria virtuale vuol dire che questo file viene trasferito dalla parte del disco

che è gestito dal file system viene copiato dalla parte del disco che gestisce il meccanismo della me-

moria virtuale e da quel momento in poi a livello applicativo leggiamo e scriviamo come se avessi-

mo tutto il file in memoria. Nei fatti questa area di memoria non esiste e il travaso tra memoria di

massa alla memoria centrale o viceversa viene gestita dal meccanismo che gestisce la memoria vir-

tuale, e questo ha il vantaggio di velocizzare gli accessi poiché la memoria virtuale funge da cache.

Altre operazioni che il S.O. può fare sui file sono:

1)Modifica di un blocco di file (cioè invece di avere i comandi di read e write abbiamo il comando

di modifica, cioè di rilettura e riscrittura)

2)Aggiunta di blocchi in coda (Append);

3)Copia di un file su disco o su altro (p.e. da un lato un disco e dall’altro un video ed una stampan-

te); 209

4)Dispositivi di I/O

Oss. I comandi di UNIX sui file sono:

CREAT per creare;

OPEN per aprire;

READ per leggere;

WRITE per scrivere;

LSINK per posizionare il puntatore;

CLOSE per chiudere;

TRUNC non cancella il file però lo azzera, cioè mantiene il nome, il descrittore e riduce a

zero la lunghezza fisica del file esistente;

RENAME nuovo nome;

CVOID per cambiare i diritti di accesso al file;

CARM per cambiare i diritti del proprietario;

START per avere informazioni del file nella directory;

210

2/5/1995

Prof. DE CARLINI

La lezione scorsa abbiamo detto che le directory erano dei file particolari i cui record erano

i descrittori dei files in esse contenuti.

In particolare, qualcuno di questi record potrebbe essere il descrittore non di un file dati

ma di un file-directory; questo significa, nei fatti che è possibile creare una struttura ad al-

bero delle directory.

Generalizzando questo discorso potremmo avere un file system a n livelli. Fino alla fine

degli anni settanta i file system erano piatti, cioè ad un solo livello. Successivamente nac-

quero i file system a due livelli (nei grossi mainframe): al livello uno c’era una directory

base che non conteneva files ma solo sottodirectory ed era accessibile unicamente al Super-

visore; al livello due vi erano le directory degli utenti, una per ogni utente, e tali directory

erano piatte perché non potevano avere sottodirectory.

I sistemi operativi odierni (a cominciare da UNIX e poi dal DOS) hanno una struttura mul-

tilivello. In generale tale struttura è una struttura a grafo. Quando il sistema non permette

la condivisione di file o directory, il grafo è un albero; quando è ammessa la condivisione,

il grafo è un grafo aciclico o addirittura un grafo generale(cioè contenente anche cicli). Ad

esempio guardiamo la figura uno:

DOCUMENTI VOCABOL TESTO1 CORRETTORE

AB MIO PIPPO WINNY ZZ 1

CIST PLUTO TIZIO

211

Nella figura 1 Abbiamo un grafo che non è un albero, infatti già se guardiamo unicamente

i files ci sono alcuni files che pur essendo comunque delle foglie, sono appesi a più rami;

del resto ci sono delle sottodirectory condivise. Se poi, qualche sottodirectory condivide

directory di livello superiore, come nel caso dell’arco uno il grafo si trasforma da aciclico

in ciclico.

Bisogna evitare che con la leicità della condivisione di directory, andando a condividere

qualche directory, si venga a creare una struttura ciclica. Il problema che si potrebbe avere

con un ciclo è che, venendo a mancare un link, si può avere un’intera parte del file system

che non è più raggiungibile in quanto manca il collegamento, ma che non essendo stata

cancellata occupa ancora dello spazio sul disco. Per recuperare tale spazio è poi necessario

far girare degli algoritmi che vanno a percorrere il file system non da un punto di vista lo-

gico ma vanno a fare un analisi a tappeto dei dischi andando a vedere se ogni blocco del

disco è referenziabile dalla parte della file system che è ancora raggiungibile e mettendo

nella lista del disponibile tutti quei blocchi del disco che non sono più raggiungibili (in so-

stanza questi programmi percorrono il file system segnando tutti i blocchi del disco che

man mano vengono raggiunti dal file system e mettendo poi nella lista del disponibile tutti

quelli che non sono stati segnati).

In ogni caso le informazioni presenti nelle directory rimaste isolate vengono perdute.

Implementazione della condivisione

La tecnica più naturale che si può pensare per realizzare la condivisione di file (o directory

ma ragioniamo in termini di files) è quella di duplicare il descrittore del file; Ad esempio,

nella figura 1 avremmo un descrittore nella directory Pippo e uno nella directory Winny.

Tuttavia questa tecnica ha dei notevoli inconvenienti. L’inconveniente principale di questa

tecnica è che avendo più descrittori che si riferiscono allo stesso file si deve poi stare attenti

a mantenere la coerenza di tali descrittori cioè a gestire il loro allineamento. Del resto, im-

plementando la condivisione attraverso la duplicazione dei descrittori non risolviamo il

problema critico della condivisione che è quello della cancellazione dei files; infatti volen-

do cancellare un file condiviso da una determinata directory si deve stare attenti a cancel-

lare il descrittore ma non il file altrimenti rimarrebbero appesi gli altri descrittori che face-

vano riferimento a quel file e che quindi andrebbero a puntare un qualcosa che non è più il

loro file.

Un terzo problema è che, avendo più descrittori di uno stesso file magari anche con nome

differente, si può rischiare di referenziare il file più volte. Ad esempio in un operazione di

backup io arrivo ad un file da due directory differenti nelle quali magari il file ha un nome

differente e quindi salvo due volte lo stesso file.

Il modo più diffuso per implementare la condivisione, usato anche da UNIX, è quello dei

link. 212

Un link è un elemento di una directory di tipo speciale che punta ad un elemento di un’al-

tra directory e quindi ad un file o ad una sottodirectory condivisa. In alcuni sistemi opera-

tivi il link può essere simbolico (soft) e quindi costituito da un pathname assoluto (cioè che

parte dalla radice) o relativo (che parte da una sottodirectory).

I link non simbolici vengono detti hard link.

Il link simbolico funziona in questo modo:

Quando viene effettuato un riferimento ad un file viene effettuata una ricerca nella directo-

ry che contiene il link, parte una routine il cui compito è quello di risolvere un pathname

(tale routine è tipica di tutti i sistemi operativi che hanno un file system organizzato a più

livelli) fino ad accedere al file reale.

Il vantaggio enorme che si ha con i link simbolici è che non c’è la possibilità che il sistema

fallisca a seguito di un accesso errato. Ad esempio se abbiamo un file condiviso da due di-

rectory e il possessore del file lo cancella, un programma applicativo che cercasse di acce-

dere a tale file dall’altra directory farebbe partire la routine che risolve il pathname che

non trovando il descrittore originale del file darebbe un messaggio di errore comportando

il fallimento dell’applicativo ma non il crollo del sistema. Cioè la routine che risolvere il

pathname prevede un uscita di errore qualora non sia stato possibile risolvere il percorso

ma non causa l’arresto del sistema. Ovviamente, a maggior ragione, cancellare un link sim-

bolico non comporta alcun tipo di problema.

Quindi, si capisce che con i link simbolici il problema della cancellazione dei files è almeno

in parte risolto in quanto un accesso ad un file inesistente viene trattato come qualsiasi ac-

cesso errato e quindi non essendoci la possibilità (come invece c’era nel caso precedente) di

accendere tramite un collegamento sbagliato, ad un file che magari appartiene a qualche

altro utente si può riallocare immediatamente l’area del disco occupata dal file cancellato.

Anche il problema (che anche apparteneva alla tecnica precedente) del disallineamento dei

descrittori ora non esiste più. Infatti con questo meccanismo noi abbiamo un solo descritto-

re in una directory e una serie di puntatori nelle altre directory che condividono il file,

quindi non essendoci più descrittori dello stesso file non abbiamo il problema del disalli-

neamento.

Anche i link simbolici hanno il problema che un file può avere più pathname assoluti.

Questo è un problema che riguarda specialmente i programmi applicativi perché vedendo

lo stesso file magari con nomi diversi possono accedere ad esso più volte.

Riguardo alla cancellazione di un file condiviso, un altro modo di procedere sarebbe quel-

lo in cui il proprietario del file nel momento in cui lo vuole cancellare controllo prima se

non ci sono più puntatori a quel file altrimenti lo cancella solo da un punto di vista logico

lasciando il descrittore fino a quando tutti gli altri utenti che condividevano il file non lo

cancellano; solo a questo punto lo spazio occupato dal file potrà essere recuperato. Questo

procedimento è facoltativo nel caso dei link simbolici (in quanto non ci sono problemi di

accessi errati) è invece necessaria nel caso degli hard link.

213

Infatti con gli hard link non si riesce ad evitare il problema (come accadeva con la stato du-

plicazione dei descrittori) che da un link appeso possa andare ad intervenire su aree di di-

sco che nel frattempo possono essere state allocate ad altri files magari di terzi.

Per realizzare una tale strategia di cancellazione si deve necessariamente disporre o di una

lista dei riferimenti oppure, perlomeno, di un contatore ai riferimenti; cioè devo sapere

quanti sono i link che insistono su un certo file in modo tale da impedire la cancellazione

del file finché questo contatore sia maggiore di uno.

In realtà questo è abbastanza semplice da realizzare. Infatti basta che nel descrittore reale

(cioè non in quello di tipo link) vi sia un campo contatore di condivisione. Ciò significa

dire che soltanto quando tale campo è uno nel momento in cui si chiede la cancellazione

del file, il descrittore vero del file, così come il file stesso vengono rimossi.

Precisamente, la cosa funziona in questo modo: una cancellazione da una directory che

contiene un link si traduce nella eliminazione del link in questione e nel decremento del

contatore di condivisione. Una cancellazione da parte della directory proprietario del file e

che quindi possiede il descrittore reale, si traduce in una reale cancellazione solo nel caso

in cui il contatore di condivisione abbia valore 1. Se il contatore di condivisione ha valore

maggiore di uno, il descrittore della file viene cancellato solo logicamente cioè viene reso

inaccessibile dalla directory proprietaria ma accessibile attraverso i link rimanenti. Il de-

scrittori ed il file stesso verranno rimossi quando il contatore assumerà il valore 1.

In UNIX, questo problema è risolto in modo molto semplice; infatti in UNIX la situazione è

un po' differente dal caso generale (vedi figura 2).

A B A B

INODE CASO GENERALE

CASO UNIX

Abbiamo due directory che condividono un file. Nel caso generale il descrittore dello file è

contenuto nella directory A mentre nella directory B c'è un hard link che punta a tale de-

scrittore che a sua mostra punta al file.

Nel caso di UNIX il descrittore è contenuto nell’i-node e nelle directory A e B ci sono solo i

puntatori al descrittore. Il contatore di condivisione è contenuto nel descrittore. Quindi la

cancellazione del file da parte di una delle due directory comporta semplicemente la elimi-

nazione del suo puntatore all'i-node e il decremento del contatore di condivisione presente

in quest'ultimo; lo spazio occupato da tale puntatore può essere immediatamente rialloca-

to a qualche altro puntatore a file. Il descrittore e nell’i-ndole e verrà eliminato quando il

contatore di condivisione assumerà il valore 0.

214

Sistemi Operativi

Prof. DE CARLINI 7/5/1997

Nella scorsa lezione abbiamo visto i metodi di allocazione ed in particolare quello più semplice pos-

sibile noto come allocazione contigua; tale metodo permette di tener conto della allocazione di un

file in modo molto semplice: basta considerare il numero di blocco assoluto nell'ambito del disco da

cui inizia il file ed il numero di blocchi consecutivi del file, cioè la sua lunghezza. In pratica però

questo metodo non si utilizza perché richiede per garantire la contiguità del file in memoria la indi-

viduazione di un buco (numero di blocchi consecutivi) idoneo ed inoltre da problemi quando il file

cresce di dimensioni perché presuppone lo spostamento dello stesso all'interno del disco; presenta

inoltre il problema della frammentazione esterna che è lo stesso che si crea con la gestione della me-

moria partizione variabili. Bisogna utilizzare allora dei tipi di allocazione diversa. Vediamoli:

-ALLOCAZIONE LINKATA (vedi fig. 11.4 pag. 364 Silberschatz)

con questa tecnica nel descrittore di file, o meglio, nella tabella al cui entry punta il descrittore di

file, abbiamo ancora il blocco di inizio del file così come nella allocazione contigua, però invece di

avere il numero di blocchi consecutivi, i vari blocchi sono linkati mediante puntatori che sono con-

tenuti nei blocchi stessi (si tratta quindi di una lista a puntatori i cui elementi sono i blocchi del di-

sco). Per percorrere il file basta quindi entrare nel primo blocco e poi utilizzare i vari puntatori che

si trovano in posizione standard nei blocchi. Con questo metodo si risolvono parecchi problemi visti

prima perché non c'è più frammentazione esterna (alloco il file usando i blocchi così come li trovo

senza bisogno di compattare), posso risalire in modo semplice al file occupando poco spazio (pro-

blema molto importante). Essa comporta comunque alcuni inconvenienti minori però della necessità

di spostare il file (allocazione contigua); un primo inconveniente si vede tenendo conto del fatto che

un sistema operativo prevede la tecnica di accesso sequenziale e quella di accesso diretto (per l'allo-

cazione contigua abbiamo visto che sono entrambe ben supportati); per l'accesso sequenziale ho una

notevole semplicità per l'allocazione linkata, mentre la tecnica diretta da grossi problemi perché si

implementa comunque tramite un accesso sequenziale (per accedere al blocco 27 devo accedere a

tutti i 26 blocchi precedenti per trovare il ventisettesimo puntatore); non posso più accedere al bloc-

co tramite una somma (allocazione contigua) ma devo comunque percorrere una lista. Tale tecnica

quindi non permette l'accesso diretto ai file perché presuppone dei tempi di accesso insostenibili. I

sistemi operativi che la adottano usano anche un'altra tecnica a seconda del tipo di accesso. Per mo-

tivi di sicurezza uso anche un puntatore all'indietro per evitare problemi nel caso in cui si spezzi la

catena perché ad esempio si perde un blocco del file. Volendo sintetizzare :

VANTAGGI: Semplicità di esprimere la posizione del file nella directory, velocità di accesso se-

quenziale

SVANTAGGI: Non utilizzabile per l'accesso diretto, perdita di spazio nel blocco per il puntatore

(minimo), inaffidabilità dovuta alla possibilità di perdere il file per interruzione della lista

(parzialmente ridotta introducendo una doppia lista linkata)

-ALLOCAZIONE LINKATA CON FAT

Alcuni sistemi come MS-DOS usano una variazione della allocazione linkata per permettere anche

l'accesso

diretto. Per fare ciò si spostano i puntatori dei vari blocchi sul disco. In particolare memorizzo in al-

cuni blocchi di disco le catene di tutti i puntatori che stanno sul disco (vettore dei puntatori o File

Allocation Table o FAT). Per maggiore chiarezza facciamo riferimento alla figura 11.5 di pag.366

215

Con tale sistema percorro sempre una lista a puntatori, ma invece di trovarla nei vari blocchi del di-

sco, la trovo in alcuni blocchi speciali che si trovano in testa al file. Questo è già un notevole van-

taggio perché per scoprire tutti questi puntatori dovrò leggere al massimo tutti i blocchi che conten-

gono l'intero vettore indipendentemente dal fatto di dover scorrere una lista a puntatori fatta da 100,

200 o 300 blocchi perché tutti i puntatori si troveranno in quel vettore che occupa comunque pochi

blocchi. Tutto ciò si migliora ancora di più facendo in modo che il sistema operativo metta tutti que-

sti puntatori nella RAM (cacheing) nel momento in cui inseriamo ad esempio il dischetto nell'unità

fisica. Quindi percorrerò una lista a puntatori che si trova in memoria e non sul disco (questo è ciò

che fa MS-DOS). Questa modifica pur presupponendo una perdita di RAM per memorizzare la FAT

permette di supportare l'accesso diretto.

Sintetizzando :

Sul disco esistono uno o più blocchi ben individuati che contengono un vettore di puntatori di lun-

ghezza pari al numero di blocchi sul disco. Il vettore viene usato per contenere le liste a puntatori

delle posizioni dei blocchi che appartengono ad uno stesso file. Ogni lista ha la testa nell'elemento

della directory relativo al file di cui esprime l'allocazione.

Osserviamo inoltre che con questa tecnica esiste un particolare file linkato che è il file dei blocchi

liberi; avrò quindi un numero di liste pari al numero di file caricati sul disco più 1; la lista in più e

quella che collega i blocchi liberi. Questo permette di gestire in modo semplice lo spazio disponibi-

le perché quando servono dei blocchi basta staccarli dalla lista dei blocchi liberi e quando si cancel-

la un file basta collegare la catena di puntatori di quel file a quella lista. In realtà quella lista non

viene mai visitata perché in entrambi i casi faccio riferimento alla sua testa perché, ad esempio pren-

do sempre i primi blocchi liberi e non gli altri.

La tecnica di allocazione dedicata a comunque dei problemi per dischi di grossa capacità; facciamo

un piccolo calcolo: immaginiamo di avere un disco di 1 giga con blocchi di 1 k; ci sono quindi un

milione di blocchi e quindi un vettore di un milione di puntatori di quattro byte ad esempio (ne ba-

sterebbero 3 per indirizzare un milione di blocchi, però 4 è lo standard); servirebbero quindi 4 me-

gabyte di RAM per la FAT

(è un discorso estremizzato perché si usa la clasterizzazione per 4 blocchi consecutivi in un settore,

quindi servirebbero molti di meno).

Per risolvere tale problema si usa la seguente tecnica :

-ALLOCAZIONE INDICIZZATA

E' la tecnica usata anche da UNIX; all'atto di allocare un file si crea un blocco che contiene tutti i

puntatori di quel file; quando voglio risalire all'allocazione del file sul disco devo sapere dove è

questo blocco di puntatori perché con esso risolvo tutti i problemi (vedi fig. 11.6 pag.367).

Troviamo una tabella le cui varie entry vengono puntate dai descrittore di file; c'è poi un puntatore

che dice quale è il blocco indice (in UNIX è l'i-node) che contiene la lista degli puntatori.

Dobbiamo comunque risolvere dei grossi problemi legati al numero di puntatori e quindi allo spazio

che devo associare per gestire file di grandi dimensioni o anche di dimensioni molto piccole. Potrei

pensare di mettere un blocco indice di una certa dimensione, ma ciò limiterebbe la massima lun-

ghezza di un file. Anche con la tecnica dei cluster avrei piccole dimensioni per i file. Potrei pensare

di mettere due blocchi indice, ma nel caso di file brevi perderei due blocchi per dei puntatori non

utilizzati. Si è visto che non conviene fissare il numero di puntatori, ma piuttosto conviene usare dei

blocchi indice variabile a seconda di quanti ne servono. Si potrebbe realizzare una lista linkata di

blocchi indice: dei 20 puntatori del blocco indice, ad esempio, 19 puntano a blocchi dati ed il vente-

simo punta ad un altro blocco indice di 20 puntatori e così via. Con questo sistema non ho più vin-

colo sulla dimensione massima del file però ho di nuovo problemi per l'accesso diretto su file molto

lunghi (è sempre una lista linkati) perché se i puntatori che mi interessano sono nell'ultima tabella

devo comunque leggere tutte le tabelle. Si usa hanno allora una soluzione diversa:

216

-PUNTATORI MULTILIVELLO

Nel vettore di puntatori si trovano oltre a puntatori a dati anche puntatori a blocchi. E' diversa dalla

lista linkata perché i puntatori non si trovano più nel blocco seguente e quindi si riduce il numero

massimo di accessi. Nell'i-node di UNIX (versione BSD più diffusa) ci sono 15 puntatori di 4 byte

(60 byte su 64) di cui 12 puntano a blocchi dati del file (molte versioni hanno 4 Kb per blocco e

quindi 48 Kb per il file ); il tredicesimo puntatore punta ad un blocco indice di livello 0 (puntatori a

soli blocchi dati), il quattordicesimo ad un blocco indice di livello 1 (puntatore a puntatori di bloc-

chi indice), il quindicesimo ad un blocco indice di livello 2. Abbiamo quindi al massimo un accesso

indiretto a 3 livelli.

Facendo un confronto possiamo dire che il metodo di allocazione condiziona molto le prestazioni

che sono legate alle modalità di accesso al file e alla sua dimensione:

L'ALLOCAZIONE CONTIGUA ha buone prestazioni per accesso sequenziale e diretto indipenden-

temente dalla dimensione del file. Comporta però problemi dovuti alla necessità di spostare i file.

L'ALLOCAZIONE LINKATA va bene per accessi sequenziali al file.

L'ALLOCAZIONE INDICIZZATA va bene per entrambi gli accessi con prestazioni che decrescono

con la dimensione del file perché devo utilizzare la tecnica a multilivello.

A questo punto ci resta da trattare la modalità di gestione dello spazio libero che deve essere co-

munque coerente con la tecnica di allocazione dei file utilizzata (ad esempio come avveniva con la

FAT).

Il modo più semplice da utilizzare è il BITMAP consistente nell'associare ad ogni blocco un bit che

vale 0 se il blocco è occupato e 1 se è libero. Scorrendo la BITMAP in cerca di un blocco libero ba-

sterà fermarsi al primo 1 trovato. Offre il vantaggio di favorire la scelta di blocchi adiacenti per allo-

care un file (all'inizio il disco è vuoto e la BITMAP è formata da tutti 1, poi alcuni blocchi si occu-

pano però mediamente è abbastanza probabile che ci siano blocchi liberi adiacenti);ha però il pro-

blema che la BITMAP deve trovarsi in memoria. Supponendo di avere un disco da 1 Gb con 1 Kb

per blocco avremo una BITMAP di 1.000.000 di bit (circa 130 Kb). Quindi tale sistema si usa per

piccoli dischi (meno di 1 Gb); un altro problema è quello di esaminare velocemente la BITMAP. E'

necessario per non avere un algoritmo particolarmente pesante che il processore abbia codici opera-

tivi ad hoc ( ad esempio il 68020 ); infatti in caso contrario dovremmo accedere ad un byte della

BITMAP, controllare se è formato da tutti zeri ed in caso contrario calcolare la posizione del primo

1 ad esempio con una serie di ROTATE e di aggiornamenti di un contatore.

Riassumendo :

-TECNICA BITMAP

Esiste un vettore di bit ( BITMAP ) con tanti bit quanti sono i blocchi del disco e ogni bit indica se

il corrispondente blocco è libero o meno. Con questa tecnica il braccio del disco si deve muovere

poco (migliorano i tempi di accesso). E' la tecnica usata da Apple Macintosh con il 68020.

VANTAGGI : Semplice ricerca di blocchi liberi adiacenti.

SVANTAGGI : Devo avere il BITMAP in memoria; necessito di codici operativi opportuni.

Abbiamo già visto prima il metodo della lista linkata :

-LISTA LINKATA

Tutti i blocchi liberi sono linkati in una lista ed un puntatore Testa della lista permette l'accesso al

primo blocco; 217

SVANTAGGIO : scarsa efficienza dovuta al tempo necessario a percorrere la lista che richiede ope-

razioni

di I/O.

Un miglioramento di tale tecnica è il seguente :

-RAGGRUPPAMENTO

Riduciamo gli svantaggi della lista linkata in quanto teniamo raggruppati più puntatori a cui possia-

mo accedere con un'unica operazione di I/O. In pratica ogni blocco della lista punta oltre che al suc-

cessivo blocco ad un gruppo di blocchi liberi (quindi abbiamo più puntatori che puntano a blocchi

liberi direttamente ed uno che punta al prossimo blocco di puntatori);in questo modo si riduce il

tempo per la individuazione degli indirizzi dei blocchi liberi richiesti per allocare un file.

Sia col RAGGRUPPAMENTO che con la LISTA LINKATA però non facilito l'allocazione del file

su blocchi liberi contigui perché non è detto che quei puntatori puntino a blocchi adiacenti.

In realtà la cosa migliore da fare è di usare il seguente metodo:

-CONTEGGIO

La lista dei blocchi liberi non linka i blocchi che sono preceduto da altri blocchi liberi contigui. Per

ogni blocco della lista si memorizza oltre al puntatore al blocco successivo anche un contatore che

indica il numero di blocchi liberi che gli sono contigui per indirizzi crescenti o decrescenti.

Nel momento in cui stacco i blocchi liberi dalla lista cerco di staccarli in modo che siano adiacenti.

VANTAGGIO : si riduce il tempo di percorrenza della lista; è più agevole trovare i blocchi contigui

che

sono da preferire in sede di allocazione di un file.

Ritorniamo ora allo schema seguente : 218

abbiamo visto quale è la struttura a blocchi del file system; ora abbiamo visto le varie tecniche di

gestione del modulo di gestione dei tipi di file. Abbiamo detto che l'applicativo effettua l'accesso fa-

cendo riferimento al numero di blocco relativo all'interno del file che poi dovrà essere trasformato

in numero di blocco assoluto all'interno del disco. Ciò si realizzerà consultando le informazioni sul-

la allocazione del file; la routine che effettua questa assolutizzazione tiene conto quindi della tecni-

ca di allocazione e da l'indirizzo assoluto all'interno del disco. A questo punto il file system base

deve trovare fisicamente questo indirizzo facendo riferimento a cilindro, traccia e settore per co-

mandare il driver e realizzare l'accesso.

Diamo per scontata la conoscenza del supporto fisico disco (vedi Fig. 2.1 pag.43 ): abbiamo un ci-

lindro, più superfici, le tracce ( 2 per faccia del disco) ed i settori. Quindi l'indirizzamento è tridi-

mensionale e devo pilotare il movimento del braccio in modo da ottimizzare i tempi di accesso ordi-

nando le varie richieste.

Per passare da un indirizzo assoluto di blocco ad un indirizzo fisico espresso da cilindro, traccia e

settore basta sapere le caratteristiche del disco cioè il numero di cilindri n, numero di facce (di solito

numero di dischi -2 perché non si usano la faccia superiore e quella inferiore) ed il numero di setto-

ri.

Si tratta di trovare i numeri i, j, k tali che :

b= k + j s + i t s dove s è il numero di settori per traccia,

t è il numero di tracce per cilindro

i è l'indirizzo di cilindro

j è l'indirizzo di traccia

k è l'indirizzo di settore

b è il numero di blocco assoluto

Ricordiamo che i tempi di accesso al disco sono due :

Tempo di SEEK impiegato per spostare radialmente il braccio per posizionare le testine sul cilindro

Tempo di LATENZA cioè il tempo che devo aspettare prima che il settore passi sotto la testina

( il tempo di SELEZIONE della testina sulla superficie è un tempo elettronico ed è trascurabile ).

In realtà la testina legge tutti i settori che gli passano sotto; ogni settore ha un MARK di inizio,

un indirizzo espresso di nuovo come la tripla cilindro, traccia e settore ed i dati che ci interessano.

Quindi basta comparare quei due indirizzi; in realtà la testina legge sempre i dati e ricopre sempre lo

stesso buffer; quando quei due indirizzi sono uguali si ha l'OK, il buffer non viene più ricoperto e

gli ultimi dati letti sono quelli cercati.

Se il disco è a teste mobili posso ottimizzare rispetto al Tempo di SEEK, rispetto al Tempo di LA-

TENZA o rispetto ad entrambi. Conviene ottimizzare rispetto a quello di SEEK che è maggiore

(tempo di spostamento

meccanico). Se invece il disco è a teste fisse ho tante testine quanti sono i cilindri e quindi il braccio

non si

sposta mai ( non ho quindi tempo di SEEK ). Esistono poi dischi in cui il numero di testine è legger-

mente minore del numero di tracce e in cui lo spostamento è quindi molto minore. Per un il disco a

teste fisse si ottimizza quindi rispetto al tempo di LATENZA (di solito considerando che esso è pic-

colo non vale la pena

di ottimizzare niente ).

Nel momento in cui mi pongo il problema di ottimizzare i tempi di accesso del disco, ho già una ri-

chiesta di accesso che ha specificato se è di input o di output ed ho già individuato il blocco a cui

accedere in quanto conosco cilindro, traccia e settore. Conosco inoltre il buffer di memoria cui fare

riferimento per l'operazione ed anche il numero di byte che voglio leggere o scrivere. (di solito è un

numero fisso).

Per realizzare questa ottimizzazione si usano vari algoritmi di scheduling :

219

FCSF (First Come First Served)

E' il più semplice possibile ed equivale a non effettuare alcuno scheduling (siamo a livello del File

system di base ). Non ottimizza sul tempo medio di servizio non effettuando alcun tipo di ordina-

mento.

(vedi Fig. 12.1 pag. 385)

SSTF (Shortest Seek Time First)

E' paragonabile all'algoritmo SJF della CPU. In questo caso servo la richiesta più vicina al punto in

cui mi trovo sul disco. Ottimizzo sullo spostamento delle testine e quindi sul tempo di SEEK. Può

dare luogo ad

attesa infinita (STARVATION) perché una richiesta molto lontana rispetto al punto in cui mi trovo

potrebbe non essere mai servita. Quindi non è un algoritmo ottimale. (vedi Fig. 12.2 pag. 387)

SCAN (Algoritmo dell'ascensore)

Lo abbiamo già visto ed è quello ottimale perché elimina la STARVATION evitando di servire una

richiesta

relativa allo stesso punto. (vedi Fig. 12.3 pag. 388)

Ne esistono alcune varianti che ora vediamo:

C-SCAN (Circular Scan)

Ha un tempo di attesa più uniforme; la testina serve le richieste a mano a mano che le incontra ma

una volta

raggiunta l'estremità del disco torna subito all'inizio senza servire le richieste che incontra al ritorno

( è come se considerasse il disco circolare, come se la prima traccia seguisse l'ultima ). Ottimizza

sul moto del disco che non è più a scatti. (vedi Fig. 12.4 pag. 389)

LOOK e C-LOOK

Si tratta di una variante degli algoritmi SCAN e C-SCAN in cui la testina non giunge mai alle estre-

mità del disco ma si sposta solo fino all'ultima richiesta in ciascuna direzione.

(vedi Fig. 12.5 pag. 389) 220

08/05/97 (DE CARLINI)

Abbiamo visto gli algoritmi di scheduling più comuni per quanto riguarda la gestione dell’ordina-

mento di servizio di richieste di accesso a blocchi di settore. Facciamo ora un confronto tra questi

algoritmi.

Gli algoritmi che abbiamo visto sono il client server (che è il più banale) che presuppone un ordina-

mento delle richieste pendenti in base all’arrivo di queste. Poi abbiamo visto l’algoritmo che mini-

mizza il tempo di seek, cioè è quello in cui lo scheduling dà maggiore priorità alla richiesta che è re-

lativa ad un cilindro prossimo a quello in cui si sta effettuando una operazione di lettura o scrittura

ed abbiamo visto che questo ottimizza il tempo di seek ed equivale all’algoritmo della CPU per

scelta del processo con routine più breve e fa in modo di trascurare le richieste di cilindri lontani se

in quel momento abbiamo richieste di cilindri più vicini. Poi abbiamo visto l’algoritmo dell’ascen-

sore e le sue varianti (classico, circolare, e quello che ottimizza il movimento del braccio dal cilin-

dro più interno a quello più esterno e viceversa).

Detto questo è chiaro che possiamo fare dei

Criteri di scelta dell’algoritmo di scheduling

L’elemento da tenere conto quando facciamo una scelta sull’algoritmo è quello che riguarda il cari-

co del disco. Infatti se facciamo riferimento ad un sistema il cui disco è scarico tutti gli algoritmi

vanno bene poiché non esiste una lista di richieste pendenti e quindi non nasce nessuna esigenza di

effettuare una scelta delle richieste che sono in attesa.

Quindi nei piccoli sistemi tutti gli algoritmi vanno bene e quindi anche l’algoritmo FCFS va bene,

viceversa la tecnica di scheduling viene applicata ai grossi sistemi dove si possono concentrare una

grossa richiesta di utenza.

Anche se ci sono molte richieste di utente, gli algoritmi individuati funzionano più o meno bene in

relazione a quella che è la

- Modalità di allocazione dei file.

Se la modalità di allocazione dei file fosse quella continua, a questo punto tutte le richieste si con-

centrano in zone più o meno prossime e quindi di conseguenza non ha molto senso ottimizzare gli

spostamenti e in questo caso i file vengono fatti in una versione linkata. Questo non esclude il fatto

che se in una versione continua, abbiamo due file entrambi allocati in modo continuo e si alternano

le richieste di accesso al primo e al secondo comunque si richiede un lieve spostamento dall’uno al-

l’altro.

Un altro elemento fondamentale che condiziona molto l’opportuna scelta dell’algoritmo è come

sono allocati i file directory e i blocchi indice. Infatti se i file directory ed i blocchi indice sono con-

tenuti in dei cilindri destinati a contenerli è chiaro che si ottiene un excursus frequente di tracce da

quelle posizioni preferenziali alle posizioni dei file e quindi di conseguenza ritorna rilevante l’op-

portuna scelta dell’algoritmo dell’accesso. Quindi, sostanzialmente sono questi i tre elementi che

possono far preferire un algoritmo d’accesso che è la routine dell’ascensore con lo scambio circolare

che viene utilizzato appunto quando i dischi sono molto carichi si crea una notevole dinamica.

Oss. Si tenga conto che questo algoritmo è implementata dalla parte bassa del file system, cioè nei

grossi sistemi il S.O. si rivolge al controller e poi sarà questo che si preoccuperà di implementare

l’ordinamento con delle routine di servizio e ovviamente gestirà il driver.

In altri grossi sistemi di solito il file system rinuncia a fare una richiesta perché l’ordinamento ri-

chiesto dal file system potrebbe andare in contraddizione con quello che è l’ordinamento di richieste

che farebbe il controller, questo perché il controller è ottimizzato per fare un certo ordinamento ed il

S.O. ne fa un altro passadogli le richieste una alla volta e quindi il controller in questo modo non ha

221

più motivo di sussistere. Quindi chi ha implementato l’algoritmo del controller lo ha fatto tenendo

conto delle caratteristiche di quel disco e tutta questa problematica viene bypassata e trascurata dal

S.O.

Nel momento in cui facciamo l’ordinamento delle richieste d’accesso ad un disco, possiamo otti-

mizzare sia rispetto al tempo di latenza che a quello di seek e il compito degli algoritmi che abbia-

mo studiato fanno riferimento esclusivamente al tempo di seek.

In realtà nei dischi a testina mobile non si fa l’ottimizzazione rispetto al tempo di latenza, mentre

per quelli a testina fissa non ha nessun senso l’ottimizzazione rispetto al tempo di seek e quindi l’or-

dinamento viene fatto rispetto ai settori per abbattere i tempi di latenza.

Ricordando come è fatto un disco (vedi FIG), nel caso in cui il disco sia a testine fisse il braccio

è fisso e le testine sono tutte po-

sizionate sulle varie tracce e

quindi sembra evidente che in

realtà, per come è fatto il siste-

ma, in un certo istante sotto le

testine troviamo sempre, indi-

pendente dal cilindro e ai settori

cui appartengono, tutti i settori

che occupano la stessa posizio-

ne per tutte le tracce.

Quello che vogliamo dire è che

facendo riferimento ad una sin-

gola traccia questa conterrà i

vari settori numerati in un certo

modo, ora in un certo istante

sotto questa testina c’è p. es. il

settore numero sette ed analogamente per le testine poste al di sotto di questa. Ora questo è vero se

abbiamo un singola testina per braccio, ma se immaginiamo che in questo braccio prenda tutti i ci-

lindri questo significa che abbiamo una testina per ciascuna traccia e quindi contemporaneamente

sotto tutte le testine in un certo istante abbiamo tutti i settori che hanno lo stesso indirizzo relativo

nell’ambito della traccia a cui appartengono, quindi possiamo leggere contemporaneamente tutte le

richieste indipendentemente dal cilindro a cui appartengono e dalla traccia a cui appartengono che

fanno riferimento allo stesso numero di settore. Quindi l’ordinamento bisogna essere fatto rispetto a

questo numero e quindi di conseguenza tutte queste richieste possono essere lette (sempre che la lar-

ghezza di banda del canale lo consenta) in parallelo. Questa è la logica in cui vengono soddisfatte le

richieste.

Oss Nella realtà quindi non si leggeranno tette le testine contemporaneamente ma viene attivata solo

la testina per cui abbiamo una richiesta di lettura e scrittura.

Naturalmente questo tipo di scheduling può essere applicato anche ad un disco a testine mobili, in

tal caso ordineremo tutte le richieste in modo tale che scelto un cilindro che è quello in cui ci siamo

posizionati per ottimizzare il tempo di seek, andiamo a leggere in sequenza tutte le richieste che

sono relative(in parallelo e quindi dipende sempre dalla larghezza di banda del canale) alla stessa

posizione di settore indipendentemente dalla traccia a cui appartengono i suoi bracci.

Questo beneficio e praticamente irrilevante nel disco a teste mobili, perché tutte le richieste a parità

di settore devono essere riferite allo stesso cilindro ( che è uno solo), quindi non ce ne sarà nessuna

e quindi questo tipo di ottimizzazione non ha senso. Se invece abbiamo il disco a testine fisse dato

222

che è indifferente per le richieste a parità di settore a quale cilindro appartengono (e quindi ne abbia-

mo più di uno ) allora li possiamo ordinare e servirle contemporaneamente.

Quindi questo tipo di algoritmo vale solo per i dischi a testina fissa :

-Scheduling di settori

Lo scheduling di settore si applica a dischi molto carichi a testine fisse oppure a dischi con control-

lori dotati di memoria cache in cui viene letta tutto un cilindro alla volta.

Abbiamo detto che un disco non solo viene utilizzato per essere il supporto in cui viene memorizza-

to il file system, ma è anche il supporto che serve per implementare la memoria virtuale ed anche

per implementare lo spazio di swap.

Oss. Ricordiamo che per la gestione della memoria centrale, qualunque sia la tecnica utilizzata, ab-

biamo sempre il problema dello swap, poiché in un certo istante (rispetto a quello che è lo spazio di

memoria RAM) abbiamo l’esigenza che alcuni processi attivi siano swappati su memoria di massa.

E’ chiaro che nel caso di memoria virtuale swappare significa portare tutte le pagine e comunque

stare nello spazio fisico di memoria virtuale, ma se tralasciamo il discorso della memoria virtuale

abbiamo bisogno di uno spazio di swap su cui andiamo a riportare il processo.

Quindi abbiamo il problema di come gestire questo spazio di swap, la quale gestione non è detto

che debba avvenire con le stesse modalità con cui avviene la gestione di uno spazio del disco che è

destinato a contenere il file system, infatti è evidente che le esigenze che abbiamo per la gestione di

uno spazio di swap sono certamente diverse rispetto alle esigenze per la gestione dello spazio del

file system.

Il problema della locazione dei file nella memoria di massa è un problema che cerca di ottimizzare

in qualche modo lo spazio di massa.

Abbiamo detto che la locazione contigua non ci va bene per la gestione di memoria di massa per

due motivi.

Il primo è dovuto ad una frammentazione interna e quindi in realtà fa sprecare memoria di massa,

poiché il problema della ricompattazione è da recuperare e quindi non ottimizziamo la disponibilità

di spazio che c’è in memoria di massa.

Il secondo motivo è dovuto al problema di dover continuamente spostare il file per fare spazio.

E quindi abbiamo avuto l’esigenza di fare altri metodi di allocazione, che tutto sommato abbassano

il trasferimento. Infatti se dobbiamo trasferire un file che è allocato in modo continuo riusciamo a

trasferire con maggiore velocità l’intero file attraverso il canale poiché leggiamo grossi blocchi

sfruttando la larghezza di banda del canale e non dobbiamo continuamente spostare le testine, men-

tre se dobbiamo effettuare il trasferimento di un intero file che si trova in un a allocazione linkata

indicizzata impiegheremo più tempo, poiché dovremo trasferire meno quantità di dati e dovremo

periodicamente spostare il braccio.

Dal momento in cui andiamo a scrivere e a leggere in memoria di massa, nel caso del file system è

ben difficile che abbiamo la possibilità di scrivere un intero file ma probabilmente dovremo effet-

tuare accessi a parti di file ma questo tutto sommato non è un grosso problema. Si capisce che la

questione è completamente diversa se trasferiamo un file con il quale abbiamo implementato lo spa-

zio di swap, infatti in quel momento dobbiamo prendere tutta l’area programmi del processo e lo

dobbiamo portare velocemente in memoria RAM perché è verificato l’evento in cui il processo è di-

ventato ready e quindi deve competere alla funzione della CPU.

Quindi lo spazio di swap va gestito con delle tecniche che non sono quelle di gestione dello spazio

destinate al file system perché gli obbiettivi prestazionali che si prefigge sono diversi, quindi a noi

non interessa che lo spazio di swap viene sfruttato male ma ci interessa che la velocità di trasferi-

mento swap-in swap-out siano alte, poiché tutto sommato un processo nello spazio di swap c’è per

un tempo breve e quindi non è un fatto abbastanza statico.

223

Un altro elemento da mettere in evidenza è che l’allocazione contigua ci dava fastidio perché gene-

rava una frammentazione esterna, ma in questo caso è una frammentazione esterna abbastanza stati-

ca.

Infatti se carichiamo un certo file e poi lo leviamo sfruttiamo male lo spazio sul disco, ma in questo

caso non abbiamo esigenza di ricompattare i buchi che si creano poiché si ha una dinamica così alta

che possiamo dire che questi buchi si “ricompattano da soli”.

Quindi tipicamente lo spazio di swap viene gestito con una locazione che è o contigua oppure lo è

quasi, cioè il programma che viene swappato occupa una serie di settori e quindi blocchi consecutivi

e viene restituito in un certo numero di blocchi di lunghezza prefissata(ma non tutti uguali) in modo

tale da ridurre il problema della frammentazione.

Oss. Esistono S.O. i quali operano in modo diverso, in cui lo spazio di swap viene gestito come è

gestito il file system (e questo è come opera windows dove appunto lo spazio di swap è gestito

come il file system), nel senso che le routine che permettono di operare su spazio di swap sono le

stesse che permettono di operare sul file system. Il che significa dire che dal momento in cui andia-

mo a swappare un programma dobbiamo creare un file, così come avviene nel file system, e dobbia-

mo trovare lo spazio di allocazione con una normale routine che utilizza il file system e questo si-

gnifica che non è una partizione a parti. Anche in questo caso abbiamo una organizzazione a direc-

tory e sottodirectory (cioè nella directory abbiamo tutti i file di swap dei vari processi).

Tornando al discorso della separazione dello spazio di swap e allo spazio destinato al file system.

Questo è quello che avviene in UNIX, dove l’allocazione non è contigua ma è a pezzi e nel momen-

to in cui il programma viene swappato esiste un vettore di puntatori di swap il quale punta ai vari

pezzi (vedi FIG) ed ognuno di

questi è di 512 Kb tranne

l’ultimo che è di grandezza

512k 512kb 512kb variabile ed in questo caso

la frammentazione interna è

bassa

Questo riguarda la parte del-

lo swap dell’area istruzione del programma (cioè il codice, quello che è chiamato il “ testo “ del

programma ).Quindi dal momento in cui un processo viene creato gli viene anche dedicato uno spa-

zio di swap su cui si appoggerà.

Oss. Quindi questo spazio di swap non è che gli viene creato e tolto dinamicamente, ma è un fatto

che nasce con il processo.

Per quanto riguarda la tabella relativa all’area dati i vari i vari segmenti non sono fissati a 512 kb

ma sono di 16-32-64k, cioè sono segmenti diversi di dimensione multipla e quindi lo spazio di swap

legato ai dati per programmi piccoli occupano poco spazio.

Quindi per la gestione dello spazio swap sia le regole di allocazione che le routine che vengono uti-

lizzate sono diverse da quelle del file system .

Oss. In realtà non ci sarebbe neanche bisogno di implementare uno spazio di swap alla parte testo

del processo poiché comunque il codice risiede nel file system.

Per quanto riguarda il codice nel caso in cui si dovrebbe swappare un programma con questa tecnica

dovremmo prendere il codice e dall’area del file system copiarlo nell’area di swap e da lì portarlo

dentro ogni volta che ci serve e quindi lo swap-out non lo facciamo mai poiché il codice è immuta-

bile durante l’esecuzione. 224

Se invece di fare ogni volta lo swap-in nell’area di swap per quanto riguarda la parte di codice, pos-

siamo fare direttamente lo swap-in dal file system e di conseguenza risparmiare ulteriormente tem-

po.

Quello che necessita invece un continuo swap e quello dell’area dati, poiché sul file system l’area

dati non esiste, ma su questo abbiamo semplicemente i valori delle variabili da inizializzare e non

quelle definite durante l’esecuzione del programma e quindi comunque abbiamo bisogno di uno

spazio che non è quello del file system.

Per concludere l’argomento sui dischi parliamo di

Particolari tecniche di miglioramento delle prestazioni o dell’affidabilità dei dischi.

Prima di parlare di affidabilità introduciamo il concetto di interliving che ci sarà utile per sviluppare

l’affidabilità. Se abbiamo una serie di banchi di memoria in un calcolatore gli indirizzi non sono nu-

merati in sequenza ordinata in un chip e poi nel secondo chip continua la sequenza del precedente,

ma si mette l’indirizzo zero nel primo chip , l’indirizzo uno nel secondo chip etc. questo perché

qualora andiamo a leggere una word anziché fare un accesso in sequenza di più byte all’interno del-

lo stesso chip, possiamo lavorare in parallelo e sfruttiamo la banda del canale in cui l’address bus

non è a otto bit ma a 32 bit.

Esempio

Supponiamo di avere quattro chip (vedi FIG) e supponiamo che il parallelismo della parola sia di 1

0 1 2 3

4 5 6 7

byte, cioè l’indirizzo uno è 1 byte, l’indirizzo due è un byte, etc.

Ora se vogliamo accedere all’indirizzo p.e. 110100...0110 allora gli ultimi due bit vengono eliminati

perché abbiamo quattro chip (se ne avessimo avuto otto chip avremmo eliminato tre bit (banale ?)),

l’indirizzo che rimane lo mettiamo sull’address bus e seleziona l’indirizzo zero, uno etc. ed in que-

sto modo possiamo scaricare o prelevare sul data bus tutta la word, questo si chiama progetto di me-

moria interlacciata e il metodo è di interliving di memoria.

Questa particolare architettura può essere sfruttata per l’affidabilità sui dischi e si fa questo partico-

lare ragionamento, invece di mettere un disco di grosse capacità in cui il blocco è di 4-8 kb, possia-

mo costruirci una batteria di dischetti di bassa capacità e che abbiano lo stesso numero di blocchi di

quello grande (p.e. blocchi di 4kb ma suddivisi in 4 dischi dove il primo kb è sul primo disco , il se-

condo kb sul secondo etc.). Quindi quando andiamo a leggere un blocco del disco di grosse capacità

dobbiamo leggere tanti blocchi uno per ognuno dei dischi di piccole capacità i quali essendo inter-

lacciati nel loro complesso fanno il grande disco.

Oss. Con il ragionamento fatto prima invece di avere un unico chip di memoria un registro di 32

bit , abbiamo 4 chip di memoria di cui su ognuno andiamo a leggere 8 bit. Nel caso dei dischi il ra-

gionamento è analogo in quanto invece di avere un unico disco in cui andiamo a leggere un unico

225

blocco di 4k , abbiamo 4 dischi in cui prendendo lo stesso indirizzo per tutti e 4 dischi adiamo a leg-

gere il primo blocco del primo disco, il primo del secondo disco etc.

Questo ci consente di avere due grandi vantaggi:

1)Abbattiamo i tempi di accesso, poiché il trasferimento avviene in parallelo e i tempi li abbattiamo

nella misura in cui disponiamo di un canale di banda adeguato.

2)Risparmio economico, poiché avere tanti dischi di piccole capacità costa meno rispetto ad un me-

gadisco di grossa capacità.

Oss. La batteria di dischi si chiama RAID (o tecnica RAID cioè array di dischi)

Tecnica di inteliving di dischi

La tecnica più banale da fare, che però fa aumentare il costo perché aumenta al massimo la ridon-

danza, è quello di fare il mirroring (da mirror, specchio), cioè abbiamo due batterie di dischi ed ogni

volta che scriviamo sulla prima, scriviamo anche sulla seconda.

Quindi abbiamo due batterie di dischi di cui uno è l’immagine speculare dell’altro, e questo fa si che

in caso di crash non interrompiamo il lavoro, poiché possiamo comunque lavorare con l’altra batte-

ria di dischi funzionante.

Oss. E’ lo stesso discorso di una macchina che ha due CPU in cui nel caso di rottura di una CPU il

sistema non si interrompe. Nel caso di rottura di una CPU le prestazioni decrescono, mentre nel

caso di rottura di una batteria di dischi non si riducono le prestazioni ma l’affidabilità.

Tecnica con disco di parità

Un altro modo di lavorare sull’affidabilità è sempre basato sulla batteria di dischi però in questo

caso anziché raddoppiare la batteria di dischi ne mettiamo uno sola in più (p.e. da due batterie di di-

schi invece di passare a 4 batterie(interliving) passiamo a tre batterie di dischi), questo è il concetto

di parità mediante un disco di parità. Vediamo un

Esempio

Supponiamo di avere due batterie di dischi, vediamo come costruire il disco di parità.

Abbiamo il disco zero ed un settore sette di una certa traccia e di un certo cilindro, sul disco uno

avrà anch’esso il settore sette della stessa traccia e dello stesso cilindro allora abbiamo un disco due

che ha ancora il settore sette sempre della stessa traccia e dello stesso cilindro dove i bit che sono

nel disco due sono i bit di parità dei bit omologhi che sono nel disco zero e uno(vedi FIG).

DISCO SETTORE

0 7

1 7

2 7

Quindi se il settore sette del disco zero è fatto dai bit 1101 e se il settore sette del disco uno è fatto

dai bit 0110 allora il settore sette del disco due è dato da:

226

1101 +

0110

1011

poiché questi sono i bit di parità rispetto ai bit omologhi.

Oss. Questa è detta parità trasversale e questo numero dipenderà dal numero di dischi che stiamo

gestendo. Conviene applicare questa tecnica quando il numero di batterie di dischi è elevato.

Quindi dal momento in cui andiamo a scrivere su un certo disco dobbiamo ricalcolare la parità e

scrivere sul disco di parità (si sfrutta la funzione XOR, cioè basta fare una XOR alla vecchia parità e

ricalcolare la nuova parità)

Oss. Naturalmente il disco di parità può essere applicato anche alle tecnica di interliving.

S.O. distribuiti

Introduzione dei S.O. distribuiti

Sotto il nome di sistemi distribuiti si racchiude una moltitudine di argomenti che sono anche molto

diversi tra di loro e che servono molte applicazioni in cui sono richieste delle elevate potenze di cal-

colo.

Le applicazioni di sistemi distribuiti su base geografica in cui l’applicazione e client-server, introdu-

ce il discorso di comunicazione su rete in cui sono collegate le varie unità di sistema . Questo di-

scorso si rivolge ad un ambiente applicativo totalmente diverso e questo è un sistema in cui in realtà

abbiamo tante macchine ed ognuno delle quali ha il proprio Kernel di S.O. e che nel loro complesso

dovrebbero costituire (questi vari Kernel) quello che è il S.O. distribuito (cioè non esiste un unico

Kernel ma esistono tanti Kernel presenti sulle macchine nel sistema).

Cosa ben diversa del parallel-computer che è una unica macchina che avrà più CPU ma ha un unico

Kernel con un unico S.O..

Quindi una classificazione per i sistemi distribuiti sono:

1)Sistemi distribuiti su una base geografica, in cui abbiamo molte macchine e molte CPU ma distri-

buite geograficamente e connessi dalla network (cioè dalla rete);

2)Sistemi distribuiti ma concentrati in un punto dove comunque abbiamo lo stesso una serie di CPU

tra di loro connessi;

I sistemi distribuiti nascono intorno agli anni ‘80, questi nascono sia con l’introduzione dei micro-

processori che danno la possibilità di avere CPU a basso costo, sia perché nascono le reti di comuni-

cazione, cioè nasce il modo per poter connettere diverse macchine.

Qualunque siano i sistemi distribuiti quello che li caratterizza rispetto ai sistemi centralizzati è il fat-

to di avere più CPU,(le quali possono essere contenute in unico box oppure saranno distribuite geo-

graficamente ) le quali sono interconnesse in vario modo ed ovviamente ogni CPU è in grado di la-

vorare sul proprio segmento di codice e operare sui propri dati.

Oss. Il segmento di codice che opera sui propri dati potrebbe essere un segmento di codice che ese-

gue un server di una applicazione in cui una parte client della applicazione stessa si è rivolta per

227

avere un servizio, come potrebbe anche essere un segmento di codice di un processo di n processi

cooperanti che servono a risolvere su una macchina (che nei fatti è concentrata in un unico box) una

unica applicazione.

I sistemi distribuiti presentano molte architetture, molte aree applicative hanno S.O. che possono es-

sere molto diversi, hanno diverse reti di comunicazione possibili tra le varie CPU e modelli elabora-

tivi che possono anche essere molto diversi tra di loro.

I sistemi distribuiti sono nati in particolare per ottenere delle velocità di calcolo che non sono otteni-

bili con i sistemi concentrati.

Un’altra motivazione della nascita dei sistemi distribuiti è una motivazione di carattere economico

poiché si era notato che il costo delle CPU non variava linearmente con la potenza offerta ma l’an-

damento era esponenziale (legge di Closch) cioè al raddoppio del costo la potenza offerta quadrupli-

cava e quindi una rete come questa guardava anche al futuro sapendo che questa aveva tutte le po-

tenzialità per crescere.

Un altro tipo di vantaggio è la programmazione parallela rispetto a quella sequenziale.

I svantaggi del sistema distribuito sono:

1)Scarso software di base ; solo adesso si sta sviluppando un software di base che è abbastanza sta-

bile cioè che permettono lo sviluppo di applicazioni in rete ed accesso a banchi dati;

2)Ambienti di sviluppo e linguaggi di programmazione poco evoluti;

3)Modesta offerta di SW applicativo;

4)Overhead dovuto alle comunicazioni ed in particolare al software che gestisce la perdita di mes-

saggi e gli errori;

5)Minore sicurezza dei dati dovuta alla condivisione;

Diamo ora delle definizioni.

Cominciamo col distinguere tra sistema distribuito in senso stretto e sistema parallelo.

-Sistema distribuito( in senso stretto ) : sistema costituito da più CPU variamente interconnesse che

consentano a più utenti di lavorare contemporaneamente eventualmente in modo cooperativo.

-Sistema parallelo : sistema costituito da più CPU variamente interconnesse dedicato ad una singola

applicazione che richiede una elevata velocità di calcolo.

Una classificazione maggiore dei sistemi distribuiti.

Secondo la tassonomia di Flyn i sistemi distribuiti sono sistemi MIMD e possono suddividersi in:

-SISD (cioè una sola istruzione un solo dato), cioè sistemi in cui ogni CPU esegue proprie istruzioni

su propri dati e quindi non esiste ne condivisione di istruzione ne condivisione di dati;

-SIMD (cioè una sola istruzione molti dati) , cioè sono sistemi in cui viene eseguita una istruzione

alla volta però questa opera su un array di dati.

-MISD (cioè molti istruzioni un solo dato)

-MIMD(cioè molti istruzioni molti dati) cioè i sistemi distribuiti;

228

Quindi come prima abbiamo suddiviso i sistemi distribuiti come sistemi distribuiti in senso stretto e

in parallelo, ora possiamo distinguere in sistemi multiprocessore e sistemi multicomputer, dove i si-

stemi multiprocessore intendiamo sistemi in cui ci sono tante CPU ed una memoria condivisa (co-

mune), nei sistemi multicomputer invece abbiamo tante CPU ognuna con la propria memoria (loca-

le) vedi FIG MIMD

Calcolatori

distribuiti e

paralleli

Multiprocessore Multicalcolato-

memoria re

condivisa memoria

locale

BUS Commutativo BUS Commutativo

E’ evidente che quello che abbiamo chiamato sistemi distribuiti in senso stretto sono certamente si-

stemi multicomputer, mentre quello che avevamo chiamato sistemi paralleli sono i sistemi multipro-

cessore.

Un’altra classificazione per i sistemi distribuiti in senso lato

1)Sistemi strettamente connessi;

2)Sistemi lascamente connessi;

Dove per strettamente e lascamente connessi si intende la velocità con cui questi sistemi comunica-

no tra di loro:

Si dicono strettamente connessi quei sistemi che hanno vie di comunicazione che lavorano a grosse

velocità, si dicono lascamente connessi quelle vie di comunicazione che lavorano a basse velocità.

I sistemi che comunicano a grande velocità hanno un bus in comune e quindi si dispone di una me-

moria comune e comunichiamo tramite un bus alla memoria.

Viceversa sono sistemi lascamente connessi sistemi che non condividono l’area di memoria e che

comunicano tramite una rete(tipo rete locale o geografica).

Quindi questa distinzione è un modo diverso di riproporre ancora la classificazione prima introdotta

poiché quello che abbiamo chiamato sistemi multiprocessore sono in realtà sistemi strettamente

connessi, quello che avevamo chiamato sistema multicomputer sono sistemi lascamente connessi.

Abbiamo poi una classificazione sulla rete di comunicazione (cioè sul modo in cui sono connesse

le CPU) e sono dati dai sistemi a BUS e quelli switch(commutati) e anche in questo caso i sistemi a

bus sono sistemi strettamente connessi e quindi sistemi multiprocessore mentre quelli switch sono

sistemi lascamente connessi e quindi sistemi multicomputer.

In figura abbiamo una struttura multiprocessore con struttura a bus in cui abbiamo tante CPU (con

la cache della CPU) con unico bus. 229

CPU CPU CPU

cache cache cache

| | | | | |

BUS

M M M M

| | | | In figura abbiamo

CPU --|-- ----- --|-- ----- --|-- ----- --|-- un sistema multipro-

--|-- ----- --|-- ----- --|-- ----- --|-- cessore in cui abbia-

CPU --|-- ----- --|-- ----- --|-- ----- --|-- mo tanti processori

--|-- ----- --|-- ----- --|-- ----- --|-- e tante memorie che

CPU --|-- ----- --|-- ----- --|-- ----- --|-- sono condivise da

--|-- ----- --|-- ----- --|-- ----- --|-- tutti i processori.

CPU --|-- ----- --|-- ----- --|-- ----- --|--

Volendo fare le stesse cose per i sistemi multicomputer abbiamo la classica rete Internet dove abbia-

mo la CPU la memoria locale (e quindi questa è una workstation) ed abbiamo tante workstation.

Workstation Workstation Workstation

Memoria locale Memoria locale Memoria locale

CPU CPU CPU

| | | | | |

BUS In quest’altra figura abbia-

----- ----- ----- mo il trasputer che però non

| | | | ha avuto molto successo ed

----- ----- ----- era un componente che po-

| | | | teva essere connesso a ma-

----- ----- ----- glia con altri componenti

| | | | uguali in cui ognuno di que-

----- ----- ----- sti ha una propria memoria

| | | |

----- ----- ----- 230

locale(in realtà ognuno di questi era un multicalcolatore e non un multiprocessore) e questo è un

caso limite tra sistemi distribuiti in senso stretto e sistemi paralleli, infatti si potrebbe vedere come

sistema distribuito in senso stretto poiché ognuno CPU ha una propria memoria locale ma nei fatti

questa risolve una unica applicazione(questa era destinata ai calcoli di compagnie petrolifere per

studi geologici).

Ci occuperemo esclusivamente della tipologia di sistemi distribuiti in senso stretto in cui abbiamo

un workstation lascamente connesse e sono connesse in rete locale (o geografica) e quindi quando

parleremo di S.O. faremo riferimento a questo tipo sistema e alla interconnessione con i vari S.O.

231

Venerdì 9 maggio

Prof. DE CARLINI

Ieri, dicemmo che vi erano due tipi di architetture distribuite, che erano in sostanza le mac-

chine multiprocessore con una memoria comune e le reti di calcolatori. Quest'ultimo è il

tipo di sistemi distribuiti che ha noi più interessa. Quindi noi ci occuperemo di sistemi ope-

rativi per questo lo tipo di architettura. Dicemmo ieri, che la tendenza attuale è in realtà

duplice: il mondo scientifico sta cercando di creare dal nuovo, sistemi operativi per queste

macchine che devono realizzare delle funzionalità che dopo vedremo, mentre le aziende,

invece, sfruttano i sistemi operativi esistenti e ci calano sopra un software di comunicazio-

ne che permette in realtà di creare un file system distribuito.

Questa duplice tendenza fa sì che si possa parlare di sistemi operativi lascamente commes-

si e sistemi operativi strettamente connessi.

Si utilizza il termine "sistemi operativi lascamente connessi" quando esso nasce dai vari si-

stemi operativi delle singole macchine integrati dal software di rete.

------------------------------------------------------------------------------------------------------

Sistemi operativi lascamente connessi

Sono costituiti dai sistemi operativi delle singole macchine e da un sistema operativo di

rete.

Possono supportare a seconda dei casi:

La condivisione di risorse: device fisici (ad esempio stampanti), file, basi di dati(que-

 sto è quello che succede quando io realizzo un files server) , gestori di comunicazio-

ne (ad esempio un programma che permetta ad una rete locale di collegarsi ad In-

ternet che è condiviso da tutte le macchine della rete) , applicativi utente.

Login remoto (cioè la possibilità di far il login da una macchina diversa da quella in

 questione)

Questi sono i servizi base che ogni sistema operativo distribuito lascamente connesso cer-

tamente mette a disposizione.

_______________________________________________________________________

I sistemi operativi distribuiti strettamente connessi, partono invece dal concetto base di

fare apparire all'utente del sistema distribuito così come se fosse un sistema monolitico,

cioè si cerca di rendere del tutto trasparente a chi si siede dietro una stazione di lavoro il

fatto che quella stazione di lavoro sia in rete.

_______________________________________________________________________

Sistemi operativi strettamente connessi

Sono i veri sistemi operativi e mirano alla trasparenza ossia a far apparire il sistema come

un sistema virtuale centralizzato.

Si caratterizzano per:

Un unico meccanismo di comunicazione globale tra i processi (cioè valido per tutti i

 processi che girano sulle macchine a prescindere da quali sono le macchine; ciò at-

tualmente non è ancora possibile nella realtà) .

232

Un unico sistema di protezione e sicurezza di accessi.

 Un'unica tecnica di gestione dei processi (questa è un altra cosa che sicuramente i si-

 stemi lascamente connessi non hanno) .

Stesso Kernel di sistema operativo sulle singole macchine (osserviamo la differenza

 fondamentale con i sistemi precedenti dove ogni macchina poteva avere un sistema

operativo differente ad esempio windows, UNIX eccetera. Del resto questo è l'unico

modo per avere l'unico meccanismo di comunicazione globale tra i processi e un

unico modo di avere una unica tecnica di gestione dei processi, in quanto, ovvia-

mente, il gestore dei processi su una macchina è il Kernel di quella macchina) .

_______________________________________________________________________

(Vedi Tanembaum Figura 9.15: a) Kernel monolitico, b) microKernel)

Il modo di realizzare ciò è quello di avere un microKernel su ogni macchina. Un microKer-

nel è un Kernel molto ridotto che fornisce solo i servizi base (ovviamente uguali per tutte

le macchine) che sono nei fatti i meccanismi di gestione dei processi.

La ragione di ciò è dovuta al fatto che alcune di queste macchine devono essere specializ-

zate, e il modo di specializzare una macchina è quello di metterci sopra del software che

poi mi realizza le funzionalità specifiche che io voglio assegnare a quella macchina. Tale

software deve essere visto come processi di sistema, che devono basarsi sul microKernel

così come si basano i normali processi utente.

Nella figura, ad esempio vediamo il microKernel su una macchina e subito sopra l'utente,

cioè il normale utente che ci lavora sopra; poi abbiamo il microKernel e sopra il file server;

poi abbiamo il microKernel e sopra il server delle directory ( questa è un ulteriore specia-

lizzazione cioè un server che serve a gestire semplicemente l'accesso alle directory del file

system distribuito, cioè su questa macchina non ci sono file di utente ma ci sono semplice-

mente i file directory con i quali poi si accede ai file di utente); poi abbiamo server dai pro-

cessi sopra il microKernel, il che vorrebbe dire una macchina specializzata per decidere su

quale macchina del sistema vanno allocati i processi di utente, potendosi spostare, questi

processi, da una macchina all'altra macchina a seconda del carico.

Alcune cose che abbiamo detto sono abbastanza futuribili; ad esempio avere da qualche

parte un server di processi che sia in grado di gestire una applicazione commerciale non

esiste. Esistono solo semplici applicazioni di laboratorio (a livello di ricerca universitaria)

che stanno studiando dei meccanismi con i quali distribuire i processi su una rete di mac-

chine, processi cooperanti ovviamente, ottimizzando la allocazione del carico di queste

macchine.

Altre cose invece sono già reali, come ad esempio il file server cioè un data base server.

Tuttavia, quelli che ci sono attualmente, vengono realizzati guardando sotto un Kernel che

non è un Kernel omogeneo ma è un Kernel di un sistema operativo centralizzato: se io vo-

glio attualmente realizzare un file server lo faccio su una macchina UNIX e quindi vedo il

Kernel di UNIX oppure su una macchina windows NT e quindi avrò il suo Kernel.

Ci siamo sicuramente resi sconto che un sistema operativo strettamente connesso non è di

facile realizzazione. Vediamo adesso quali sono i principali problemi.

233

Il primo è quello della trasparenza; questa è la cosa più difficile da fare se si vuole realiz-

zarla pienamente. La difficoltà sta nel fatto che vi sono diverse cose da garantire se voglia-

mo garantire la trasparenza perché noi dobbiamo garantire diversi tipi di trasparenza.

_______________________________________________________________________

Trasparenza :

Alla posizione: gli utenti ignorano le posizioni delle risorse (se esso

 vede un unica macchina virtuale, non deve ad esempio vedere che il file sul quale

sta lavorando si trova sulla macchina Pinco o sulla macchina Pallino, ma esso si tro-

va sulla sua macchina virtuale).

Alla migrazione: le risorse, migrano sempre senza che gli utenti ne abbiano perce-

 zione (rifacendoci all’esempio del punto

precedente, l'utente deve anche ignorare se il file sul quale sta lavorando, per qual-

che motivo, sia fatto spostare dalla macchina Pinco alla macchina Pallino, il che si-

gnifica poi, che la sua applicazione (la quale ricordiamo è stata scritta una sola volta

ed è sempre la stessa) deve continuare a funzionare a prescindere dalla locazione

del file, il che implica a sua volta che nella applicazione non ci deve essere nessun

riferimento alla macchina, in modo tale che questo file possa migrare da un server

ad un altro server senza che lui se ne accorga).

Alla replicazione: esistono più copie di una risorsa gestite dal sistema all'insaputa

 dell'utente (ad esempio ho più

stampanti, una stampante Pinco si guasta e il sistema redireziona automaticamente

l'output verso la stampante Pallino senza che l'utente se ne accorga. Oppure, ad

esempio, se io tengo un file duplicato e una copia non è più utilizzabile in quanto ha

avuto un crash e automaticamente il sistema mi fa lavorare sulla seconda copia sen-

za che io me ne accorga mantenendomi inoltre allineata la coppia a e la copia b)

Alla concorrenza: il sistema gestisce l'accesso sequenziale alle risorse condivise

 (cioè, significa che io gestisco la mutua esclusione senza che l'utente se ne accorga,

cioè senza che lui me lo abbiamo detto)

Al parallelismo: il sistema riconosce le attività da svolgere in parallelo.

_______________________________________________________________________

Ancora una volta, alcune delle cose sopra elencate sono futuribili mentre quelle che attual-

mente sono state già in parte risolte con del software apposito che viene già adesso utiliz-

zato ad integrazione di un sistema distribuito sono la trasparenza alla migrazione (in par-

te),alla posizione e duplicazione dei file.

Un altro requisito che i sistemi distribuiti devono avere è la flessibilità.

Il discorso della flessibilità è risolto dal fatto che i sistemi operativi di questo genere devo-

no essere costituiti da un microKernel uguale per tutte le macchine. Questo mi da' la flessi-

bilità in quanto la disponibilità di un microKernel uguale su tutte macchine mi offre tutti i

servizi base identici, e nello stesso tempo, permette di avere servizi più sofisticati con del

software apposito che su certe macchine ci sta e su delle altre no.

234

_______________________________________________________________________

Flessibilità:

Requisito che si traduce nella necessità di realizzare il sistema operativo delle singole mac-

chine con un microkernel che garantisca servizi di:

Comunicazione dei processi

 Gestione base della memoria (di basso livello, non della memoria virtuale)

 Gestione base della schedulazione dei processi

 Ingresso/uscita a basso livello

spostando quindi all’esterno gli altri servizi in modo da implementarli a seconda delle esi-

genze (esempio file server UNIX e DOS sulla stessa macchina in quanto essi vengono rea-

lizzati da qualche software che è calato sul microkernel della macchina)

_______________________________________________________________________

Gli ultimi requisiti che questo tipo di sistemi dovrebbero avere, sono la affidabilità, le pre-

stazioni e la scalabilità.

_______________________________________________________________________

Affidabilità:

Requisito che si presenta sotto gli aspetti di:

Disponibilità del sistema in presenza di guasti di alcune sue componenti .

Sicurezza nei confronti di usi di risorse non autorizzati

Tolleranza ai guasti, cioè la capacità di recuperare i malfunzionamenti senza che l'utente

intervenga (nel caso di malfunzionamento avere comunque parte del sistema che funziona

ancora e che mi recuperi i guasti)

Prestazioni

Requisito che dipende in positivo dal numero di CPU disponibili per le computazioni e in

negativo dall’overhead delle comunicazioni.

Richiede quindi un'accorta definizione della grana delle computazioni perché la tolleranza

ai guasti condiziona le prestazioni di un sistema distribuito

Scalabilità

Requisito che misura la capacità del sistema operativo e del software applicativo di essere

calato su un sistema distribuito indipendentemente dal numero di CPU

_______________________________________________________________________

Questi ultimi, sono comunque requisiti più lontani dal essere attualmente garantiti.

_______________________________________________________________________

La tendenza attuale è di realizzare sistemi operativi di tipo aperto basati su UNIX (anche

se ultimamente si sta diffondendo anche l'utilizzo della macchina windows NT; potremmo

avere anche una rete nella quale invece di utilizzare windows NT utilizziamo windows 95

235

ma in tal caso avremmo un sistema distribuito neanche lascamente connesso ma avremmo

solo dei PC che sono collegati in rete attraverso un qualche software).

Esistono due organizzazioni che hanno tale obiettivo:

Open Software Fondation (DEC,HP,IBM,Microsoft e altri) essa è un associazione

 fatta da costruttori i quali hanno scelto una certa versione di UNIX. La scelta è ca-

duta su UNIX della Digital (hanno a che fare con il Distribuited Computer Environ-

ment DCE).

(inoltre, c'era un'altra organizzazione di cui non sono riuscito a prendere nota ma

 che non ha molta importanza N.d.A.).

_______________________________________________________________________

Quindi, ripetendo ancora una volta, quello che si sta cercando di fare è quello di mettere

su queste macchine UNIX del software che permetta di realizzare un sistema lascamente

connesso. Cioè si cerca di implementare quella che è stata definita l'architettura DCE).

_______________________________________________________________________

Architettura del DCE

Questo tipo di architettura prevede che su macchina UNIX siano disponibili i seguenti

componenti:

File sistema distribuito (DFS)

 Gestione di un direttorio di oggetti (servizi di rete, mailbox, computers, eccetera)

 Chiamate a procedura remota (il che significa garantire l'implementazione di appli-

 cazioni client server)

Gestione dei thread (quando abbiamo parlato dei processi abbiamo detto che ci po-

 tevano essere due tipi di processi: i processi classici e i processi leggeri. Questi ulti-

mi sono i thread. La differenza tra un processo classico e un processo leggero è che

mentre i processi classici (diciamo alla UNIX) hanno ciascuno una propria area di

memoria, quindi non c'è condivisione di area di memoria, nel caso dei processi leg-

geri io ho nei fatti dei processi che condividono del tutto l'area di memoria e quindi

mi devo preoccupare dei conflitti e delle mutue esclusioni sulle strutture dati. Quin-

di il sistema operativo deve mettere a disposizione dei meccanismi tali che mi con-

sentono di risolve facilmente la mutua esclusione sulla memoria condivisa (dei mec-

canismi tipo semafori).

Gestione dei clock e di macchine (cioè avere la capacità di gestire l'allineamento dei

 vari clock di macchina in modo tale da definire con una certa approssimazione un

tempo globale di riferimento)

_______________________________________________________________________

236

Quindi noi andiamo a definire un architettura complessiva in cui andiamo a mettere i vari

componenti software che ci servono a garantire l'insieme di servizi sopra visto.

Teniamo presente che non è detto che questi componenti software siano prodotti dalla

casa costruttrice che ha creato il sistema operativo UNIX che gira su quella macchina, per-

ché io posso tranquillamente avere un sistema operativo UNIX comprato dalla DIGITAL,

questo supporta la architettura DCE e io ci vado a mettere sopra un software della casa

Pinco o della cassa Pallino (la quale aderisce a questo schema architetturale) che fornisce

uno dei servizi sopra elencati.

Con questo abbiamo finito la parte di introduzione ai sistemi distribuiti; adesso dovremmo

entrare un po' più in dettaglio in alcuni degli argomenti sopra citati. Prima di fare ciò dob-

biamo fare ancora una piccola introduzione ai concetti generali che riguardano le reti di

calcolatori.

_______________________________________________________________________

Classificazione delle reti: tipologie LAN, WAN, MAN

Sì diversificano per

Distanza fra i nodi

 Rete di comunicazione

 Velocità di trasmissione

_______________________________________________________________________

_______________________________________________________________________

LAN: Local Area Network

Utenti dislocati a moderata distanza (in un edificio o un campus)

 Rete di comunicazione costituito da apposito cablaggio che include elementi attivi

 (hub, repeater, terminal server, bridge) (nell’hardware della rete abbiamo degli ele-

menti attivi e degli elementi passivi cioè esiste un supporto fisico che serve a mante-

nere i segnali elettrici che viaggio sulla rete e ci sono poi degli elementi attivi che

permettono di gestire la comunicazione, cioè i bit, che viaggiano sulla rete. Nei fatti

per elemento passivo intendiamo il filo elettrico che può essere il cavo coassiale op-

pure può essere il cavo giallo che si usava fino a sette o otto anni fa, un altro sup-

porto di rete può essere il doppino telefonico).

Velocità di trasmissione da 1 a 100 Mb/sec. (anche se adesso stiamo a livelli più alti)

_______________________________________________________________________

_______________________________________________________________________

Sempre riguardo all LAN. 237

Metodi di trasmissione e controllo degli accessi in una LAN:

Doppino telefonico cavo coassiale in banda base e a larga banda, fibra ottica (Il dop-

 pino telefonico viene utilizzato specialmente quando cresce il numero di macchine

che sono in rete. Teniamo presente che con il doppino telefonico, la connessione fisi-

ca è a stella, cioè a livello topologico la rete appare a stella, mentre con il cavo coas-

siale la rete e a bus).

CSMA/CD (reti Eternet) e CSMA/CA (reti token ring e token bus)

 velocità di trasmissione da 50 a 600 Mb/sec

 Integrazione completa in sistemi di cablaggio strutturati Eternet Token Ring ecc.

 sono in pratica dei protocolli di rete.

_______________________________________________________________________

Man: Metropolitan Area Network

utenti dislocati in un'area metropolitana

 rete di comunicazione costituita da linee telefoniche e da infrastrutture tipiche di

 un'area metropolitana

velocità di trasmissione da 50 a 600 Mbps, grazie ad ATM e DQDB

_______________________________________________________________________

Wan: Wide Area Network

Utenti a grande distanza (ad esempio migliaia di chilometri)

 Rete di comunicazione costituita da linee telefoniche, canali a microonde, fibra otti-

 ca, collegamenti via satellite

Velocità di trasmissione da 1200 b/sec a 1 Mb/sec

Attualmente siamo su livelli di 32 Mb/sec. a 622 Mbps con la tecnologia ATM

_______________________________________________________________________

La comunicazione nei sistemi distribuiti

E' l'elemento chiave di sistemi distribuiti lascamente connessi e a memoria locale in

 cui lo scambio di dati tra processi avviene unicamente mediante uno scambio di

messaggi

Deve essere strutturata a livelli astratti per supportare tutti i possibili aspetti della

 comunicazione coinvolti nel colloquio tra entità sia di alto che di basso livello di

astrazione (esempio: la comunicazione tra due programmi applicativi, è un tipo di

comunicazione che si pone ad un livello molto alto e che deve essere permessa. Per

permettere ciò noi dobbiamo fare comunicare due processi di cui uno crea un mes-

saggio e un altro lo riceve; i processi mittente e destinatario si trovano ad un livello

di astrazione certamente più basso di quello dei programmi applicativi a cui i pro-

cessi appartengono. Dobbiamo quindi avere un software di gestione dello scambio

di un messaggio tra due macchine. Del resto, per scambiare un messaggio c'è biso-

gno di scambiare dei byte, anzi dobbiamo scambiare i singoli bit del messaggio e

238

quindi dobbiamo far colloquiare i due attori che permettono di trasmettere un bit

sulla linea (che saranno sicuramente degli attori hardware. Prima i due attori che

dovevano colloquiare erano rispettivamente i due programmi applicativi e i due

processi ). C'è bisogno quindi anche di una logica per l'invio di un singolo bit su una

linea seriale). Riassumendo quindi, la comunicazione coinvolge due attori che si

pongono ad un livello di astrazione diversa e ciò significa dire che la comunicazione

deve prevedere necessariamente dei meccanismi che sono strutturati a livelli così

come sono strutturati a diversi livelli di astrazione gli attori che utilizzano questi

meccanismi.

Richiede la definizione di protocolli ossia di regole che devono essere seguite dalle

 entità di pari livello per poter colloquiare tra loro

_______________________________________________________________________

Quindi, riassumendo i concetti riportati in questo lucido, abbiamo che:

noi dobbiamo far colloquiare due attori che si pongono allo stesso livello di astrazione; per

far questo debbo definire un protocollo ossia delle regole, ed è chiaro che, probabilmente,

poiché gli attori ad un certo livello di astrazione non possono colloquiare tra di loro diret-

tamente, il colloquio tra le due entità di un certo livello avviene, nei fatti, utilizzando col-

loqui tra due entità del livello immediatamente inferiore le quali a loro volta, ovviamente,

utilizzeranno un protocollo di livello inferiore, cioè delle regole di livello inferiore, e così

via a scendere fino a quando non arriviamo alle due entità che sono in grado di scambiarsi

un bit, che sono lei uniche entità che sono in grado di colloquiare direttamente tra loro.

Quindi, nei fatti, abbiamo il concetto non solo di protocollo, ma anche il concetto di livello

di protocollo.

Un esempio:

Abbiamo due manager che debbono comunicare fra di loro, per fare un certo affare. Questi

due attori colloquiano nell’ambito di un contesto ben definito in cui ognuno conosce esat-

tamente la semantica del problema che deve trattare cioè conoscere il livello cognitivo.

Questi due manager scambiano queste informazioni probabilmente non in modo diretto

(nei fatti anche se parlassero uno di fronte all'alto userebbero dei meccanismi di più basso

livello come il suono che si propaga nell’aria, e viene ascoltato ) ma parleranno tramite le

loro segretarie che rappresentano gli attori di livello più basso. Quindi ogni manager par-

lerà con la propria segretaria per poter comunicare con l'altro manager. La segretaria non

conosce il significato delle parole che deve comunicare all'altro manager ma batterà sem-

plicemente a macchina il messaggio da comunicare. Peraltro questo messaggio non sarà

dato a mano all'altra segretaria ma, per esempio, sarà inviato via fax cioè verrà messo su

un fax che parlerà con l'altro fax.

Quindi, in sostanza, abbiamo un discorso di attori che devono comunicare tra di loro ad

un certo livello di astrazione caratterizzato da delle proprie regole e da una propria se-

mantica del messaggio. Del resto questi attori non sono in grado di comunicare tra di loro

e quindi devono usare attori di livello più basso che hanno una comunicazione a loro volta

caratterizzata da una propria semantica e da un proprio protocollo. Questo discorso si ri-

pete fintanto non si giunge ad un livello in cui i due attori sono in grado di comunicare tra

239

loro, anche questo livello è caratterizzato dalla propria semantica e dalle proprie regole di

comunicazione.

Ne segue che la comunicazione non coinvolge un unico protocollo, ma coinvolge n proto-

colli che sono tutti i protocolli degli attori di livello più basso che sono coinvolti nel collo-

quio tra i due attori diciamo base.

Il lucido seguente non fa altro che ribadire i concetti appena esplicitati.

_______________________________________________________________________

Un altro esempio:

La comunicazione tra le persone coinvolge:

un livello cognitivo che include concetti e conoscenze note alle persone che comuni-

 cano tra loro(ad es. il concetto i libro se si sta parlando del suo contenuto)

un livello linguaggio utilizzato per tradurre la semantica dei messaggi scambiati in

 parole (ad esempio la lingua italiana)

un livello fisico che fornisce il mezzo utilizzato per lo scambio di messaggi (ad

 esempio le onde sonore o un foglio di carta)

_______________________________________________________________________

Concetti chiave

Ciascun livello include servizi di comunicazione

 I servizi offerti da livelli distribuiti sono tra loro indipendenti

 La comunicazione avviene sempre tra entità di pari livello allocate in distinti nodi

 della rete

La comunicazione tra due entità di livello di astrazione I richiede necessariamente

 il supporto di livelli inferiori in quanto un servizio di livello I viene implementato

mediante servizi di livello I-1

Le regole e i formati che specificano lo scambio di dati a due livelli adiacenti costi-

 tuiscono una interfaccia tra i livelli

_______________________________________________________________________

Una cosa fondamentale che bisogna sottolineare, è che ogni entità offre dei servizi alle en-

tità di livello più alto utilizzando servizi offerti dai livelli più bassi, e, ciascuna entità, è in-

dipendente sia da quelle di livello più alto che da quelle di livello più basso. Questo signi-

fica dire che in una suite di protocolli io posso sfilare un livello, sostituirlo con un altro, e

far rimanere inalterati sia tutto lo stack dei livelli inferiori che quello dei livelli superiori.

Questo concetto, in termini pratici, è importantissimo ed è quello che per esempio mi per-

mette di realizzare lo scambio di un file tra due macchine mediante un protocollo T C P A

P il quale viene implementato su una rete locale che può essere sia Eternet che token ring

dove il protocollo Eternet e il protocollo token ring sono due protocolli di livello più basso

che sono tra di loro in alternativa. Quindi io posso avere, ad esempio, una rete costituita

da un doppino telefonico ( quindi ci sarà un protocollo per scambiare un bit sul doppino),

su cui è implementato il protocollo Eternet per scambiare i frame (pacchetti, insieme dei

byte che bit dopo bit vengono mandati. Esiste una differenza fondamentale tra il termine

240

pacchetto e il termine frame: pacchetto e è un fatto logico, il frame è un fatto fisico, quindi

essi si pongono a due livelli di astrazione differenti) e sul frame implemento il protocollo

IP (Internet Protocol) che è un protocollo che mi permette di scambiare i frame tra due

macchine che non sono strettamente connesse ma due macchine che sono connesse attra-

verso una terza macchina. Quindi I P può tenere sotto un livello che è sia Eternet sia token

ring che sono in alternativa e quindi io posso sostituire un protocollo di un livello con un

altro protocollo dello stesso livello e questo mi deve consentire di far rimanere inalterati

sia i protocolli utilizzati ai livelli più alti che quelli utilizzati dai livelli più bassi. Se imma-

giniamo che questi livelli sono molti capiamo le combinazioni che possono venire a crearsi

e ciò è importante per chi studia i sistemi di comunicazione perché deve capire se due li-

velli sono tra di loro compatibili perché in realtà appartengono a livelli distinti oppure se

sono tra di loro in antagonismo perché appartengono allo stesso livello e quindi non pos-

sono coesistere nell'ambito di uno stesso sistema.

E' implicito in quello che abbiamo appena detto che se io sostituiscono un livello con un al-

tro devo rispettare l'interfaccia stabilita tra quei due livelli.

________________________________________________________________________

Vantaggi della struttura a livelli

Indipendenza tra i livelli. Un livello deve conoscere i servizi offerti dal livello imme-

 diatamente inferiore ma non come sono implementati

Flessibilità. Una modifica ai servizi offerti da un livello non ha conseguenze sui li-

 velli inferiori e una modifica alle modalità di erogazione di un servizio offerto da un

livello non ha conseguenze sugli altri livelli

La decomposizione facilita il progetto modulare dei livelli e la loro manutenzione

 Con la struttura in livelli e l'incapsulamento delle funzioni che implementano i ser-

 vizi è il presupposto della attività di standardizzazione della struttura di un sistema

di comunicazione (Ad esempio il grande successo avuto dal modello OSI è dovuto

al fatto che esso ha fissato una struttura a livelli e standardizzato ciascun livello di

protocollo. In questo modo è stato possibile recepire una buona parte dei protocolli

esistenti e ciò ha consentito di far comunicare tra di loro macchine tra loro diverse).

_______________________________________________________________________

Figura 10.1 livelli, interfaccia e protocolli nel modello osi

________________________________________________________________________

Il modello Open System Interconnection (OSI)

È stato definito dal ISO (International Standard Organizzation)

 E' strutturato in 7 livelli ed include gli aspetti sia hardware che software di una ar-

 chitettura di rete (pensa ad attori che devono colloquiare su ciascuno di questi livel-

li e quindi definisce le caratteristiche dei servizi che debbono offrire i protocolli a

ciascuno di questi 7 livelli)

Include sia i protocolli orientati alla connessione che deve essere stabilita tra mitten-

 te e destinatario prima di scambiarsi i dati, che protocolli privi di connessione

Una intestazione (header) viene sempre aggiunta ad un messaggio prima di essere

 scambiato tra livello I e il livello I-1 della macchina mittente. Un messaggio che

241

viaggia su di una rete è quindi preceduto da molte intestazioni (il numero di inte-

stazioni quindi cresce quando scendiamo nei livelli della stessa macchina mentre di-

minuisce quando saliamo tra questi livelli. Ogni livello aggiunge un'intestazione

che il livello omologo della macchina destinataria considera come informazione,

tale informazione viene soppressa se si deve passare dal livello I-1 al livello I men-

tre viene sostituita se si deve riscendere per passare alla macchina successiva. Nel-

l'intestazione vi sono non solo le informazioni che servono a gestire il messaggio ma

anche quelle che mi consentono di capire se debbono continuare a salire o scendere

e tutte le informazioni che mi permettono di effettuare i controlli ( debbo controllare

che l'informazione che mi è arrivata è corretto). Tali controlli sono previsti a tutti i

livelli; quando uno di questi controlli fallisce, la trasmissione si interrompe e c'è un

messaggio di ritorno lungo la rete di comunicazione per poter permettere all'attore

principale di poter ritrasmettere il messaggio che ha subito il guasto cioè ha perso il

contenuto informativo. Questo recover della comunicazione è del tutto trasparente

allo attore principale che ha iniziato il protocollo )

_______________________________________________________________________

_______________________________________________________________________

figura 10.2: un tipico messaggio.....

_______________________________________________________________________

I vari livelli che sono descritti nella figura 10 verranno da noi ora analizzati uno ad uno

mettendo in luce i protocolli che appartengono a questi livelli in modo tale che li possiamo

riconoscere avendoli in qualche forma sentiti (tuttavia non saranno approfonditi perché

non riguardano esplicitamente il nostro corso).

________________________________________________________________________

Livello fisico

Si occupa della trasmissione dei singoli bit. A tale livello viene definita:

La modalità di rappresentazione fisica dei bit

 La velocità di trasmissione

 L'unidirezionalità e la bidirezionalità

 Il mezzo di comunicazione

 La dimensione e la forma dei connettori

Esistono molti protocolli standard per tale livello fisico; il più noto è il protocollo RS232C

per la linea di comunicazione seriale

_______________________________________________________________________

Il livello fisico tipicamente riceve un byte dal livello superiore (immaginiamo la tipica in-

terfaccia seriale) e spara ad uno ad uno i vari bit sulla rete. Questo viene certamente imple-

mentato dall'hardware della interfaccia seriale cioè dalla scheda Eternet. Quindi in questo

livello viene definito un protocollo che fissa i punti elencati nel lucido precedente.

242

14/05/97 (DE CARLINI)

La comunicazione nei sistemi distribuiti

Nel modello Open System Interconection abbiamo degli algoritmi che colloquiano a più livelli:

Livello 1 : il livello fisico

Si occupa della trasmissione tramite cavo e con un protocollo specifico. Ma questo non ci interessa

molto;

Livello 2 : livello dei dati

Questo è il protocollo che permette di trasmettere dei frame tra due macchine.

La caratteristica importante è che le macchine che si scambiano il frame (che è un insieme di byte),

sono macchine in cui esiste una connessione fisica.

Oss. Un frame è un messaggio a livello di link ed indica un qualcosa di strutturato che non dipende

dalla particolare semantica dei byte.

Ricordiamo che il messaggio che va sulla rete si presenta come in FIG:

Messaggio Coda

esiste il contenuto informativo dove è stato aggiunto delle “testate” (sono 6) e la coda è relativa del

livello dati.

Quindi abbiamo marcato , aggiungendo la testa e la coda, l’inizio e la fine del frame ed abbiamo ag-

giunto nella coda delle informazioni di controllo. In particolare abbiamo calcolato ed aggiunto il

byte di checksum ed introducendo in questo il concetto di pacchetto numerato. Il ricevente può sco-

prire degli errori di trasmissione o tramite il checksum (il quale avrà un valore errato) o tramite il

numero di frame (il quale o è per salto oppure è per ripetizione).

Per vedere come funziona vediamo la fig. Abbiamo il trasmitten-

A B te A e il ricevente B, il

Dato0 trasmittente A manda il

Dato0 Dato0 a B il quale si

Dato1 Controllo accorge dell’errore, ma

Controllo nel frattempo A sta

mandando il Dato1, ma

con il segnale di con-

trollo B avverte A di ri-

trasmettere il Dato0. 243

Riepilogando per il livello 2 :

Si occupa della trasmissione dei frame su di un link e della individuazione e correzione degli errori

di trasmissione. Un frame è un messaggio a livello di link.

Inoltre si fa carico di:

1)Marcare l’inizio e la fine di un frame;

2)Calcola ed aggiunge il byte di checksum;

3)Verifica il checksum;

4)Associa ad un frame un numero;

5)Richiede e ritrasmette i frame errati (per checksum o sequenza);

Citiamo dei protocolli di livello 2:

-Avanced Data Communication Control Procedure (ADCCP) dell’ANSI;

-High level Data Link Control (HDLC) dell’ISO;

-**Syncronus Data Link Control (SDLC) della System Network Architeture (SNA) dell’IBM;

-**I Protocolli IEEE 8022 (Carrier Sense Multiple Access with Collision Detection - CSMA/CD

TOKEN BUS e TOKEN RING);

N.B. con il simbolo ** abbiamo indicato i protocollo più usati.

Parliamo in particolare del CSMA/CD (e CSMA/CA) perché è quella che utilizza l’Ethernet.

Si chiama Collision Detection poiché la struttura fisica di trasmissioni dati ipotizzata per l’Ethernet

il bus che è strutturato con un protocollo dato da :

10 base T twisted poiv

10 base 2 their

10 base 5 thick

Questi tre supporti fisici permettono di creare il bus.

Ora grazie al CSMA/CD permette di far vedere l’Ethernet come se fosse collegato ancora ad un bus

anche quando abbiamo un grosso numero di accessi ad un nodo(vedi FIG) e fa in modo che quando

ci sono delle collisioni (cioè due attori chiedono il server) ritarda i frame segmentando la rete (vedi

244

FIG) ed il Bridge deve riconoscere l’indirizzo della macchina per dargli l’autorizzazione a passare o

tenerlo sospeso in un segmento.

Vediamo il CSMA/CA

Supponiamo di avere delle macchine che sono connesse ad anello, quindi ogni macchina ha una

macchina antecedente ed un macchina seguente ed ogni macchina colloquia con quella seguente.

Quindi per poter colloquiare due macchine distanti devono essere coinvolte tutte le macchine che

stanno in mezzo e deve avere una autorizzazione dalla macchina che la seguente (token, cioè puoi

farlo).

Ora se la macchina ha qualcosa da trasmettere passa il messaggio alla successiva la quale riconosce

se il messaggio è rivolto a lei oppure ad un’altra macchina. Se il messaggio era destinato ad un’altra

macchina aspetta a sua volta il token della macchina successiva e così via fino a quando il messag-

gio è arrivato a destinazione.

Livello 3 : livello rete

Si occupa della trasmissione dei frame tra macchine che non sono strettamente connesse, e quindi

ha il compito di trovare il percorso che il pacchetto deve fare per la destinazione. Quindi questo li-

vello deve riconoscere verso chi si deve instradare il pacchetto. Allora in altre parole :

1)Si occupa della trasmissione dei pacchetti ossia dei messaggi a livello nodi rete;

2)Si fa carico di controllare la congestione della rete e instradare (routing) i messaggi;

3)Al livello appartengono sia protocolli orientati alla connessione quali X.25 del CCITT che proto-

colli senza connessione quali IP (Internet Protocol che usa una tabella ) del DoD;

Livello 4 : il livello trasporto

Ha il seguente compito:

1)Si occupa della trasmissione dei messaggi che provengono dal livello superiore suddividendoli in

pacchetti da affidare al livello inferiore;

2)Al livello appartengono sia protocolli orientati alla connessione, cioè quelli che garantiscono una

connessione diretta virtuale senza controllo dell’errore, quali TCP (Trasmission Control Protocol)

che protocolli senza connessione UDP (Universal Datagram Protocol) entrambi del DoD;

3)Entrambi i tipi di protocollo possono essere implementati con protocolli del livello inferiore sia

orientati alla connessione che privi di connessione (il tipo di connessione dipende dal tipo di Bus);

4)Le combinazioni più note sono TCP/IP e UDP/IP;

Questi sono i livelli più importanti quelli che seguono li vedremo più velocemente.

Livello 5 : il livello di sessione

In questo livello un attore colloquia con un altro attore per una intera sessione di lavoro e il messag-

gio per una intera sessione di lavoro vengono passati al livello 4 il quale che li suddivide in pacchet-

ti etc.

Livello 6 : il formato dei messaggi

Nella strutturazione del messaggio si deve conoscere il formato;

Livello 7 : livello delle applicazioni

In questo livello i due attori sono due applicazioni che colloquiano;

245

Il modello a scambio di messaggio

Vediamo quali sono le modalità di interazione (o comunicazione)di processi che sono allocate su

macchine distinte, le quali modalità abbiamo già cominciato a discutere quando abbiamo parlato

della cooperazione tra processi.

Abbiamo visto che due processi possono cooperare(e non c’è competizione) nel modello a scambio

di messaggio.

Ricordiamo che nel modello a scambio di messaggio, ciascun processo evolve in un proprio am-

biente e non può essere modificato da altri processi e questo implica che una qualsiasi iterazione tra

processi avviene mediante scambio diretto di messaggi tramite Kernel o in fase di compilazione.

Per i vari processi esiste un gestore di risorse e da questo nasce il concetto di server il quale è un ge-

store per contro di altri processi di proprie risorse private.

Quindi i processi dovranno :

1)Ricevere messaggi di richiesta ;

2)Dovranno operare sulle risorse che gestiscono ;

L’implementazione di questi processi per la gestione delle risorse funziona bene nel modello client

server in cui il file fa la richiesta e va in attesa del servizio

Oss. Due operazioni si dicono sincronizzati quando in ogni momento l’uno sa lo stato dell’altro.

Lo scambio di messaggio avviene tramite :

-send (dest,mptr)

-receive(addr,mptr)

in cui per la send i parametri sono il destinatario e il messaggio e nel caso della receive sono un

puntatore address il quale potrebbe qualificare il messaggio ricevente o una nostra mailbox tra le va-

rie mailbox che possediamo.

In realtà address è l’indirizzo di ascolto della macchina destinataria.

Primitive bloccanti e non bloccanti

Ricordiamo che send bloccante(sincrona, cioè se il mittente e il destinatario non appartengono alla

stessa macchina il blocco permane fin quanto il messaggio viene copiato a cura del Kernel dal buf-

fer del mittente al destinatario, se il mittente e destinatario appartengono alla stessa macchina il

blocco non si ha quando il messaggio è ricevuto ma quando è trasmesso ) e non bloccante(asincro-

na), quindi:

send bloccante:

Il mittente è bloccato finche il messaggio non viene completamente trasmesso e si libera quando il

buffer ha depositato il messaggio;

Client in esercizio Client in esercizio

Messaggio

246

send non bloccante :

Il mittente è bloccato soltanto per il tempo necessario a liberare il buffer in cui ha depositato il mes-

saggio copiandolo in un buffer interno al Kernel

Il mittente non viene affatto bloccato ed il suo buffer liberato ma il Kernel segnalerà questa evenien-

za mediante una interruzione. Cliente

bloccato

Messaggio

copiato

receive bloccante:

Il destinatario che esegue la receive è bloccato se il messaggio non è disponibile e lo sarà finche

questo non perviene;

receive non bloccante :

Il destinatario non viene mai bloccato. Si possono avere i seguenti casi:

-la receive riceve il messaggio e questo è presente;

-la receive non riceve il messaggio perché questo non è presente (passaggio al Kernel);

Oss. Le primitive non bloccanti sono anche dette sono anche dette sincrone mentre quelle bloccanti

sono dette anche sincrone

Primitive bufferizzate e non bufferizzate

La send non bloccante presuppone una bufferizzazione di messaggio, cioè non basta avere il buffer

del mittente ma dobbiamo avere anche un buffer nel Kernel, poiché dobbiamo liberare il buffer del

mittente per una prossima send, mentre le send bloccanti non sono bufferizzate.

nello stesso modo possiamo avere:

receive non bufferizzata :

Una receive comporta la perdita di un messaggio se il destinatario segnala la volontà di ascoltare un

messaggio (cioè esegue la receive) dopo che il mittente si blocca

receive bufferizzata:

Il processo può richiedere al Kernel di creare un mailbox associata ad un indirizzo

Oss una send non bloccante non è affidabile 247

15/5/1997 Sistemi Operativi

Prof. U. De Carlini

Lezione tratta da Tanenbound.

Il modello client server:

per modello client-server si intende: vi sono due processi i quali cooperano, nel contesto della stes-

sa applicazione, di questi due processi in realtà nel contesto del primo viene eseguito un programma

chiamante mentre nel secondo, il server, viene eseguito (diciamo) il sottoprogramma che l'applica-

zione richiede.

abbiamo utilizzato questo esempio brutale perché nel contesto di un unico processo, nel momento

in cui ho una applicazione sequenziale, in tale contesto viene eseguito sia programma chiamante che

il chiamato; e è quindi ovvio che in esecuzione può esserci sono uno dei due. nel modello client

server nel momento in cui faccio l'ipotesi che chiamante e chiamato vengono eseguiti nel contesto

di due processi differenti allora il chiamante e chiamato potrebbero evolvere in parallelo nel qualca-

so non si tratterebbe di una computazione client server perché nella computazione client server si fa

l'ipotesi che il processo chiamante si sospendere per il tempo necessario acciocché la routine, che

verrà eseguita nel contesto di un altro processo, venga completamente eseguita e finquando questa

routine (ossia il processo nel cui contesto la routine è eseguita) non restituisce al contesto in cui vie-

ne eseguito il chiamante il risultato dell'elaborazione.

ecco quindi il concetto di cliente-servitore ciò significa dire nei fatti che il processo nel cui contesto

viene eseguito il chiamante funge da cliente per il processo nel cui contesto viene eseguito il chia-

mato, nel senso che richiede un servizio a quest'ultimo.

Cerchiamo ora di capire perché si utilizza questo modello: ovviamente il modello Client-Server

non è utilizzato per esplodere il parallelismo intrinseco dell'applicazione in quanto tale (il client è

bloccato quando lavora il server) ma serve per mettere a comune il processo servitore a più pro-

cessi clienti; e quindi nei fatti il servitore è un gestore di risorse che permette la condivisione di ri-

sorse proprietarie (del servitore) tra più processi clienti.

Introduzione alle architetture client server:

Se pensiamo alle architetture comunemente realizzate negli ultimi anni tipicamente non siano più in

un ambiente centralizzato ma andiamo verso grosse macchine dove anziché avere terminali stupidi

collegati ad un host abbiamo terminali stupidi collegati ad una macchina che gestisce terminali e

essa è collegata all'host, questo tipo di macchina ha dato vita ad un'architettura denominata

master/slave in cui nei fatti l'applicazione girava sull'host e i terminali gestivano l'input/output. Que-

sto modello è stato poco usato, viene qui citato in quanto è un modello dall’architettura intermedia

tra la macchina centralizzata e il modello client-server.

248

Figura 1 Client

Client Client

LAN

O

WAN

Server

Disco Stampante

Nel modello client server si può anche avere un client che gestisce sono l'input/output ma l'avvio

dell'applicazione non avviene sull'host (server) ma sul client stesso; si ha quindi un capovolgimento

di logica. Un'architettura di certo più sofisticata è l'architettura pear to pear (pari a pari) dove ho più

processi che cooperano tra di loro ma non esiste un legame di tipo master/slave (non c'è alcuna ge-

rarchia) ad esempio il processo A può richiedere l'attivazione del processo B, ricevuti i dati li può

spedire a un processo C richiedendo la sua attivazione e così via. Questa è l'architettura che viene

usata nelle macchine parallele.

Quindi posso dire che quando parlo di un'applicazione client-server mi riferisco ad un'architettura

del tipo in figura 1, dove ho una rete locale (o geografica), tante macchine intelligenti (client) su cui

gira l'applicazione e infine ho il server su cui gira la restante parte dell'applicazione, in particolare si

occupa di gestire le risorse condivise dai client.

La sofisticazione sta’ nella modalità con cui partiziono l'applicazione, vedi figura 2, anziché avere

un sistema monolitico distribuisco l'applicazione tra la macchina client e la macchina server. ve-

diamo ora quali sono le possibilità di distribuzione intendendo con ciò che posso suddividere l'ap-

plicazione in diverse parti, di cui una si occuperà delle modalità di input/output, 1'altra parte invece

si occuperà della fase elaborativa (algoritmica), avrò inoltre la gestione dei dati (Data Base), quindi

occorrerà un DBMS che è parte integrante della mia applicazione nonostante sia una piattaforma

sulla quale si poggia l'applicazione.

Il livello di sofisticazione dipendere da dove vado a tagliare questo discorso, cioè come vado a di-

stribuire i pezzi dell'applicazione una parte sul client e una parte sul server; la logica più banale è

quella di dire: la logica di presentazione sta sul client e tutto il resto sul server. Ancora più banale è

quella di utilizzare il client come emulatore di terminali e tutto quanto è poi gestito dal server, ma

questo non è client-server in quanto sottoutilizzo le macchine client e demando tutto alla macchina

server. 249

Figura 2 Server

Application

Presentation Logic Then

If

Business Logic SQL

Data Base Logic

DBMS

Disco

L'altra suddivisione classica è quella di tagliare all'opposto ovvero sulla macchina server ho il data

base e il DBMS (Data base management server), in tal caso si usa dire data base server, mentre sulla

macchina client ho tutto il resto dell'applicazione; quando faccio un accesso al Data base questo vie-

ne intercettato sulla macchina client e quindi si passa al server che fornisce il servizio. Questo tipo

di suddivisione già fornisce un'applicazione client-server di un certo livello di sofisticazione.

Altro esempio ancora più sofisticato è quello di avere sul client la presentation logic e una parte del

business logic dove la parte algoritmica viene distribuita su 2 o 3 macchine che lavorano in paralle-

lo, (sto’ facendo riferimento in questo caso ad un modello pari a pari).

Siamo ora giunti al punto in cui nei fatti dobbiamo risolvere (indipendentemente dalla distribuzione

dell'applicazione) questo problema: devo porre su di una macchina una procedura chiamante sull'al-

tra macchina dello allocare una procedura chiamata e devo fare in modo di costruire un'enviroment

intorno al chiamante e al chiamato in modo tale che invece di fare una normale call a procedura,

devo far sì che scateni un insieme di attività tali che la call viene fatta ma non alla reale procedura

bensì ad una procedura fittizia (in modo tale che il chiamante non si accorga di nulla) che provvede

poi ad implementare tutto il meccanismo di scambio di messaggi che permetterà di attivare un altro

processo, sul server, il quale, a sua volta, farà in modo di lanciare la procedura chiamata senza che

questa si renda conto che non è stata lanciata direttamente dal chiamante ma bensì in modo indiret-

to.

Per chiarire meglio le idee vediamo la figura 3. Come si può notare nel contesto del processo client

viene eseguito il chiamante, la procedura chiamata viene eseguita nel contesto del processo server

(come già più volte esposto), il problema fondamentale è che chi scrive l'applicazione non deve ve-

dere questa situazione ma per lui le cose devono funzionare come se si trovasse di fronte alla mac-

china centralizzata: quindi esistono ambienti di sviluppo che consentono di fare ciò creando auto-

maticamente degli stub (parti in grigio). Lo stub funge praticamente da interfaccia in quanto il chia-

mante esegue la normale call a procedura ma in realtà, a sua insaputa, viene attivato lo stub che

250

si mette in contatto con lo stub del chiamato, gli passa i parametri con una delle procedure studiate

(send e receive), quindi viene attivata la procedura chiamata che non si accorge di essere stata lan-

ciata dallo stub, esegue le sue operazioni quindi restituisce i parametri allo stub, in modo del tutto

analogo vengono ritornati i parametri alla procedura chiamante.

Figura 3 Stub del Server Stub del Client

Macchina del Client Macchina del Server

chiamata chiamata

Client Server

ritorno ritorno

KERNEL KERNEL

Impacchetta parametri

Spacchetta parametri

Facciamo ora un passo indietro:

Vediamo un esempio di codice client-server. Figura 10.6 da Tanenbound.

L'analisi di tali procedure è facilitato dai commenti inoltre sono anche auto esplicative.

Suppongo di avere un file-server (utility per creare, leggere, scrivere, cancellare un file). Il server ha

una receive bloccante in testa: al momento dell'accensione il server parte e attende, da buon servito-

re, la richiesta. Questo praticamente è il send stub: quando riceve un messaggio viene sbloccato,

quindi fa il case su M1 e lancia la routine che esegue il servizio. Fatta l'operazione mette il risultato

in un campo di un messaggio il quale viene spedito con una send al processo client. Il fatto caratte-

rizzate è la receive in testa e la send in coda.

Notiamo che questo server è solo didattico in quanto un buon processo server deve essere multi

trend (con più flussi di controllo in grado di evolvere parallelamente). Vediamo come si articola il

client: il client vuole copiare un file, infatti fa read e write consecutivamente. Il codice a questo pun-

to non ha bisogno di altre spiegazioni.

Indirizzamento del server:

quando fa faccio una send devo individuare sia la macchina che il processo, vediamo come posso

fare ciò:

 indirizzo assoluto: individuo un solo indirizzo in cui alcune cifre (generalmente quelle di

maggior peso) individuano l'host e altre il processo. Questo tipo d'indirizzamento ha l'incon-

veniente di non fornire alcuna trasparenza (non possono spostare il processo server su di

un'altra macchina).

 Identifico il nodo tramite un indirizzo e il processo tramite una mailbox. Ciò implica però

che il server quando nasce delle dichiarare la mailbox. Anche in questo caso non ho traspa-

renza.

 Identifico il server mediante un indirizzo assoluto e chiedo quindi al server di identificare il

client in modo simbolico. È un tipo strano lasciamolo stare.

251

 Vediamo ora il caso più generale: identifico il processo server mediante un nome simbolico,

risolvono la corrispondenza individuando nell'ambito della rete un particolare server che è

un server di nomi, il quale possiede una tabella che restituisce la posizione dell'host e il numero di

processo che mi serve. La stessa cosa è fatta nell'esempio su visto con la routine INIZIALIZE.

 Altra soluzione è quella di mandare i messaggi su tutta la rete, quello che risponde sarà ov-

viamente il server.

Altra cosa da sottolineare è il protocollo:

Quando faccio una send posso stare su rete locale o su rete geografica, quindi come faccio a decide-

re il protocollo da utilizzare?

In generale devo utilizzare un protocollo che si colloca al livello 5 della modello ISO-OSU; un mes-

saggio per me rappresenta un qualcosa che deve essere suddiviso in pacchetti per cui il Kernel uti-

lizzerà un protocollo che probabilmente (dipendere dallo specifico Kernel) ha una routine che affet-

ta il messaggio, mi crea dei pacchetti e li passa a IP sulla rete. L'applicativo sta a livello 5 chiama

TC/ IP che sta a livello 5 fa la send al Kernel e spedisce il messaggio sulla rete.

Posso avere dei problemi nel senso dell'efficienza: se mi trovo su di una rete locale è inutile usare

TC/IP ma posso usare UDP facendo solo l'affettamento e posso non mettere su IP scendendo diretta-

mente a livello 2.

Nello sviluppo dell'applicativo in genere non si fanno protocolli ad hoc per mantenere la genericità.

Entriamo ora nel merito del problema da cui siamo partiti:

Come si realizzano i client-stub e i server-stub; ovvero come tradurre una chiamata a procedura re-

mota in modo tale che sembri una normale chiamata a procedura.

252

Venerdì 16/5/97

Ing. FASOLINO

Esempio: Produttore-Consumatore

Descrizione del problema:

Abbiamo un produttore e un consumatore che condividono un buffer. Abbiamo bisogno

di due semafori: uno che indica che il buffer è libero e l'altro che indica che il messaggio è

disponibile. Tutto il problema della mutua esclusione si riduce a due wait e due signal in-

vertite tra produttore e consumatore; cioè il produttore fa prima la wait sul suo semaforo,

scrive il messaggio nell'area di memoria e poi fa la signal sul semaforo del consumatore e

viceversa per il consumatore.

Realizzazione:

#include <stdio.h>

#include <sys/types.h>

#include <sys/wait.h>

#include <sys/ipc.h>

#include <sys/shm.h>/*primitive per la gestione della memoria condivisa*/

#include <sys/sem.h>/*primitive per la gestione dei semafori*/

#define DIM 16 /*dimenisone del buffer*/

#define NVOLTE 16 /*numero di volte in cui viene ripetuto il ciclo

produzione/consumo*/

#define SEM_PROD 0 /*identificatore del semaforo del produttore*/

#define SEM_CONS 1 /*identificatore del semaforo del consumatore*/

main () {

pid_t pid; /*identificatore del processo figlio */

int status; /*modalità di terminazione del figlio */

key_t chiave=IPC_CREAT,c_sem=IPC_PRIVATE; /*chiavi che

identificano rispettivamente, il buffer e

il gruppo di due semafori che ci servono */

int id_shared,id_sem; /*identificatori delle due risorse, usati dai due

processi per referenziarle */

struct sembuf sem_buf[2]; /*array di due record di tipo sembuf associato

al nostro gruppo di semafori */

char *ptr_shared, /*puntatore al buffer */

*cursor,

val='a'; /*il messaggio è costituito da una sequenza di caratteri */

int i,k=1;

/*assegna un segmento di memoria condivisa */

253

id_shared=shmget(chiave,DIM,IPC_CREAT|0644); /*il processo padre

richiede il rilascio di un buffer di 16 byte da condividere con il figlio */

ptr_shared=shmat(id_shared,0,0); /*attaccamento dell'area di memoria ottenuta con

la propria area dati: otteniamo il puntatore al buffer */

/*richiede il gruppo di due semafori da mettere a disposizione ai due figli */

id_sem=semget(c_sem,2,IPC_CREAT|0644);

/*setta il semaforo del produttore a verde */

/*siccome si tratta di operazioni di inizializzazione poteva anche essere usata la "semctl"

con il comando "setval" */

sem_buf[SEM_PROD].sem_num=SEM_PROD;

sem_buf[SEM_PROD].sem_op=1;

sem_buf[SEM_PROD].sem_flg=SEM_UNDO;

semop(id_sem,&sem_buf[SEM_PROD],1);

/*inizializza il semaforo del consumatore */

sem_buf[SEM_CONS].sem_num=SEM_CONS;

sem_buf[SEM_CONS].sem_op=1;

sem_buf[SEM_CONS].sem_flg=SEM_UNDO;

semop(id_sem,&sem_buf[SEM_CONS],1);

/*setta il semaforo del consumatore a rosso */

sem_buf[SEM_CONS].sem_num=SEM_CONS;

sem_buf[SEM_CONS].sem_op=-1;

sem_buf[SEM_CONS].sem_flg=SEM_UNDO;

semop(id_sem,&sem_buf[SEM_CONS],1);

/*genera il figlio PRODUTTORE*/

pid=fork();

if(!pid) { /*codice del produttore (processo figlio)*/

printf("sono il produttore: pid=%d ppid=%d\n",getpid(),getppid());

for(k=0; k<NVOLTE; k++) {/*ciclo di scrittura */

/*wait sul suo semaforo */

sem_buf[SEM_PROD].sem_num=SEM_PROD;

sem_buf[SEM_PROD].sem_op=-1;

sem_buf[SEM_PROD].sem_flg=SEM_UNDO;

semop(id_sem,&sem_buf[SEM_PROD],1);

printf("produttore: %c\n",val);

*ptr_shared=val; /*il carattere viene depositato nel buffer */

/signal sul semaforo del consumatore */

sem_buf[SEM_CONS].sem_num=SEM_CONS;

sem_buf[SEM_CONS].sem_op=1;

sem_buf[SEM_CONS].sem_flg=SEM_UNDO;

254

semop(id_sem,&sem_buf[SEM_CONS],1);

val++; /*viene "prodotto" il nuovo valore */

sleep(5); /*da il tempo di visualizzare i vari messaggi a video */

}

exit(0);

}

/*genera il figlio consumatore: il codice è del tutto simile come abbiamo sopra anticipato

*/ else { pid=fork();

if(!pid) { /*codice del consumatore */

printf("sono il consumatore:pid=%d ppid=

%d\n",getpid(),getppid());

sleep(3);

for(k=0; k<NVOLTE; k++) {/*ciclo di lettura */

/*wait sul suo semaforo */

sem_buf[SEM_CONS].sem_num=SEM_CONS;

sem_buf[SEM_CONS].sem_op=-1;

sem_buf[SEM_CONS].sem_flg=SEM_UNDO;

semop(id_sem,&sem_buf[SEM_CONS],1);

val=*ptr_shared;

printf("consumatore: %c\n",val);

/signal sul semaforo del produttore */

sem_buf[SEM_PROD].sem_num=SEM_PROD;

sem_buf[SEM_PROD].sem_op=1;

sem_buf[SEM_PROD].sem_flg=SEM_UNDO;

semop(id_sem,&sem_buf[SEM_CONS],1);

sleep(3);

}

exit(0);

}

else {/*padre */

pid=wait(&status); /*attende la terminazione del 1° figlio */

printf("1° figlio terminato: pid=%d S=%d\n",pid,status);

pid=wait(&status); /*attende la terminazione del 2° figlio */

printf("2° figlio terminato: pid=%d S=%d\n",pid,status);

semctl(id_sem,IPC_RMID,0); /*rilascia il semaforo */

shmctl(id_shared,IPC_RMID,0); /*rilascia la memoria cond. */

printf("Programma terminato \n");

255

}

}

}

Una soluzione più elegante di questo problema sarebbe quella di modularizzarlo, cioè di

scrivere una libreria di funzioni che vadano ad eseguire le singole operazioni: wait e si-

gnal, richiesta di semafori, inizializzazione del semaforo ecc.

______________________________________________________________________

LO SCAMBIO DI MESSAGGI TRA PROCESSI

- UNIX System V mette a disposizione una tecnica di comunicazione fra processi

(anche indipendenti) basata sullo scambio di MESSAGGI

-Un messaggio è un blocco di informazioni, senza formato predefinito.

I messaggi vengono inseriti in una CODA. Ogni coda ha un proprio nome (la CHIA-

VE),che viene usata dai processi che ne richiedono l'accesso, ha un proprietario, un grup-

po di appartenenza, un insieme di PROTEZIONI di lettura/scrittura (relative al proprieta-

rio/gruppo/utenti). ultimo messaggio

primo messaggio

Commento:

Quindi tutti i processi possono richiedere il rilascio di una coda di messaggi che consiste in

una zona riservata del Kernel in cui essi possono andare ad inserire i messaggi. La gestione

della coda di messaggi è del tutto a carico del Kernel, il processo non se ne rende affatto

conto. Con il termine indipendenti intendiamo processi non appartenenti alla stessa fami-

glia. Ci possono essere più code che vengono richieste dai processi. Ogni coda è gestita con

la tecnica FIFO , cioè i messaggi vengono inseriti in coda e prelevati in testa come indica la

figura. UNIX richiede che ogni messaggio sia tipizzato e quindi la struttura del messaggio

prevede una parte fissa che consiste in un intero che esprime il tipo del messaggio, e di

una stringa di caratteri di lunghezza scelta di volta in volta che rappresenta il vero e pro-

prio messaggio. Ogni volta che un messaggio viene prelevato dalla coda (cioè viene rice-

vuto), esso viene cancellato dalla coda e quindi non sarà più disponibile.

256

MSGGET : Richiesta di una coda di messaggi

Le dimensioni della coda ed il numero massimo di messaggi accodabili sono parametri

della configurazione del sistema.

Nella richiesta andranno specificati CHIAVE e MODALITA' DI ASSEGNAM.:

msgget ( chiave , modo)

chiave : in generale è un intero (ricordiamo che esso deve essere univoco per tutte le

risorse a prescindere dal loro tipo)

modo: IPC_CREAT |permessi (per chi crea)

IPC_ALLOC per chi usa

msget restituisce:

- per esito positivo, l'identificativo della risorsa....

- altrimenti restituisce -1 (un esempio classico è quello in cui si è già richiesto il numero

massimo di code che il sistema poteva rilasciare e quindi non se ne poteva ottenere un'al-

tra).

Sintassi:

#include<sys/types.h>

#include<sys/ipc.h>

#include<sys/msg.h> /*libreria per la gestione dei messaggi*/

int msgget (chiave , opzione);

key_t chiave;

int opzione;

esempio:

key_t chiave

int id_msg

.................

chiave = ftok("usr/des",'a');

id_msg = msgget (chiave,IPC_CREAT|6640);

if (id_msg==-1) /*controllo errore */

{perror ("msgget");

exit(2);}

/*utilizzo della coda */

.

.

.

.

msgctl(id_msg, IPC_RMID,0); /*rimozione risorsa */

257

Ricordiamo che il comando per andare a controllare lo stato delle risorse è invece

IPC_STAT.

MSGSND: Invio di un messaggio

Il messaggio da inviare deve avere la seguente struttura:

struct msgbuf

{ long mstype; /*identifica il tipo di messaggio */

char mtext[ ]; /*array di caratteri che rappresenta il messaggio */

}

Il messaggio può essere inviato mediante:

msgsnd ( id_msg , messaggio , dimensione , opzione )

id_msg : identificativo della coda

messaggio : indirizzo del messaggio (cioè puntatore alla struttura dati privata al

processo)

dimensione : dimensione del messaggio (cioè numero di caratteri)

opzione : flag (decide se il processo si debba sospendere o meno in caso che

l'operazione non posso essere effettuata)

La msgsnd per esito positivo restituisce 0, altrimenti -1.

Commento:

Quindi l'utilizzo di questa primitiva richiede che nell'area di memoria del processo che la

usa sia stata dichiarata una opportuna struttura dati che abbia la stessa forma prevista per

il messaggio, in cui di volta in volta, vado prima a preparare il messaggio che devo inviare

e poi lo invio (stessa cosa varrà per la receive in cui prelevo il messaggio dalla coda e lo co-

pia nella mia area privata).

Tale struttura dati sarà un record di due campi come quello sopra indicato.

Sintassi:

#include<sys/types.h>

#include<sys/ipc.h>

#include<sys/msg.h>

int msgsnd (id_msg , ptr_msg , sz_txt , flag);

int id_msg,sz_txt,flag; 258

struct msgbuf *ptr_msg; /*puntatore alla nostra struttura interna */

esempio:

struct msg { long tipo;

char testo[256];} area_msg;

........

id_msg =msget (chiave,IPC_CREAT|6640);

.

.

.

area_msg.tipo =1; /*assegnamo un tipo al nostro messaggio */

strcpy(area_msg.testo,messaggio); /*copiamo il messaggio */

msgsnd (id_msg, &area_msg,sizeof(area_msg.testo),0);

/*messaggio spedito (nel caso in cui vi siano le cond. per farlo)*/

.

.

.

.

MSGRCV: Ricezione di un messaggio

L'estrazione di un messaggio dalla coda viene eseguita (secondo l'ordine FIFO) con la sy-

stem call:

msgrcv ( id_msg , messaggio , dimensione , tipo , opzione )

messaggio: indirizzo di dove depositare il messaggio

tipo: tipo messaggio

Abbiamo detto, a proposito della send, che il messaggio può essere tipizzato; ne deriva, al-

lora, che la receive può essere selettiva sul tipo di messaggio, cioè da la possibilità di

estrarre messaggi solo di un determinato tipo oppure tutti i messaggi a prescindere dal

tipo (sempre in modo FIFO).

Con questa tecnica si potrebbe ad esempio implementare un protocollo di comunicazione,

in cui si distingue tra messaggi che mi permettono di effettuare la sincronizzazione e mes-

saggi veri e propri (vedi fine lezione).

A proposito del tipo da ricevere:

Valore di tipo:

0 restituisce il primo messaggio in coda, qualunque sia il suo

tipo >0 restituisce il primo messaggio di tipo specificato

259

<0 restituisce il primo messaggio con tipo minore o uguale al

valore assoluto del tipo specificato (es. -4: estrae il primo mes-

saggio secondo l'ordine FIFO tra quelli di tipo 1,2,3,4).

Commento:

E' come se il sistema operativo vedesse più code (coda multipla) : una per ogni tipo (fisica-

mente però è sempre un'unica coda , ma a noi questo non interessa).

Sintassi:

#include<sys/types.h>

#include<sys/ipc.h>

#include<sys/msg.h>

int msgrcv (id_msg , ptr_msg , sz_txt , types , flag);

int id_msg,sz_txt,flag;

long types;

struct msgbuf *ptr_msg;

per esito positivo, restituisce il numero di bytes della parte testo del messaggio, altrimenti

-1.

esempio:

struct msg { long tipo;

char testo[256];} area_msg;

........

id_msg =msget (chiave, IPC_ALLOC);

.

.

.

msgrcv (id_msg, &area_msg, sizeof(area_msg.testo),1,0);

/*estraiamo soltanto messaggi di tipo 1*/

.

.

.

.

Qualche osservazione sui flag:

msgsend (id_msg , msg_ptr , dim , flag)

1 solo possibile valore : IPC_NOWAIT

- se IPC_NOWAIT non è settato, il processo che esegue la MSGSND si sospende-

rà qualora non ci siano le condizioni per inviare il messaggio (es. coda satura)

260

_ se il flag è settato a IPC_NOWAIT , il processo , che prova ad eseguire la MSG-

SEND ,senza riuscirci, non si sospenderà ma la supervisor call restituirà il valore -1.

msgsend (id_msg , &area_msg , dim ,type, flag)

_ se il flag è settato a IPC_NOWAIT, i process che tenta di ricever un messaggio

di un certo tipo ma non disponibile nella coda, non si sospende e la s.c. restituisce -1

(Receive non bloccante)

_ se il flag non è settato a IPC_NOWAIT, il processo che non trova messaggi nella

coda, si sospenderà (Receive bloccante). Lo sblocco avviene per effetto della disponibilità

di un messaggio o se qualche altro processo invia un determinato segnale (quelli tipo

"kill").

MSGCTL: Funzione di controllo sulle code

per eseguire comandi sulla risorsa.

Il principale comando è quello per il rilascio della risorsa:

msgctl ( id_msg , comando , buffer )

msgctl ( id_msg , IPC_RMID , 0 )

buffer : puntatore ad un area di memoria in cui vengono depositate le informazioni

prelevate.

altri comandi:

IPC_STAT per la lettura dei dati della coda

IPC_SET per modificare caratteristiche della coda

______________________________________________________________________

Esercizio in aula:

/*Programma per la trasmissione di messaggi di un solo carattere */

/*tra padre e figlio con l'uso della coda */

/*Programma: messaggi */

#include<stdlib.h>

#include<stdio.h>

#include<sys/types.h>

#include<sys/ipc.h>

#include<sys/msg.h> 261

#define MSGKEY 111

struct msform

{

int mtype;

char mtext;

};

main()

{

struct msform msgp,msgf;

int msgid;

printf ("identificatore dl messaggio=%d\n"",

msgid=msgget (MSGKEY, IPC_CREAT|6660);

if (fork())

{ /*PADRE*/

printf("sono il padre\n");

msgrcv(msgid, &msgp, sizeof(msgp.mtext),1,0); /*prima ricez. */

printf ("il messaggio ricevuto di tipo 1 è%\n",msgp.mtext);

msgrcv(msgid, &msgp, sizeof(msgp.mtext),2,0); /*seconda ricez.*/

printf ("il messaggio ricevuto di tipo 2 è%\n",msgp.mtext);

}

else {

printf("sono il figlio F1 id=%d\n",getpid());

msgf.type=2;

msgf.mtext='B';

printf ("il messaggio in trasmissione di tipo 2 è%c\n",msgf.mtext);

msgsnd(msgid, &msgf, sizeof(msgf.mtext),0); /*primo invio */

msgf.type=1;

msgf.mtext='A';

printf ("il messaggio in trasmissione di tipo 1 è%c\n",msgf.mtext);

msgsnd(msgid, &msgf, sizeof(msgf.mtext),0); /*Secondo invio */

}

}

_______________________________________________________________________

Commento:

In realtà non servivano due buffer (uno per il padre ed uno per il figlio) ma ne bastava uno

in quanto con la fork l'area dati del padre viene duplicata e quindi il figlio avrà automati-

262

camente il proprio buffer (come abbiamo fatto noi sprechiamo due buffer: uno per il padre

e uno per il figlio).

La dinamica è la seguente: il padre aspetta prima un messaggio di tipo 1 e poi uno di tipo 2

attraverso receives bloccanti. Siccome il figlio manda (inversamente) prima il messaggio di

tipo 2 e poi quello di tipo 1, il padre si sospenderà finché il figlio non avrà fatto entrambe

le send, e poi riceverà i due messaggi senza bloccarsi.

_______________________________________________________________________

PRIMITIVE PER LO SCAMBIO DI MESSAGGI FRA PROCESSI

-- SEND ASINCRONA

Il processo mittente continua la sua esecuzione subito dopo l'invio del

messaggio (cioè non si blocca in attesa che il destinatario riceva il messaggio).

Richiede l'impiego di meccanismi di accodamento dei messaggi (na-

scosti al programmatore).

_ SEND SINCRONA

Il processo mittente si blocca in attesa che il messaggio venga ri-

cevuto (non sono richiesti meccanismi di accodamento nascosti).

_ RECEIVE BLOCCANTE

Il processo, che esegue la receive e non trova messaggi sul canale, si

blocca fino a che un messaggio diventa disponibile.

_ RECEIVE NON BLOCCANTE

Il processo, che non trova il messaggio, non si blocca e restituisce una

indicazione per l'eccezione verificatasi

IN UNIX

le primitive SEND e RECEIVE sono asincrone.

Qualora sia necessario realizzare una trasmissione sincrona, questa potrà essere realizzata

impiegando degli opportuni protocolli di comunicazione.

Esempio

Implementare il problema Produttore/Consumatore nel modello a scambio di messaggi (in

effetti si tratta di una trasmissione sincrona in quanto il produttore manda il messaggio,

aspetta che il consumatore lo prelevi e poi manda il nuovo messaggio):

263

canale

Send (req.to send) Receive (R.t.S)

receive(ok to sen.) send (ok to se)

P1 P2

Send (mesage) Receive (mess.)

Protocollo di trasmissione:

_ Il processo produttore P1 comunica al consumatore P2 la propria intenzione di tra-

smettere (SEND Request to send)

_ Il processo consumatore P2 riceve la richiesta di trasmissione da parte del

produttore e gli invia un OK a trasmettere (SEND ok to send)

_ Il produttore P1 riceve l'ok to send e quindi invia il messaggio (SEND message)

_ Il processo P2 riceve il messaggio dal produttore P1

A proposito dell'implementazione del canale di comunicazione 2 soluzioni:

1) si possono utilizzare 3 canali distinti : 2 canali per l'invio di segnali di controllo,

ed 1 canale per le informazioni

2) Si può utilizzare un unico canale per l'invio e la ricezione sia dei segnali di

controllo che delle informazioni (ciascun messaggio ha un proprio tipo)

soluzione 1: C1

P1 C2 P2

C3

Il processo P1 (produttore) esegue:

Send (C1, 'Request to Send')

Receive (C2, 'ok to send)

Send (D, Message)

Invece il consumatore P2 esegue:

Receive (C2, 'Request to Send')

264

Send (D,'ok to send')

Receive (D, Message) 265

Sistemi Operativi Lez.

Prof. DE CARLINI 21/5/1997

Completiamo ora quanto detto la volta scorsa. Abbiamo già detto che per realizzare una applica-

zione tra Client - Server dobbiamo disporre di un meccanismo che permette ad una procedura chia-

mante di essere eseguita nel contesto di un processo A, a partire da una certa macchina, e ad una

procedura chiamata nell'ambito della stessa applicazione, di girare nel contesto di un processo B, su

una macchina distinta.

Tutto questo si realizza sfruttando delle primitivo del Kernel dei sistemi operativi delle macchine 1

e 2 (Client e Server) su cui risiedono i processi A e B. Si deve far in modo, quindi, che la procedura

chiamante, o meglio il processo nel cui contesto essa viene eseguita, finisca in attesa nel momento

in cui effettua la chiamata, cioè nel momento in cui viene richiesto il servizio da parte del Client;

questa attesa deve durare per tutto il tempo che impiega il Server per eseguire il servizio, cioè finché

il Client non riceve la risposta che abbiamo visto corrisponde dal punto di vista concettuale al Re-

turn della procedura chiamata verso la procedura chiamante. Per implementare tutto questo si usano

dei costrutti linguistici di basso livello, appartenenti al Kernel del sistema operativo; queste proce-

dure sostanzialmente sono una Send asincrona ed una Receive bloccante. La volta scorsa notammo

inoltre che questo meccanismo deve essere noto a colui il quale sviluppa la applicazione affinché lo

possa usare correttamente. Sorge allora il problema di fare qualcosa per nascondere tutto questo

meccanismo a chi sviluppa l'applicazione in modo che egli debba soltanto sviluppare la procedura

chiamante e quella chiamata senza preoccuparsi della implementazione della chiamata a procedura

remota (per lui si tratterà allora di risolvere i problemi relativi alla propria applicazione soltanto,

senza considerare quelli dell'architettura Client Server che dovrebbe essere offerta in modo traspa-

rente dall'ambiente di sviluppo). Per fare questo abbiamo bisogno di creare due stub (mozziconi),

uno montato sul Client che viene visto dalla procedura chiamante come procedura chiamata e l'altro

montato sul Server che appare alla procedura chiamata come procedura chiamante; questi due stub

devono contenere la Send e la Receive per realizzare la chiamata a procedura remota e devono prov-

vedere allo scambio di informazioni tra procedura chiamante e procedura chiamata, cioè devono tra-

sformare il normale passaggio di parametri tramite stack tra le due procedure in uno scambio che

avviene tramite un messaggio che lo stub Client invia allo stub Server ;lo stub Server farà l'opera-

zione opposta per trasformare il messaggio in parametri da scambiare tramite stack. Al ritorno av-

viene il processo opposto e stavolta lo stub Server invierà il messaggio allo stub del Client che lo

trasformerà in parametri per poi tornare alla procedura chiamante.

I due stub riescono perfettamente a simulare le situazioni di loro competenza eccetto per il fatto che

sono costretti ad implementare uno scambio di parametri per referenza con un meccanismo diverso

che è quello della copia e ripristino. Tale meccanismo è perfettamente trasparente all'esterno a meno

di casi anomali.

Un altro problema da risolvere è quello riferito al fatto che le macchine Client e Server non sono

macchine identiche, perché realizzate magari da diversi costruttori, e quindi presentano una diversa

architettura a basso livello e cosa più importante, hanno diversa rappresentazione dei dati. Esistono

quindi degli elementi che provocano problemi a livello di trasferimento:

1) Problemi di scambio di numeri, caratteri, valori logici (non perfetta compatibilità fra questi tipi);

- I tipi numerici possono avere differenti rappresentazioni (virgola fissa o mobile) su un numero

diverso di bit ad esempio per mantissa ed esponente o per i nume-

ri negativi ecc...;

- Alcuni hanno indirizzo basso a destra alto a sinistra, altri viceversa (BIG ENDIAN, LITTLE EN-

DIAN);

- Non tutte le macchine usano i caratteri ASCII

- Non tutti usano le stesse stringhe di bit per indicare TRUE e FALSE

266

Per risolvere tali problemi ci sono diversi modi :

1) FACCIAMO RIFERIMENTO AD UNA RAPPRESENTAZIONE CANONICA

Non è molto efficiente perché in caso di macchine Client e Server uguali opererei due passaggi alla

forma canonica di rappresentazione inutili tramite due conversioni.

Questo fatto è oggi molto frequente perché si tende ad utilizzare dei Pentium Pro che offrono presta-

zioni molto elevate a prezzo contenuto (per misurare tali prestazioni si usano degli appositi bench-

mark come ad esempio SPEC 95).

2) L'AMBIENTE DI SVILUPPO, DOPO AVER RICEVUTO UNA DESCRIZIONE DELLE MAC-

CHINE CLIENT E SERVER, SI OCCUPA DELLE CONVERSIONI, QUANDO VIENE ATTI-

VATO LO STUB SERVER (VENGONO SELEZIONATE OPPORTUNE ROUTINE DI CON-

VERSIONE DI FORMATO QUANDO NECESSARIO).

L'ambiente prevede un linguaggio di descrizione del Client e del Server e aggancia agli stub le op-

portune routine di conversione. Il problema viene risolto in maniera trasparente all'atto della crea-

zione del messaggio di domanda o di risposta. Si potrebbe inoltre riferire tutto al Client il quale

provvederebbe soltanto lui alle conversione prima di mandare il messaggio e dopo averlo ricevuto.

Occupiamoci ancora di un altro problema. Abbiamo visto che lo scambio dei parametri viene fatto

con il meccanismo di copia e ripristino; ciò comporta una perdita di tempo inutile perché è difficile

che i parametri scambiati per referenze siano parametri di ingresso e uscita tra chiamante e chiama-

to: si verifica molto spesso che essi siano solo parametri di uscita oppure solo parametri di ingresso

(ad esempio se opero su un vettore). Quindi si può verificare il caso in cui la copia è inutile in fase

di chiamata (parametro di uscita) o in fase di ritorno (parametro di ingresso). Per evitare questo fatto

l'ambiente dovrà prevedere nella fase di descrizione dell'interfaccia tra procedura chiamante e proce-

dura chiamata, anche la possibilità di indicare chiaramente se i parametri scambiati per riferimento

sono di ingresso o di uscita.

Un altro problema da risolvere è quello della identificazione della macchina Server da parte del

Client quando viene attivato il meccanismo di comunicazione. Abbiamo già visto che si può identi-

ficare il processo Server, a cui devo inviare il messaggio quando faccio la Send dal lato Client, con

una costante numerica che mi fornisce l'indirizzo dell'host cioè della macchina su cui gira il Server e

poi l'indirizzo del processo Server nell'ambito dell'host:

____________________

|______|_____________| Identifica la Mailbox a cui inviare i

messaggi HOST PROCESSO

La soluzione ottimale è quella di avere un processo BINDER di nomi, il quale è uno speciale Server

che conosce la allocazione dei processi Server sulla rete e quindi è in grado di accoppiare al nome

simbolico del Server il suo indirizzo fisico. Per evitare che il BINDER di nomi si trovi sempre su

una certa macchina che è di per sé un Server che potrebbe creare un collo di bottiglia per il sistema,

lo si può replicare in più punti della rete così come lo si può replicare in caso di guasto da una mac-

china all'altra. Per garantire questo e per evitare problemi ogni volta che cambia la posizione del

BINDER di nomi (il suo indirizzo deve essere noto al Client che deve essere in grado di trovato), si

fa la identificazione della macchina su cui esso si trova in Broadcast: il Client la prima volta che

deve inviare il messaggio, e quindi la prima volta che il processo Client esegue il proprio stub,(fase

di test dello stub), manda in Broadcast sulla rete un particolare messaggio con cui chiede al Server

di nomi di conoscere la posizione (indirizzo assoluto) di un certo Server specifico che gli interessa;

tale messaggio viene catturato dal Server di nomi che risponde adeguatamente.

267

Gli altri vantaggi del Server di nomi sono:

- Posso fornire dei meccanismi di sicurezza perché il Server di nomi rappresenta un filtro tra i pro-

cessi Client e i processi Server; infatti un processo Client per potersi agganciare al processo Server

che desidera, deve farlo attraverso il Server di nomi che gli deve fornire l'indirizzo e che lo farà solo

se esso è autorizzato.

- Posso gestire facilmente l'aggiornamento dei Server; volendo sostituire infatti le procedure presen-

ti nei Server con delle nuove versioni non allineate con le versioni presenti sui Client potrebbero na-

scere dei malfunzionamenti; potrei far in modo quindi che per effetto di una modifica ad un Server,

non sia più possibile collegarsi ad esso da parte di certi Client su cui sono presenti delle versioni

meno aggiornate.

Tutto questo si realizza facilmente con una procedura di registrazione del Server. Nel BINDER di

nomi ci sarà una tabella contenente i nomi simbolici, gli indirizzi, gli attributi eccetera e ogni riga

finisce nella tabella per effetto di una procedura di registrazione sviluppata dal processo Server: nel

momento in cui si da' vita al processo Server, questi si registra sul Server di nomi e si mette in attesa

di eventuali messaggi dai Client. Con questo sistema non è possibile inviare messaggi a Server che

non sono collegati perché essi non risultano registrati.

Nome Server Indirizzo Attributi

_________________________________

|___________|__________|___________|

|___________|__________|___________|

|___________|__________|___________|

BINDER dei nomi

Ricapitolando esiste una parte del Server Stub che viene eseguita una tantum, che è la registrazione,

così come esiste una parte del Client Stub che viene eseguita una sola volta quando il Client si deve

collegare con il Server la prima volta perché deve passare attraverso il Server di nomi. Ovviamente

quando il processo Server deve terminare, prima dell'exit dovrà deregistrarsi cancellando la sua riga

nella tabella del Server di nomi.

Un altro aspetto è quello del protocollo per lo scambio di messaggi. Posso pensare di implementare

una architettura Client Server con delle Send e Receive che avvengono tramite TCP/IP come avvie-

ne ad esempio in UNIX (socket TCP/IP). In alcuni casi comunque le applicazioni Client Server ven-

gono sviluppate con dei protocolli che sono più elementari ad esempio perché non ci sono problemi

di livello 3 (senza IP) o perché la rete è un bus quindi non c'è possibilità di scambio dei pacchetti,

chi parte prima arriva prima, (senza TCP).

Per completare l'argomento dobbiamo esaminare casi di fallimento dell'applicazione Client Server

dovuti alla caduta del Client o del Server (gestione dei casi anomali). Non diremo molto su questo

argomento, ma ci limiteremo ad elencare gli eventi che si possono verificare :

- Il Client non riesce ad identificare il Server (si gestisce col Binder di nomi); se non c'è il binder

l'applicazione non risponde e quando scatta il timer finisce il tentativo di comunicazione

- La richiesta del Client viene persa oppure viene persa la replica del Server.

In questo caso il Client se ne accorge perché non riceve risposta dopo un certo tempo; questo si

realizza aggiungendo un timer sul lato Client, quanto esso scatta posso inviare di nuovo la richiesta

(questo fatto lo prevede lo Stub che viene svegliato dal timer); se effettivamente il messaggio è stato

268

perso non sorgono problemi. Questa ripetizione viene fatta un certo numero di volte e se non si è

mai avuta risposta, vuol dire che è accaduto un evento strano e quindi si può smettere di inviarlo. Se

invece viene persa la risposta del Server, il messaggio inviato la seconda volta verrà di nuovo ela-

borato dal Server. Ciò potrebbe essere pericoloso perché, se non si tratta di una semplice scrittura

che non comporta problemi, ma si tratta invece di una operazione non idempotente (esempio incre-

mento di un puntatore o di una variabile contatore), si potrebbe causare una operazione non deside-

rata. Per evitare questo problema i messaggi vengono numerati e quindi il Server può rendersi conto

se il messaggio giunto è una replica o meno e comportarsi di conseguenza non ripetendo certe ope-

razioni.

- Server fuori servizio dopo aver ricevuto la richiesta ma prima di aver prodotto la risposta.

Si tratta di un caso più serio perché potrebbe causare, nel caso di Server di dati ad esempio, il non

allineamento della Base Dati.

- Client fuori servizio dopo aver inviato la richiesta ma prima di aver ricevuto la risposta.

In questo caso si generano messaggi sulla rete che non possono essere più consegnati a nessuno.

Tali messaggi pendenti vengono eliminati dopo un certo tempo.

L'ultimo aspetto da considerare è quello dei vincoli sulla lunghezza dei messaggi.

In Ethernet la lunghezza massima per un Frame è di 1536 byte quindi i messaggi più lunghi devono

essere tagliati; sulle Sun il limite è 8 Kbyte. Quando non ci sono vincoli è conveniente usare mes-

saggi molto lunghi perché in questo modo si velocizza il trasferimento. Se invece il trasferimento

avviene in n colpi successivi peggiora il suo RATE ma in caso di errore devo recuperare solo una

parte del messaggio.

Trattiamo ora l'ultimo argomento del corso e cioè il FILE SYSTEM DISTRIBUITO, che è poi l'a-

spetto più eclatante che uno vede quando ha a che fare con un sistema distribuito.

In una rete possiamo trovare sia PC che Terminali passivi oppure sistemi di elaborazione che si

comportano come tali solo in determinate applicazioni (es. TELNET); quindi un utente può vedere

come suo posto di lavoro un PC che fa da terminale passivo o un terminale passivo vero e proprio;

in questo caso se stanno in rete, cioè se non sono direttamente collegati ad una porta seriale di una

macchina che sta’ in rete (in questo caso il terminale è relativo alla macchina HOST che sta’ essa in

rete) ma si trova in rete tramite una scheda Terminal Server. Detto questo è evidente che un utente

vorrebbe vedere il file system di rete senza nessuna differenza da quello della macchina concentrata

a cui eventualmente il suo terminale passivo risulta collegato. Quindi a maggior ragione il requisito

di trasparenza tipico dei sistemi distribuiti deve trovarsi anche nel file system e l'utente non deve ac-

corgersi del fatto di essere collegato ad un file system di rete piuttosto chi ha un file system monoli-

tico. Purtroppo tale requisito non è soddisfatto completamente nei file system odierni e addirittura

non lo è per niente in alcuni di essi e per poter operare l'utente deve capire esattamente come è di-

stribuito sulle varie macchine il file system. Naturalmente un file system distribuito deve offrire gli

stessi servizi di un file system centralizzato e quindi tutti quei servizi relativi ai file e alle directory

(creazione, apertura, cancellazione, scrittura ecc. ). Tutti questi servizi vengono forniti dai processi

che vengono creati nell'ambito del sistema operativo e che sono processi Server collocati su qualche

macchina. La prima cosa da dire è che i file system centralizzati offrono tutti i servizi di apertura e

chiusura di un file; ciò permette di performare meglio il sistema utilizzando la cache in fase di aper-

tura. Vediamo ora se è conveniente trasferire questa logica nei sistemi distribuiti: sorge il problema

di garantire da qualche parte il caching del file aperto ; si potrebbe pensare di fare il cash nella RAM

del Client così come si farebbe localmente; in questo modo i messaggi di Read e Write partiranno

tenendo conto dei dati presenti nella cache e allo stesso modo si verificheranno i diritti di accesso.

In questo modo però il sistema è meno robusto perché si potrebbe creare un disallineamento tra le

269

informazioni nella cache che potrebbero non corrispondere più per una modifica o per una caduta.

Per risolvere tale problema in alcuni file system i file Server sono privi di stato, cioè non offrono il

servizio di apertura e chiusura del file; l'utente non può richiedere l'apertura del file, ma questo vie-

ne aperto automaticamente ad ogni richiesta di Read e Write. All'atto della lettura o della scrittura

nello scambio di messaggi tra macchina Client e macchina Server vengono mandate tutte le infor-

mazioni relative alla coppia di operazioni apertura-lettura o apertura-scrittura, questo poi a prescin-

dere dal fatto che il Server apra e chiuda ogni volta il file (potrebbe aprirlo alla prima richiesta, poi

portarlo nella sua cahe e operare successivamente su di esso senza aprirlo di nuovo).

Devo comunque inviare il messaggio di apertura-scrittura perché il file potrebbe non essere nella ca-

che (non si riduce il traffico sulla rete). Gli altri Server sono quelli con stato (permettono apertura e

chiusura dei file).

vediamo ora le operazioni sui file:

-il servizio sui file può essere soltanto di tipo upload-download , cioè all'atto dell'accesso al file tut-

to il file viene scaricato; il Client allora non può scrivere direttamente su un record del file ma può

trasferirlo tutto, scriverci e poi ritrasferirlo. Con questo meccanismo che è il più semplice si inibisce

l'accesso del file a tutte le altre macchine (lock); naturalmente per problemi di ottimizzazione potrei

non trasferire tutto il file ma soltanto un pezzo, ma comunque non quel solo pezzo che mi serve per

una specifica operazione.

- si lavora su un pezzo di file che non è più locked; naturalmente si impedisce che più utenti lavori-

no sullo stesso pezzo di file; se non ci sono cache da qualche parte, certamente viene impedito che

due utenti contemporaneamente accedono allo stesso blocco. Se invece pensiamo che il Server ab-

bia il suo disco e la sua cache del disco, esso legge o scrive nella propria cache o sul disco e viene

impedito l'accesso a due che vogliono lavorare sullo stesso blocco, anche se è possibile che un uten-

te legge dalla cache e uno scrive sullo stesso blocco del disco (potrei avere un sistema multi- tread

che gestisce due richieste contemporaneamente con due tread distinti che permettono ai due utenti

di fare la loro operazione in parallelo, ma il parallelismo è garantito in questo caso dal fatto che il

blocco non è lo stesso e comunque esiste una cache del disco o in assenza di questa una sequenzia-

lità di accesso al disco garantita dalla parte bassa del file system che gestisce lo schedulazione dello

stesso).

Vediamo ora come si presenta la struttura delle directory nel caso di file system distribuiti. abbiamo

già visto che anche nel caso di file system concentrato, non sempre si ha una struttura che si presen-

ta sotto forma di un unico albero o di un unico grafico; ad esempio su una macchina DOS il file sy-

stem è una foresta formata dall'albero del dischetto e dall'albero del disco rigido; con WINDOWS

invece si ha un solo albero, quello della gestione risorse. In UNIX si vede un solo albero (o grafo

aciclico) tanto è vero che bisogna fare il mounting del dischetto, cioè montare il sottoalbero del di-

schetto all'interno di una sottodirectory del disco rigido. anche nei file system distribuiti l'obiettivo è

quello di far vedere un solo albero; tale obiettivo non viene raggiunto da file system distribuiti mol-

to rudimentali come ad esempio quelli per gestire reti di PC (NETWARE e simili), perché in questo

caso vedo tanti dischi di cui alcuni sono virtuali poiché rappresentano sottoalberi che sono presenti

su dischi di sistemi remota.

Con questo sistema ho 2 inconvenienti :

1) L'utente vede una foresta di alberi che corrispondono a vari sottoalberi su diverse macchine

2) Egli è vincolato alla propria postazione di lavoro perché da essa dipende la sua visione del file

system (la vista delle directory dipende dalla macchina su cui lavora e quindi si riduce la mobilità

dell'utente) 270

Sarebbe conveniente invece avere un solo albero visibile da ogni postazione.

La situazione cambia usando file system distribuiti più evoluti; vediamone degli esempi :

NFS (Network File System) della Sun

funziona su una qualsiasi rete UNIX o PC; offre sia una vista unitaria del file system (presuppone

però l'operazione di mounting che si può fare in modo differente in vari punti della rete) sia viste

differenti e parziali a seconda del mounting effettuato. E' utile per utenti che lavorino sempre dalla

stessa postazione e magari sempre con le stesse applicazioni. Il mounting avviene al Boot del siste-

ma automaticamente; nelle ultime versioni esso avviene alla prima richiesta di un file presente su

macchina remota. Ciò evita problemi nel caso in cui un'applicazione sia disabilitata perché il Server

è spento, perché si potrebbe avere un errore sul mounting di una applicazione, delle tante disponibi-

li, che però non serve in quel momento (Automounting).

ANDREW FILE SYSTEM della Transare

nasce per consentire ad un gran numero di studenti universitari di lavorare sui propri file indipen-

dentemente dalla postazione di lavoro; consente quindi ad ognuno di avere una vista nota con una

sola radice, un sottoalbero standard da un lato ed una parte variabile generata da un mounting del di-

schetto di uno studente che però è visibile da ogni macchina. Questa filosofia è più evoluta ed è for-

zata dal particolare uso che se ne doveva fare.

Resta da dire come vengono nominati i file:

- un file può essere nominato con un path che inizia col nome simbolico del Server che lo contiene

(certamente il file è trasparente alla locazione nel senso che posso cambiare la macchina fisica che

lo contiene associando a questa nuova macchina lo stesso nome simbolico).

In realtà il file dovrebbe essere trasparente alla posizione nel senso che non si dovrebbe dare il nome

simbolico della macchina che lo ospita per poter cambiare il Server cui esso appartiene (fatto non

facilmente ottenibile).

Concludiamo ricordando ed estendendo il problema della condivisione dei file. Tale problema che

già si presenta su una macchina UNIX, è ancora più pesante nel caso di un file system distribuito.

Nasce infatti il problema della consistenza dei file condivisi che nasce quando più utenti tengono

contemporaneamente aperto lo stesso file, o meglio hanno contemporaneamente attiva una sessione

di lavoro su uno stesso file. Abbiamo già citato due modelli di consistenza dei file ; vediamoli di

nuovo per poi approfondire meglio il discorso la prossima volta :

- Semantica della consistenza di tipo UNIX (Unix-like)

Quando l'utente scrive su un file, la modifica apportata è visibile a tutti gli altri utenti che lo condi-

vidono.

Non è facile da implementare perfettamente , quindi si preferisce una semantica di tipo Unix-like

che fa si che la modifica sia visibile dopo un po’ di tempo e non subito.

- Semantica delle sessioni

Fotografo la situazione dei file all'inizio della sessione di lavoro; altri utenti che aprono la sessione

mentre sto’ ancora operando hanno la mia stessa vista iniziale e non vedono le modifiche da me ap-

portate finché non chiudo la sessione.

Altre due semantiche sono quelle dei File Immutabili e delle Transazioni.

Tutte queste semantiche condizionano pesantemente il sistema perché coi file system distribuiti esso

ha il grave problema del caching (deve garantire le semantiche di consistenza).

271

Notiamo infine che scopo del sistema operativo è quello di dare regole certe sulla consistenza a par-

tire dalle quali poi le varie applicazioni potranno fornire servizi adeguati all'utente.

272

22/05/97 Sistemi Operativi

Prof. U. De Carlini

File System Distribuiti (riepilogo):

Un file sistema distribuito ha l'obiettivo di apparire all'utente come un file system di una macchi-

na monolitica, quindi appare evidente che un file system distribuito deve fornire gli stessi servizi

di un file system concentrato; con riferimento a tali servizi alcuni file system distribuiti non di-

spongono di apertura e chiusura file.

I file system si distinguono in file system che offrono un'immagine unitaria della struttura delle

directory e altri che offrono all'utente delle viste che dipendono dalla macchina su cui l'utente

opera (vedi esempio network file system della Sun).

Semantica della condivisione:

Fornisce le regole di comportamento che il file system assume nel momento in cui esistono files

condivisi e almeno due utenti che condividono un file hanno una sessione aperta contemporanea-

mente su quel file.

Si fissano le regole con le quali un utente deve vedere o non vedere le modifiche apportate su

quel file da parte è di un altro utente.

Semantica UNIX:

Si ha che un'operazione di scrittura da parte di un utente viene resa subito visibile agli altri

utenti che hanno una sessione aperta su quel file.

Questa è una semantica " ovvia " ma ha il difetto che è facilmente implementabile su sistemi

monolitici, tipo macchina UNIX, mentre invece è difficile da realizzare su sistemi distribuiti.

In particolare la realizzazione su sistemi distribuiti è ancora semplice se il sistema è privo di

cache, non ci sono i disallineamenti tra cache e file server.

Semantica delle sessioni:

Si presuppone che se un utente apre un file dispone dell'immagine del file così com'è quando è

terminata l'ultima sessione di lavoro, da quel momento in poi, l'utente, è svincolato dalla pri-

ma copia (le modifiche vengono effettuate sulla propria copia) e queste modifiche verranno ri-

portate in toto al termine della sessione di lavoro.

Nei fatti significa dire che si verifica un inconveniente: se due utenti aprono lo stesso file essi

dispongono ognuno della propria copia, quindi ogni utente può lavorarci come meglio crede,

quando uno dei due finisce la sessione le modifiche da questo apportate saranno salvate nel si-

stema ma quando terminerà il secondo utente sarà la coppia di quest'ultimo ad essere salvata

distruggendo le modifiche prima apportate.

Questa semantica di comportamento è di facile implementazione anche su sistemi che suppor-

tano il caching, anzi è stata definita appunto per venire incontro ad una facile realizzazione

della condivisione della semantica della consistenza su di un sistema distribuito, il quale nor-

malmente presuppone il caching dei dati. Quindi la ratio di questa semantica è che è facile da

capire e realizzare sui sistemi che prevedono il caching di files o di parti di files sulle stazioni

client.

Ciò può sembrare un escamotage in quanto il grave inconveniente prima esposto non viene ri-

solto, allora la risposta è: il sistema operativo deve fornire una regola certa per la condivisio-

ne, se all'applicativo questa regola non va bene penserà lui a risolvere il problema.

Semantica dei files immutabili:

Il file system ammette verso file operazioni di creazione e lettura ma non ammette operazioni

di scrittura, ciò vuol dire che la consistenza è pienamente garantita, ma a che serve un file sy-

stem che non mi consente la scrittura, la soluzione è banale in quanto non poter scrivere su di

un file non vuol dire che non si può creare un altro file, nei fatti se voglio scrivere su di un file

273

esistente creo un nuovo file dopodiché sostituisco al vecchio file un nuovo file. L'operazione

di scrittura avviene a livello di directory: effettuo l'aggiornamento sostituendo alla vecchia co-

pia la nuova copia.

Il problema si è ora spostato, infatti se due processi vogliono sostituire lo stesso file avviene la

stessa cosa della semantica delle sessioni: sopravvive l'ultimo.

In realtà vi è un altro problema: cosa accade se un utente sostituisce il file nel momento in cui

una altro utente legge il file stesso? in genere si continua a vedere una coppia vecchia anche

quando questa è stata sostituita, così come nel sistema UNIX quando il file su cui si sta lavo-

rando viene cancellato da un altro processo si continua a vedere una copia temporanea, che

poi eventualmente può essere ripristinata.

Semantica delle transazioni:

Una transazione è un'operazione semantica che ha inizio e fine, e coinvolge normalmente let-

ture e scritture su più files.

Esistono dei transaction monitor che garantiscono di scrivere transazioni atomiche; nei fatti

quando avviene una parte di una transazione noi abbiamo che essa viene anche conclusa. L'or-

dine delle transazioni è indifferente nel senso che se avviene prima A e poi B il risultato è lo

stesso se avviene prima B e poi A. Il file system garantisce il roll back nel senso che per delle

transazioni che coinvolgono più files nel momento in cui una transazione fallisce vengono ri-

pristinate le operazioni prima eseguite sui vari files.

Questa semantica garantisce la consistenza in quanto essendo garantita in un sistema transa-

zionale l'atomicità è automaticamente garantita la consistenza di tutti i files oggetto della tran-

sazione.

Un file condiviso viene aggiornato serializzando e rendendo atomiche le operazioni di aggior-

namento così come avviene per gli accessi in memoria nel modello a memoria comune (in

realtà c'è la mutua escussione).

Cerchiamo ora di capire come vengono implementate le varie semantiche su sistemi che ammet-

tono il caching dei file. Quindi capiremo che una semantica di tipo UNIX non viene mai realizza-

ta ma tutt’al più una semantica di tipo UNIX-like.

Il Caching:

Per quanto riguarda la consistenza i problemi sono sempre dovuti al caching a livello client e

non a livello server. L'obiettivo di un cache è quello di ridurre l'interformare del sistema e ov-

viamente il caching prescinde dalla tecnica di servizio (nel senso che il problema è lo stesso

sia che applico il modello upload-download sia che utilizzano l'accesso remoto).

Caching a livello server:

Il caso non è interessante in quanto non crea alcun problema alla consistenza.

Caching a livello client:

Nei fatti devo risolvere il problema sulla consistenza in quanto non voglio che, l'introdu-

zione della cache a livello di client e l'accoppiamento di una semantica della consistenza

difficile da realizzare, mi costringa ad avere sulla rete un traffico di messaggi che ho cerca-

to di abbattere con il caching.

Con il caching a livello client voglio ridurre il traffico sulla rete, ma se l'introduzione del

caching e l'implementazione difficile della semantica mi riporta al traffico elevato, e quindi

annullo i benefici che volevo apportate con il caching.

Con ciò stiamo dicendo che è inutile inserire il caching e avere una semantica UNIX, infat-

ti lo scambio di messaggi sarebbe talmente alto da portare il traffico pari se non superiore

che senza il caching. 274

Politiche di aggiornamento della cache:

Cerchiamo ora di capire cosa accade in fase di aggiornamento, per far ciò supponiamo

di avere un server e due client ognuno con la propria cache, supponiamo poi che i due

client stiano lavorando sullo stesso file che hanno quindi caricato e che il client1 scrive

mentre il client2 è abilitato alla sola lettura (vedi figura 1).

Le politiche che posso adottare sono due.

Scrittura diretta:

Nel momento in cui il client1 scrive nella propria cache automaticamente il sistema

provvede ad aggiornare il record sul server. Nei fatti ogni scrittura sulla cache genera

l'invio di un messaggio al server. Al livello di scrittura quindi la cache è praticamente

inutilizzata; l'utilità sta nel fatto che se successivamente voglio leggere lo stesso re-

cord esso è già presente nella cache e quindi non c'è alcun bisogno di ritrasferirlo.

Questo tipo di politica di aggiornamento va bene qualunque sia la semantica di consi-

stenza adottata.

Il problema nasce quando avendo aggiornato un record nella cache questo viene tra-

sferito sul server ma allo stesso tempo si trova anche nella cache di altri client, in

questo caso nasce il problema di coerenza in quanto il record sta nella cache del se-

condo non è più coerente con quello del server; quindi se ho una semantica UNIX

devo creare un meccanismo che rifà l'allineamento. Se ho la semantica delle sessioni

tutto va già bene.

La scrittura diretta risolvere da sola problemi di crash del client.

È una politica non performance per l'elevato traffico sulla rete.

SERVER

CLIENT1 CLIENT1

Scrive Legge

Figura 1

Scrittura ritardata:

Nel momento in cui chi scrive modifica la propria cache non è vero che viene genera-

to un messaggio che viaggia sulla rete. Periodicamente, grazie a un timer, i record

modificati all'interno della cache vengono trasferiti al server e quindi aggiornati; ciò

ottimizza il traffico sulla rete in quanto è vero che viene generato un messaggio più

lungo ma è certamente migliore di tanti messaggi brevi.

Naturalmente nascono ora problemi in caso di crash del client.

275

Ci rendiamo subito conto, anche nel caso in cui l'allineamento tra server e client2 sia

immediato, ciò che legge client2 è in generale diverso da ciò che si trova nella cache

di client1.

La scrittura ritardata non consente quindi la realizzazione della semantica UNIX.

Posso però implementare una semantica di tipo UNIX-like nel senso che c'è un delay

fra la scrittura di client1 e la sua disponibilità sul client 2.

Scrittura su chiusura:

L'aggiornamento del server avviene solo quando è terminata la sessione su client1.

Ovviamente il traffico sulla rete è ulteriormente ridotto. Ora però non posso più im-

plementare neanche una semantica di tipo UNIX-like ma posso facilmente realizzare

una semantica delle sessioni. Permane ancora il problema dell'allineamento tra server

e client2.

Politiche per garantire la coerenza:

Sono politiche che consentono di garantire l'allineamento tra server e client e sono di

due tipi:

Approccio garantito dal server:

Quando un processo ha scritto un record sul server (ovvero quando il server aggiorna

i record prelevandoli dalla cache), il server conosce quali sono gli altri client a posse-

derne una copia in cache, provvede quindi ad aggiornarli.

Naturalmente affinché il server possa far ciò deve disporre di una maxi tabella conte-

nente tutte le informazioni necessarie e che deve essere scansionata ogni volta.

Non si utilizza questa politica in quanto il server quando aggiorna le cache non si

comporta più da server ma da client perché richiede un servizio al client.

Approccio garantito dal client:

Periodicamente i record all'interno del client vengono aggiornati; si utilizza un proce-

dimento con un timer per ogni record. In pratica si invalida un record con un bit.

Cosa molto più frequente è quella di aggiornare l'intera cache periodicamente.

Questa politica la bene per la semantica UNIX-like, accoppiandolo con la scrittura ri-

tardata.

Una semantica delle sessioni può essere realizzata con una scrittura su chiusura e ap-

proccio garantito dal client con timer infinito; in questo caso il traffico sulla rete crol-

la totalmente.

Il Network File System (NFS) della Sun:

Ogni client ha la propria lista del file system distribuito che viene costruita montando nel file

system locale le directory del file server (la vista dipende dal client).

Per effettuare il mounting devo sapere cosa mi permette di montare ciascun server, all'atto del

boot di un server si rendono disponibili ai client tutte le informazioni circa i sotto alberi del

server che sono montabili in sotto directory dei client; all'atto del boot il server rende accessi-

bili le directory che possono essere importate, e quindi montate, nei sotto alberi come sotto di-

rectory del client.

È comodo fare il mounting al momento dell'apertura di una file e non al momento del boot, in

tal modo evito montaggi inutili (ciò è obbligatorio se i client sono privi di disco).

La caratteristica di network file system è che i server sono privi di stato e non hanno disponi-

bilità open e close.

La semantica della consistenza è UNIX debole: si utilizza una scrittura ritardata ogni 30 se-

condi; è applicata inoltre una politica di approccio garantito dal client con 2 timer distinti uno

per i record di dati e uno per il record directory: i dati vanno aggiornati più spesso delle direc-

tory. 276

Un file dati condiviso viene aggiornato ogni 3 secondi mentre un file directory condiviso vie-

ne aggiornato ogni 30 secondi, ciò garantisce che network file system applichi una politica

UNIX-like per quanto riguarda la semantica della consistenza.

Ripetiamo che nel momento in cui un client accede alla cache va a vedere il bit di validità as-

sociato al blocco, se il demon è già scattato questo bit sarà falso e viene generato un messag-

gio che richiede al server il trasferimento del blocco.

277

ULTIME LEZIONI DEL CORSO:

UN ESEMPIO: IL SISTEMA OPERATIVO UNIX.

LA STORIA DI UNIX

Facciamo un po' di storia di UNIX, per capire perché ci sono in uso attualmente diverse

versioni di UNIX con caratteristiche differenti.

UNIX non è nato ne come prodotto commerciale ne come sistema proprietario altrimenti

non avrebbe avuto il successo che ha avuto in quanto sistema aperto.

UNIX è nato all'inizio degli anni settanta per una serie di eventi fortuiti.

Come abbiamo detto quando abbiamo parlato dello sviluppo dei sistemi operativi, i siste-

mi operativi della fine anni sessanta erano dei sistemi operativi molto rozzi, insoddisfacen-

ti, e erano in grande auge i sistemi operativi time scering. In America, ci fu un gruppo di

persone che decisero di sviluppare un nuovo sistema operativo timescering estremamente

evoluto che metteva assieme tutte le conoscenze a cui si era arrivati fino a quel momento.

Questo progetto si chiamava MULTIX.

Tale progetto benché intelligente era troppo ambizioso per le macchine che si avevano al-

lora. Esso venne sviluppato per una macchina che come potenza di calcolo era paragonabi-

le a un PC IBM AT (PC con hard disk da 10 Mb ). Si capisce allora la difficoltà di attaccare

decine di utenti timeshering vicino ad un PC IBM AT. Quindi questo progetto si dimostrò,

per questa ragione, un grosso fallimento e quindi il gruppo che stava cercando di svilup-

pare questo sistema operativo abbandonò il progetto. A questo gruppo apparteneva un

certo signore che si chiamava Thomson che era un dipendente dei laboratori della Bell che

era uno dei laboratori che erano coinvolti in questo progetto (la Bell fa parte del gruppo

AT&T). Thomson pensò di trasferite i concetti che lui considerava più importanti del siste-

ma operativo MULTIX nel progetto di un sistema operativo molto semplificato per un mi-

nicomputer (a quel tempo i minicomputer erano le più piccole macchine esistenti, non esi-

stevano i PC, ed erano essenzialmente delle macchine a word a 16 bit) che lui aveva nel la-

boratorio: il PDP-7 (DIGITAL). Questo sistema operativo "giocattolo", sviluppato da

Thomson, riscosse l'attenzione degli altri ricercatori del laboratorio di Thomson, in partico-

lare di Ritchie che collaborò con lui nello sviluppo di questo sistema operativo.

La prima cosa che Ritchie fece, fu quella di inventarsi un linguaggio per scrivere questo si-

stema operativo. Un primo tentativo fu quello di scrivere il S.O. in un linguaggio che si

chiamava B (BCPL) che era un linguaggio che era stato definito a partire dal PL1 il quale

era il linguaggio più evoluto del periodo (in effetti "troppo" evoluto per avere dei compila-

tori funzionanti in quanto metteva in se assieme le caratteristiche sia del FORTRAN che

del COBOL). Poi Ritchie progettò un successore del B che fu appunto chiamato C. Esso

scrisse il compilatore C per il PDP-11 che era un'evoluzione del PDP-7. Nel linguaggio C

venne finalmente scritto UNIX, che nasceva quindi sulle spoglie del progetto MULTIX.

Nel 1971 Thomson e Ritchie scrissero un articolo per descrivere questo sistema operativo

che loro avevano realizzato. Le caratteristiche che si evidenziano in questo sistema operati-

vo erano veramente interessanti rispetto a quelli che erano gli standard del periodo. Ne

278

derivò che molte università americane contattarono i laboratori della Bell per avere dispo-

nibile la versione di questo sistema operativo. In realtà UNIX veniva garantito pratica-

mente gratis perché in quel momento la AT&T non poteva entrare nel settore dei compu-

ter perché se lo avesse fatto sarebbe caduta sotto la legge antitrust americana: essa non

aveva alcuno interesse commerciale sul prodotto.

Il risultato di tutto ciò fu che le università di tutto il mondo, a valle delle articolo del 1971

richiedevano ed ottenevano una versione di UNIX completa del codice sorgente C per po-

chi soldi. Si ebbe quindi una grossa diffusione di UNIX anche perché avendo il codice C e

il compilatore C (il compilatore C inizialmente esisteva solo per la famiglia PDP ma la fa-

miglia PDP rappresentava il minicomputer che a quel tempo, insieme all'HP, era il più dif-

fuso nel pianeta: non esisteva un dipartimento universitario che non possedesse una mac-

china di queste famiglie) si poteva benissimo trasportare UNIX da una macchina ad un al-

tra. Cioè una volta scritto il compratore C per la nuova macchina il passaggio era imme-

diato in quanto i pezzi di UNIX scritti in assembler erano a quel tempo ben pochi (erano

semplicemente quelle parti in cui si devono trasferite i parametri tramite i registri del pro-

cessore o le ISR); quello che stiamo cercando di dire è che UNIX era estremamente potabi-

le.

Scrivere il compilatore C per una nuova macchina era del resto molto semplice perché bi-

sognava semplicemente riscrivere la parte del compratore che genera il codice oggetto.

Quindi bastava prendere il compilatore C per una macchina vecchia andare a sostituire la

parte generatrice del codice che era una piccola parte, e si creava il compilatore per la nuo-

va macchina. Il risultato di tutto questo fu che in tutte le università vi erano dei gruppi di

ricercatori che lavoravano su UNIX per cercare di migliorarne le caratteristiche o per tro-

varne eventuali errori. Nacquero quindi una serie di versioni di UNIX tra cui quella ideata

dalla università di Berkeley. In questa università si sviluppò una versione autonoma di

UNIX che è una delle colonne portanti nello sviluppo di UNIX fino ai giorni nostri: la ver-

sione BSD. Nel frattempo la AT&T stessa ebbe la opportunità di entrare nel settore compu-

ter con una propria divisione, e di sviluppare una propria versione di UNIX che rappre-

senta in effetti l'altro ramo dello sviluppo di questo sistema operativo: versione System.

Man mano che questi due rami sviluppavano nuove versioni di UNIX comparivano nuo-

ve caratteristiche che rinnovavano il sistema. Una di queste nuove caratteristiche, introdot-

ta da Berkeley (BSD 4.2), fu la memoria virtuale. Inizialmente UNIX aveva una gestione

della memoria a pagine o addirittura, nelle versioni iniziali aveva una gestione della me-

moria a segmenti.

Verso la fine degli anni 80, le versioni di UNIX erano talmente tante (a parte queste due

più importanti) che si creò una situazione caotica in cui su molte macchine vi era un siste-

ma UNIX ma nei fatti un programma che girava su una macchina non girava sull'altra; ve-

diamo perché.

L'architettura di un sistema UNIX è quella mostrata nella figura 7.2 a pag. 280 di Tanem-

baum. Abbiamo una struttura a livelli costituita da un Kernel nella quale si entra mediante

delle supervisor call, esiste cioè un'interfaccia (rappresentata appunto dalle supervisor

call) che io debbo rispettare per entrare nel sistema. Queste system call sono nei fatti sem-

pre sviluppate mediante delle routines di libreria che sono chiamate da C quelle che chia-

miamo quando facciamo gli esercizi) che sono quelle che nei fatti sviluppano la vera e pro-

279

pria trap preoccupandosi di sviluppare l'operazione di scambio dei parametri. Quando

facciamo una chiamata a queste routines i parametri vengono passati tramite lo stack, la

routine di libreria piglia i parametri dallo stack li va a mettere nei registri del processore

tenendo conto della peculiarità del processore su cui sta girando il sistema operativo e ge-

nera poi la trap. Un altro modo per entrare in UNIX risulta essere quindi quello di usare i

programmi di libreria di UNIX.

Da quando detto, deriva che il modo per eliminare il caos esistente era quello di standar-

dizzare l'interfaccia delle system call e quindi fissare una volta per tutte quali sono le SC

lecite e quindi quali sono nei fatti le routine di libreria che devono accompagnare il sistema

operativo e quindi il compilatore c per poter poi agganciare il sistema operativo e definire

una volta per tutte quali sono i programmi di utilità e come questi programmi di utilità

vengono attivati come interfaccia come lista dei parametri. La filosofia era quella che se si

riusciva a standardizzare queste due interfacce si poteva fare in modo che un programma

diventasse portabile purché il programma scritto in C o in un altro linguaggio utilizzasse

queste system call standard o queste programmi di utilità standard. (Notiamo che non si fa

menzione delle chiamate di sistema in quanto queste ultime non sono proprio viste dai

programmi applicativi).

L’IEEE si occupò della standardizzazione e diede origine ad una serie di standard che ven-

gono detti POSIX (Portable Operating Sistem unIX ). I due standard più importanti sono

quelli riguardanti la libreria standard per le system call (open, close, read, write, fork, ecc.)

e i programmi di utilità (shell, editor, compilatori ecc.). A pagina 287 di Tanembaum viene

riportata una lista dei più comuni programmi di utilità di UNIX richiesti da POSIX.

Malgrado questa standardizzazione, non fu risolto il problema. Infatti la standardizzazio-

ne fu fatta facendo un intersezione tra le cose offerte dai vari produttori, in modo da non

scontentare nessuno. Il risultato fu che tutti questi produttori continuarono a fornire le

cose che fornivano prima dividendole nell’insieme che faceva part dello standard e in

quello degli “optional” aggiuntivi che essi fornivano. I programmatori che utilizzavano

queste versioni di UNIX, o si attenevano allo standard generando programmi portabili o

utilizzavano le caratteristiche peculiari di quella determinata versione producendo pro-

grammi non portabili.

Si creò quindi nuovamente il caos che terminò nel momento in cui tutti i produttori si riu-

nirono in due consorzi : OSF (Open Software Fondation, IBM, DEC, HP, ecc. ) e UNIX IN-

TERNATIONAL (gruppo di produttori guidati dalla AT&T). Il gruppo che ha avuto più

successo è quello dell’OSF il cui sistema UNIX è sostanzialmente quello della Digital.

UNIX è un sistema a processi e come tutti i sistemi di questo tipo si inizializza a partire da

un unico processo che viene creato all’atto del boot del sistema. All’atto del boot del siste-

ma, parte un processo unico che è detto INIT, il cui compito è quello di inizializzare il si-

stema operativo. Il processo INIT va a leggere un file in cui sono presenti tutti i terminali

che sono agganciati al sistema. Teniamo presente che UNIX è sostanzialmente un sistema

operativo Timesharing come lo doveva essere MULTIX; ciò si vede dal modo con cui

UNIX fa lo scheduling dei processi. Data la sua natura di timesharing il processo INIT

deve necessariamente attivare tutti i possibili terminali che sono stati collegati al sistema,

terminali che non debbono essere necessariamente tutti uguali. INIT crea un processo per

ognuno dei terminali attivi. Ciascuno di questi processi, una volta creato, esegue nel pro-

280

prio contesto , il programma di login il quale non fa altro che fare uscire sul terminale la

parola login e si blocca in attesa che qualcuno si decida a fare il login. Abbiamo quindi una

situazione iniziale in cui c’è un processo INIT che ha creato i vari processi di login associati

ai vari terminali e poi si è messo in attesa della loro terminazione (anche se in realtà tali

processi non terminano mai) ; mentre i processi di login sono in attesa di qualcuno che ef-

fettui il login. Nel momento in cui l’utente si siede al terminale e da il proprio login che

deve essere noto al sistema (ad esempio “gruppo 14”) il programma di login chiede la pas-

sword ; tale password viene acquisita, viene criptografata, si entra nel file delle password e

si va a vedere se al login fornito corrisponde la password criptografata che è stata costrui-

ta.

Qualora le cose corrispondano, il programma di login termina caricando nel proprio con-

testo un altro programma che è lo shell (nei fatti viene fatta una execv) vedi fig. 7.6. Lo

shell che viene caricato è quello corrispondente all’utente che ha fatto il login ; ciò significa

che ogni utente può avere il proprio shell personalizzato, cioè con il proprio linguaggio di

comando che egli si definisce tramite gli script di UNIX. Lo shell si mette in attesa di un co-

mando, nel momento in cui arriva il comando, lo shell interpreta il comando e crea un pro-

cesso nel cui contesto deve essere eseguita l’azione specificata dal comando e si mette in at-

tesa della sua terminazione. PROCESSI

UNIX associa ad ogni processo corrisponde un identificatore di processo (PID). Tale iden-

tificatore è l’entry nella tabella (array) dei descrittori di processo. La particolarità di UNIX

rispetto agli altri sistemi operativi è che il descrittore di processo (che è in corrispondenza

biunivoca con il proprio processo ) non è una struttura monolitica ma è suddiviso in due

parti : 1. Una parte contenuta nella tabella dei descrittori (indirizzata dal PID)

2. User Structur (US)(indirizzata da un puntatore presente nella prima )

La ragione di questa suddivisione sta nel tentativo di ottimizzare l’occupazione della me-

moria, data la presenza in UNIX dello swapping.

Le informazioni che servono al sistema UNIX per gestire un processo, sono una buona

quantità ; queste informazioni, sono tutte necessarie quando il processo e running, ma

quando il processo è sottoposto a swapping, molte di esse non servono. Queste ultime

sono quelle presenti nella US e vengono swappate insieme al processo, precisamente in te-

sta al suo segmento testo.

Contenuto del descrittore di processo e della sua US

Nel descrittore di processo della descriptor table ci sono le seguenti informazioni :

Puntatori in avanti ed indietro che servono per costruire le code in cui questi de-

 scrittori capiteranno (cada dei ready, code di I/O, ecc. tenere presente l’esempio

di Kernel fatto dal Boari). 281

Puntatori ai processi parenti, ad esempio al descrittore del processo padre, ai

 processi figli e a parenti più remoti usati più di frequente (tenere presente che ai

parenti remoti si potrebbe comunque risalire attraverso la catena dei puntatori).

Parametri di schedulazione. Sia schedulazione a breve termine (assegnazione

 della CPU) che schedulazione a lungo termine (schedulazione di swapping) :

Tempo di CPU più recentemente speso (concorre a stabilire la priorità del

 processo e quindi a determinare in quale coda a livelli deve essere inseri-

to).

Altre informazioni tra cui la priorità vera e propria del processo.

Informazioni che permettono di risalire all’immagine in memoria del programma

 (dipendenti dalla particolare tecnica di gestione della memoria fisica. Ad esem-

pio, se la memoria è gestita a segmenti, avremo i puntatori a questi segmenti, se è

gestita a pagine avremo il puntatore alla posizione della tabella delle pagine in

memoria).

Quando il processo e swappato abbiamo anche l’informazione relativa alla posizio-

ne delle sue parti su disco.

In UNIX, ogni processo è costituito da 3 segmenti logici in ognuno dei quali gli indirizzi

sono contigui :

1. Segmento testo

2. Segmento dati

3. Segmento stack

Il segmento testo è a sola lettura per evitare che ci siano dei programmi in grado di auto-

modificarsi (codice rientrante). Tale segmento può essere condiviso da più processi ai fini

di ottimizzare l’occupazione della memoria. Nel caso di condivisione, esiste una tabella,

Shared Text Segment Table (STST), la quale contiene tutti i riferimenti ai segmenti testo

che sono condivisi. Tutti i descrittori dei processi che condividono un segmento punto sul

rigo della STST in cui è descritta la allocazione di questo segmento ; ad esempio :

numero di processi che condividono il segmento

 puntatori ai descrittori di questi processi (in modo da risalire a quali sono i pro-

 cessi che condividono quel codice)

informazioni che mi permettono di risalire alla posizione del segmento sul disco

 (ciò mi serve quando ci sono dei problemi di swapping)

Vediamo la seguente figura :

STACK STACK STACK

STACK

BSS 282

DATI

BSS BSS BSS

DATI DATI DATI

TESTO

TESTO TESTO

Sistema

Spazio degli indi- Spazio degli indi-

operativo

rizzi virtuali del rizzi virtuali del

processo A processo B

Memoria fisica

Notiamo che il segmento dati è diviso in due parti :

1. Dati inizializzati

2. Dati non inizializzati (BSS)

La memoria fisica sopra riportata è una memoria allocata a segmenti, cosa atipica per

UNIX. La maschera dei segnali (un segnale è visto da un processo come una sorta di

 software interrupt che provoca su di esso una serie di effetti che sono descritti in

questa maschera ad esempio :

signal 1 : abort

 signal 2 :ignora

 signal 3 : esegui la routine Pippo (in questo caso il processo in que-

 stione si comporta come una sorta di ISR di utente nei confronti del

processo B).

User identifier dell’utente ed altre informazioni.

Le principali informazioni presenti nella US sono (per ogni voce sotto elencata notare che

essa non e necessaria quando il processo è swappato):

Registri di macchina che vengono salvati nel momento in cui il processo ha la-

 sciato la CPU per qualche motivo

Tabella dei descrittori dei file aperti (ciascun rigo di questa tabella permette di ri-

 salire ad un descrittore di file)

Informazioni di accounting

 Kernel Stack (nel momento in cui io entro nel Kernel io ho salvato i registri del

 processore, prendendoli dallo stack, in quanto nel momento in cui il processo

entra nel Kernel significa che è stato sospeso. Siccome il Kernel è sviluppato a

strati, io passo da uno strato all’altro sempre mediante delle supervisor call

283

(quindi attraverso il meccanismo delle interruzioni) e ad ogni passaggio debbo

salvare i registri del processore. La dimensione di questo stack dipende dal nu-

mero di registri che vengono salvati ad ogni passaggio e dal numero massimo di

livelli di Kernel che si possono attraversare.

Scheduling della CPU

Come abbiamo detto in precedenza, UNIX è un sistema timesharing e come tale io debbo

avere il prerilascio della CPU al termine della time slice. L’algoritmo di scheduling non è a

round robin semplice ma è a round robin su più code, nel senso che ci sono più code circo-

lari (circolari in quanto nel momento in cui un processo perde la CPU viene rimesso in

coda alla stessa coda da cui era stato prelevato) vedi figura 7.16.

Abbiamo la coda dei processi utenti a priorità 3,2,1,0 ecc. La priorità cresce al diminuire

del numero della coda e le priorità possono essere anche negative. Un processo può girare

in user mode e in Kernel mode. E’ in Kernel mode nel momento in cui fa una supervisor

call ; ciò significa che sta eseguendo istruzioni del Kernel all’interno del proprio contesto.

In questo caso il processo viene tipicamente bloccato ad esempio perché inizia un’opera-

zione di I/O e attente la risposta. Quando viene svegliato esso deve fare un percorso che

lo porta fuori dal Kernel ; come si vede dalle priorità negative indicate nella figura (tenia-

mo presente che essa rappresenta la coda dei processi ready che sono stati sbloccati in se-

guito agli eventi descritti nei righi della tabella) , UNIX fa in modo che questo processo

esca quando più presto possibile dal Kernel. Vediamo perché di ciò attraverso un esempio

classico. Se io sono entrato nel Kernel perché dovevo leggere un buffer del disco è probabi-

le che io appena prendo la CPU ed esco dal Kernel, vi rientri perché sono un processo I/O

bound e quindi subito rilascio la CPU.

Vediamo ora come è possibile far variare la priorità di un processo in user mode : esse ven-

gono dinamicamente calcolate. Il metodo con cui avviene questo ricalcolo cerca di appros-

simare l’algoritmo ottimale dello scheduling di CPU che è quello ShortestNextCPUBurst-

First. Il tempo di CPU viene conteggiato in quanto esiste un DEMON che scatta ogni 20

millisecondi. Tale demon incrementa sia il contatore del tempo globale di utilizzo della

CPU dal momento in cui il processo è stato creato, sia il contatore

che mi da il tempo di utilizzo parziale (ovviamente questi contatori verranno incrementati

se nel momento in cui scatto il demon, il processo è running e mi diranno il numero di

time slice di cui il processo ha usufruito). Ogni secondo scatta poi un altro demon che va a

prendere il contatore di utilizzo parziale e lo dimezza. Ne deriva che avere un valore pic-

colo di questo secondo contatore significa che ultimamente il processo ha utilizzato poco

la CPU (in quanto ci sono stati molti dimezzamenti e pochi incrementi ) e quindi è diventa-

to I/O bound (cioè con utilizzi brevi della CPU) e quindi la sua priorità deve aumentare.

Questo demon a secondi calcola quindi in base al valore del contatore dimezzato la nuova

priorità del processo che però va sommata alla priorità statica che gli è stata assegnata dal-

l’utente e che non può essere negativa, ma al massimo nulla.

CURIOSITA’

284

Esistono delle supervisor call che possono essere fatte solo da un utente privilegiato : il, su-

peruser, in quanto sono delicate per il sistema (ad esempio accedere ai blocchi iniziali del

disco). Un utente normale, del resto, potrebbe aver bisogno di accedere al file delle login

per vedere chi è il proprietario di un certo file. Questo è un esempio di un’operazione che è

consentita solo al superuser ma che potrebbe essere utile anche ad un normale utente.

UNIX risolve questo problema utilizzando un bit che si trova nel descrittore di file ed è

detto setwit. Se il superuser ritiene che l’utente possa fare un supervisor call che tipica-

mente è privilegiata, racchiude tale azione in un file eseguibile di proprietà del superuser.

Se l’utente carica con execv questo file nel suo contesto e il setwit è ad 1 lo esegue con gli

stessi diritti del superuser performando quindi l’azione che altrimenti gli era impedita.

Questo è un modo di far fare delle cose delicate all’utente in modo controllato. Questo set-

wit può essere usato anche tra due utenti normali su i propri file.

GESTIONE MEMORIA

Ieri abbiamo detto che UNIX associava ad ogni processo tre segmenti logici :

1. Segmento testo

2. Segmento dati

3. Segmento istruzioni

Questi 3 segmenti verranno collocati nella memoria fisica a seconda della tecnica di gestio-

ne della memoria adottata. Le tecniche di gestione della memoria che solitamente si asso-

ciano a UNIX sono due :

1. Allocazione contigua (gestione a swap della memoria)

2. Paginazione e memoria virtuale (ormai utilizzata in tutte le versioni

UNIX)

Quando veniva adottata la tecnica di gestione contigua veniva utilizzata la tecnica a parti-

zioni variabili : una partizione per ognuno dei tre segmenti. Il funzionamento di questi si-

stemi, era garantito dalla presenza di un ottimo swapper che era in grado di gestire auto-

maticamente il livello di multiprogrammazione del sistema. Teniamo presente che quando

si adottavano queste tecniche, UNIX girava su macchine tipo PDP 11 che avevano un limi-

tata capacità di memoria (tipicamente 64 Kb) ed era inoltre richiesto che un processo per

funzionare doveva risiedere interamente in memoria (assenza di memoria virtuale) e quin-

di per ottenere un livello di multiprogrammazione accettabile era indispensabile avere una

buona tecnica di swapping. Lo swapper trasferiva continuamente processi dalla memoria

centrale alla memoria di massa e viceversa garantendo il fatto che fossero contemporanea-

mente vivi all’interno del sistema un elevato numero di processi.

Lo swapper era un DEMON che veniva attivato ogni certo numero di secondi (tipicamente

4) e cercava di portare dei processi ready che stavano sul disco in memoria ; i casi che si

potevano verificare erano due :

c’era spazio sufficiente in memoria (non c’era bisogno di swap out)

 non c’era spazio in memoria

Una volta liberata la memoria (se c’era bisogno ) la tecnica di allocazione utilizzata era la

best- fit. 285

Lo swap-out veniva fatto dal demon periodicamente, se ce n’era bisogno, oppure in modo

dinamico qualora si verificava l’esigenza di nuova memoria.

L’esigenza dinamica di nuova memoria in UNIX si può verificare ad esempio quando un

processo genera un processo figlio, oppure quando si ha allocazione dinamica di variabili

oppure ancora quando lo stack va in overflow.

Quando veniva fatto lo swap-out venivano portati fuori prima i processi bloccati e poi, se

lo spazio liberato non era ancora sufficiente, anche i processi ready. All’interno di ognuna

di queste due classi la scelta veniva effettuata in base alla:

Priorità dei processi

 Al tempo di permanenza in memoria

 Utilizzo della CPU

Osserviamo che la modalità di funzionamento del demon poteva causare il trashing del si-

stema. Infatti poteva succedere che dopo aver portato fuori dalla memoria tutti i processi

bloccati e tutti i processi pronti (in quanto c’era bisogno di una grande quantità di memo-

ria) il demon cominciasse a portare fuori i processi pronti che aveva appena portato dentro

andando così in loop. Per evitare ciò, esisteva la regola che un processo portato dentro non

poteva essere riportato fuori se non era stato in memoria per almeno 2 secondi.

Ne deriva che il demon termina o quando ha portato in memoria tutti i ready che stavano

sul disco : terminazione corretta ; oppure quando sono finiti i processi che potevano essere

swappati e non c’è ancora lo spazio sufficiente per portare dentro tutti i ready : termina-

zione anomala.

Questo modo di gestire la memoria è attualmente ovviamente superato. In una versione di

Berkeley circa 10 anni fa comparve infatti la memoria virtuale che è attualmente univer-

salmente adoperata nei sistemi UNIX.

Teniamo presente che anche in un sistema a memoria virtuale c’è bisogno di swapping. In-

fatti si può verificare che un processo vivo non abbia alcuna pagina in memoria centrale

ma sia residente completamente su disco, anche in questo caso c’è bisogno di uno swapper

come vedremo in seguito.

Gestione memoria virtuale.

sistemi a pagine piene

 sistemi a pagine vuote

UNIX lavora a pagine libere

algoritmo di sostituzione locale

 algoritmo di sostituzione globale

UNIX è a sostituzione globale e non potrebbe essere altrimenti usano un algoritmo a sosti-

tuzione globale. 286

Il pagedeamon viene attivato ogni 250 millisecondi ed ha il compito di ripristinare il livello

di pagine libere del sistema......due versioni con 1 o due livelli......algoritmo di seconda

chance ... Ricordiamo che quando una pagina viene swappata, essa viene messa nel pool di

pagine libere ma il sistema ricorda a quale processo essa appartiene, in modo che se quel

processo acquisisce la CPU e crea un page fault, il sistema riconosce che nel pool di pagine

libere è rimasta la pagina che serviva, e la semplicemente ripristina ottimizzando i tempi.

Il page deamon in realtà non esamina semplicemente il reference bit, cioè nel suo “giro”

nell’applicare l’algoritmo della seconda chance non scandisce un semplice vettore di bit,

ma ha la possibilità di accedere ad un vettore di record detto core map che contiene altre

informazioni aggiuntive riguardo alla pagina.

La core map si trova negli indirizzi bassi di memoria appena dopo l’area occupata dal Ker-

nel, e occupa una pagina di 1k. Il suo elemento 0 descrive la pagina 0, il suo elemento 1 de-

scrive la pagina 1 ecc. Ogni elemento è 16 byte. I primi 2 elementi di ogni entry contengo-

no i puntatori per formare la lista concatenate delle pagine libere. Le successive tre locazio-

ni si usano per localizzare la pagina nel momento in cui essa si trova sul disco (notiamo

che la gestione dell’area di swap è diversa da quella del file system, infatti, nell’area di

swap, ad ogni pagina è associata un determinato e fissato blocco sul disco ed è l’indirizzo

di questo che è riportato nella mappa). Un’altra informazione è il tipo di segmento che si

trova in quella pagina (codice dati o stack) e l’offset che quella pagina contiene, cioè a par-

tire da quale offset del segmento la pagina ne contiene le informazioni. Altre informazioni

riguardano alcuni bit di gestione come ad esempio quello che mi dice se la pagina non può

essere swappata perché loccata ecc.

Vediamo adesso come si incastra lo swapping in una gestione a memoria virtuale. In effetti

lo swapping interviene nel momento in cui l’attività di paginazione del sistema è troppo

sostenuta. In tali casi può essere utile far si che alcuni processi vivi non abbiano in memo-

ria alcuna pagina in modo da fare spazio per gli altri che rimangono in memoria. Il sistema

si accorge della forte paginazione attraverso il deamon che ogni volta che interviene deve

ripristinare un gran numero di pagine. Non ci confondiamo tra swapper e deamon in

quanto lo swapper non utilizza l’algoritmo della seconda chance ma vede semplicemente

quale è il processo che è bloccato da più tempo e butta fuori tutte le sue pagine.

FILE SYSTEM

In UNIX il file system è organizzato in file e directory e qust’ultime non sono altro che par-

ticolari file contenenti descrittori di file. La struttura delle directory è ad albero o a grafo.

Un file directory è un array di descrittori di file ma non è detto che quest’ultimo debba es-

sere allocato interamente nel file directory ma ve ne può essere solo una parte + un pointer

che mi rimanda alla parte restante del descrittore. Questa è la soluzione che è stata adotta-

ta in UNIX. Quindi in UNIX se io vado a vedere un file directory io trovo un array di de-

scrittori ma ciascun descrittore contiene delle informazioni molto limitate. In particolare

esso contiene soltanto due campi di cui uno è il nome del file e un altro è un pointer il

quale punta alla rimanente parte del descrittore. In un file directory ci saranno quindi tan-

te entry quanti sono i file e le sottodirectory contenute in quella directory e ciascuna di

queste entry, che dovrebbe essere un descrittore di file, in realtà è solo 16 byte di cui 14

287

sono riferiti al nome del file e gli altri due non è altro che un puntatore alla rimanente par-

te del descrittore del file. Questa restante parte, viene detta inode (index node). Un file di-

rectory essendo un normale file può trovarsi in qualsiasi punto del disco (ricordiamo che

in UNIX abbiamo una allocazione ad indice, cioè il file viene acceduto in blocchi che non

sono contigui e la cui posizione viene indicata dall’indice che si trova appunto nel suo ino-

de). Ne deriva che nel momento in cui io voglio conoscere tutto su un file io debbo prima

localizzare il suo entry nella directory a cui appartiene, ottengo il puntatore all’inode e so

tutta l’allocazione di quel file sul disco.

L’inode è ovviamente un record, vediamo quali sono i campi di questo record. Gli inode

non si trovano distribuiti a caso sul disco ma occupano i primi blocchi (vedi figura 7.18). Il

blocco 0 è ignorato dal sistema operativo, il blocco 1 e il superblocco e gli altri sono gli ino-

de. E’ possibile che in un blocco ci siano più inode come adesso vediamo.

Il superblocco ci dice quanti sono gli inode, altre informazioni come quanti sono in totale i

blocchi di disco, qual’è la dimensione dei blocchi e dei frammenti e un puntatore alla lista

dei blocchi liberi (questi sono collegati a lista e il puntatore è alla cima della lista).

Un inode è grande 64 byte contiene :

identificatore del proprietario

 identificatore del gruppo associato al file

 tipi di accesso consentiti (9 bit)

 dimensione del file

 ora di ultima modifica, ultimo accesso ecc.

 numero di link (hard link) per gestire la cancellazione del file quando que-

 sto è condiviso

tipo del file (file dati, file directory, link simbolico, file di dispositivo di I/O

 perché in UNIX i dispositivi di I/O vengono visti come file speciali, cioè io

scrivo e leggo su un file per fare le operazioni di I/O ; in questo caso io ho

un inode ma poi in realtà non ho uno spazio fisico su disco in quanto

quando leggo e scrivo su quel file io non leggo e scrivo su disco ma in

realtà io leggo e scrivo sul dispositivo di I/O).

allocazione del file : ricordiamo che essa è fatta mediante indice e UNIX

 gestisce un indice multilivello. Abbiamo 15 puntatori di cui 12 sono punta-

tori di livello zero (cioè puntano direttamente ai dati) poi abbiamo un

puntatore di livello 1 (indirizzi di blocchi che contengono a loro volta indi-

rizzi), un puntatore di livello 2 e un puntatore di livello 3 (vedi figura 7.19

Guardiamo la figura abbiamo un processo in esecuzione che ha aperto un

certo numero di file, nel momento in cui vuole accedere al file ci accede

mediante il suo inode (che nel caso della figura sta in memoria RAM in

quanto il file è stato aperto). Vediamo che nella figura sono riportate per

l’inode le informazioni da noi elencate).

Ogni processo ha una tabella dei file aperti ; quando io apro un file non faccio altro che ag-

giungere un entry in questa tabella. Nell’entry di questa tabella io tengo nei fatti un punta-

tore che mi deve rimandare ovviamente sull’i-node. Ricordiamo che aprire un file significa

288

portare in memoria RAM il descrittore del file. Nel nostro caso, questo pezzettino è dato

da una parte che sta nella directory e un altra che sta nell’inode. Quindi quello che devo

portare dentro è il nome del file e poi tutto l’inode ad esso associato. Nella tabella dei file

aperti del processo io metto il nome del file e un puntatore che mi deve rimandare alla

zona della memoria RAM in cui si trova il corrispondente inode. Peraltro sappiamo che

UNIX gestisce la lettura e scrittura su disco andando a gestire un puntatore che mi dice la

posizione corrente nel file. Infatti quando io vado a fare una lettura o una scrittura , io in-

dico semplicemente quanti sono i byte che io voglio leggere o voglio scrivere sottintenden-

do che quei byte verranno letti o scritti a partire dalla posizione corrente del file. Quindi

nel momento in cui viene aperto un file gli viene associato un puntatore che il sistema ge-

stisce automaticamente ad ogni read o write. Questo puntatore non è ovviamente presente

nel descrittore di file perché fino a quando il descrittore di file sta su memoria di massa

esso non alcuna utilità.

Si potrebbe pensare di mettere nella tabella dei file aperti sia questo puntatore alla posizio-

ne corrente sia l’intero inode (a cui sono arrivato tramite il suo puntatore che o preso nel

descrittore nella directory) ; cioè avrei una tabella dei file aperti in cui ad ogni entri c’è il

nome del file la posizione corrente e l’intero inode. In realtà in UNIX , tutte queste infor-

mazioni sono distribuite in 3 tabelle. Nella prima, come già abbiamo detto, mi trovo un

nome e puntatore ad una seconda tabella, in cui c’è la posizione corrente + un puntatore

all’inode che sta nella memoria RAM. La ragione per cui faccio questa suddivisione e di

non duplicare informazioni che debbono essere condivise nel momento in cui più processi

aprono lo stesso file. Infatti se più processi aprono lo stesso file essi devono sicuramente

vedere tutti lo stesso inode che nella prima soluzione (unica tabella) dovrebbe essere du-

plicato un numero di volte pari al numero di processi che condividono il file. Un’altra ra-

gione di questa disposizione delle informazioni è quella di implementare in modo sempli-

ce ed efficiente la politica della consistenza tipo UNIX (cioè se uno dei processi apporta

una modifica al file questa deve essere immediatamente visibile a tutti gli altri) : Se avessi

duplicazione di inode essi dovrebbero essere mantenuti consistenti nel momento in cui

viene effettuata qualche modifica. Del resto UNIX dice che i file devono condividere anche

la posizione corrente nel file per permettere la cooperazione attraverso file. A questo pun-

to l’ultimo dubbio che ci potrebbe venire è che questi stessi risultati potevano essere rag-

giunti usando semplicemente due tabelle. La ragione per cui se ne usano tre e che sono di-

versi i processi che devono condividere queste informazioni.

289

COMPENDIO SISTEMI DISTRIBUITI

realizzato da L.Esposito

CAP.9 - INTRODUZIONE

Vi sono due tipi di architetture distribuite: le macchine multiprocessore con una

memoria comune e le reti di calcolatori. Quest'ultimo è il tipo di sistemi distribuiti che ci

interessa di più.

Si utilizza il termine "sistemi operativi lascamente connessi" quando il sistema

operativo distribuito nasce dai vari SO delle singole macchine, integrati da un software di

rete. I sistemi operativi lascamente connessi possono supportare a seconda dei casi:

la condivisione di risorse: device fisici (ad esempio stampanti), file, basi di

dati, gestori di comunicazione (ad esempio un programma che permetta ad una rete locale

di collegarsi ad Internet, che viene così condiviso da tutte le macchine della rete), applicati-

vi utente. il login remoto (cioè la possibilità di far il login da una macchina diversa da

quella in questione)

I sistemi operativi distribuiti strettamente connessi sono invece finalizzati a fare

apparire il sistema distribuito al singolo utente come se fosse un sistema monolitico; cioè si

cerca di rendere del tutto trasparente a colui che si siede dietro una stazione di lavoro il

fatto che quella stazione di lavoro sia collegata in rete.

Si caratterizzano per:

un unico meccanismo di comunicazione globale tra i processi (cioè valido per

tutti i processi che girano sulle macchine a prescindere dalle macchine di appartenenza) .

un unico sistema di protezione e sicurezza di accessi.

 un'unica tecnica di gestione dei processi .

 stesso Kernel di sistema operativo per tutte le singole macchine (si noti la dif-

ferenza fondamentale rispetto ai sistemi precedenti, dove ogni macchina poteva avere un

sistema operativo differente (WINDOWS, UNIX ...).

Un modo di realizzare ciò è quello di avere un microKernel su ogni macchina

(vedi Tanenbaum Figura 9.15 b) .Un microKernel è un Kernel molto ridotto che fornisce

290

solo i servizi base (ovviamente uguali per tutte le macchine) che sono poi nei fatti i mecca-

nismi di gestione dei processi.

Inoltre, alcune delle macchine di rete vengono specializzate, cioè equipaggiate con

il software che realizza funzionalità specifiche e in particolare quelle che non sono previste

dal microkernel ma che devono essere messe a disposizione da un Kernel che si rispetti (in

particolare: file system) e altre funzionalità specifiche di rete (es: server dei processi, che

trasferisce i processi da una macchina all’altra a seconda del carico).

La tendenza attuale è di realizzare sistemi operativi di tipo aperto basati su UNIX

(ultimamente si sta diffondendo però anche l'utilizzo della macchina WINDOWS NT).

L’obbiettivo è di mettere sulle singole macchine UNIX del software che permetta di realiz-

zare un sistema lascamente connesso. Fra le architetture maggiormente implementate ci-

tiamo la DCE. Essa prevede che su ogni macchina UNIX siano disponibili i seguenti com-

ponenti: file system distribuito (DFS);

 gestione di un direttorio di oggetti (servizi di rete, mailbox, computers, ecce-

tera); chiamate a procedura remota (il che significa garantire l'implementazione di

applicazioni client server);

gestione dei thread [abbiamo detto a suo tempo che ci sono due tipi diversi

di processi: i processi ‘classici’ e i processi ‘leggeri’. Questi ultimi sono detti anche thread.

La differenza tra un processo classico e un processo leggero è che mentre i processi classici

(diciamo alla UNIX) hanno ciascuno una propria area di memoria (quindi non c'è condivi-

sione di memoria), i processi leggeri condividono del tutto l'area di memoria e quindi ci

dobbiamo preoccupare dei conflitti e delle mutue esclusioni sulle strutture dati. Il SO deve

mettere a disposizione dei meccanismi (tipo semafori) tali da consentire di risolvere facil-

mente la mutua esclusione sulla memoria condivisa].

gestione dei clock (cioè la capacità di gestire l'allineamento dei vari clock di

macchina in modo tale da definire con una certa approssimazione un tempo globale di ri-

ferimento per l’intera rete).

Classificazione delle reti: le tipologie LAN (locale), MAN (metropolitana), e

WAN (geografica) sì diversificano per i seguenti fattori:

Distanza fra i nodi;

 Rete di comunicazione;

 Velocità di trasmissione.

CAP.10 - LA COMUNICAZIONE NEI SISTEMI DISTRIBUITI

291

Nei sistemi distribuiti lascamente connessi e a memoria locale lo scambio di dati

tra processi avviene unicamente mediante il modello dello scambio di messaggi.

La comunicazione deve essere strutturata a livelli astratti; deve cioè supportare

tutti i possibili aspetti del colloquio tra entità sia di alto che di basso livello di astrazione.

Ad esempio, deve essere permessa la comunicazione tra due programmi applicativi (due

‘attori’), la quale evidentemente si pone ad un livello molto alto. Questo significa fare in

modo che due processi, di cui uno crea un messaggio e un altro lo riceve, possano comuni-

care; i processi mittente e destinatario si trovano ad un livello di astrazione certamente più

basso di quello dei programmi applicativi a cui i processi appartengono. Dobbiamo quindi

avere un software di gestione dello scambio di messaggi tra due macchine. Del resto,

scambiare un messaggio significa scambiare dei byte, anzi per la precisione significa scam-

biare singoli bit, e quindi dobbiamo far colloquiare anche i due ‘attori’ hardware che per-

mettono di trasmettere un bit sulla linea. Riassumendo, la comunicazione coinvolge coppie

di attori che si pongono a livelli di astrazione diversa; pertanto, la comunicazione stessa

deve prevedere necessariamente dei meccanismi che sono strutturati a livelli.

Infine, la comunicazione richiede la definizione di protocolli, ossia delle regole che

devono essere seguite dalle entità di pari livello per il colloquio relativo a quel determina-

to livello. Naturalmente si parlerà non solo di protocollo, ma anche di livello di protocollo.

Due attori che devono comunicare tra di loro ad un certo livello di astrazione seguiranno

un protocollo caratterizzato da regole proprie e da una propria semantica di messaggio.

Tuttavia, questi attori non sono in grado di comunicare direttamente e quindi devono uti-

lizzare attori di livello più basso, la cui comunicazione sarà a sua volta caratterizzata da

una propria semantica e da un proprio protocollo. Questo discorso si ripete fintanto non si

giunge ad un livello in cui i due attori possono comunicare direttamente, e nel nostro caso

questo e il livello fisico, con i due attori che si scambiano singoli bit.

Mettiamo di seguito in luce alcuni concetti chiave relativi alla comunicazione a li-

velli astratti. Ciascun livello include servizi di comunicazione.

 I servizi offerti da livelli distribuiti sono tra loro indipendenti.

 La comunicazione avviene sempre tra entità di pari livello allocate in distinti

nodi della rete.

La comunicazione tra due entità di livello di astrazione i richiede necessaria-

mente il supporto di livelli inferiori, in quanto un servizio di livello i viene implementato

mediante servizi di livello i-1.

Le regole e i formati che specificano lo scambio di dati a due livelli adiacenti

costituiscono una interfaccia tra i livelli.

È fondamentale sottolineare che ogni entità offre dei servizi alle entità di livello

più alto utilizzando servizi offerti dai livelli più bassi; tuttavia, ciascuna entità è indipen-

dente sia da quelle di livello più alto che da quelle di livello più basso. Ciò comporta che in

una suite di protocolli è possibile sostituire un determinato livello con un altro, lasciando

292

inalterata la ‘pila’ di livelli inferiori e quella dei livelli superiori. Questo concetto, in termi-

ni pratici, è importantissimo.

Ad esempio, il protocollo TCP/IP per lo scambio di informazioni fra due macchine

collegate in rete può essere applicato su di una rete locale che può essere sia Eternet che

token ring. Eternet e token ring non sono altro che due protocolli di livello più basso tra di

loro in alternativa. Una rete può essere costituita da un doppino telefonico (e occorrerà

dunque un protocollo per lo scambio di bit sul doppino), su cui è implementato il proto-

collo Eternet per scambiare i frame, e sul frame montiamo il protocollo IP (Internet Proto-

col), il quale consente di scambiare i frame fra due macchine connesse attraverso una terza

macchina (piuttosto che direttamente). In alternativa, IP può essere ‘appoggiato’ sopra un

livello token ring. In questo modello (conosciuto anche con la sigla CSMA/CA) le macchi-

ne sono collegate ‘ad anello’; quindi ogni macchina ha una macchina ‘antecedente’ ed un

macchina ‘seguente’. Perché possa avvenire il colloquio fra due macchine distanti, devono

essere necessariamente coinvolte tutte le macchine che ‘stanno in mezzo’. La macchina che

intende trasmettere il messaggio attende un segnale di autorizzazione da parte della mac-

china seguente (‘token’) e quindi le passa il messaggio; la macchina determina se il mes-

saggio è indirizzato a lei o no. Se il messaggio era destinato ad un’altra macchina, aspetta a

sua volta il token della macchina successiva, e così via finché il messaggio non perviene a

destinazione.

Se due protocolli di comunicazione appartengono a livello distinti sono nella mag-

gioranza dei casi compatibili; se invece appartengono ad uno stesso livello, sono in antago-

nismo: non possono convivere nell’ambito di uno stesso sistema, ma solo essere sostituiti

l’uno all’altro. E' implicito che se si sostituisce un livello con un altro, è necessario rispetta-

re l'interfaccia di comunicazione tra i livelli coinvolti, sia verso l’alto che verso il basso. Poi-

ché i livelli di astrazione possono essere molto numerosi, le combinazioni che possono ve-

nire a crearsi rimpiazzando i livelli con altri sono molteplici.

Fra i modelli reali che aderiscono allo schema a livelli, il più famoso è certamente

l’OSI (Open System Interconnection), il quale ha fissato una struttura a livelli e ha standar-

dizzato ciascun livello di protocollo (vedi Tanenbaum pag. 405, fig.10.1). Le sue caratteri-

stiche principali sono le seguenti:

è stato definito dall’ISO (International Standard Organizzation);

 è strutturato in 7 livelli, i quali includono sia gli aspetti hardware che quelli

software di un’architettura di rete;

prevede sia protocolli orientati alla connessione (nei quali quest’ultima deve

in qualche modo essere stabilita tra mittente e destinatario prima dell’effettivo scambio dei

dati), che protocolli privi di connessione;

un’intestazione (header) viene sempre aggiunta ad un messaggio prima di

essere trasferito dal livello i al livello i-1 della macchina mittente (vedi Tanenbaum pag.

406, fig.10.2). Un messaggio che viaggia su di una rete è quindi preceduto da molte intesta-

zioni ; il numero di intestazioni cresce quando il messaggio scende nei livelli di una stessa

293

macchina, diminuisce man mano che sale tra i livelli. Ogni livello aggiunge un'intestazione

che il livello omologo della macchina destinataria utilizza come informazione; tale infor-

mazione viene soppressa nel passare dal livello i-1 al livello i mentre viene sostituita con

una nuova se si presenta la necessità di ridiscendere per inviare il messaggio ad una terza

macchina. Nell'intestazione saranno presenti non solo le informazioni che servono a gesti-

re il messaggio, ma anche quelle che consentono di determinare se è necessario continuare

a salire o a scendere di livello e ulteriori informazioni di controllo (occorre controllare se

l'informazione pervenuta è corretta o presenta degli errori di trasmissione).

Per inciso, tali controlli sono previsti a tutti i livelli; quando uno di questi controlli fallisce,

la trasmissione si interrompe e parte un messaggio di ritorno lungo la rete che segnala al-

l'attore principale la necessità di ritrasmettere il messaggio che ha perduto il proprio conte-

nuto informativo. Questa procedura di controllo, detta recover è del tutto trasparente al-

l’attore che ha dato inizio al colloquio.

I vari livelli descritti nella figura 10.2 verranno da noi ora brevemente analizzati

uno ad uno.

Livello fisico. Questo livello riceve i byte dal livello superiore e si interessa di tra-

smettere i singoli bit sulla linea. A tale livello vengono definite la modalità di rappresenta-

zione fisica dei bit, la velocità di trasmissione, l'unidirezionalità o la bidirezionalità, il mez-

zo di comunicazione e la dimensione e la forma dei connettori. Il più noto protocollo stan-

dard di livello fisico è RS232C.

Livello dei dati. Si occupa di rilevare e correggere gli errori di trasmissione. I bit

vengono raggruppati in unità dette FRAME e i controlli si effettuano sui frame; ogni frame

viene delimitato mediante una speciale configurazione di bit all’inizio e alla fine, e inoltre

gli viene aggiunto un checksum, ottenuto sommando in un certo modo tutti i byte del fra-

me. Il livello dati della macchina destinazione ricalcola il checksum a partire dai dati e lo

confronta con quello effettivamente pervenuto. In caso di errore, il ricevente chiede nuo-

vamente la trasmissione del messaggio.

Livello di rete. Perché un messaggio raggiunga il destinatario, deve percorrere un

certo cammino sulle linee della rete. Compito del livello rete è di selezionare il cammino

più conveniente (routing). I due protocolli più usati a questo proposito sono X.25 (orienta-

Call request

to alla connessione), in cui viene spedito dapprima un messaggio (richiesta di

chiamata) verso la destinazione, la quale può anche rifiutare la connessione proposta; se la

connessione viene accettata, si seleziona un cammino ottimale (che non necessariamente è

quello più corto; dipende dal traffico di rete) e si restituisce un identificatore al mittente

che questo dovrà usare nelle richieste successive; e IP (Internet Protocol, non orientato alla

connessione) in cui ciascun pacchetto di bit viene instradato verso la destinazione in ma-

niera indipendente da tutti gli altri e in cui non si seleziona né si mantiene alcun cammino.

294

Livello di trasporto. È responsabile di sopperire al problema delle perdite di mes-

saggi che potrebbero verificarsi. Esso suddivide in pacchetti i messaggi provenienti dal li-

vello superiore (sessione), assegna loro un numero di sequenza e li spedisce. Rifacendoci

agli esempi menzionati nel paragrafo precedente, si noti che se il protocollo di rete è orien-

tato alla connessione come nel caso di X.25, i pacchetti arriveranno a destinazione nello

stesso ordine in cui sono stati spediti; se invece non lo è, come nel caso di IP, è compito del

livello di trasporto di riordinare i pacchetti in arrivo nella sequenza giusta. Il protocollo di

trasporto del DoD (dipartimento della difesa americana) è il noto TCP (Transmission Con-

trol Protocol) e la combinazione TCP/IP è fra le più usate, ad esempio nelle università e

nelle reti Unix. Si noti come sia possibile associare un protocollo di trasporto orientato alla

connessione, come il TCP, con un protocollo di rete che non lo è, come IP. UDP è viceversa

un protocollo di trasporto privo di connessione, ed anche la combinazione UDP/IP è molto

diffusa. Livello di sessione. È sostanzialmente una versione perfezionata del livello di tra-

sporto. In alcuni protocolli questo livello è del tutto assente.

Livello di presentazione. In questo livello viene definito il formato dei messaggi

binari in arrivo, in modo che il destinatario possa riconoscerli e dare loro un significato

(nomi di persone, indirizzi, ammontare di conti etc.).

Livello delle applicazioni. È a sua volta una collezione di protocolli relativi alle ti-

piche attività di rete di alto livello, come la posta elettronica, il login remoto etc.

IL MODELLO CLIENT SERVER. L‘idea che sta alla base di questo modello è

quella di strutturare il SO come un insieme di processi cooperanti, detti SERVER, che of-

frono servizi agli utenti, o CLIENT. Come sempre, su tutte le macchine coinvolte gira lo

stesso microkernel.

Il protocollo su cui si basa questo modello è il semplicissimo request/reply senza

connessioni: il client spedisce un messaggio di richiesta al server richiedendo un qualche

servizio, e il server esegue il lavoro e restituisce i risultati oppure un messaggio di errore.

Relativamente al modello a livelli, possiamo dire che occorrono in effetti tre livelli soltanto:

reque-

quelli più interni (fisico e di dati) ed il livello di sessione, che possiamo ribattezzare

st/reply e che definisce l’insieme delle richieste e delle risposte legali.

La semplicità di questo modello si riflette anche nel fatto che i servizi messi a di-

sposizione dal nucleo si riducono a due soltanto: una chiamata di sistema SEND per spe-

dire messaggi e una RECEIVE per riceverne. La sintassi delle due primitive può essere la

seguente:

SEND (dest, &mptr) con dest = nome del destinatario; mptr = puntatore alla loca-

zione della memoria privata del mittente contenente il mesaggio da spedire;

295

RECEIVE (addr, &mptr) con addr = ‘indirizzo di ascolto’ del destinatario (si tratta

mailbox:

in genere di una vedi oltre) e mptr = indirizzo di un buffer nel quale copiare il

messaggio in arrivo.

Seguono alcune considerazioni di carattere generale (cioè non specificamente rela-

tive al modello client server) sulla struttura di queste primitive.

Primitive bloccanti e non bloccanti. Nella maggior parte dei casi, queste primitive

sono bloccanti (o sincrone). Un processo che effettua la SEND si mette in attesa che il mes-

saggio arrivi a destinazione, e solo a questo punto viene sbloccato. Questo avviene di nor-

ma se mittente e destinatario fanno parte della stessa macchina. Se no, è più diffusa la solu-

zione in cui il mittente viene bloccato non fino alla ricezione del messaggio ma solo fino

alla sua trasmissione, ovvero viene sbloccato non appena il messaggio medesimo è stato

processo trasmettitore

consegnato ad un apposito incaricato di inoltrarlo sulla linea. La RE-

CEIVE blocca il processo che la esegue fino a che non arriva un messaggio, che viene me-

mptr;

morizzato all’indirizzo si ha quindi lo sblocco del processo.

Alternativamente vengono utilizzate le primitive non bloccanti o asincrone: una

SEND non bloccante restituisce immediatamente il controllo al chiamante, consentendogli

di riprendere a usare la CPU. Nella versione più diffusa di questa SEND, il messaggio da

spedire viene memorizzato dal buffer privato del mittente ad un buffer del kernel. Lo

svuotamento del buffer del mittente provoca un’interruzione che lo fa ripartire. Una RE-

CEIVE non bloccante comunica semplicemente al kernel l’intenzione del processo che la

esegue a ricevere un messaggio. Se questo è presente, viene copiato nel buffer specificato,

altrimenti il processo riprende il controllo della CPU senza bloccarsi.

Primitive bufferizzate e non bufferizzate. La send non bloccante presuppone una

bufferizzazione del messaggio: è necessario disporre di un buffer residente nel kernel. In

questo modo, il buffer privato del processo mittente viene liberato ed è disponibile per es-

sere utilizzato in occasione di successive operazioni di invio. Per le send bloccanti non è in-

vece necessaria la presenza di questo buffer, e si dice che le send bloccanti sono non buffe-

rizzate. La bufferizzazione della receive consiste invece nel mettere a disposizione ai pro-

mailbox. addr

cessi che possono ricevere messaggi delle In questo caso, quindi l’indirizzo

della receive è relativo ad una mailbox. Tutti i pacchetti in arrivo contrassegnati da questo

indirizzo vengono inseriti nella mailbox corrispondente. Una receive non bufferizzata in

presenza di una send non bloccante rappresenta una soluzione non buona. In questo caso,

addr

infatti, rappresenta l’”indirizzo di ascolto” del destinatario. Se la receive del destinata-

rio viene eseguita prima della send del mittente, tutto va bene; se invece è prima il mitten-

te ad effettuare la send, il kernel del destinatario non può sapere quale particolare proces-

addr

so usa l’indirizzo del messaggio che è appena arrivato, visto che nessun processo de-

stinatario si è ancora fatto avanti. Il messaggio viene quindi perduto.

296

Primitive affidabili e non affidabili. Sarebbe preferibile usufruire di meccanismi

che consentano di sapere se le operazioni di comunicazione siano andate a buon fine o no.

Da questo punto di vista, le primitive non bloccanti (in particolare le send) rappresentano

senza dubbio la soluzione meno affidabile. Ma anche le send bloccanti possono essere non

affidabili. Ad esempio, se si usa il metodo del processo trasmettitore, si ha solo la garanzia

che il messaggio è stato trasmesso, ma non che sia arrivato effettivamente a destinazione. Il

sistema in cui il kernel del server invia al kernel del client un segnale di ‘avvenuta ricezio-

ne’ (se ne è parlato a suo tempo, a proposito del modello a scambio dei messaggi) costitui-

sce un’evidente perdita di tempo, ma fornisce anche maggiore affidabilità. Infine, la send

tipo ‘chiamata a procedura remota’, che è fra le primitive di trasmissione quella che com-

porta i più lunghi tempi di attesa, è per natura affidabile in quanto la restituzione dei risul-

tati funge anche da conferma di avvenuta ricezione.

Tornando ora al modello client server, abbiamo compreso che si tratta di un mo-

dello caratterizzato da una notevole semplicità. Tuttavia essa presenta l’inconveniente di

fondarsi prevalentemente su operazioni di ingresso uscita fra macchine connesse in rete

(mediante l’invocazione delle primitive SEND e RECEIVE), e questo ostacola il proposito

di fare apparire un sistema distrubuito come uno centralizzato. Per rendere l’idea di che

cosa si intenda realmente per ‘applicazione client server’, dobbiamo pensare a due processi

residenti su macchine diverse; nel contesto del primo, il client, viene eseguito un program-

ma chiamante, mentre nel secondo, il server, viene eseguito un sottoprogramma richiesto

dal chiamante. Naturalmente, trattandosi di due processi differenti, i quali per di più gira-

no, generalmente, su macchine differenti, è evidente che processo chiamante e processo ri-

chiamato potrebbero evolvere in parallelo, ma in questo caso non si tratterebbe di una

computazione di tipo client server. In una computazione client server si fa l'ipotesi che il

processo chiamante si sospenda per il tempo necessario affinché la routine richiamata (ese-

guita nel contesto di un altro processo) venga completamente eseguita, e fino a quando

questa routine (ovvero il relativo processo) non restituisce al processo chiamante il risulta-

to della propria elaborazione. Il processo nel cui contesto viene eseguito il chiamante fun-

ge da client per il processo nel cui contesto viene eseguito il chiamato, nel senso che richie-

de un servizio a quest'ultimo.

Uno degli obbiettivi basilari del modello client-server è quello di mettere in comu-

gestore di risorse

ne il processo servitore a più processi clienti; nei fatti, il servitore è un che

permette la condivisione di risorse proprietarie (le sue) tra più processi clienti.

Quando parliamo di un’ “applicazione client-server” ci riferiamo ad un'architettu-

ra tipo quella indicata nella figura seguente, in cui si ha una rete locale (o geografica), tante

macchine client sulle quali gira una determinata applicazione, ed un server su cui gira la

restante parte dell'applicazione, e che in particolare si occupa di gestire le risorse condivise

dai client.

La sofisticazione sta nella modo in cui si sceglie di partizionare l'applicazione: le

operazioni di I/O, la fase algoritmica (elaborativa), la gestione dei data base, etc. Si potreb-

be pensare di implementare tutte queste parti sul server e di limitarsi quindi ad usare i

client come dei ‘terminali stupidi’. Ma in questo caso non avremmo un vero e proprio si-

297

stema client-server, in quanto le macchine client ne risulterebbero sottoutilizzate e l’intero

lavoro sarebbe riversato sul server. Tale sproporzione sarebbe leggermente attenuata

emulando il ‘livello presentazione’ sui client e demandando tutto il resto al server.

All’estremo opposto, si può pensare di porre sul server il solo gestore data base

(DBMS, Data Base Management Server) e sistemare tutto il resto dell’applicazione su ogni

singolo client. In tal caso il server viene rinominato Data Base Server; un eventuale accesso

al Data Base intercettato su di una macchina client comporta la chiamata in causa del ser-

ver che fornisce il servizio richiesto. Questo tipo di suddivisione costituisce di per sé già

un'applicazione client-server di un certo livello di sofisticazione.

In un modello ancora più sofisticato, si ha sul client la ‘presentation logic’ e una

parte del ‘business logic’ mentre la sezione algoritmica viene distribuita su due o tre mac-

chine che lavorano in parallelo.

Figura 1 Client

Client Client

LAN

O

WAN

Server

Disco Stampante

LA CHIAMATA DI PROCEDURA REMOTA. È ora giunto il momento di risol-

vere il problema cruciale del modello client-server: poniamo su di una macchina una ‘pro-

cedura chiamante’ e su di un’altra una ‘procedura chiamata’; costruiamo un’ “enviroment”

intorno al chiamante e al chiamato in modo che si scateni un insieme di attività tali da dare

298

loro l’impressione di avere effettivamente a che fare con un meccanismo di chiamata a sot-

toprogramma. La ‘call’ viene fatta ad una procedura fittizia, che provvederà a realizzare

tutto il meccanismo di scambio di messaggi necessario ad attivare il processo situato sul

server; quest’ultimo, a sua volta, farà in modo di lanciare la procedura chiamata senza che

questa si renda conto del fatto che non è stata lanciata direttamente da nessun chiamante.

Colui scrive l'applicazione non deve vedere questa situazione; per lui le cose devo-

no funzionare come se si trovasse di fronte ad una macchina centralizzata. Esistono allora

ambienti di sviluppo che consentono di fare ciò creando automaticamente degli ‘stub’

(=mozziconi). Per chiarire le idee, consideriamo la figura 10.15 a pag.429 del Tanenbaum.

Uno stub funge praticamente da interfaccia: il chiamante esegue la normale call a procedu-

ra ma in realtà, a sua insaputa, viene attivato lo stub.

Lo stub della macchina client ‘impacchetta’ i parametri in un messaggio e lo invia

al server mediante la SEND messa a disposizione dal kernel su cui sta lavorando; subito

dopo si mette in attesa tramite una RECEIVE. Quando al server arriva il messaggio, il ker-

nel lo passa allo stub del server, il quale si sarà preventivamente bloccato mediante una

RECEIVE in attesa di messaggi. Lo stub del server spacchetta i parametri e quindi attiva la

procedura di servizio (la quale è, come il chiamante, del tutto ignara di questo meccani-

smo), che esegue le sue operazioni e restituisce i parametri di uscita allo stub. Lo stub im-

pacchetta il risultato in un messaggio e lo invia (SEND) al client, tornando poi ad eseguire

una RECEIVE per attendere il prossimo messaggio.

Il kernel del client copia il messaggio nel buffer di attesa dello stub del client e lo

sblocca; lo stub spacchetta il messaggio e restituisce i risultati al chiamante.

Un esempio di codice client-server. L’esempio è relativo alla fig.10.6 a pagina 415

del Tanenbaum.

L'analisi delle procedure è facilitato dai numerosi commenti presenti. Il server è di

tipo speciale: è un file-server (utility per creare, leggere, scrivere, cancellare un file). Il ser-

ver ha in testa una receive bloccante in testa: all'accensione il server parte e attende, da

buon servitore, una prima richiesta. Si noti che praticamente si tratta del server stub: rice-

vuto un messaggio viene sbloccato, effettua il case su m1 e lancia la routine che esegue il

servizio richiesto. Terminata l'operazione, pone il risultato in un campo di un messaggio il

quale viene spedito con una send al processo client. Sottolineiamo la caratteristica saliente

di un server come questo: esso parte con una receive e termina una send.

Il client dal canto suo desidera copiare un file; difatti, esegue in serie una read ed

una write consecutivamente.

Indirizzamento del server. Quando viene effettuata una send, occorre individuare

la macchina su cui risiede il processo destinatario ed il destinatario stesso. Ci sono varie so-

luzioni al riguardo:

indirizzo assoluto: si usa un solo indirizzo, in cui alcune cifre (generalmente

quelle di maggior peso) individuano l'host (=server) e le altre il processo. Questo tipo di in-

299

dirizzamento ha l'inconveniente di non fornire alcuna trasparenza (non è cioè possibile tra-

sferire il processo server su di un'altra macchina);

il nodo viene identificato tramite un indirizzo e il processo tramite una mail-

box. In tal caso è obbligatorio che il server dichiari una o più mailbox. Anche in questo

caso non si ha trasparenza;

identifichiamo il processo server mediante un nome simbolico; nella rete sarà

presente uno speciale server, detto server di nomi, il quale possiede una tabella che resti-

tuisce (a partire dal nome simbolico) la posizione dell'host e il numero di processo che mi

serve.

i messaggi vengono diffusi in ogni caso su tutta la rete; il server giusto lo ‘prenderà al

volo’. 300


PAGINE

332

PESO

2.00 MB

PUBBLICATO

+1 anno fa


DETTAGLI
Corso di laurea: Corso di laurea in ingegneria informatica
SSD:
A.A.: 2013-2014

I contenuti di questa pagina costituiscono rielaborazioni personali del Publisher cecilialll 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à Napoli Federico II - Unina o del prof De Carlini Ugo.

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

Sistemi operativi - domande esame
Appunto
Sistemi operativi - Appunti
Appunto
Sistemi operativi - Problema dei produttori e dei consumatori
Appunto
Sistemi operativi - Esercitazioni varie Linux
Esercitazione