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