Cenni sul Sistema Operativo Unix - Università degli Studi di Trieste

Appunti di Sistemi Operativi
Enzo Mumolo
e-mail address :[email protected]
web address :www.units.it/mumolo
Indice
1 Introduzione al sistema operativo Unix
1
1.1
Storia del sistema operativo Unix . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1
1.2
Caratteristiche generali del sistema operativo Unix
3
. . . . . . . . . . . . . . . . . . .
2 Controllo e gestione dei processi in Unix
5
2.1
Operazione di attivazione del sistema Unix (bootstrap) . . . . . . . . . . . . . . . . .
5
2.2
Accesso Utenti e identicatori in Unix
. . . . . . . . . . . . . . . . . . . . . . . . . .
5
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
12
3 Generalitá sul le system di Unix
7
3.1
Il processo di Shell
3.2
I processi in Unix . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
14
3.3
Gli stati di un processo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
16
3.4
Esecuzione dei processi utente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
21
i
Capitolo 1
Introduzione al sistema operativo Unix
1.1 Storia del sistema operativo Unix
La losoa di Unix segna un passo in avanti nell'avvicinamento tra il linguaggio umano ed il linguaggio di programmazione della macchina. La logica astrusa della macchina sta facendo quindi pian
piano posto ad una logica piu' vicina al modo di pensare dell'essere umano. Per cui, rispetto agli
anni passati, si cerca di avvicinare la macchina al linguaggio dell'uomo piuttosto che il contrario,
invertendo quindi la tendenza.
Durante gli anni Sessanta, alcuni ricercatori dei Laboratori Bell della AT&T, lavoravano al MIT
su di un progetto chiamato
MULTICS (MULtiplexed Information and Computing Service).
MULTICS e' stato un precursore dei sistemi operativi a divisione di tempo e presentava molti
concetti tipici dei sistemi concorrenti odierni, ma sfortunatamente risulto' piu' complesso ed intricato
del necessario, forse a causa del suo ruolo innovativo tanto che alla ne del decennio, si decise di
abbandonare il progetto MULTICS. Nel 1969, due di questi ricercatori, Ken Thompson e Dennis
Ritchie, svilupparono, su un progetto di Rudd Canadat, il primo Unix che era soltanto un piccolo
sistema operativo codicato in assembly per un mini computer della serie Digital PDP-7.
Come accade per la maggior parte dei progetti migliori, Thompson e Ritchie scrissero inizialmente un gioco per il Digital PDP-7, in particolare scrissero un gioco di navigazione spaziale dal
nome Space Travel.
Dopo questa eperienza, decisero di concentrare i loro sforzi verso obiettivi
piu' appaganti e poco dopo crearono un nuovo le system che assomigliava moltissimo a quelli
odierni. Successivamente potenziarono il sistema operativo Unix denendo un ambiente a processi
con scheduling.
Agli inizi degli anni Settanta Unix veniva supportato soltanto dalla serie Digital PDP-7 ed a
meta' degli anni Settanta anche dalla classica e diusissima serie Digital PDP-11, in particolare dai
computer Digital PDP-11/44, Digital PDP-11/60 e Digital PDP-11/70.
Per parecchi anni l'utilizzo di Unix e' stato circoscritto prima all'interno della AT&T e poi ad
ambienti di ricerca ed universitari, che hanno utilizzato il sistema operativo Unix a supporto dei
corsi di scienza dei calcolatori e che hanno contribuito non poco a far conoscere ed apprezzare Unix
in ambienti applicativi e gestionali.
Dal 1969 Unix e' passato attraverso molte versioni ed e' tuttora in fase di ricerca di nuove implementazioni ed aggiunte. In particolare ricordiamo che nel 1973 il kernel del sistema operativo Unix
e' stato riscritto quasi completamente nel linguaggio di programmazione C, infatti tuttora soltanto
pochissime routine di kernel, che necessitano di elevate prestazioni, risultano ancora scritte in linguaggio assembly. Nel 1977 fu sviluppata la prima edizione facilmente trasferibile cioe' portabile su
piu' elaboratori. La prima Universita' a fare il porting su altri sistemi fu la Wollongong University
in Australia. La
portabilita' deriva dal fatto che il sistema operativo Unix e' stato scritto quasi
completamente in linguaggio di programmazione C e non in codice macchina. Il codice macchina
1
1.1 Storia del sistema operativo Unix
2
e' infatti relativo al particolare hardware, mentre il linguaggio di programmazione C e' identico su
tutte le macchine.
A partire dal 1978 la Berkeley University in California sviluppo' una versione del sistema oper-
DARPA (Defense Advanced Research
Projects Agency), conosciuto anche come ARPA (Advanced Research Projects Agency), nanziato dal Dipartimento della Difesa degli Stati Uniti d'America e lo chiamo' BSD (Berkeley Software
Distribution). La versione di Berkeley rappresenta tuttora una pietra miliare per lo sviluppo del
ativo Unix su Digital VAX e PDP, nell'ambito del progetto
sistema operativo Unix e gioca un ruolo importante anche nelle piu' recenti versioni del sistema
operativo Unix.
Nel frattempo diverse compagnie costruttici di hardware iniziarono ad eettuare il porting del
sistema operativo Unix sui loro microprocessori. Ogni casa costruttrice cerco' di migliorare il sistema
operativo Unix aggiungendo qualche caratteristica e qualche miglioramento rispetto alla versione
originale. Tutto ció ha portato ad una proliferazione di versioni spesso incompatibili tra di loro per
cui si preferisce parlare della famiglia dei sistemi operativi tipo Unix.
Nel 1982 la Western Electric della AT&T inizio' a commercializzazione di Unix System III e
nell'anno 1983 presento' anche la versione V di Unix su Digital VAX.
Nel 1982 la Berkeley University produsse invece la versione 4.1 di Unix e nel 1982 la versione 4.2
piu' nota come BSD 4.2. Nella BSD 4.2 fu riprogettato il kernel del sistema operativo introducendo
il protocollo TCP/IP e l'interfaccia socket.
Nel 1984 il sistema operativo Unix divenne un brevetto della AT&T e fu commercializzata
la Unix System V Revisione 2, sulla quale si sono poi basate quasi tutte le versioni distribuite
dalle case costruttrici di sistemi di elaborazione.
Le caratteristiche principali di Unix System V
Revisione 2, sono un'interfaccia a menu' per l'Amministratore di Sistema (System Administrator),
la gestione di diversi livelli di shell (shell layers) ed inne l'autocongurazione del sistema operativo,
che permette al sistema operativo Unix System V Revisione 2 di riconoscere in maniera automatica
la congurazione hardware della macchina e quindi di generare il kernel con i device driver necessari.
In Unix System V Revisione 2.1 e' stata introdotta la gestione della memoria con la tecnica a
(
demand paging).
pagine
Nel 1986 apparve sul mercato Unix System V Revisione 3 con nuove sostanziali caratteristiche
per quanto riguarda l'ambiente di rete.
Nel 1986 la Berkeley University produsse invece la versione 4.3 di Unix e ne concluse subito
lo sviluppo per il termine dei nanziamenti per il progetto DARPA. L'implementazione Unix della
Berkeley University non e' stata pero' abbandonata del tutto, in quanto ha trovato seguito anche
nel sistema operativo SunOs della Sun Microsystem.
Nel 1987 la AT&T ha sviluppato Unix System V Revisione 3.1 con la quale e' possibile scaricare
nella swap area della memoria di massa le regioni dei processi non attivi in memoria centrale.
Nel 1990 la AT&T ha formato una nuova organizzazione denominata
Laboratory)
USL (Unix System
che continua a sviluppare ed a commercializzare il sistema operativo Unix.
E' da
notare che quando ci si riferisce a questo specico Unix prodotto dalla USL scriveremo il nome in
lettere maiuscole. Il nome
UNIX e' infatti un marchio registrato dalla AT&T e dalla USL.
Attualmente la versione di ultima generazione di UNIX e' UNIX System V Revisione 4 a cui
ci si riferisce spesso come System V.4 dove V e' il realta' il numero romano 5 e si pronuncia come
system ve-dot-four. Si puo' anche trovare
SVR4 (System V Revisione 4).
Quando si parla di Unix in termini di sistema operativo non si intende generalmente il prodotto
UNIX della USL, ma ci si riferisce ad ogni sistema operativo membro della famiglia dei sistemi
operativi tipo Unix. Come gia' osservato, il nome uciale dell'Unix di Berkeley e'
Software Distribution).
BSD (Berkeley
La versione piu' recente di questo sistema operativo e' BSD 4.4.
Osserviamo inne che recentemente USL ha sviluppato una nuova versione completa di UNIX
denominata UNIX System V Revisione 8 oppure Research UNIX System che, sebbene non sia ancora
commercializzata, e' stata largamente distribuita nelle Universitá.
1.2 Caratteristiche generali del sistema operativo Unix
3
Con la recente nascita di versioni di Unix destinate a piccoli calcolatori, il mercato ha cominciato
a diondersi anche in ambienti di elaborazione di portata piu' limitata come gli uci, i piccoli studi
commerciali ed applicazioni domestiche.
Un esempio per tutti di versione di Unix destinata a piccoli calcolatori é il sistema operativo
LINUX.
LINUX e' un'implementazione liberamente distribuita (free software) di Unix.
puo' coesistere con altri sistemi operativi come il
LINUX
MS/DOS (MicroSoft / Disk Operating System)
della Microsoft, i sistemi Windows della Microsoft oppure l'OS/2 dell'IBM.
La storia di Unix e' unica in confronto ad altri sistemi operativi, in quanto il suo sviluppo e'
dovuto alle idee creative di singole persone oltre che alle necessita' degli utenti e non deriva in alcun
modo da decisioni burocratiche. Questo e' ancora vero oggi, tanto che il sistema operativo Unix
puo' essere considerato un ambiente molto favorevole per la denizione di nuovi concetti di programmazione. Unix e' un sistema operativo utilizzato in tutto il Mondo e che, almeno virtualmente,
puo' essere utilizzato su ogni tipo di computer. Al giorno d'oggi Unix e' diventato una cultura su
scala mondiale e, come ogni vera cultura, comprende idee, strumenti e consuetudini.
1.2 Caratteristiche generali del sistema operativo Unix
Il sistema operativo Unix é multiprogrammato (multi-tasking) ed a divisione di tempo (timesharing).
Il sistema operativo Unix consente quindi l'uso della CPU a molti processi contempo-
raneamente, ma cio' va inteso nel senso che questi processi sono eseguiti uno alla volta per una
fettina di tempo (time slice) limitata.
Tale azione detta
schedulazione
non e' in alcun modo
percettibile dal processo, che infatti puo' benissimo pensare di essere l'unico in esecuzione.
File e processi sono le due caratteristiche principali della losoa del sistema operativo Unix.
Un le prima di Unix era soltanto una sequenza di informazioni memorizzate su di una memoria di
massa. File, terminali, memorie di massa, drive, praticamente ogni unita' e' invece vista da Unix
come un
le.
Questo rende omogenea la struttura del sistema operativo Unix. Un
programma é
processo
un le contenente la descrizione ad alto livello del l'algoritmo che si intende eseguire. Un
e' un'istanza del programma in esecuzione.
In altre parole in Unix, un programma e' un entita'
passiva che descrive le azioni da compiere, mentre il relativo processo é l'entitá attiva che rappresenta
l'esecuzione di tali azioni.
Il le system costituisce invece l'interfaccia tra l'utente ed i dispositivi di I/O (Input ed
Output). Un le system e' strutturato in una sequenza di blocchi logici, ognuno contenente 512
byte, 1024 byte, 2048 byte, oppure qualsiasi multiplo di 512 byte a seconda delle necessitá.
Qualsiasi entitá di Unix é vista come un le, per cui tutto l'I/O e' indipendente dal dispositivo
sico in cui avviene ed e' trattato in maniera identica. La dimensione di un blocco logico é omogenea
all'interno di un le system per cui un le di 513 caratteri occupa due blocchi di memoria. L'uso di
blocchi logici grandi aumenta la velocita' eettiva di trasferimento dei dati tra disco e memoria, ma
riduce la capacita' eettiva di memorizzazione per cui e' necessario raggiungere un compromesso.
E' stato notato che il sistema operativo Unix da' l'illusione che il le system abbia posti e che i
processi abbiano vita. I le del le system sono di tre tipi dierenti: le normali, direttori e le
speciali.
le normale e' una sequenza di blocchi logici.
Un direttorio (directory) e' una sequenza di blocchi
Un
denisce il legame tra i nomi dei le ed i le stessi.
logici, ognuno contenente 512 byte, e
Un direttorio e' praticamente un elenco di
le che viene aggiornato automaticamente dal sistema operativo Unix a seconda delle richieste di
ogni utente. Il le system e' organizzato ad albero cioe' in senso gerarchico. La radice dell'albero
e' il direttorio
root,
rappresentato dal carattere ASCII / (slash) di codice decimale 47, che e' il
direttorio di sistema da cui parte l'intero le system.
I le speciali detti
device sono una sequenza di blocchi logici, e rappresentano tutti i dispositivi
sici e logici di I/O. Il sistema operativo Unix possiede due tipi di device di I/O: device di I/O a
1.2 Caratteristiche generali del sistema operativo Unix
4
device di I/O a blocchi sono i nastri ed i dischi e per questo
motivo sono detti anche device di I/O di memoria secondaria. I device di I/O a blocchi sono
visti dal resto del sistema come normali device ad accesso diretto cioe' random. I device di I/O
a caratteri includono invece tutti gli altri device come stampanti, schermo video del terminale,
tastiera del terminale e dispositivi di rete e sono detti anche raw device. I device di I/O a carattere
blocchi e device di I/O a caratteri. I
sono visti dal resto del sistema come normali device ad accesso sequenziale. Il sistema operativo
Unix memorizza i le normali ed i direttori su device di I/O a blocchi cioe' su nastri e su dischi.
A causa della grossa dierenza nel tempo di accesso tra i due device di I/O a blocchi, ben pochi
sistemi operativi Unix usano i nastri per i loro le system.
I
device driver sono invece il software di gestione ad interruzione (interrupt) dei device di I/O
/dev. I device di I/O a blocchi sono unita' di memoria
e sono memorizzati sempre nel direttorio
secondaria ad accesso diretto cioe' random, tuttavia, ad esempio, i loro device driver possono farli
vedere al resto del sistema come unita' ad accesso sequenziale.
Ogni installazione del sistema operativo Unix puo' essere prevista su diversi dischi, ognuno
contenente uno o piu' le system. La suddivisione di un disco in piu' le system rende piu' agevole
l'amministrazione dei dati memorizzati.
Il kernel tratta infatti a livello logico con i le system,
piuttosto che con i dischi, trattando ognuno di essi come un device logico indipendente da quello
sico.
L'architettura del sistema operativo Unix puo' essere descritta da un livello shell, da un livello
utente, da un livello kernel e da un livello hardware.
Capitolo 2
Controllo e gestione dei processi in Unix
2.1 Operazione di attivazione del sistema Unix (bootstrap)
La procedura di login é descritta dal seguente pseudocodice:
•
come prima cosa viene caricato il blocco 0 del disco (Boot block) che contiene il programma
di caricamento del kernel
•
dopo che il codice del Kernel é caricato in memoria, l'esecuzione viene fatta partire dall'entry
point del kernel attivando cosí quello che é chiamato Processo 0, che esegue in modalitá sistema
•
Il Processo 0 crea un altro processo con la chiamata di sistema fork, carica ed esegue il processo
/etc/init creando il Processo 1 che esegue in modalitá utente
gettty per ogni terminale esistente.
•
il Processo 1 esegue un ciclo innito crea il processo
•
La getty aspetta no a quando rileva un collegamento. A questo punto chiede lo username
e la password e, se sono entrambi vericati, esegue il programma di shell che costituisce
l'interfaccia tra l'utente e il sistema
2.2 Accesso Utenti e identicatori in Unix
Il primo passo nella registrazione di un nuovo utente é quello di denire lo username, la password,
un identicatore numerico che é lo user ID e il gruppo al quale l'utente appartien, descritto da una
altro identicatore numerico. Inoltre, altre informazioni necessarie per l'attivazione dell'ambiente
dell'utente sono: in quale directory l'utente si troverá una volta avuto l'accesso al sistema e il nome
del programma di shell. Queste informazioni sono memorizzate in un le (
password le)
che é
realizzato secondo questa struttura:
Username:PasswordCrittografata:UserID:GroupID:Nome reale:HomeDirectory:Shell
Naturalmente ogni utente é descritto da una di queste righe.
Normalmente la password crit-
le shadow) che puó essere
tografata non é presente in questo le ma viene scritta in un altro le (
letto solo dal processo con privilegi di amministratore.
Gli utenti hanno un loro identicatore numerico (UID) e appartengono ad un gruppo (GID),
secondo quanto memorizzato nel le di password. Possono cambiare di gruppo con una chiamata
di sistema.
I processi sono caratterizzati da un denticatore numerico, PID, da un identicatore di gruppo
(PGID) e, visto che ogni processo é generato da un alro processo, sono caratterizzati dall'identicatore del processo padre (PPID). Questi identicatori sono generati dal kernel. Se un processo ha
5
2.2 Accesso Utenti e identicatori in Unix
il PGID uguale al PID, é un
Processo Leader.
6
Inizialmente tutti i processi sono Leader. Durante
l'esecuzione i processi possono essere distribuiti in gruppi con una chiamata di sistema. L'organizzazione di processi in gruppi puó essere vantaggioso perché si possono organizzare certe funzioni
secondo la divisione in gruppi.
I processi peró hanno anche alcuni altri identicatori, cioé il Real Process User ID e il Real
Process Group ID. Questi valori provengono dal le password dell'utente che ha avuto l'accesso al
sistema. Cosí l'utente con l'UID 40 esegue processi che hanno un RPUID pari a 40; in questo modo
é possibile risalire alla identitá degli utenti che hanno eseguito un certo processo, cosa necessaria
per consentire di svolgere funzioni di contabilitá d'uso delle risorse.
Esistono altri identicatori, cioé il Eective Process User ID e il Eective Process Group
ID che sono normalmente uguali agli identicatori Real ma in qualche caso sono diversi. Questi
identicatori sono usati per stabilire i permessi ai le.
Visto che i le hanno il UID e GID del proprietario del le, il meccanismo dei permessi é il
seguente:
Se EPUID == UID del proprietario del le oppure EPGID == GID del proprietario la protezione
dei le é stabilita dai bit di protezione corrispondenti vuoi al proprietario vuoi al gruppo. Altrimenti,
la prtezione é stabilita dai bit di protezione corrispondenti al campo 'Other'.
Quando gli identicatori Eective sono diversi da quelli Real? Ci sono casi inei quali é necessario
dare il permesso d'accesso ai le bypassando il meccanismo della protezione dei le ora vista. Questo
é il caso del processo
passwd
che appartiene all'utente
Root.
Il suo compito é di mdicare le
password, cioé di modicare il contenuto del le Shadow che non é visibile a nessuno. La soluzione
é di modicare temporaneamente l'EPUID del processo passwd (che, quando attivato da un utente
ha come EPUID quello dell'utente che lo ha attivato e quindi non potrebbe accedere il le Shadow).
Questo viene fatto mediante il bit SetUserID che caratterizza il le.
Capitolo 3
Generalitá sul le system di Unix
Il disco di Unix é diviso in partizioni, in ognuna delle quali viene caricato un le system. La partizione Unix é composta dal Boot Block, che contiene il codice per il bootstrap, poi dal SuperBlocco,
che contiene informazioni generali sul le system. Segue una lista di blocchi di Inode, oi i blocchi
di dati.
Si è iniziato a parlare dell'inode, ovvero
index node:
vediamo brevemente il perché di questa
denominazione. Il disco agli occhi dell'utente appare un array di blocchi logici che corrispondono ai
settori del disco (lunghi ad esempio 4096 byte l'uno), numerati e accessibili in maniera diretta
Figura 3.1 Rappresentazione dei blocchi di un le in un Inode
Un blocco è la più piccola unità di allocazione in un le system interno. In questo contesto l'inode
di un particolare le contiene, oltre alle informazioni viste prima, anche una lista di puntatori che
puntano ai blocchi che compongono il le in questione: pertanto la funzione principale dell'inode
consiste nell'indicare la posizione dei blocchi contenenti i dati (un indice, appunto).
(vedi gura
??).
Il meccanismo che sta alla base del le system di Unix si basa appunto sull'inode: consideriamo
per esempio il PCB; parlando di le aperti si intende far riferimento a una struttura globale, un
array (gura
??) chiamato User File Descriptor Table che è fondamentale in quanto rappresenta
una descrizione per utente dei le aperti. I primi tre elementi della UFDT (0, 1 e 2) sono deniti
dal sistema operativo e puntano allo standard input (tastiera), output (monitor) ed error (monitor):
è importante osservare che per il sistema Unix questi
nella cartella
Ogni
\etc)
entry
device
sono visti come dei le (i driver, messi
e come tali vengono trattati.
della UFDT punta ad una struttura globale chiamata
File Table:
al suo interno
per ogni processo che accede a un le sono presenti (assieme ad altre informazioni) un oset che
descrive il punto in cui il processo è arrivato a leggere o a scrivere il le e un puntatore all'inode
che descrive il le.
Consideriamo ora il caso in cui ci siano due processi: anche il secondo processo avrà una sua
7
CAPITOLO 3. GENERALITÁ SUL FILE SYSTEM DI UNIX
8
Figura 3.2 Rappresentazione dei blocchi di un le in un Inode
PCB il cui campo
File Aperti punterà ad un'altra UFDT (ogni processo ha una propria User File
Descriptor Table); il terzo campo della UFDT farà riferimento ad un elemento della File Table (che è
una tabella globale); può ora accadere che quest'ultimo elemento punti ad uno stesso inode puntato
dal primo processo: ovvero i due processi accedono allo stesso le (e ciò è possibile in quanto i le
sono risorse condivisibili).
User ID e Group ID
Ad ogni utente del sistema sono associati due numeri non negativi chiamati:
- UID:
User ID (numero di utente)
- GID:
Group ID (numero di gruppo)
Questi due numeri sono stabiliti una volta per tutte dal responsabile tecnico del sistema (e sono in
genere presenti nel le
/etc/passwd
a cui solo l'utente
root
ha accesso). Lo User ID è unico ed
identica quindi l'utente. Lo User ID è univocamente associato allo username mentre il Group ID
al groupname (anche questi vengono assegnati dal responsabile tecnico del sistema).
D'altra parte anche per ogni processo creato (per esempio al login ne viene creato uno) sono
deniti i due numeri:
- Process
Real User ID
- Process
Real Group ID
che vengono ereditati dallo User ID e Group ID dell'utente che ha creato il processo: questi due
numeri pertanto caratterizzano gli aspetti legati al proprietario del processo. È infatti necessario
conmoscere l'utente che ha generato un dato processo per motivi di accesso alla rete, per motivi
legati a statistiche sull'uso delle risorse etc. . . Normalmente Process Real User ID e Process Real
Group ID rimangono inalterati per tutta la vita del processo.
Il processo è però caratterizzato anche da:
CAPITOLO 3. GENERALITÁ SUL FILE SYSTEM DI UNIX
9
Figura 3.3 Esempio di File System di Unix.
- Process
Eective User ID
- Process
Eective Group ID
che normalmente sono uguali rispettivamente a Process Real UserID e Process Real Group ID ed
anch'essi solitamente rimangono costanti per l'intero processo. A dierenza dei Real ID gli eective
ID vengono utilizzati in tutto quello che concerne la protezione all'accesso dei le.
Ora possiamo analizzare come avviene l'accesso e la protezione dei le. Quando un utente crea
un le, tra le caratteristiche del le (presenti nel descrittore di le: l'
inode, index node) vengono
registrati anche il relativo User ID e Group ID. In questo modo anche il le possiede uno User ID e
owner (proprietario) del
Group ID. L'utente che corrisponde allo User ID del le viene anche detto
le. Quando un processo tenta di accedere in qualche modo ad un le, UNIX confronta lo User ID
ed il Group ID del le con quelli Eective del processo. Da questo confronto viene determinato
se il processo può, ed eventualmente in che misura, accedere al le.
Nei confronti di un le, gli utenti (e di conseguenza i loro processi) si dividono in tre insiemi:
1. il
proprietario (indicato con U, user) cioé l'unico utente che è proprietario (owner) del le,
quello il cui User ID coincide con quello del le;
2. il
gruppo (indicato con G, group) cioé l'insieme di tutti gli utenti che hanno lo stesso Group
ID del le;
3. gli
altri (indicato con O, other) cioé tutti gli utenti.
Nei le Unix per ciascuna di queste tre categorie di utenti sono deniti tre permessi (quindi in
Read), scrittura (Write)
totale nove permessi, descritti in nove bit all'interno dell'inode): lettura (
Xecute), quest'ultimo detto anche di ricerca nel caso dei le direttorio.
ed esecuzione (e
Questi permessi possono essere listati richiedendo il formato lungo (opzione
ls:
$ ls -l ... -rw-r----- 1 mumolo 12 Oct 2 10:52 dati01 ...
-l)
del comando
CAPITOLO 3. GENERALITÁ SUL FILE SYSTEM DI UNIX
10
Figura 3.4 Rappresentazione dei permessi nel le mode.
Il primo campo del listato è una stringa di dieci caratteri. Il primo carattere identica il tipo
di le ( d per i direttori, - per i le ordinari ed altri caratteri per le di tipo speciale).
I
rimanenti nove caratteri identicano appunto i permessi sopra descritti. I primi tre dei nove caratteri
relativi ai premessi si riferiscono al proprietario, i secondi tre al gruppo ed i terzi agli altri. Quindi,
nell'esempio, i nove permessi sono così assegnati:
proprietario
RWX
rw-
gruppo
RWX
r--
altri
RWX
---
I permessi accordati vengono indicati con una lettera mentre quelli negati con un tratto. Quindi
nel caso in esame il proprietario del le ha permesso di lettura (r) e scrittura (w) sul le ma non di
esecuzione (x). Gli utenti del gruppo hanno solo il permesso di lettura e gli altri nessun permesso.
In binario e in ottale la loro rappresentazione sarà:
shell:
Es.: il le
rw-
r--
/etc/passwd
--- binario:
110
appartenente all'utente
100
root
000 ottale:
avrà come le mode
6 4
0
7 0 0.
I permessi vanno interpretati in modo dierente a seconda che il le sia ordinario oppure sia un direttorio.
Caso 1: le ordinario
r→
è possibile esaminare il contenuto del le o copiarlo. Quasi ogni comando che usa un le esistente ha
bisogno del permesso di lettura su quel le. Per esempio anche possedendo il permesso di esecuzione
non è possibile eseguire un le senza possedere anche il permesso di lettura;
w→
è possibile modicare il contenuto del le cioé creare, alterare o cancellare (in una parola editare)
il contenuto del le (il contenuto, non il nome, per questo si vedano le interpretazioni dei permessi
associate ai direttori).
Esiste anche una relazione tra il permesso in scrittura di un le e la sua
cancellabilità (verrà introdotto tra poco);
x→
è possibile eseguire il le.
Caso 2: le direttorio
r→
è possibile accedere in lettura al contenuto del direttorio cioè elencare (con il comando
ls senza opzioni)
*) da parte
i nomi dei le contenuti. Anche l'espansione di nomi di le (per es. con il metacarattere
delle shell ha bisogno di questo permesso per poter operare. Se si desiderano maggiori informazioni sui
le (per esempio le informazioni ottenibili con un comando
ls -l) è necessario avere accesso anche in
esecuzione. In ogni caso l'accesso al direttorio non è suciente a garantire l'accesso al contenuto dei
singoli le che esso lista: per esaminare il contenuto di uno specico le del direttorio si devono avere
i permessi opportuni per quel le;
w→
è possibile modicare il direttorio cioé inserire o cancellare le (più precisamente link). Per modicare
invece il contenuto di uno dei le del direttorio si deve possedere il permesso di scrittura su quel le;
CAPITOLO 3. GENERALITÁ SUL FILE SYSTEM DI UNIX
x→
11
a parte quanto è stato già detto in proposito nel caso del permesso in lettura (caso
ls -l), il permesso
in esecuzione per un direttorio consente ad un utente di eettuare un cd nel direttorio. Quando si
cita un pathname relativo o assoluto per un le tutti i direttori citati nel pathname devono essere
accessibili in esecuzione per poter ottenere l'accesso al le.
Problema:
Supponiamo ora che l'utente A faccia eseguire un suo processo (di A) e che questo processo
(contenuto in un le eseguibile) abbia le mode
rwx --x --x (quindi l'utente A permette agli
altri utenti di eseguire quel processo); supponiamo ora che questo processo crei un le e che
quest'ultimo (in quanto generato dal processo di A) abbia le mode
Supponiamo ora che l'utente B (un
other)
rwx --- ---.
esegua il processo di A: questa è un'operazione
senza dubbio lecita, però non appena il processo (che in questo caso ha come Eective User
ID quello dell'utente B) tenta di accedere al le, il processo torna errore in quanto non c'è
congruenza tra l'utente che ha generato il processo e il proprietario del le.
Soluzione:
Nel le mode (parola binaria formata da 16 bit) esistono dei bit aggiuntivi rispetto a quelli
visti sin'ora tra i quali il
set user id che se è settato a 1 allora l'eective user id del processo
viene posto uguale allo user id del proprietario del le che contiene il codice eseguibile.
Figura 3.5
Il
set group id
funziona nel medesimo modo, con l'unica dierenza che ha a che fare con
l'eective group id del processo e con il group id del le.
Da Unix a Windows: la FAT
Figura 3.6 Esempio di FAT.
Se il File System di Unix si basa sulconcetto di inode, quello di Windows si basa sulla
File
Allocation Table (FAT) che (semplicando) descrive la posizione del le sulla memoria di massa.
Nell'esempio in gura un particolare le è memorizzato nei settori 3 - 7 - 5 - 2.
A seconda se ogni entry della FAT è su 16 o 32 bit si parla di FAT16 o FAT32.
I processi interagiscono con il
sottoinsieme di gestione dei processi per mezzo di un insieme
fork( ) che alloca un ingresso (entry) nella tabella dei
di particolari chiamate di sistema, come
processi del kernel
e duplica le regioni del processo chiamante detto processo padre (parent
process) senza liberare la memoria occupata dal processo padre in modo che due copie (processo
padre e processo glio) del processo padre siano in esecuzione allo stesso momento,
exec(
) che
3.1 Il processo di Shell
12
sovrappone un programma al processo glio (child process) in esecuzione,
processo glio in esecuzione che allora assume lo stato di
exit( ) che conclude il
zombie cioe' di morto vivente in quanto
lascia una traccia nella tabella dei processi ed esegue il compito previsto da un'eventuale precedente
attivazione della chiamata di sistema
wait(
) cioe' libera lo spazio occupato nella
tabella dei
processi del kernel dal processo glio nello stato di zombie e sveglia il processo padre che dallo
stato di pronto (ready) in attesa di usare la CPU transita nello stato di esecuzione (running),
wait( ) che mette il processo padre in stato di pronto (ready) in attesa di usare la CPU e ne
sincronizza la ripresa dello stato di esecuzione (running) con la exit( ) del processo glio rimasto
in esecuzione, brk( ) che controlla la dimensione della memoria dedicata la processo e signal( ) che
controlla il ritorno del processo per eventi inattesi.
Il
sottoinsieme di gestione dei le ed il sottoinsieme di gestione dei processi interagis-
cono tra di loro durante il caricamento di un le dalla memoria secondaria alla memoria principale
per l'esecuzione. Il
modulo di comunicazione tra processi infatti deve attendere la lettura dei
le eseguibili in memoria secondaria prima di eseguirli in memoria principale.
3.1 Il processo di Shell
Vediamo ora di chiarire cosa accade quando una linea di comando Unix data per mezzo della
tastiera del terminale oppure attraverso un le viene intercettata dal
anche
processo di interfaccia utente.
processo di shell
detto
fork( ) che alloca
tabella dei processi del kernel e duplica le regioni del processo di shell
Il processo di shell, per prima cosa esegue tre chiamate di sistema e cioe' una
un ingresso (entry) nella
chiamante detto processo padre (parent process) senza liberare la memoria occupata dal processo
padre in modo che due copie (processo padre e processo glio) del processo padre siano in esecuzione
wait( ) che mette il processo padre in stato di pronto (ready) in attesa
esecuzione (running) con la exit( ) del
processo glio (child process) rimasto in esecuzione ed una exec( ) che sovrappone il programma
allo stesso momento, una
di usare la CPU e ne sincronizza la ripresa dello stato di
specicato dalla linea di comando al processo glio in esecuzione.
A questo punto, ci sono tre possibilita': la prima possibilita' e' che la linea di comando specichi
un programma non esistente nel direttorio corrente di lavoro (process's working directory) oppure
in $PATH, che e' il valore della variabile d'ambiente (environment)
PATH,
cioe' nella lista di
direttori (cammino di ricerca) in cui cercare i programmi oppure specichi un programma esistente
nel direttorio corrente di lavoro oppure in $PATH che non e' eseguibile, la seconda possibilita' e'
che la linea di comando specichi un programma esistente nel direttorio corrente di lavoro oppure
in $PATH che e' eseguibile e la terza possibilita' e' che la linea di comando specichi un comando
Unix predenito (built-in) oppure un comando shell predenito.
La prima possibilita' e' dunque che la linea di comando specichi un programma non esistente
nel direttorio corrente di lavoro (process's working directory) oppure in $PATH cioe' nella lista di
direttori in cui cercare i programmi oppure specica un programma esistente nel direttorio corrente
di lavoro oppure in $PATH che non e' eseguibile, allora il sistema operativo stampa un messaggio di
exit( ) dal processo glio
zombie cioe' di morto vivente in quanto
dignostica sullo schermo video del terminale, esegue la chiamata di sistema
(child process) in esecuzione che allora assume lo stato di
lascia una traccia nella tabella dei processi ed esegue il compito previsto dalla precedente attivazione
wait( ) cioe' libera lo spazio occupato nella tabella dei processi del
kernel dal processo glio nello stato di zombie e sveglia il processo di shell padre che dallo stato
di pronto (ready) in attesa di usare la CPU transita nello stato di esecuzione (running).
della chiamata di sistema
La seconda possibilita' e' che la linea di comando specichi un programma esistente nel direttorio
corrente di lavoro (process's working directory) oppure in $PATH che e' eseguibile allora la chiamata
di sistema
exec( ) sovrappone, come gia' osservato, il programma specicato dalla linea di comando
al processo glio in esecuzione. A questo punto ci altre sono due possibilita': la linea di comando
3.1 Il processo di Shell
13
specica un programma esistente nel direttorio corrente di lavoro oppure in $PATH che e' eseguibile
e scritto in linguaggio di programmazione C oppure la linea di comando specica un programma
esistente nel direttorio corrente di lavoro oppure in $PATH che e' eseguibile e scritto in linguaggio
di programmazione Assembler.
Se il nuovo processo glio in esecuzione e' relativo ad un programma scritto in linguaggio di
programmazione C allora alcune chiamate di sistema, come per esempio
exit( ), sono realizzate con
delle chiamate di funzione di libreria relative alla libreria standard di I/O. La libreria standard di I/O
aggiunge allora del codice, in fase di linking, a queste chiamate di funzione di libreria e le trasforma
nelle omonime chiamate di sistema costituite da una procedura codicata in linguaggio macchina
direttamente nel kernel. La maggior parte delle chiamate di sistema, come
open( ), read( ) e write(
), sono naturalmente costituite da una procedura codicata in linguaggio macchina direttamente
nel kernel e quando sono invocate da un programma scritto in linguaggio di programmazione C
somigliano a normali chiamate di funzione.
Analogo discorso se il processo in esecuzione e' relativo ad un programma scritto in linguaggio
di programmazione Assembler.
Il nuovo processo glio, relativo per esempio ad un programma scritto in linguaggio di programmazione C, puo' terminare naturalmente la sua esecuzione, forzare la ne della sua esecuzione
con una chiamata alla funzione di libreria
exit(
), oppure terminare la sua esecuzione per cause
esterne in quanto e' stato intercettato un segnale di sistema.
Nel sistema operativo Unix esiste
infatti la possibilita' di comunicare ai processi il vericarsi di determinati eventi asincroni, cioe' di
eventi che richiedono conferma (acknowledgment).
segnali. I
signal( ), che in
Questi eventi asincroni sono detti
processi possono predisporre la gestione dei segnali tramite la chiamata di sistema
linguaggio di programmazione C somiglia ad una normale chiamata di funzione. I segnali possono
riguardare le eccezioni indotte dal processo come, per esempio, il segnale SEGV (SEGmentation
Violation) che scatta quando un processo tenta di accedere ad un indirizzo esterno al suo spazio
indirizzi di memoria virtuale, quando cerca di scrivere in una locazione di memoria centrale a sola
lettura oppure per errori hardware. I segnali possono riguardare condizioni non piu' recuperabili
durante l'esecuzione di una chiamata di sistema come, per esempio, durante l'esecuzione di una
fork(
) al di fuori delle risorse del sistema.
I segnali possono essere causati da una condizione
di errore non attesa durante una chiamata di sistema come, per esempio, la scrittura di una pipe
che non ha processi consumatori. I segnali possono essere causati da interazioni con il terminale
come, per esempio, la sconnessione di un terminale da parte dell'utente, la caduta della portante
su una linea e la pressione sulla tastiera del terminale dei tasti <break> oppure <delete> da parte
dell'utente. Va osservato che sarebbe preferibile restituire un messaggio di errore anziche' generare
un segnale, ma l'uso di segnali per uccidere i processi che si comportano male e' piu' pragmatico.
exit( ) quando il nuovo processo glio termina
exit( ), sia perche'
e' stato intercettato un segnale di sistema. Quando il kernel esegue una chiamata di sistema exit( )
libera tutti i buer di I/O relativi al processo glio, costruisce lo status di uscita del processo glio,
assegna al processo glio lo stato di zombie cioe' di morto vivente ed esegue il compito previsto
dalla precedente attivazione della chiamata di sistema wait( ) cioe' libera lo spazio occupato nella
tabella dei processi del kernel dal nuovo processo glio nello stato di zombie e sveglia il processo
di shell padre che dallo stato di pronto (ready) in attesa di usare la CPU transita nello stato di
esecuzione (running). Un processo nello stato di zombie e' un morto vivente. E' morto perche' la
sua esecuzione e' terminata ed i suoi segmenti testo e dati non esistono piu', ma e' vivente perche'
occupa un posto nella tabella dei processi del kernel. Lo stato di un processo viene trasformato
in zombie per poter consentire al processo padre di ottenere informazioni sui processi gli morti
per mezzo della chiamata di sistema wait( ), che in linguaggio di programmazione C somiglia
Ebbene il kernel esegue una chiamata di sistema
la sua esecuzione sia naturalmente, sia con una chiamata alla funzione di libreria
naturalmente ad una normale chiamata di funzione. Se infatti al termine dell'esecuzione del nuovo
processo glio il relativo elemento nella
tabella dei processi del kernel venisse immediatamente
3.2 I processi in Unix
14
cancellato allora il processo padre perderebbe ogni traccia dello status di uscita e dei tempi di
esecuzioni del nuovo processo glio morto.
Osserviamo subito che le informazioni sullo status di
uscita e sui tempi di esecuzione sono contenuti in un'estensione dell'ingresso (entry) nella
dei processi del kernel che e' relativo al processo.
tabella
La terza possibilita' e' che la linea di comando specichi, come si puo' osservare nella precedente
gura, un comando Unix predenito (built-in) oppure un comando shell predenito allora la chiamata di sistema
exec( ) sovrappone, come gia' osservato, il comando predenito (built-in) specicato
dalla linea di comando al processo glio in esecuzione. Il kernel esegue naturalmente una chiamata
di sistema
exit( ) quando il nuovo processo glio termina la sua esecuzione sia naturalmente, sia
perche' e' stato intercettato un segnale di sistema.
Per aspettare la terminazione di un processo glio é possibile far seguire la chiamata di sistema
fork(
) da una chiamata di sistema
wait(
) in modo da mettere il processo padre chiamante
pronto (ready) in attesa di usare la CPU e di sincronizzarne la ripresa dello stato
esecuzione (running) con la exit( ) del processo glio rimasto in esecuzione, che il processo
padre chiamante ha creato con la chiamata di sistema per la gestione dei processi fork( ), che in
in stato di
di
linguaggio di programmazione C somiglia naturalmente ad una normale chiamata di funzione. Il
programma eseguibile
processi, descritto nei prossimi tre paragra, segue proprio questa losoa.
Osserviamo inne che i programmi eseguibili possono essere suddivisi in programmi eseguibili utente
programmi eseguibili utente sono forniti con il sistema
operazioni sui le e sui processi. I programmi eseguibili
ed in programmi eseguibili applicativi. I
operativo e permettono di eettuare
applicativi sono invece scritti dagli utenti e permettono di risolvere le problematiche piu' disparate
come la contabilita', il controllo della gestione ed i problemi di ingegneria.
3.2 I processi in Unix
Un processo puó essere denito come un programma in esecuzione, anzi é l'ambiente nel quale
esegue un programma. Un processo consiste di codice, dati e stack (naturalmente un processo puó
leggere e scrivere i suoi dati e stack ma non puó leggere o scrivere i dati o lo stack di altri).
Ogni processo ha un ingresso (entry) in una tabella detta
tabella dei processi del kernel.
tabella dei processi del kernel contiene cinque campi. Il primo campo
area u (AREA User) oppure area u block. Ogni
processo possiede infatti una tabella privata detta area u, che in realta' e' un'estensione dell'ingresso
relativo al processo nella tabella dei processi del kernel. L'area u contiene informazioni locali
Questo ingresso nella
contiene un puntatore ad una tabella detta
sul processo, come ad esempio, i puntatori ai le aperti dal processo stesso e le informazioni sullo
status di uscita e sui tempi di esecuzione.
area u del processo in esecuzione
Il kernel, tramite il modulo della gestione della
Il kernel accede all'
area u del sistema.
memoria, cambia infatti la sua mappa di traduzione degli indirizzi di memoria virtuale a seconda
del processo in esecuzione per accedere all'area u corretta. Anche un processo puo' accedere alla
sua area u, ma soltanto quando e' in esecuzione in modalita' sistema. Per questa caratteristica
l'area u e' una tabella di sistema. Poiche' il kernel puo' accedere ad una sola area u alla volta,
l'area u denisce parzialmente il contesto del processo in esecuzione. Quando il kernel schedula un
processo per l'esecuzione, trova l'area u corrispondente nella memoria centrale e la rende accessibile.
come se questa fosse l'unica
Ogni elemento della tabella processi contiene puntatori al codice, ai dati e allo stack e contiene
l'area U del processo. Tutti i processi di Unix (tranne il primo processo, il processo 0) sono creati
con la system call fork.
La tabella dei processi contiene le seguenti informazioni:
•
stato del processo
•
UID
3.2 I processi in Unix
15
Figura 3.7 La tebella dei processi in Unix
•
Area U
L'area U contiene le seguenti informazioni:
•
Puntatore alla tabella dei processi
•
tabelle pregion
•
descrittori di tutti i le aperti
•
directory corrente
•
radice corrente
•
Parametri di I/O
•
Limiti del processo e dei le
regione codice (text nella terminologia
Unix), la regione dati e la regione stack. La regione codice e' composta da tutte le istruzioni
che sono relative al processo in esecuzione. La regione dati e' costituita dalle variabili globali
del processo. Se un processo tenta di uscire dalla propria regione dati, per esempio tentando
Un processo in Unix e' composto da tre regioni: la
per mezzo di un puntatore di accedere ad un indirizzo esterno al suo spazio indirizzi di memoria
virtuale o di scrivere memoria a sola lettura, il kernel genera un segnale
SEGV (SEGmentation
Violation), che come azione di default fa terminare l'esecuzione del processo e stampa sullo schermo
video del terminale il messaggio Segmentation violation (coredump). La regione stack e' inne un
insieme di lunghezza variabile di locazioni di memoria nella quale vengono memorizzate le variabili
locali durante l'esecuzione del processo. La dimensione della
regione stack viene inoltre aggiustata
dinamicamente dal kernel durante l'esecuzione del processo.
Poiche' un processo puo' essere in esecuzione in modalita' utente oppure in modalita' sistema vengono in realta' riservate due regioni stack e cioe' una regione stack dell'utente e della
regione stack di sistema per cui il processo risulta composto da quattro regioni.
Il formato delle regioni di un processo dipende dalle versioni del sistema operativo Unix. Ad
ELF (Extensible
Linking Format), mentre con le Revisione precedenti era COFF (Common Object File Format).
esempio a partire dal sistema operativo UNIX System V Revisione 4 il formato e'
3.3 Gli stati di un processo
16
In particolare il kernel del sistema operativo UNIX System V divide lo spazio di indirizzi di memoria
virtuale di un processo in regioni logiche. Una
regione e' un'area contigua dello spazio di indirizzi
di memoria virtuale di un processo che puo' essere trattata come un unico oggetto da condividere
oppure da proteggere.
Una regione puo' essere condivisa contemporaneamente da piu' processi
diversi, ad esempio vari processi possono eseguire lo stesso programma e quindi condividono una
copia della
regione testo.
Analogamente diversi processi possono cooperare a condividere una
regione comune di memoria condivisa.
tabella dei processi del kernel e per
tabella dei processi del kernel e l'area
Ogni processo ha dunque un ingresso (entry) nella
ogni processo e' allocata un'
area u.
L'ingresso nella
u contengono tutte le informazioni di controllo e di stato del relativo processo.
Il secondo campo
contiene un ag che descrive lo stato del processo, cioe' che informa in quale degli otto stati si trova
il processo. Il terzo campo contiene l'identicatore
(Owner) del processo.
UID (User IDentity) dell'utente proprietario
Il quarto campo contiene un insieme di descrittori di eventi di I/O validi
quando il processo e' nello stato di
bloccato (blocked) in memoria centrale in attesa di un evento
di I/O, ad esempio di leggere dati dalla tastiera del terminale. Inne il quinto campo contiene un
tabella delle regioni per ogni processo
pregion.
L'ingresso della pregion relativo al processo contiene, per ognuna delle tre regioni relative al
puntatore ad un ingresso (entry) nella tabella detta
oppure per brevita'
processo, quattro campi.
regione.
Il primo campo contiene l'indirizzo di memoria virtuale di partenza della
Il secondo campo contiene una descrizione degli attributi della regione, per esempio se
contiene testo oppure dati, se e' condivisa oppure privata al processo. Il terzo campo contiene una
descrizione del tipo di accesso alla regione che e' consentito al processo, cioe' sola lettura oppure
lettura e scrittura.
Inne il quarto campo contiene un puntatore ad un ingresso (entry) nella
tabella delle regioni attive.
Ebbene l'ampiezza della regione codice e' la dierenza tra gli indirizzi di memoria virtuale
di partenza della regione dati e della regione codice stessa, l'ampiezza della regione dati e'
la dierenza tra gli indirizzi di memoria virtuale di partenza della regione stack e della regione
dati stessa, mentre, come gia' osservato, la dimensione della regione stack viene aggiustata di-
tabella detta
namicamente dal kernel durante l'esecuzione del processo. L'ingresso relativo ad un altro processo
ha infatti un campo di indirizzamento virtuale che non ha nulla a che fare con quello del nostro
processo anzi, come gia' osservato, una regione puo' essere condivisa contemporaneamente da piu'
processi diversi.
3.3 Gli stati di un processo
Il sistema operativo Unix e', come abbiamo gia' osservato, multiprogrammato (multi-tasking) ed
a divisione di tempo (time-sharing). Il sistema operativo Unix consente quindi l'uso della CPU a
molti processi contemporaneamente, ma cio' va inteso nel senso che questi processi sono eseguiti
uno alla volta per una fettina di tempo (time slice) limitata. Tale azione detta
schedulazione non
e' in alcun modo percettibile dal processo, che infatti puo' benissimo pensare di essere l'unico in
esecuzione. Ebbene quando il kernel decide di schedulare un altro processo, eettua un
switch, cosi' da potere eseguire nel contesto un altro processo.
context
Il kernel permette un context switch
solo in certe condizioni ed assicura l'integrita' e la coerenza delle strutture dati vietando i context
switch arbitrari.
Quando esegue un context switch il kernel salva le informazioni necessarie per
poter poi tornare ad eseguire il processo abbandonato.
modalita' utente all'esemodalita' sistema il kernel salva le informazioni per poter ritornare all'esecuzione in
Si osservi che anche quando un processo passa dall'esecuzione in
cuzione in
modalita' utente e proseguire l'esecuzione da dove l'ha interrotta, ma attenzione che cio' non e'
un context switch. Il kernel permette il context switch soltanto in quattro particolari circostanze:
quando un processo entra nello stato di sospensione perche' prima che il processo si svegli potrebbe
3.3 Gli stati di un processo
17
passare molto tempo ed altri processi possono eseguire nel frattempo, quando termina l'esecuzione
con una chiamata di sistema
exit(
) se non altro perche' non c'e nulla altro da fare, quando un
processo torna in modalita' utente da una chiamata di sistema ma non e' piu' il processo prioritario
oppure quando un processo torna alla modalita' utente dopo che il kernel ha completato la gestione
delle interruzioni (interrupt) ma non e' piu' il processo prioritario.
Il kernel e' responsabile anche della gestione delle interruzioni (interrupt), sia che essi provengano
dall'hardware, come le interruzioni generate dal clock e le interruzioni generate dalle periferiche (per
esempio da uno dei dischi), sia che si tratti di un'interruzione programmata cioe' di un interrupt
software oppure di eccezioni come sono gli errori di paginazione. Il kernel gestisce le interruzioni con
il seguente protocollo: salva il contenuto attuale dei registri del processo in esecuzione e crea un nuovo contesto, determina la causa dell'interruzione, identicando il tipo di interruzione (clock oppure
periferica, per esempio disco) ed il numero di unita' di interruzione se possibile (come per esempio
quale disco ha provocato l'interruzione), chiama il relativo gestore delle interruzioni ed attende che
il gestore delle interruzioni completi il suo compito e ritorni il controllo al processo. Il kernel esegue
una sequenza di istruzione specica per la macchina, per recuperare il contesto dei registri e lo stack
kernel del precedente contesto cosi' come erano prima dell'interruzione e riprende l'esecuzione del
contesto recuperato Il comportamento del processo puo' essere pero' alterato dalla gestione delle
interruzioni poiche' tale gestione puo' aver alterato le strutture dati del kernel e svegliato processi
sospesi, normalmente pero' il processo continua la sua esecuzione come se l'interruzione non fosse
mai avvenuta.
La procedura di context switch e' simile alla procedura di gestione delle interruzioni ed alla
procedura delle chiamate di sistema, tranne per il fatto che il kernel recupera lo stato di contesto di
un altro processo, anziche' lo stato di contesto precedente dello stesso processo. La scelta di quale
processo schedulare dopo un context switch e' una decisione di strategia che non tocca i meccanismi
di context switch.
esecuzione (running)
bloccato (blocked) cioe' lo stato di sospensione in attesa di un evento esterno. Un
Esistono concettualmente due stati della vita di un processo: lo stato di
e lo stato di
processo in stato di sospensione non e' eseguibile anche se la CPU e' libera.
si aggiunge lo stato di
pronto
A questi due stati
(ready) in attesa di usare la CPU naturalmente per motivi di
limitazione di risorse della CPU stessa. Ogni processo transita, durante la sua vita, tra questi tre
stati ed e' compito del sistema operativo Unix gestire queste transizioni.
Nella pratica invece si
possono distinguere per la vita di un processo ben otto stati, che dipendono dal particolare istante
di elaborazione del processo, dalla sua storia precedente, dal fatto che al processo sia assegnata
o meno la CPU, dal fatto che esso sia residente in memoria centrale o in memoria di massa in
particolare nella swap area, oppure dal fatto che sia o meno in stato di pronto per l'esecuzione. Lo
stato di esecuzione puo' essere inoltre suddiviso in esecuzione in modalita' di sistema (kernel mode)
ed in esecuzione in modalita' utente (user mode). La
di codice che puo' essere eseguita dal programma.
modalita' utente si riferisce a quella parte
Quando invece il processo richiede servizi al
sistema operativo, ad esempio l'apertura o la lettura di un le di dati, entra in
modalita' sistema,
ovvero viene eseguito il codice del kernel. Le routine del kernel permettono di espletare tutti i servizi
richiesti dai processi. Alla ne di tutti questi servizi, il processo ritorna in modalita' utente.
Si osservi che la gura riportata nella precedente pagina, che descrive gli otto possibili stati
della vita di un processo e le relative transizioni, potrebbe dare un'idea statica dell'esecuzione del
processo mentre, in realta', ogni processo cambia continuamente stato, secondo regole ben precisate.
La gura riportata nella precedente pagina e' un grafo direzionato i cui nodi rappresentano gli stati
che un processo puo' assumere ed i cui cammini rappresentano gli eventi che provocano le transizioni
di stato. Le transizioni di stato sono permesse soltanto se esiste un arco dal primo al secondo stato.
A partire da uno stato, per esempio dallo stato 4, possono essere possibili diverse transizioni, ma per
ogni processo vi sara' una ed una sola transizione per ogni evento di sistema. Il kernel permette un
context switch soltanto quando un processo passa dallo stato 2 di esecuzione in modalita' sistema
3.3 Gli stati di un processo
18
allo stato 1 di esecuzione in modalita' utente.
I processi vanno invece in stato 4 di sospensione in memoria centrale perche' aspettano il vericarsi di certi eventi, come il termine di un'operazione di I/O da parte di un'unita' periferica, la
ne dell'esecuzione di un processo e la disponibilita' di risorse del sistema operativo Unix. Questi
processi sono allora detti in attesa del vericarsi di un evento. Quando si verica l'evento atteso
questi processi si risvegliano ed entrano nello stato 3 di pronto ad eseguire in memoria centrale
(ready to run), dove attendono di essere scelti piu' tardi dal
modulo di scheduling.
Il kernel gestisce i le su device di I/O a blocchi, che sono i nastri ed i dischi, e quindi permette ai
processi di immagazinare nuove informazioni oppure di recuperare le informazioni precedentemente
caricate. Quando un processo desidera accedere ai dati contenuti in un le, il kernel copia questi
dati in memoria centrale dove il processo puo' esaminarli, elaborarli ed eventualmente richiedere che
i dati vengano nuovamente immagazzinati nello stesso le oppure in un le diverso che puo' essere
gia' esistente o meno nel le system.
Il kernel potrebbe compiere le operazioni di lettura e di scrittura direttamente su device di I/O
a blocchi, che sono i nastri ed i dischi, ma i tempi di risposta non sarebbero accettabili a causa della
bassa velocita' di trasferimento su e da device di I/O a blocchi.
Il kernel minimizza la frequenza degli accessi al disco mantenendo un insieme di buer dati
al suo interno, chiamato
buer cache,
che contiene i dati dei blocchi di device di I/O a blocchi
piu' recentemente usati. Si faccia molta attenzione che il
del kernel da non confondere con la
buer cache e' una struttura software
cache hardware che serve invece a velocizzare i richiami in
memoria.
Quando il kernel legge i dati da device di I/O a blocchi, che sono i nastri ed i dischi, cerca in
buer cache sono immediatamente
buer cache il
kernel li legge dal device di I/O a blocchi e li copia nel buer cache utilizzando, per entrambe le
realta' di leggerli dal
buer cache.
I dati memorizzati nel
disponibili e non serve leggerli dal device di I/O a blocchi. Se i dati non sono nel
operazioni, un algoritmo di ottimizzazione.
Quando l'operazione di I/O del processo termina, l'hardware interrompe la CPU ed il gestore
delle interruzioni sveglia il processo, che si trova nello stato 4, provocando il suo ingresso nello stato
3 di pronto ad eseguire in memoria centrale (ready to run) dove attende di essere scelto piu' tardi
dal
modulo di scheduling.
Se il kernel sta eseguendo tanti processi da superare la disponibilita' di memoria centrale allora
lo
swapper, cioe' il processo 0 (zero), scarica dalla memoria centrale almeno un processo per far
posto ad un altro processo che e' nello stato 3 di pronto ad eseguire in memoria centrale (ready to
run).
Quando viene scaricato dalla memoria centrale ogni processo passa nello stato 6 di bloccato in
swap area.
Quando lo
swapper
lo scegliera' come processo da caricare in memoria centrale il processo
modulo di
ritornera' nello stato 3 di pronto ad eseguire in memoria centrale (ready to run), il
scheduling
lo fara' poi partire ed esso entra' nello stato 2 di esecuzione in modalita' sistema
(running kernel mode) per poi proseguire.
Quando un processo nisce la sua esecuzione il kernel esegue la chiamata di sistema
exit( ), che
fa transitare il processo dallo stato 2 di esecuzione in modalita' sistema (running kernel mode) allo
stato 8 di zombie.
Descriviamo ora dettagliatamente gli otto stati possibili di un processo. Lo stato 1 e' relativo alesecuzione in modalita' utente (running user mode) mentre lo stato2 e' relativo all'esecuzione
in modalita' sistema (running kernel mode). Il processo transita dallo stato 1 allo stato 2 quan-
l'
do un processo esegue una chiamata di sistema (system call) oppure in seguito ad un'interruzione
(interrupt). La transizione dallo stato 2 allo stato 1 e' invece gestita direttamente dal kernel stesso
che puo' in tal caso decidere di realizzare anche un context switching in attesa che il
scheduling dia via libera al processo dopo aver eventualmente dato la precedenza,
modulo di
per esempio,
3.3 Gli stati di un processo
19
Figura 3.8 Gli stati di un processo in Unix
3 e' lo stato di pronto in memoria centrale in
ad un processo con priorita' maggiore. Lo stato
attesa di usare la CPU, cioe' di pronto per l'esecuzione (ready to run).
Un processo nello stato
3 non e' in esecuzione, ma e' pronto a partire non appena verra' schedulato dal kernel.
E' nello
stato 3 un processo che ha terminato lo stato 4 di bloccato in memoria centrale, oppure che e' stato
4 di bloccato in memoria
centrale indica un processo in attesa di un evento di I/O, ad esempio di leggere dati dalla tastiera
del terminale. Lo stato5 di pronto in swap area in attesa di usare la CPU indica un processo
appena creato nello stato 7 e caricato in memoria centrale. Lo stato
che, pur essendo pronto a partire, deve prima essere caricato in memoria centrale. Lo stato di un
processo
bloccato in swap area e' lo stato 6 di bloccato.
Lo stato
7 di partenza e lo stato 8 di
zombie si riferiscono, rispettivamente, alla creazione di un processo ed alla sua ne.
La transizione di un processo dallo stato 2 di esecuzione in modalita' sistema allo stato 4 di
bloccato in memoria centrale in attesa che termini l'I/O, prevede quasi sempre un context switching
in modo che altri processi possano intanto utilizzare la CPU, che altrimenti resterebbe disoccupata
in attesa che termini l'I/O.
tabella dei processi del kernel c'e' almeno un ingresso libero allora una
fork( ) ha successo ed il processo appena creato e' nello stato 7 di partenza. A
Se per esempio nella
chiamata di sistema
questo punto ci sono due alternative e cioe' la transizione del processo dallo stato 7 verso lo stato 3
3.3 Gli stati di un processo
20
oppure la transizione del processo dallo stato 7 allo stato 5. La transizione del processo dallo stato
7 di partenza verso lo stato 3 di pronto in attesa di usare la CPU in memoria centrale puo' avvenire
soltanto se in memoria centrale c'e' suciente spazio.
La transizione del processo dallo stato 7
di partenza verso lo stato 5 di pronto in attesa di usare la CPU ma in swap area avviene invece
se in memoria centrale non c'e' suciente spazio. Un processo nello stato 5 di pronto in attesa di
usare la CPU ma in swap area, per poter essere eettivamente eseguito deve prima essere caricato in
memoria centrale. Appena c'e' spazio libero in memoria centrale il kernel infatti recupera il processo
in stato 5, copia velocemente le tre regioni di tale processo in memoria centrale e cambia lo stato
da 5 in 3. Sia allora comunque lo stato 3 quello di partenza. Quando il
modulo di scheduling
seleziona il processo per l'esecuzione, il processo passa dallo stato 3 allo stato 2 di esecuzione in
modalita' sistema dove completera' la sua parte di chiamata di sistema
fork(
).
Finito il suo
compito il kernel cambia nuovamente lo stato del processo da 2 ad 1, attraverso una transizione
che gestisce direttamente e durante la quale puo' decidere di realizzare un context switching in
attesa che il
modulo di scheduling dia via libera al processo, dopo aver eventualmente dato la
precedenza, per esempio, ad un altro processo a priorita' maggiore.
puntatore alla
swap area
Inne il kernel inizializza il
in modo da liberare spazio su disco per il corrente utente o per altri
utenti. Osserviamo che il processo e' ora nello stato 1 di esecuzione in modalita' utente, ma non
e' nita perche' dopo un certo tempo il clock puo' interrompre il processo che passera' nuovamete
nello stato 2 di esecuzione in modalita' sitema. Quando il gestore di clock concludera' la gestione di
questa interruzione (interrupt), il kernel potra' decidere di schedulare anche un altro processo per
l'esecuzione, in questo modo il primo processo passera' nello stato 4 di bloccato in memoria centrale
e l'altro processo andra' in esecuzione.
Consideriamo ora un processo che e' in attesa della ne di un'operazione di input, per esempio e'
in attesa che venga letto un carattere dalla tastiera del terminale, allora e' nello stato 4 di bloccato
in memoria centrale. A questo punto ci sono due alternative: la transizione risveglio del processo
dallo stato 4 verso lo stato 3 oppure la transizione scaricamento in swap area dallo stato 4 verso lo
stato 6 di bloccato in swap area. Ebbene lo stato del processo transita verso lo stato 3 di pronto in
memoria centrale se si conclude l'operazione di input prima che altri processi richiedano l'accesso
alla memoria centrale. Tuttavia per veloce che sia la ne dell'input, la CPU potrebbe eseguire nel
frattempo milioni di operazioni per cui il kernel produce un context switching in modo che un altro
precesso, eventualmente di un altro utente, possa utilizzare la CPU. In tal caso la CPU non e' piu'
disoccupata, ma il processo nello stato 4 occupa memoria centrale.
Se un terzo processo esegue
fork( ), allora il kernel puo' aver bisogno di spazio in memoria centrale e
swap area tutte le regioni dei processi che non sono attivi in memoria centrale,
la chiamata di sistema
allora copia in
compreso il nostro processo nello stato 4, ed aggiorna il puntatore alla memoria centrale in modo
da liberare spazio. In particolare il nostro processo transita dallo stato 4 di bloccato in memoria
swap area,
paginazione,
centrale allo stato 6 di bloccato in swap area e le sue tre regioni vengono copiate nella
che e' una memoria di massa su disco gestita molto velocemente perche' non usa la
ma memorizza le tre regioni del processo in maniera contigua. Una volta che la memoria centrale e'
di nuovo libera il kernel recupera il processo, che dallo stato 6 di bloccato in swap area e' passato
allo stato 5 di pronto in swap area, e lo copia in memoria centrale con le stesse modalita' descritte
nel precedente esempio.
Come gia' osservato e come appare nella precedente gura, che rappresenta gracamente gli otto
possibili stati di un processo e le relative transizioni, la transizione dallo stato 1 di esecuzione in
modalita' utente (running user mode) allo stato 2 di esecuzione in modalita' sistema (running kernel
mode) viene provocata dalle chiamate di sistema (system call) oppure dalle interruzioni (interrupt).
Ebbene, come esempio nale, consideriamo due processi, che possono essere benissimo relativi anche
a due utenti diversi. Il processo1 sia nello stato 1 di esecuzione in modalita' utente ed il processo2
sia nello stato 4 di bloccato in memoria centrale in attesa di un evento di I/O, per esempio della
ne di una stampa. Quando la stampante termina il suo compito manda la relativa interruzione
3.4 Esecuzione dei processi utente
21
(interrupt), che viene ricevuta dal kernel. Per poter realizzare la
routine di interrupt il kernel fa
transitare in stato 2 di esecuzione in modalita' sistema il processo1, esegue toccata e fuga la routine
di interrupt e fa nuovamente transitare il processo1 nello stato 1 di esecuzione in modalita' utente.
Il sistema operativo Unix permette infatti a device, come le periferiche di I/O oppure al clock di
sistema, di interrompere la CPU in modo asincrono. All'arrivo di una interruzione (interrupt), il
kernel salva il contesto corrente cioe' un'immagine congelata di cio' che il processo stava facendo,
cerca la causa dell'interruzione e la gestisce.
Dopo aver gestito l'interruzione, il kernel ripristina
il contesto interrotto e continua come se niente fosse successo.
Il processo2 evolve con le stesse
modalita' descritte nei due precedenti esempi.
3.4 Esecuzione dei processi utente
Diciamo ora qualche cosa di piu' sull'esecuzione dei programmi utente che, come abbiamo gia' osservato, sono forniti con il sistema operativo Unix e permettono di eettuare operazioni sui le. Ebbene,
come abbiamo gia' osservato, l'esecuzione dei programmi utente, sul sistema operativo Unix, e' differenziata tra esecuzione in modalita' utente (running user mode) ed esecuzione in modalita' sistema
(running kernel mode). Quando un processo utente esegue una chiamata di sistema, l'esecuzione del
processo cambia da esecuzione in modalita' utente ad esecuzione in modalita' sistema ed il sistema
operativo Unix tenta di soddisfare tutte le richieste di ogni utente, restituendo eventualmente un
codice d'errore. Anche se l'utente non fa richieste esplicite dei servizi del sistema operativo, il sistema operativo Unix compie comunque operazioni di amministrazione, che si riferiscono ai processi
utente, alla gestione delle interruzioni (interrupt), allo scheduling dei vari processi, alla gestione
della memoria centrale, eccetera. I processi in esecuzione in modalita' utente possono accedere alle
proprie istruzioni ed ai propri dati, ma non ai dati ed alle istruzioni del kernel oppure a quelle di
altri processi. I processi in esecuzione in modalita' sistema possono invece accedere a tutto. Per
esempio, la memoria virtuale di un processo puo' essere suddivisa in parti di cui alcune accessibili
ed altre inacessibili in modalita' utente, ma certamente tutte accessibili in modalita' sistema.
Alcune istruzioni macchina sono privilegiate e danno errore se vengono eseguite in modalita'
utente. Per esempio, se una macchina contiene un'istruzione che manipola il registro di stato del
processore, i processi eseguiti in modalita' utente non devono poter far uso di questa possibilita'.
Sebbene il sistema operativo Unix esegua un processo alternativamente in una sola delle due modalita' di utente (running user mode) oppure di sistema (running kernel mode), il kernel lavora sempre
per conto di un processo utente. Il kernel infatti non e' un insieme di processi eseguiti parallelamente
ai processi utente, ma e' parte di ciascun processo utente. Quando si dice che il kernel alloca delle
risorse oppure compie diverse operazioni signica in realta' che un processo, che e' in esecuzione in
modalita' sistema, alloca le risorse e compie le diverse operazioni. Per esempio, il processo di shell
legge l'input dal terminale dell'utente per mezzo delle chiamate di sistema, il processo di shell e'
allora in modo sistema ed il kernel e' parte del processo di shell stesso. Il kernel restituisce poi alla
schell i caratteri digitati sulla tastiera del terminale oppure letti da un le. Il processo di shell quindi ritorna in modalita' utente, interpreta il usso di caratteri digitato dall'utente sulla tastiera del
terminale oppure letto da un le ed esegue l'insieme di operazioni specicate, il che puo' richiedere
l'uso di altre chiamate di sistema e quindi nuovi cambiamenti di stato del processo di shell.
Ogni processo e' generato da un altro processo, secondo lo schema gerarchico padre-glio. Ogni
processo, eccetto
swapper cioe' eccetto il processo 0 (zero), viene creato quando un altro processo
fork( ). Anzi, l'unico modo in cui l'utente puo' creare un nuovo
esegue una chiamata di sistema
processo nel sistema operativo Unix e' quello di eseguire la chiamata di sistema per la gestione dei
processi
fork( ).
In linguaggio di programmazione C, ad esempio, alcune chiamate di sistema, come
ad esempio la chiamata di sistema per la gestione dei processi
fork(
), sono realizzate con delle
particolari chiamate di funzione di libreria relative alla libreria standard di I/O.
3.4 Esecuzione dei processi utente
22
#include <sys/types.h>
pid_t fork ( void );
.....
main ()
{}
....
pid = fork( );
....
}
Il le di dichiarazioni
/usr/include/sys/types.h viene raggiunto da ogni programma scritto
in linguaggio di programmazione DEC C attraverso una richiesta di inclusione interna e collocato
sempre in testa al programma in modo che il preprocessore sostituisca la linea
#include <sys/types.h>
/usr/include/sys/types.h del quale interessa sapere soltanto che il tipo
pid_t e' denito come int cioe' come intero.
La funzione di libreria fork( ), come abbiamo gia' osservato, alloca un ingresso (entry) nella
tabella dei processi del kernel, crea un nuovo processo detto processo glio (child process)
duplicando le regioni del processo chiamante detto processo padre (parent process), cioe' copia il
con il contenuto del le
contesto del processo padre, senza pero' liberare la memoria occupata dal processo padre in modo
che due copie del processo padre siano in esecuzione allo stesso momento. In caso di successo la
funzione di libreria
fork(
) restituisce il valore intero 0 (zero) al processo glio ed il numero del
processo o identicatore di processo
PID (Process IDentity) del processo glio al processo padre.
Il numero intero PID e' molto importante perche' il kernel identica ogni processo per mezzo del
relativo PID. Se la funzione
fork(
) fallisce restituisce il valore intero -1 al processo padre ed il
processo glio non viene creato.
fork( ), che realizza la chiamata di sistema fork( ), fa parte della libreria
lib.a per cui non e' necessario comunicare esplicitamente al loader il nome di questa
La funzione di libreria
standard di I/O
libreria. Se invece vengono utilizzate funzioni di libreria che non fanno parte della libreria standard
di I/O
lib.a, allora bisogna rendere esplicita la necessita' della nuova libreria dichiarandone il nome
sulla linea di comando del compilatore.
Il processo glio e' un clone (copia identica) del processo padre da cui dierisce soltanto per un
particolare: il valore intero di ritorno della funzione di libreria
fork( ) che e' 0 (zero) per il processo
glio, mentre e' diverso da 0 (zero) per il processo padre infatti, come gia' osservato, la funzione
di libreria
fork(
PID (Proces IDentity) del processo glio. Al
fork( ), i due processi padre e glio hanno copie identiche del loro
) restituisce al processo padre il
ritorno dalla chiamata di sistema
contesto a livello utente, eccettuato il valore di ritorno del PID. Se non ci sono risorse disponibili la
chiamata di sistema
fork invece fallisce.
Il processo glio si riconosce come tale basandosi proprio
su questo valore PID ritornato dalla funzione di libreria
fork( ).
Di norma il processo glio chiede di
essere trasformato in un altro processo che sia relativo ad un le binario eseguibile memorizzato nel
le system. Ebbene esiste un'intera famiglia di funzioni di libreria che trasformano il processo glio
in un nuovo processo sovrascrivendo il contesto del processo glio con una copia di un programma
eseguibile: la famiglia delle sei funzioni di libreria che realizzano la chiamata di sistema
exec( ).
Se e' conosciuto l'esatto pathname del le binario eseguibile relativo al nuovo processo allora il
execl( ), oppure la funzione di libreria execv(
execve( ). La funzione
di libreria execlp( ) e la funzione di libreria execvp( ) cercano invece il pathname del le binario
processo padre puo' eseguire la funzione di libreria
), oppure la funzione di libreria
execle( ),
oppure la funzione di libreria
eseguibile relativo al nuovo processo in tutte i direttori specicati dal valore $PATH della variabile
d'ambiente (environment)
PATH. Tutte queste sei funzioni di libreria che realizzano la chiamata di
3.4 Esecuzione dei processi utente
sistema
23
exec( ) fanno parte della libreria standard di I/O lib.a per cui non e' necessario comunicare
esplicitamente al loader il nome di questa libreria.
#include <unistd.h>
extern char **environ;
int execl (}
const char *path,
const char *arg0,
const char *arg1,
const char *arg2,
................,
const char *argN);
path e' un puntatore al pathname che identica un le binario eseguibile memorizzato nel le system e che la chiamata alla funzione di libreria standard di I/O execl( ) sovrascrive
al processo glio, mentre le variabili arg0, arg1, arg2, ... ed argN sono dei puntatori a carattere
dove la variabile
cioe' ad una stringa che termina con il carattere ASCII di codice 0, che di solito viene indicato
con
NULL. Il linguaggio di programmazione C non fornisce esplicitamnte meccanismi per la ma-
nipolzione delle stringhe. Esistono pero' due convenzioni fondamentali, che sono rispettate anche
dal particolare sistema operativo Unix utilizzato. La prima convenzione e' che il nome di un vettore
(array) di caratteri e' una costante non modicabile di tipo puntatore e che questo puntatore punta
al primo carattere del vettore. La seconda convenzione e' che una stringa di lunghezza pari ad n
caratteri, cioe' una successione di n caratteri viene memorizzata in un vettore, per esempio,
almeno n + 1 caratteri, assegnando ad
della stringa ed ad
ab[
0 ],
ab[
1 ], ...,
ab[
ab di
n - 1 ] ordinatamente i caratteri
ab[ n ] il carattere NULL. In linguaggio di programmazione C esiste inoltre una
comoda abbreviazione per inizializzare le stringhe, infatti il frammento di programma
char *pp;
...
pp="ciao mondo";
puts( pp );}
ha i seguenti eetti: viene allocato un puntatore
pp a carattere, viene riservata un'area di 11 byte (
ciao mondo + NULL ) nella zona statica dell'area dati, gli 11 byte della zona statica dell'area dati
vengono inizializzati con i caratteri ciao mondo, l'indirizzo del primo carattere viene assegnato al
puntatore
pp, la funzione di libreria standard di I/O puts( ) viene chiamata con argomento pp e
viene percio' stampata sullo schermo video del terminale la linea
ciao mondo
La chiamata di sistema exec é disponibile in diverse versioni:
#include <unistd.h>
extern char **environ;
int execv (const char *path, char * const argv[ ]);
#include <unistd.h>
extern char **environ;
int execle (const char *path,const char *arg0,...,const char *argN,char * const envp[ ] );
3.4 Esecuzione dei processi utente
24
#include <unistd.h>}
extern char **environ;}
int execve (const char *path,char * const argv[ ],char * const envp[ ] );
#include <unistd.h>}
extern char **environ;}
int execlp (const char *file,const char *arg0,...,const char *argN );
#include <unistd.h>}
extern char **environ;}
int execvp (const char *file,char * const argv[ ] );
path e' un puntatore al pathname che identica un le binario eseguibile memorizzato nel le system e che la chiamata alla funzione di libreria standard di I/O execv( ) sovrascrive
al processo glio, mentre la variabile argv e' un vettore (array) di puntatori a carattere cioe' ad
dove la variabile
una stringa che termina con il carattere NULL.
Una volta che e' terminata l'azione di una delle sei funzioni di libreria che realizzano la chiamata
di sistema
exec( ), il nuovo processo glio viene allora schedulato dal kernel.
Il nuovo processo glio puo' terminare naturalmente la sua esecuzione, forzare la ne della sua
esecuzione con una chiamata alla funzione di libreria
corrispondente chiamata di sistema
exit(
exit( ), che la relativa libreria mappera' nella
) costituita da una procedura codicata in linguaggio
Assembler, oppure terminare la sua esecuzione per cause esterne in quanto e' stato intercettato un
segnale di sistema. Nel sistema operativo Unix esiste infatti la possibilita' di comunicare ai processi il
vericarsi di determinati eventi asincroni, cioe' di eventi che richiedono conferma (acknowledgment).
segnali. Ai segnali e' dedicato un omonimo primo paragrafo del
Il comando predenito KornShell trap. I processi, che hanno il sorgente scritto in
Questi eventi asincroni sono detti
capitolo
linguaggio di programmazione C, possono predisporre la gestione dei segnali tramite la funzione di
libreria
signal( ), che la relativa libreria mappera' nella corrispondente chiamata di sistema signal(
) costituita da una procedura codicata in linguaggio Assembler. I segnali possono riguardare le
eccezioni indotte dal processo come, per esempio, il segnale
SEGV (SEGmentation Violation)
che scatta quando un processo tenta di accedere ad un indirizzo esterno al suo spazio di indirizzi
virtuali, quando cerca di scrivere memoria a sola lettura oppure per errori hardware.
I segnali
possono riguardare condizioni non piu' recuperabili durante l'esecuzione di una chiamata di sistema
come, per esempio, durante l'esecuzione di una
fork( ) al di fuori delle risorse del sistema.
I segnali
possono essere causati da una condizione di errore non attesa durante una chiamata di sistema
come, per esempio, la scrittura di una pipe che non ha processi consumatori.
I segnali possono
essere causati da interazioni con il terminale come, per esempio, la sconnessione di un terminale da
parte dell'utente, la caduta della portante su una linea e la pressione sulla tastiera del terminale dei
tasti
break oppure delete da parte dell'utente.
Va osservato che sarebbe preferibile restituire un
messaggio di errore anziche' generare un segnale, ma l'uso di segnali per uccidere i processi che si
comportano male e' piu' pragmatico.
exit( ) quando il nuovo processo glio termina
exit( ), sia perche'
e' stato intercettato un segnale di sistema. Quando il kernel esegue una chiamata di sistema exit(
) libera tutti i buer relativi al processo, costruisce lo status di uscita del processo, assegna al
processo lo stato di zombie ed esegue il compito previsto da un'eventuale precedente attivazione
della chiamata di sistema wait( ) cioe' libera lo spazio occupato nella tabella dei processi dal nuovo
processo glio nello stato di zombie e sveglia il processo padre che dallo stato di pronto (ready)
in attesa di usare la CPU transita nello stato di esecuzione (running).
Un processo nello stato di zombie e' un morto vivente. E' morto perche' la sua esecuzione
Ebbene il kernel esegue una chiamata di sistema
la sua esecuzione sia naturalmente, sia con una chiamata alla funzione di libreria
3.4 Esecuzione dei processi utente
e' terminata ed i suoi segmenti
posto nella tabella dei processi.
testo e dati
25
non esistono piu', ma e' vivente perche' occupa un
Lo stato di un processo viene trasformato in
zombie per poter
wait( ), sui
consentire al processo padre di ottenere informazioni, per mezzo della funzione di libreria
processi gli morti. Se infatti al termine dell'esecuzione del nuovo processo glio il relativo elemento
nella
tabella dei processi del kernel venisse immediatamente cancellato allora il processo padre
perderebbe ogni traccia dello status di uscita e dei tempi di esecuzioni del nuovo processo glio
morto.
Ci potremmo chiedere perche' il linguaggio di programmazione C utilizza le librerie?
Ebbene, le operazioni di I/O, cioe' lo scambio di informazioni tra un processo ed il mondo esterno al calcolatore, sono spesso le parti meno elaganti di un algoritmo. Infatti non solo si devono
gestire da programma tutti gli aspetti architetturali e circuitali delle periferiche, come tastiera e
schermo video del terminale, ma si deve fare i conti anche con le caratteristiche dei diversi dispositivi
d'I/O, che per loro natura sono variabili da un calcolatore ad un altro. L'inventore del linguaggio
di programmazione C, cioe' Dennis Ritchie, e' partito con l'idea di denire un insieme minimo di
requisiti tra i quali non rientra alcuna specica riguardante l'I/O delle informazioni. In altre parole
il linguaggio di programmazione C si presenta assai povero in quanto non fa alcuna ipotesi sulle
modalita' di acquisizione dei dati dalle varie periferiche d'ingresso oppure sull'invio dei risultati alle
varie periferiche di uscita. Naturalmente questo non signica che il linguaggio di programmazione
C non preveda l'utilizzo di periferiche d'ingresso e di uscita. Il fatto che ogni programma scritto in
linguaggio di programmazione C deve essere sempre eseguito sotto il controllo del sistema operativo
Unix ha permesso di delegare ad un programmatore di sistema il compito di scrivere un numero
opportuno di funzioni d'I/O. Queste funzioni d'ingresso e di uscita sono costituite di solito da procedure codicate in linguaggio Assembler e sono in grado di collegare un programma C con i supporti
alle operazioni d'I/O disponibili a livello di sistema operativo. Questa losoa di gestione dell'I/O,
che e' tipica del linguaggio di programmazione C, ha portato alla codica di alcune funzioni che
sono di fatto diventate una dotazione standard che, sotto forma di funzioni di libreria, accompagna
in pratica ogni versione di compilatore C. Gli implementatori del linguaggio di programmazione C
hanno preferito infatti mantenere il linguaggio piccolo e quindi facilmente portabile e delegare alle
funzioni di libreria i compiti di varia utilita'.
I concetti piu' importanti da tenere presenti sono quelli di Standard Input e di Standard Output,
che sono visti da ogni programma C come due ussi di informazione, che raggiungono il programma
C senza che questo deva conoscere minimamente la natura dei dispositivi periferici. In altre parole,
e' il sistema operativo che si occupa di gestire il collegamento tra di un particolare dispositivo
sico e lo Standard Input e lo Standard Output senza che sia necessario tenerne conto a livello di
programma. Lo Standard Input e' di solito la tastiera del terminale, ma anche un lettore di nastri
oppure di schede. Lo Standard Output e' di solito e' lo schermo video del terminale, ma anche una
stampante oppure un perforatore di schede.
Il processo che invoca la chiamata di sistema
fork( ) e' dunque il processo padre ed il processo
appena creato e' il processo glio. Un processo puo' generare piu' processi gli, ma puo' avere solo
un processo padre.
processo, chiamato
Il kernel identica ogni processo per mezzo di un numero intero associato al
ID (IDentity) del processo oppure, piu' brevemente, PID (Proces IDentity).
Il processo 0 (zero) e' un processo speciale creato al bootstrap che dopo aver creato un processo
fork( ), diventa il processo chiamato swapper. Il processo 1 e' noto
come init, in alcune Revisione sched, e' il primo processo cioe' l'antenato di tutti gli altri processi.
Il processo 1 cioe' init esegue un ciclo innito, durante il quale legge il le /etc/inittab, dal quale
glio, il processo 1, con una
prende le informazioni particolari sulle azioni da intraprendere nel momemto in cui il sistema entra
in stati particolari.
Il processo padre esegue, come abbiamo gia' osservato, anche la chiamata di sistema
exec( ),
passandole come parametro il nome del programma che deve essere eseguito. L'area di memoria
del processo glio viene allora utilizzata per il nuovo processo. La shell stessa e' un processo il cui
3.4 Esecuzione dei processi utente
26
compito e' quello di interpretare i comandi interattivi che si inseriscono al prompt di un qualsiasi
terminale oppure da un le.
Un
comando
e' un programma del sistema operativo composto dal nome, eventuali opzioni
ed una lista di argomenti (generalmente nomi di le) su cui il comando ha eetto.
Il nome del
comando e' l'istruzione data e puo' essere il richiamo di un compilatore, dell'editor di testo di
sistema o la visualizzazione o la stampa del contenuto di un le.
delle variazioni al comando stesso.
intervenire il comando.
Le opzioni del comando sono
Gli argomenti del comando sono di solito i le su cui deve
Con questa logica e' possibile talvolta ricostruire alcuni comandi Unix
senza conoscerne la sintassi esatta in quanto la sintassi dei comandi Unix segue degli standard.
L'architettura del sistema operativo Unix spinge i programmatori a scrivere piccoli programmi
modulari che compiano soltanto poche operazioni e poi combinarli usando la primitiva di sistema
pipe e la primitiva redirezione dell'I/O per compiere operazioni piu' sosticate.