Sei sulla pagina 1di 28

Appunti di Sistemi Operativi

Enzo Mumolo

e-mail address :mumolo@units.it


web address :www.units.it/mumolo

Indice
1 Introduzione al sistema operativo Unix

1.1

Storia del sistema operativo Unix . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

1.2

Caratteristiche generali del sistema operativo Unix

. . . . . . . . . . . . . . . . . . .

2 Controllo e gestione dei processi in Unix

2.1

Operazione di attivazione del sistema Unix (bootstrap) . . . . . . . . . . . . . . . . .

2.2

Accesso Utenti e identicatori in Unix

. . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

12

3 Generalit sul le system di Unix

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

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 Storia del sistema operativo Unix

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

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

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.

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

CAPITOLO 3. GENERALIT SUL FILE SYSTEM DI UNIX

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

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).

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

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;

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);

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;

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

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.