|
Appunti informatica |
|
Visite: 3754 | Gradito: | [ Grande appunti ] |
Leggi anche appunti:Generazione dei processi in unixGENERAZIONE DEI PROCESSI IN UNIX I processi vengono creati a mezzo della UnixUNIX Verso la fine degli anni '60 è in corso presso il MIT l'implementazione La storia di unixLa storia di UNIX Facciamo un po' di storia di UNIX, per capire perché ci |
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 sistemi operativi della fine anni sessanta erano dei sistemi operativi molto rozzi, insoddisfacenti, 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 allora. Esso venne sviluppato per una macchina che come potenza di calcolo era paragonabile 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 sviluppare 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 sistema operativo MULTIX nel progetto di un sistema operativo molto semplificato per un minicomputer (a quel tempo i minicomputer erano le più piccole macchine esistenti, non esistevano i PC, ed erano essenzialmente delle macchine a word a 16 bit) che lui aveva nel laboratorio: il PDP-7 (DIGITAL). Questo sistema operativo 'giocattolo', sviluppato da Thomson, riscosse l'attenzione degli altri ricercatori del laboratorio di Thomson, in particolare 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 sistema 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 compilatori 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 operativo erano veramente interessanti rispetto a quelli che erano gli standard del periodo. Ne derivò che molte università americane contattarono i laboratori della Bell per avere disponibile la versione di questo sistema operativo. In realtà UNIX veniva garantito praticamente gratis perché in quel momento la AT&T non poteva entrare nel settore dei computer 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 pochi 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 famiglia PDP rappresentava il minicomputer che a quel tempo, insieme all'HP, era il più diffuso nel pianeta: non esisteva un dipartimento universitario che non possedesse una macchina di queste famiglie) si poteva benissimo trasportare UNIX da una macchina ad un altra. Cioè una volta scritto il compratore C per la nuova macchina il passaggio era immediato 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 processore o le ISR); quello che stiamo cercando di dire è che UNIX era estremamente potabile.
Scrivere il compilatore C per una nuova macchina era del resto molto semplice perché bisognava 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 nuova 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 trovarne 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 versione BSD. Nel frattempo la AT&T stessa ebbe la opportunità di entrare nel settore computer con una propria divisione, e di sviluppare una propria versione di UNIX che rappresenta 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 nuove caratteristiche che rinnovavano il sistema. Una di queste nuove caratteristiche, introdotta 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 memoria 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 sistema UNIX ma nei fatti un programma che girava su una macchina non girava sull'altra; vediamo perché.
L'architettura di un sistema UNIX è quella mostrata nella figura 7.2 a pag. 280 di Tanembaum. 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 sempre sviluppate mediante delle routines di libreria che sono chiamate da C quelle che chiamiamo quando facciamo gli esercizi) che sono quelle che nei fatti sviluppano la vera e propria 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 genera 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 standardizzare 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 vengono 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 standardizzazione 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 programmi non portabili.
Si creò quindi nuovamente il caos che terminò nel momento in cui tutti i produttori si riunirono in due consorzi : OSF (Open Software Fondation, IBM, DEC, HP, ecc. ) e UNIX INTERNATIONAL (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 sistema, parte un processo unico che è detto INIT, il cui compito è quello di inizializzare il sistema 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 proprio 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 effettui 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 password ; 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 costruita.
Qualora le cose corrispondano, il programma di login termina caricando nel proprio contesto 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 comando, nel momento in cui arriva il comando, lo shell interpreta il comando e crea un processo nel cui contesto deve essere eseguita l'azione specificata dal comando e si mette in attesa della sua terminazione.
PROCESSI
UNIX associa ad ogni processo corrisponde un identificatore di processo (PID). Tale identificatore è 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 :
Una parte contenuta nella tabella dei descrittori (indirizzata dal PID)
User Structur (US)(indirizzata da un puntatore presente nella prima )
La ragione di questa suddivisione sta nel tentativo di ottimizzare l'occupazione della memoria, 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 testa 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 descrittori capiteranno (cada dei ready, code di I/O, ecc. tenere presente l'esempio di Kernel fatto dal Boari).
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 inserito).
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 esempio, 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 posizione delle sue parti su disco.
In UNIX, ogni processo è costituito da 3 segmenti logici in ognuno dei quali gli indirizzi sono contigui :
Segmento testo
Segmento dati
Segmento stack
Il segmento testo è a sola lettura per evitare che ci siano dei programmi in grado di automodificarsi (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 processi 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 :
Notiamo che il segmento dati è diviso in due parti :
Dati inizializzati
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 questione 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 lasciato la CPU per qualche motivo
Tabella dei descrittori dei file aperti (ciascun rigo di questa tabella permette di risalire 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 (quindi attraverso il meccanismo delle interruzioni) e ad ogni passaggio debbo salvare i registri del processore. La dimensione di questo stack dipende dal numero 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 circolari (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'operazione 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 (teniamo presente che essa rappresenta la coda dei processi ready che sono stati sbloccati in seguito 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 è probabile 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 vengono dinamicamente calcolate. Il metodo con cui avviene questo ricalcolo cerca di approssimare l'algoritmo ottimale dello scheduling di CPU che è quello ShortestNextCPUBurstFirst. 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 piccolo 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 è diventato 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 dall'utente e che non può essere negativa, ma al massimo nulla.
CURIOSITA'
Esistono delle supervisor call che possono essere fatte solo da un utente privilegiato : il, superuser, 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 tipicamente è 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 setwit 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 :
Segmento testo
Segmento dati
Segmento istruzioni
Questi 3 segmenti verranno collocati nella memoria fisica a seconda della tecnica di gestione della memoria adottata. Le tecniche di gestione della memoria che solitamente si associano a UNIX sono due :
Allocazione contigua (gestione a swap della memoria)
Paginazione e memoria virtuale (ormai utilizzata in tutte le versioni UNIX)
Quando veniva adottata la tecnica di gestione contigua veniva utilizzata la tecnica a partizioni variabili : una partizione per ognuno dei tre segmenti. Il funzionamento di questi sistemi, era garantito dalla presenza di un ottimo swapper che era in grado di gestire automaticamente il livello di multiprogrammazione del sistema. Teniamo presente che quando si adottavano queste tecniche, UNIX girava su macchine tipo PDP 11 che avevano un limitata 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 quindi 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 contemporaneamente 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.
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 sistema. 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 memoria) 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 : terminazione 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 universalmente adoperata nei sistemi UNIX.
Teniamo presente che anche in un sistema a memoria virtuale c'è bisogno di swapping. Infatti 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.
n sistemi a pagine piene
n sistemi a pagine vuote
UNIX lavora a pagine libere
n algoritmo di sostituzione locale
n algoritmo di sostituzione globale
UNIX è a sostituzione globale e non potrebbe essere altrimenti usano un algoritmo a sostituzione globale.
Il pagedeamon viene attivato ogni 250 millisecondi ed ha il compito di ripristinare il livello di pagine libere del sistemadue versioni con 1 o due livellialgoritmo 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 Kernel, e occupa una pagina di 1k. Il suo elemento 0 descrive la pagina 0, il suo elemento 1 descrive la pagina 1 ecc. Ogni elemento è 16 byte. I primi 2 elementi di ogni entry contengono i puntatori per formare la lista concatenate delle pagine libere. Le successive tre locazioni 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 partire 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 memoria 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 particolari 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 essere 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 adottata in UNIX. Quindi in UNIX se io vado a vedere un file directory io trovo un array di descrittori 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 tante 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 sono riferiti al nome del file e gli altri due non è altro che un puntatore alla rimanente parte del descrittore del file. Questa restante parte, viene detta inode (index node). Un file directory 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 inode). 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 inode. 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 questo è 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 puntatori di livello zero (cioè puntano direttamente ai dati) poi abbiamo un puntatore di livello 1 (indirizzi di blocchi che contengono a loro volta indirizzi), 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 aggiungere un entry in questa tabella. Nell'entry di questa tabella io tengo nei fatti un puntatore che mi deve rimandare ovviamente sull'i-node. Ricordiamo che aprire un file significa 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 indico semplicemente quanti sono i byte che io voglio leggere o voglio scrivere sottintendendo 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 gestisce 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 posizione 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 informazioni 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 duplicato un numero di volte pari al numero di processi che condividono il file. Un'altra ragione di questa disposizione delle informazioni è quella di implementare in modo semplice 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 punto l'ultimo dubbio che ci potrebbe venire è che questi stessi risultati potevano essere raggiunti usando semplicemente due tabelle. La ragione per cui se ne usano tre e che sono diversi i processi che devono condividere queste informazioni.
Appunti su: |
|