Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
Enzo Mumolo
Indice
1 Introduzione al sistema operativo Unix
1.1
1.2
. . . . . . . . . . . . . . . . . . .
2.1
2.2
. . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
12
3.1
Il processo di Shell
3.2
I processi in Unix . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
14
3.3
16
3.4
21
Capitolo 1
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.
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
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-
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.
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)
E' da
notare che quando ci si riferisce a questo specico Unix prodotto dalla USL scriveremo il nome in
lettere maiuscole. Il nome
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
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
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.
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
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.
schedulazione
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.
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.
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 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
sici e logici di I/O. Il sistema operativo Unix possiede due tipi di device di I/O: device di I/O a
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
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.
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
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
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
password le)
che
Username:PasswordCrittografata:UserID:GroupID:Nome reale:HomeDirectory:Shell
Naturalmente ogni utente descritto da una di queste righe.
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
Processo Leader.
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
Root.
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
index node:
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
??).
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
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
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:
- GID:
Questi due numeri sono stabiliti una volta per tutte dal responsabile tecnico del sistema (e sono in
genere presenti nel le
/etc/passwd
root
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:
- 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'
registrati anche il relativo User ID e Group ID. In questo modo anche il le possiede uno User ID e
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,
gruppo (indicato con G, group) cio l'insieme di tutti gli utenti che hanno lo stesso Group
ID del le;
3. gli
Nei le Unix per ciascuna di queste tre categorie di utenti sono deniti tre permessi (quindi in
Xecute), quest'ultimo detto anche di ricerca nel caso dei le direttorio.
ed esecuzione (e
ls:
$ ls -l ... -rw-r----- 1 mumolo 12 Oct 2 10:52 dati01 ...
-l)
del comando
10
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:
6 4
7 0 0.
I permessi vanno interpretati in modo dierente a seconda che il le sia ordinario oppure sia un direttorio.
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).
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
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;
11
a parte quanto stato gi detto in proposito nel caso del permesso in lettura (caso
ls -l), il permesso
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
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)
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
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
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
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
12
lascia una traccia nella tabella dei processi ed esegue il compito previsto da un'eventuale precedente
attivazione della chiamata di sistema
wait(
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
cono tra di loro durante il caricamento di un le dalla memoria secondaria alla memoria principale
per l'esecuzione. Il
processo di shell
detto
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
PATH,
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
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
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
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
exit(
segnali. I
signal( ), che in
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(
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.
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
14
cancellato allora il processo padre perderebbe ogni traccia dello status di uscita e dei tempi di
esecuzioni del nuovo processo glio morto.
uscita e sui tempi di esecuzione sono contenuti in un'estensione dell'ingresso (entry) nella
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
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
fork(
wait(
di
processi, descritto nei prossimi tre paragra, segue proprio questa losoa.
Osserviamo inne che i programmi eseguibili possono essere suddivisi in programmi eseguibili utente
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.
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
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.
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:
UID
15
Area U
tabelle pregion
directory corrente
radice corrente
Parametri di I/O
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
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'
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
di memoria virtuale di un processo che puo' essere trattata come un unico oggetto da condividere
oppure da proteggere.
diversi, ad esempio vari processi possono eseguire lo stesso programma e quindi condividono una
copia della
regione testo.
area u.
L'ingresso nella
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.
di I/O, ad esempio di leggere dati dalla tastiera del terminale. Inne il quinto campo contiene un
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.
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.
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
context
solo in certe condizioni ed assicura l'integrita' e la coerenza delle strutture dati vietando i context
switch arbitrari.
modalita' utente all'esemodalita' sistema il kernel salva le informazioni per poter ritornare all'esecuzione 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
17
passare molto tempo ed altri processi possono eseguire nel frattempo, quando termina l'esecuzione
con una chiamata di sistema
exit(
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
processo in stato di sospensione non e' eseguibile anche se la CPU e' libera.
si aggiunge lo stato di
pronto
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.
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' 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
18
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,
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.
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
modulo di
scheduling
lo fara' poi partire ed esso entra' nello stato 2 di esecuzione in modalita' 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,
19
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
Lo stato
7 di partenza e lo stato 8 di
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
questo punto ci sono due alternative e cioe' la transizione del processo dallo stato 7 verso lo stato 3
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.
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
swap area
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.
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
21
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.
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
processo nel sistema operativo Unix e' quello di eseguire la chiamata di sistema per la gestione dei
processi
fork( ).
fork(
22
#include <sys/types.h>
pid_t fork ( void );
.....
main ()
{}
....
pid = fork( );
....
}
Il le di dichiarazioni
#include <sys/types.h>
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(
Il numero intero PID e' molto importante perche' il kernel identica ogni processo per mezzo del
relativo PID. Se la funzione
fork(
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
glio, mentre e' diverso da 0 (zero) per il processo padre infatti, come gia' osservato, la funzione
di libreria
fork(
contesto a livello utente, eccettuato il valore di ritorno del PID. Se non ci sono risorse disponibili la
chiamata di sistema
fork( ).
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
execle( ),
eseguibile relativo al nuovo processo in tutte i direttori specicati dal valore $PATH della variabile
d'ambiente (environment)
23
exec( ) fanno parte della libreria standard di I/O lib.a per cui non e' necessario comunicare
#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
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
char *pp;
...
pp="ciao mondo";
puts( pp );}
ha i seguenti eetti: viene allocato un puntatore
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
ciao mondo
#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[ ] );
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
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(
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).
signal( ), che la relativa libreria mappera' nella corrispondente chiamata di sistema signal(
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
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
messaggio di errore anziche' generare un segnale, ma l'uso di segnali per uccidere i processi che si
comportano male e' piu' pragmatico.
la sua esecuzione sia naturalmente, sia con una chiamata alla funzione di libreria
testo e dati
25
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
appena creato e' il processo glio. Un processo puo' generare piu' processi gli, ma puo' avere solo
un processo padre.
processo, chiamato
Il processo 0 (zero) e' un processo speciale creato al bootstrap che dopo aver creato un processo
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
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
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.
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.