Caricare documenti e articoli online 
INFtub.com è un sito progettato per cercare i documenti in vari tipi di file e il caricamento di articoli online.


 
Non ricordi la password?  ››  Iscriviti gratis
 

INTRODUZIONE A UNIX

informatica



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?

Il significato delle quattro funzioni citate non è chiaro.


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


Hits: 1751
Apprezzato: scheda appunto

Commentare questo articolo:

Non sei registrato
Devi essere registrato per commentare

ISCRIVITI



Copiare il codice

nella pagina web del tuo sito.


Copyright InfTub.com 2024