![]() | ![]() |
|
|
INTRODUZIONE A UNIX
Presentiamo di seguito alcune brevi nozioni sull'utilizzo del noto SO UNIX. Ne approfitteremo per ricordare anche alcuni fondamentali concetti relativi ai SO o per introdurne di nuovi.
È opportuno dire subito che quando si è in linea su di una macchina UNIX, si può sempre accedere ad un help in linea tramite il comando MAN seguito dal nome del comando del quale si desiderano spiegazioni (come peraltro accade in DOS e WINDOWS).
Veniamo ai principali comandi per la gestione delle directory. Il carattere separatore delle directory è '/'.
MKDIR serve a creare una nuova directory. CDIR cambia directory. LS 'cataloga' la directory nella quale ci si trova, ossia mostra l'elenco dei file ivi contenuti (come la dir del DOS).
UNIX è un SO multiutente. Inoltre, è possibile organizzare gli utenti in GRUPPI DI UTENZA. La multiutenza ha reso necessario l'inclusione di meccanismi per la protezione dei file. È possibile associare ad ogni file degli specifici permessi di accesso. Essi vengono modificati mediante l'istruzione CHMOD seguita da una serie di numeri, che indicano il proprietario del file, il gruppo di utenza di cui egli fa parte e il mondo esterno. I 'permessi' riguardano la scrittura, la lettura e l'esecuzione dei files.
Le principali funzionalità esterne che un SO offre sono le seguenti :
GESTIONE DEI PROCESSI: creazione, terminazione, sincronizzazione e scambio di messaggi tra processi;
GESTIONE DELLA MEMORIA CENTRALE: allocazione della stessa ai vari processi presenti nel sistema, che, indipendentemente dal proprio stato, necessitano di aree private di memoria;
GESTIONE DELLA MEMORIA AUSILIARIA: i processi hanno sovente bisogno di scambiare informazioni con le memoria di massa, in particolare i dischi;
GESTIONE FILES: creazione, eliminazione e gestione di file e directory;
INTERPRETAZIONE DEI COMANDI: la cosiddetta shell del SO.
Veniamo ora il caso particolare di UNIX. Lo schema a seguire mostra la strutturazione delle sue varie componenti.
919d39j 919d39j 919d39j UTENTI
919d39j 919d39j SHELL E COMANDI
919d39j 919d39j COMPILATORI E INTERPRETI
interfaccia tra livello esterno e
KERNEL: il suo attraversamento può avvenire solo attraverso SYSTEM CALL
919d39j 919d39j 919d39j
FILE SYSTEM
919d39j 919d39j 919d39j
SCHEDULAZIONE CPU (gestione dei processi)
919d39j KERNEL
919d39j 919d39j GESTIONE DELLA MEMORIA
919d39j 919d39j 919d39j DRIVER (di terminali, nastri, etc.)
interfaccia tra KERNEL e HARDWARE
HARDWARE
CONTROLLORI DI CONTROLLORI DI 919d39j CONTROLLORE DI
TERMINALI 919d39j NASTRI &
DISCHI 919d39j
MEMORIA
TERMINALI 919d39j NASTRI,
DISCHI 919d39j MEMORIA
Le SYSTEM CALL possono essere classificate in tre categorie.
1) manipolazione dei files (in questo ambito ricadono anche quei dispositivi che vengono visti dal SO come files speciali);
2) controllo dei processi (incluse le loro intercomunicazioni);
3) manipolazioni di informazioni (ad esempio, le chiamate di sistema con le quali si chiede qual è la data o l'ora corrente, quanti utenti sono collegati, etc.).
Com'è noto, la parte che riguarda peculiarmente questo corso è quella della GESTIONE DEI PROCESSI. Consideriamo, per cominciare, una visione astratta (cioè, non legata ad un contesto particolare) dell'IMMAGINE DI UN PROCESSO IN MEMORIA. Anche in questo caso faremo ricorso ad uno schema.
TESTO Le istruzioni
del programma.
DATI Rappresenta
tutte le informazioni che sono utilizzate dal processo durante il
919d39j proprio ciclo di vita (dati globali e locali)
HEAP
Quest'area
serve per l'allocazione dinamica della memoria, le variabili locali
919d39j (temporanee) e per i record di attivazione delle procedure richiamate
STACK
USER Quest'area
è accessibile solo al KERNEL e contiene informazioni
BLOCK che concernono la gestione del processo.
Alla creazione di un processo corrisponde la creazione del suo descrittore. Esso consiste in una struttura dati che contiene tutte le informazioni fondamentali per la gestione del processo: il suo identificatore, il riferimento al padre, i puntatori all'area dati e all'area testo, quali sono i files che esso utilizza, etc.. In UNIX gli identificatori dei vari processi sono raccolti in una apposita tabella dei processi o PROCESS STRUCTURE[1] che contiene i seguenti identificatori:
PID = Process IDentifier
PPID = Parent Process IDentifier
PGID = Process Group IDentifier
UID e GID = identificatori dell'utente che ha lanciato il processo, usati per il
919d39j controllo dei permessi
DIRECTORY CORRENTE
PRIORITÀ (per lo scheduling dei processi)
TERMINALE da cui è stato lanciato
In UNIX il PID è un numero intero positivo che viene automaticamente associato al processo nel momento della sua creazione. La creazione di un processo avviene sempre da parte di un altro processo, che viene indicato come processo PADRE. Il PPID conservato nel descrittore ha lo scopo di identificarlo univocamente.
Quanto al PGID, esso si deve alla possibilità offerta da UNIX di dichiarare processi appartenenti ad uno stesso gruppo. Ad esempio, è conveniente raggruppare più processi che devono lavorare su uno stesso determinato file: uno che deve leggerlo, uno che lo elabora, uno che lo riscrive etc..
I comandi possono essere seguiti da opzioni, premettendo a queste ultime un trattino (-). Ad esempio, il comando % PS - AUX (il segno di percentuale non è altro che un prompt, cioè un simbolo usato dal SO per comunicare che si trova in attesa di comandi) permette di visualizzare tutte le informazioni relative ai processi di un determinato utente .
In LINUX c'è il comando TRACE che, dato un particolare processo, permette di seguire 'dal vivo' la catena di system call che il processo richiede:
% TRACE - P pid
GENERAZIONE DEI PROCESSI IN UNIX
I processi vengono creati a mezzo della ben nota istruzione FORK. FORK genera due processi concorrenti. Si noti, a questo proposito, il grafo che segue.
919d39j dati P testo
919d39j 919d39j
FORK
919d39j
dati P testo F dati
919d39j WAIT 919d39j EXEC
919d39j nuovo F nuovi
919d39j 919d39j
testo 919d39j dati
919d39j
919d39j WAIT 919d39j EXIT
Proviamo a spiegarne il significato. Al processo P sono associate un'area dati e un'area testo. La FORK crea due processi che sono assolutamente concorrenti, nel senso che ognuno procede secondo il proprio flusso di controllo. Dalla figura scaturisce che padre e figlio condividono, immediatamente dopo la FORK, lo stesso testo (FORK crea infatti una copia esatta del testo originale), ma hanno aree dati differenti. Quindi, eventuali modifiche apportate dal padre alle variabili non avranno alcun effetto sulle corrispondenti variabili del figlio, e viceversa.
EXEC è una system call eseguita dal figlio, che fa sì che quest'ultimo ricopi nella propria area di testo una nuova area testo. Infatti la EXEC è accompagnata da vari parametri, incluso il codice del nuovo programma che deve essere eseguito (contemporaneamente, i dati precedenti potrebbero non avere più alcun senso e quindi potrebbe rendersi necessario usarne di nuovi). A questo punto, il figlio si sarà reso completamente indipendente dal padre.
Mediante la system call EXIT il figlio segnala al sistema la propria terminazione la 'volontà' di ricongiungersi al padre. Nel frattempo il padre aveva eseguito una WAIT che comunicava al sistema il fatto di essere in attesa della terminazione del figlio. Dunque l'esecuzione della EXIT da parte del figlio ha l'effetto di 'risvegliare' il padre, che può così ripartire.
Il padre deve trovarsi nella situazione di attesa prima che il figlio termini. Se è il figlio a terminare prima ancora che il padre abbia avuto il tempo di eseguire la sua WAIT, si dice che il figlio diventa ZOMBIE: le aree dati, testo e stack che lo riguardano vengono deallocati, ma il suo descrittore non viene eliminato della tabella dei processi fino a quando il padre non fa richiesta di ricongiungersi con il figlio. Si usa il termine 'zombie' perché si è in presenza di un puntatore al descrittore di un processo del quale, nei fatti, non è rimasto più nulla.
Esaminiamo ora più nel dettaglio ciò che avviene quando si genera un processo con la system call FORK.
All'atto della FORK saranno presenti in memoria padre e figlio. La creazione del figlio comporta l'inizializzazione di un nuovo elemento all'interno della tabella dei processi (descrittore del figlio). Nel descrittore saranno presenti tutte le informazioni e le risorse che il figlio ha ereditato dal padre, come variabili, files ai quali si ha accesso, UID, GID, PGID etc.. Padre e figlio continuano quindi la loro esecuzione.
La FORK restituisce, all'atto della sua chiamata, un valore di ritorno che è diverso per padre e figlio e che serve quindi a distinguerli. FORK restituisce il valore -1 nel caso in cui non è possibile generare un figlio, ad esempio perché si è raggiunto il massimo numero consentito di processi. Contrariamente, restituisce 0 per il processo figlio, e il PID del figlio per il padre. Sulla base del valore restituito dalla FORK sarà possibile discriminare, nelle istruzioni che seguono, il padre dal figlio.
UNIX, come è noto, è scritto in linguaggio C. Per usare tutte le primitive viste fin qui occorre includere la libreria di sistema types:
# INCLUDE < SYS / TYPES.H >
e dichiarare il valore restituito dalla FORK, che apparterrà in generale al tipo pid-t (nome del tipo predefinito utilizzato per i PID dei processi; può essere ad esempio il tipo intero). La sintassi adoperata è la seguente:
PID-T FORK ()
Il seguente listato mostra la creazione di un processo figlio e come il codice del padre e quello del figlio vengono diversificati. Dopo la FORK, un secondo processo, il figlio, prende a seguire le istruzioni del programma; entrambi quindi controllano il valore del PID e agiscono di conseguenza.
# include < sys / types.h >
pid-t proc ; /* dichiarata una variabile proc di tipo PID-T */
proc = FORK () ;
if (proc = = -1) /* ERRORE: stampa di un messaggio su un determinato file
919d39j 919d39j e uscita dal programma */
919d39j
if (proc = = 0) /* viene inserito qui il codice del figlio */
919d39j
else /* codice del padre */
919d39j
La chiamata EXEC viene effettuata per sostituire l'immagine di memoria con quella di un nuovo programma. Di esso esistono alcune varianti, ad esempio:
- EXECL: specifica il pathname del programma eseguibile che dovrà entrare il gioco e una lista di eventuali argomenti da passare al nuovo processo;
- EXECV: specifica il pathname più un vettore di puntatori ai dati sui quali il nuovo programma dovrà operare.
La sintassi di EXECL è la seguente:
INT EXECL (path, arg 0, ..., arg i, ..., arg n, (char *)0)
(char *)0 è un Null usato per terminare la lista degli argomenti. 'Path' è il pathname dell'eseguibile. Arg 0 è il puntatore al primo argomento, che poi è il nome del processo stesso. Arg i è il puntatore all'i-esimo argomento. Si ha quindi:
char *path, *arg 0, ..., *arg i
La sintassi di EXECV è:
INT EXECV (path, arg v)
char *path, *arg v[ ]
'arg v' è il vettore dei puntatori agli argomenti del nuovo programma.
Una funzione usualmente restituisce il valore di ritorno -1 per segnalare che la sua esecuzione ha avuto esito negativo. Così si avrà, ad esempio:
int i ;
i = EXECL ("usr/dir/prog", "prog", "uno", (char *)0) ;
if (1 = = -1) perror (" EXEC ") /* controllo validità operazione */
Una EXEC potrebbe essere effettuata anche in un programma monoprocesso, cioè non dopo una FORK. In tal caso, l'effetto sarebbe semplicemente quello di sostituire l'area testo del programma in corso con quella di un nuovo programma (si interrompe l'esecuzione del programma in corso per eseguirne un altro).
Altre system call di questa famiglia sono [5]:
EXECLE, EXECVE: consentono di passare nella lista degli argomenti anche un vettore con tutte le variabili di ambiente.
EXECLP, EXECVP: il file dell'eseguibile deve essere cercato nella directory specificata dalla variabile di ambiente path.
Consideriamo ora la system call WAIT, la quale, come è ormai noto, comunica l'intenzione da parte di un padre di attendere la terminazione del figlio. Una volta che un processo abbia eseguito una FORK, prima o poi esso dovrà eseguire senz'altro anche una WAIT con la quale si sospenderà e si metterà in attesa che il figlio termini.
D'altronde, la terminazione del figlio può essere :
- terminazione NORMALE ;
- terminazione FORZATA : avviene in presenza di un evento che impone la terminazione del figlio, ad esempio una 'kill' da parte di un altro processo; viene restituito un segnale che indica il genere di terminazione che si è verificata.
Nel momento in cui si esegue la WAIT, le viene passata come parametro di ingresso una variabile di stato a 16 bit, indicante le caratteristiche della terminazione del figlio secondo la seguente modalità: se la terminazione è stata normale, gli 8 bit bassi (quelli 'a sinistra') contengono il valore restituito dalla EXIT, di cui si dirà in seguito, mentre gli 8 bit alti contengono degli zeri. Se la terminazione è stata forzata, gli 8 bit bassi contengono degli zeri, mentre gli 8 bit alti contengono il numero del segnale di terminazione.
Da parte sua, quando ha esito positivo la WAIT restituisce il PID del figlio terminato, mentre se ha esito negativo, o se si verifica un'interruzione, restituisce -1. Naturalmente ogni figlio ha il suo PID: in questo modo il padre può sempre sapere quale di essi è terminato.
La sintassi di WAIT è la seguente:
# INCLUDE < SYS / TYPES.H >
PID-T WAIT (STATUS)
int *status
'Status' è l'indirizzo dell'intero nel quale vengono depositate tutte le informazioni relative alla terminazione del figlio, come si è detto.
Si noti il seguente programma di esempio, che è di facile comprensione.
Pid-t pidf ;
int stat ;
if (FORK ( ))
La system call EXIT, della quale si può dire sinteticamente che causa la terminazione di un processo, ha in realtà tutta una serie di effetti:
- chiusura dei file aperti;
- restituzione di un valore agli 8 bit bassi della variabile di stato summenzionata;
- prosecuzione del padre, se quest'ultimo era in stato di WAIT,
- se il processo nel quale compare la EXIT aveva generato dei figli, essi diventano figli di INIT, un particolare processo lanciato dal kernel all'avvio del SO, che serve ad accettare i comandi utente. Eventuali gerarchie presenti fra i figli del processo terminato (figli dei figli) non viene conservata: essi diventano tutti figli indistinti di INIT;
- se il processo era il capostipite di un gruppo di processi (il suo pid è uguale al suo pgid) a tutti i processi del gruppo viene inviato un segnale di HANG UP (terminazione forzata).
La sintassi di EXIT è :
VOID EXIT (STATUS)
int status
'Status' è il valore da restituire al processo padre, e vale 0 per esito positivo. Facciamo un esempio:
main ( )
exit (0)
Per esercizio, scrivere un programma concorrente in cui vengono creati due processi, padre e figlio, e sia il figlio che il padre devono stampare il proprio PID. Si può fare uso della funzione GETPID, che restituisce sempre il PID del padre di un processo.
Sembra di capire (anche dal seguito) che il descrittore, l'insieme delle informazioni relative ad un certo processo, sia un sottoinsieme della process structure, relativa all'insieme di tutti i processi, come una singola riga è sottoinsieme di una tabella. Dal Silberschatz-Galvin risulta però che 'process structure' è sinonimo di 'descrittore' (pag.579), o sbaglio?
Riporto testualmente ciò che segue subito dopo negli appunti dalle lezioni di De Carlini, il cui significate non sono riuscito a capire:
"Digitando questo comando (con eventuali opzioni) si ha a video l'elenco di tutti i propri processi con altre informazioni come :
USER PID %CPU %MEM SIZE RSS
919d39j 919d39j 919d39j 919d39j text
919d39j 919d39j 919d39j 919d39j data
919d39j 919d39j 919d39j 919d39j stack "
Perror è una funzione C che stampa un messaggio di errore (Kernighan-Ritchie, "Linguaggio C seconda edizione", pag.330).
In C l'espressione ' char *a ' è la dichiarazione di una variabile a di tipo puntatore a carattere e può essere usato per indicare l'indirizzo del primo carattere di una stringa. Ma perché si usa il tipo char anche per gli arg?
Si ricordi che in C l'espressione ' & a ' significa "l'indirizzo della variabile a". È stato corretto, quindi, dichiarare stat come variabile intera (e non puntatore).
Open è una chiamata di sistema del C che permette di aprire un file e restituisce un intero (-1, al solito, in caso di errore). Oltre al nome del file ("usr/proc" nel nostro esempio), esso accetta come parametro un flag che specifica la modalità di apertura. O_rdonly, uno dei possibili flags, apre il file in sola lettura (cfr. Kernighan-Ritchie, pag. 223).
Privacy |
Articolo informazione
Commentare questo articolo:Non sei registratoDevi essere registrato per commentare ISCRIVITI |
Copiare il codice nella pagina web del tuo sito. |
Copyright InfTub.com 2025