Introduzione ai Sistemi Operativi
|
|
|
- Daniella Fabbri
- 10 anni fa
- Просмотров:
Транскрипт
1 Introduzione ai Sistemi Operativi Daniel P. Bovet, Vito Asta corso di Sistemi Operativi Facoltà di Ingegneria Università di Roma 1 La Sapienza - sede di Latina Anno Accademico Versione 0.7, aprile 2003 Il materiale contenuto in queste dispense è soggetto al copyright GNU GENERAL PUBLIC LICENSE
2 GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODI- FICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The Program, below, refers to any such program or work, and a work based on the Program means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term modification.) Each licensee is addressed as you. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program s source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict
3 the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royaltyfree redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and any later version, you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PRO- GRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM AS IS WI- THOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY CO- PYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, IN- CIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PRO- GRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS
4 Indice 1 Classificazione dei Sistemi Operativi Introduzione Definizione e ruolo di un Sistema Operativo Software di base Classificazione dei Sistemi Operativi Gestione a lotti Gestione a lotti multiprogrammata Sistemi a partizione di tempo Sistemi personali Sistemi transazionali Sistemi di controllo di processi Una classificazione complementare Sistemi di sviluppo Sistemi paralleli Sistemi distribuiti Sistemi in tempo reale Sviluppi futuri Struttura dei Sistemi Operativi Introduzione Standardizzazione e Sistemi Aperti Componenti di un Sistema Operativo Gestione dei processi Gestione della memoria primaria Gestione della memoria secondaria Gestione del sistema di I/O i
5 ii INDICE Gestione dei file system Sistema di protezione Networking L interprete di comandi Servizi e funzionalità di un Sistema Operativo Panoramica delle chiamate di sistema Chiamate di sistema e API Implementazione di un Sistema Operativo La programmazione di sistema Struttura dei Calcolatori Elettronici Introduzione Architettura dei sistemi di calcolo Interruzioni hardware Interruzioni software I due modi della CPU Protezioni hardware Protezione della CPU Protezione della memoria Protezione dell I/O Acceso diretto alla memoria (DMA) Dispositivi di memorizzazione Il caching Appendice - Gestione dello Stack Software di Base Introduzione Messa a punto di programmi Assemblatori, compilatori e interpreti Linkaggio di programmi Librerie statiche e dinamiche Immissione di programmi Caricamento in memoria di un file eseguibile Supporto durante l esecuzione Salvataggio/ripristino di dati Amministrazione del sistema
6 INDICE iii 5 Il File System Introduzione Ruolo di un file system Protezione delle informazioni Strutturazione di un file system Struttura dei file Struttura dei nomi di file Attributi dei file Struttura e implementazione delle cartelle Operazioni su file e cartelle Implementazione dei file system Dimensione dei blocchi Allocazione di blocchi ai file Gestione dello spazio libero Ottimizzazione delle prestazioni Il file system di Unix Strutturazione del file system File e cartelle Link hard e soft Tipi di file Inode, i-number, descrittore di file Diritti d accesso e file mode Chiamate di sistema per la gestione di file Apertura di un file Indirizzamento di un file Chiusura di un file Cambiamento di nome e cancellazione di file Esempi Appendice - Funzioni di hashing
7 iv INDICE 6 Interfaccia con l Utente Introduzione Interfaccia a menu Interfaccia a comandi Lo shell di Unix Struttura dello shell Interfacce grafiche Interfacce grafiche per Unix Interfaccia grafica o a comandi Gestione dei Processi e della CPU Introduzione Multitasking Cicli di uso della CPU e di I/O Processi Operazioni su processi Stati di un processo Eventi e risorse Descrittore di processo Cambio di contesto Thread Scheduling di processi Code di scheduling Tipi di scheduler Criteri di scheduling Scheduling a breve termine Algoritmi di scheduling della CPU Scheduling a lungo termine Processi Concorrenti Introduzione Programmi concorrenti Controllo di processi in Unix Sincronizzazione tra processi Ruolo delle risorse consumabili
8 INDICE v Paradigma della mutua esclusione Paradigma produttore/consumatore Primitive di sincronizzazione Primitive di sincronizzazione in Unix Stallo tra processi Programmazione in tempo reale Spazio degli Indirizzi di un Processo Introduzione Indirizzi e regioni Modifiche dello spazio degli indirizzi Linking dinamico Mapping di file in memoria Altri tipi di mapping Struttura Interna del Nucleo Introduzione Nucleo e processi Che cosa è il Nucleo Ruolo dei segnali di interruzione Gestori delle interruzioni Implementazione delle chiamate di sistema Descrittori di risorse Interrompibilità del Nucleo Microkernel e kernel monolitici Implementazione dei processi in Linux Scelta del linguaggio di programmazione Gestione della Memoria Primaria Introduzione Indirizzamento della RAM Paginazione Indirizzi logici riservati ai programmi del Nucleo L algoritmo Buddy System Estensioni del Buddy System Demand Paging Swapping
9 vi INDICE 12 Gestione della Memoria Secondaria Introduzione Gestione dello spazio su disco Scheduling del disco Scheduling FCFS Scheduling SSTF Scheduling SCAN Scheduling C-SCAN Scheduling LOOK e C-LOOK Gestione dei dischi Formattazione dei dischi Gestione del blocco di bootstrap Gestione dei blocchi difettosi Gestione della zona di swap Gestione dei Dispositivi di I/O Introduzione Architettura di I/O Dispositivi di I/O riconosciuti dal file system Programmazione di un device file Supporto del Nucleo Sincronizzazione tra CPU e dispositivo di I/O Uso di cache nei driver per dischi
10 Elenco delle tabelle 4.1 Confronto tra linguaggi compilati e interpretati Differenza di produttività indotta dal multitasking Esempio di processi per l algoritmo di scheduling SJF Esempio di processi per gli algoritmi di scheduling SJF e RR vii
11 viii ELENCO DELLE TABELLE
12 Elenco delle figure 1.1 Componenti di un sistema di calcolo - schema Organizzazione della memoria RAM per un sistema batch monoprogrammato Organizzazione della memoria RAM per un sistema batch multiprogrammato Stati dei task in un sistema a partizione di tempo Schema architetturale di un sistema SMP Esecuzione di una chiamata di sistema mediante Trap API, chiamate di sistema e Nucleo Architettura di un sistema di calcolo Struttura della ALU Struttura fisica di un disco Gerarchia dei dispositivi di memorizzazione Organizzazione di uno stack Un esempio di riferimenti tra moduli Un esempio di collegamento tra moduli Fasi di messa a punto di un programma Organizzazione di un file system di Unix Collocazione dei file nello spazio dei nomi Tecnica delle tabelle di hashing Architettura minima di riferimento per un sistema concorrente Un esempio di esecuzione multitasking di programmi Stati di un processo e relative transizioni ix
13 x ELENCO DELLE FIGURE 7.4 Commutazione e cambio di contesto tra due processi Code di scheduling dei processi Migrazione dei processi tra le code di scheduling Diagramma degli stati dei processi e ruolo degli scheduler Punti di intervento dello scheduler di CPU Scheduling FCFS, primo caso Scheduling FCFS, secondo caso Scheduling FCFS, senza prelazione Scheduling FCFS, con prelazione Scheduling SJF, confronto con RR Scheduling RR Un esempio di sincronizzazione tra processi Creazione di una pipe e condivisione con un processo figlio Interazione tra processi di sistema in un sistema per la gestione a lotti Esempio di stallo tra due processi Spazio degli indirizzi di un processo Struttura interrupt driven del Nucleo Esecuzione di una chiamata di sistema da parte del Nucleo Esecuzione annidata di programmi del Nucleo Chiamata di sistema che pone il processo in uno stato di attesa Esecuzione di un processo prioritario con gestione interruzione incompiuta Interazione client/server tra processo utente e processo di sistema Trasformazione di un indirizzo logico in un indirizzo fisico Swapping Architettura di I/O
14 Premessa La presente dispensa è una versione derivata da quella omonima scritta dal Prof. Daniel P. Bovet, con adattamenti ed ampliamenti a cura dell ing. Vito Asta, ed è destinata ai corsi di Sistemi Operativi tenuti da quest ultimo per i corsi di laurea e di laurea specialistica in Ingegneria Gestionale, Ingegneria delle Telecomunicazioni e Ingegneria Elettronica dell Università Tor Vergata - Roma 2, nonché per i corsi di laurea in Ingegneria Informatica e Ingegneria delle Telecomunicazioni dell Università La Sapienza - Roma 1 (sede distaccata di Latina). La maggior parte degli argomenti trattati è accompagnata, ovunque possibile, con esempi concreti di codice in ambiente Unix/Linux, in particolare per illustrare l uso reale delle chiamate di sistema o funzioni di libreria trattate. Ogni esempio di codice è preceduto dalla sua esposizione sintetica in pseudo-codice, per facilitarne la comprensione ed eventualmente per permettere anche al lettore non pratico del linguaggio C di capirne il significato e la logica. Nel testo, i riferimenti a comandi, chiamate di sistema e funzioni di libreria sono annotati in questa font di caratteri; i corrispondenti riferimenti a pagine di manuale sono invece annotati secondo la convenzione invalsa nella documentazione del Sistema Operativo Unix, nome(sezione). Così read() è un generico riferimento alla omonima chiamata di sistema, mentre read(2) è un riferimento alla relativa pagina di manuale, nella sezione 2 (e quindi un esplicito invito al lettore a consultarla). Per informazioni sulla strutturazione delle sezioni della manualistica Unix, si veda Asta [3]. La dispensa è al momento in uno stato non ancora definitivo; pertanto alcuni degli argomenti sono accennati solo schematicamente. Lo studente è invitato, per queste parti, a far riferimento alle dispense dalle lezioni, pubblicate sul sito WEB del corso: 1
15 Capitolo 1 Classificazione dei Sistemi Operativi 1.1 Introduzione Lo studio del software di base in generale e dei Sistemi Operativi in particolare si rivela complesso in quanto doppiamente condizionato, sia dalla architettura (hardware) del calcolatore per cui è progettato tale software, sia dalle diverse esigenze dell utenza a cui il sistema finale (hardware più software) è destinato. L approccio seguito in questo testo è quello di scindere, per quanto possibile, le esigenze dell utenza, e quindi le caratteristiche funzionali del software di base, dalle tecniche di realizzazione. A tale scopo, si introducono in questo capitolo alcuni concetti preliminari attinenti alla definizione stessa e al ruolo di un Sistema Operativo, quindi all area del software di base; successivamente si illustrano, prescindendo dai criteri realizzativi, le funzionalità di varie classi di Sistemi Operativi, mettendo in evidenza al tempo stesso l evoluzione storica che tali sistemi hanno subìto in funzione delle innovazioni tecnologiche conseguite. Tecnicamente parlando, un Sistema Operativo consiste essenzialmente in quattro moduli principali: un programma di inizializzazione che viene eseguito quando viene avviato l elaboratore 1 ; un Nucleo che esegue chiamate di sistema, ossia particolari procedure mediante le quali i programmi possono richiedere uno dei vari servizi offerti dal Nucleo 2 ; un sistema d archiviazione (file system) 3 ; un interfaccia che consente all utente di esprimere le proprie richieste 4. 1 Questo argomento è trattato nel Paragrafo 10.3 pag Argomento trattato nel Paragrafo pag Argomento trattato nel Capitolo 5. 4 Argomento trattato nel Capitolo 6. 2
16 1.2. DEFINIZIONE E RUOLO DI UN SISTEMA OPERATIVO 3 In effetti, i testi di Sistemi Operativi più accreditati si limitano a trattare i suddetti argomenti. Dato il carattere introduttivo di questo testo, si ritiene tuttavia opportuno studiare i Sistemi Operativi seguendo un approccio top down, partendo quindi da una descrizione delle varie componenti di un sistema di calcolo, fino ad arrivare (nei prossimi capitoli) all analisi delle varie componenti importanti di un Sistema Operativo. 1.2 Definizione e ruolo di un Sistema Operativo A differenza di altre macchine, il calcolatore, considerato come congerie di circuiti hardware, non è un oggetto facilmente utilizzabile di per se stesso: esso è in grado di eseguire istruzioni e quindi programmi, purché tali programmi siano scritti in linguaggio macchina binario, l unico riconosciuto dal calcolatore. Inoltre, quando un calcolatore inizia ad operare in seguito all accensione, esso cerca in una area di memoria (generalmente, su una traccia di disco prefissata) le istruzioni da caricare in memoria e da eseguire per prime. Com è ovvio, questa inizializzazione della macchina, chiamata bootstrapping, sarà effettuata con successo, a condizione che la traccia di disco letta contenga effettivamente un programma scritto in linguaggio macchina. Si desume da quanto appena detto che è praticamente impossibile per l utente utilizzare direttamente l hardware di un calcolatore, anche accettando di programmare in linguaggio binario, a meno di non disporre di strumenti che gli consentano, da un lato, di immettere programmi nella macchina e, dall altro, di estrarre dalla memoria verso l esterno i risultati dei calcoli eseguiti. È da queste considerazioni, ed altre ad esse correlate, che scaturisce la necessità di disporre di Sistemi Operativi. Un Sistema Operativo è un programma (o un insieme di programmi) che agisce da intermediario tra l hardware e l utente. Gli scopi di un Sistema Operativo sono fondamentalmente due: eseguire programmi dell utente e rendere più facile la soluzione dei suoi problemi; rendere il calcolatore conveniente ed efficiente da usare. Si noti, ed è questo un dettaglio non da poco, che i termini convenienza (cioè facilità d uso) ed efficienza sono spesso in contraddizione tra loro: molto spesso la facilità d uso viene ottenuta ampliando gli strati intermedi di software, per aggiungere funzionalità più sofisticate o più ergonomiche, e ciò appesantisce inevitabilmente il sistema globale. Si pensi, per fare un esempio tipico, alla pesantezza (in termini di risorse di calcolo, di dispositivi hardware, di spazio in memoria centrale e su disco) di un qualunque sistema di interfaccia grafica: tale interfaccia è con tutta evidenza piacevole e potente, tanto da permettere tipicamente l uso di un calcolatore anche a persone senza nessuna esperienza informatica; ma è estremamente più pesante rispetto ad una semplice interfaccia a carattere (cioè a riga di comando), come quella offerta dallo shell di Unix 5. 5 Lo shell di Unix viene trattato nel Paragrafo pag. 90; si veda inoltre Asta [3] e [4].
17 4 CAPITOLO 1. CLASSIFICAZIONE DEI SISTEMI OPERATIVI User 1 User 2 User 3 User N... Compilatore Assembler Editor di testo Sistema di database Software di base Nucleo Hardware del computer Figura 1.1: Componenti di un sistema di calcolo - schema. Da un punto di vista macroscopico, un Sistema Operativo può pensarsi diviso in due componenti principali 6 : un Nucleo, o Kernel, che è la parte residente di un Sistema Operativo (sempre in memoria e sempre pronta ad eseguirsi); Un insieme di programmi 7 e di funzioni di libreria a corredo, facente parte stabile della distribuzione del Sistema Operativo, detto software di base. Tale insieme, commentato più in dettaglio nel Paragrafo 1.3, comprende a sua volta diverse categorie. Allargando la visuale, e ricordando come detto che il Sistema Operativo fa da intermediario tra l hardware e l utente, si può dire che le componenti di un sistema di calcolo sono dunque quattro, illustrate nella Figura 1.1: hardware fornisce le risorse di calcolo basilari: CPU (unità centrale), memoria, dispositivi di I/O (ingresso/uscita); Nucleo controlla e coordina l uso dell hardware tra i vari programmi per tutti gli utenti; come già detto, è la parte di un Sistema Operativo sempre in memoria e sempre pronta a eseguirsi; 6 Alcuni autori tendono a definire un Sistema Operativo come la sola parte sempre residente in memoria, cioè lo identificano col solo Nucleo. 7 Tali programmi sono talvolta divisi in due gruppi: programmi detti di sistema e programmi applicativi di base; la distinzione comunque non è netta ed univoca nella letteratura.
18 1.3. SOFTWARE DI BASE 5 software di base tale strato definisce i modi in cui le risorse del sistema vengono usate per risolvere i problemi computazionali degli utenti, e comprende tra l altro compilatori 8, sistemi di data base, sistemi di automazione d ufficio, etc. utenti sono in definitiva coloro che devono usufruire delle capacità dei sistemi di calcolo, e possono essere, in pratica, non solo persone fisiche, ma anche macchine dedicate, o altri calcolatori. 1.3 Software di base Si è già accennato, in apertura di capitolo, alla necessità di disporre di adeguati strati di software che permettano all utente di non dover gestire l hardware di un calcolatore in modo diretto; da ciò scaturisce, tra l altro, l esigenza di disporre di programmi di base e di funzioni di libreria che permettano all utente di porsi ad un livello più macroscopico ma più agevole nel servirsi di una macchina da calcolo. Fortunatamente, già negli anni 40 i progettisti delle prime macchine scoprirono che era conveniente sviluppare programmi in maniera modulare, introducendo sottoprogrammi (o subroutine) generiche richiamabili dal programma principale; il concetto si è poi esteso a programmi standard di uso comune, presenti nella distribuzione di un dato Sistema Operativo: era nato, in nuce, il software di base, inteso come corredo di programmi e funzioni di libreria standard accessibili all utente con lo scopo di facilitare l interazione uomo/macchina. Da allora, il software di base è evoluto di pari passo all hardware con delle marcate interdipendenze: sviluppi nel campo delle architetture e delle apparecchiature periferiche rendono necessario lo sviluppo di nuovo software di base; allo stesso tempo, tecniche di programmazione collaudate condizionano la progettazione dei più recenti microprocessori. Anche se non esistono definizioni rigorose, si indica dunque come software di base l insieme di programmi e funzioni di libreria, a disposizione di tutti gli utenti di un sistema, che complementano il Nucleo nel fornire le funzionalità di un Sistema Operativo. Il software di base fornisce un ambiente standard per lo sviluppo e l esecuzione di programmi e di procedure di comandi, e più in generale per l utilizzazione pratica del sistema. Il software di base può essere diviso in varie categorie 9 : Programmi a complemento delle funzionalità del kernel (lilo, init, etc.; vari daemon 10 : telnetd, ftpd, httpd, sendmail, etc.) 8 Il meccanismo di funzionamento di compilatori ed interpreti verrà descritto più in dettaglio nel Paragrafo pag Per ognuna delle categorie si danno esempi reali, propri del Sistema Operativo Linux; per maggiori dettagli, si veda Asta [3], o si consultino le relative pagine di manuale: ad es., init(8), bash(1), cc(1); per le librerie di funzioni, si consulti intro(3). 10 Il termine daemon, in ambiente Unix, indica genericamente un programma che funge da server per un applicazione di rete; così ad esempio ftpd indica il server per l applicazione FTP (File Transfer Protocol) di trasferimento di file su reti di tipo Internet, cioè basate
19 6 CAPITOLO 1. CLASSIFICAZIONE DEI SISTEMI OPERATIVI Interfaccia utente (sh, ksh, bash, etc.) Librerie di funzioni standard di uso comune (libc, libm, libcrypt, etc.) Supporto per linguaggi di programmazione (cc, as, ld, ar, pc, perl, python, javac, etc.) Caricamento, esecuzione e profiling di programmi (sh, adb, sdb, prof, time, etc.) Modifica di file (ed, vi, emacs, etc.) Manipolazione di file e cartelle (cp, mv, rm, mkdir, etc.) Informazione sullo stato del sistema (date, ls, who, df, du, etc.) Comunicazione (vari client: telnet, ftp, mutt 11, netscape, rlogin, rcp, rsh, ping, etc.) A tal proposito, è importante notare che la maggior parte degli utenti vede (e giudica) un Sistema Operativo proprio dai programmi appartenenti al software di base, e non dalle chiamate di sistema e dalle funzioni di libreria; i primi sono di fatto l interfaccia verso l utente, e quindi costituiscono l interfaccia di base tra l utente e il Sistema Operativo(interfaccia utente), mentre le seconde realizzano un secondo tipo di interfaccia (interfaccia di programmazione), che però è visibile solo a quel sottoinsieme di utenti che scrivono programmi. A differenza dei programmi applicativi veri e propri, che servono, come il nome indica, a specializzare il calcolatore per risolvere problemi legati al mondo della produzione e del lavoro, i programmi nell area del software di base si collocano a un livello intermedio tra l hardware e i programmi applicativi. Anche qui la linea di demarcazione è alquanto soggettiva, ma si può dire ad esempio che un programma di gestione magazzino sviluppato per una azienda è senz altro un esempio di programma applicativo; viceversa, un programma flessibile che consente, partendo da uno schema generale, di costruire un programma di gestione magazzino, fornendo informazioni specifiche sul modo in cui l azienda registra i prodotti immagazzinati, può essere considerato software di base. 1.4 Classificazione dei Sistemi Operativi Risulta opportuno, prima di procedere oltre nello studio dei Sistemi Operativi, dare una schematica classificazione di tali sistemi che metta in evidenza le sulla suite di protocolli TCP/IP. Il nome di questi server, come si può notare dagli esempi, termina di solito con la lettera d, che sta appunto ad indicare che si tratta di un daemon. Il client corrispondente (cioè il programma che viene attivato dall utente e interagisce con esso, richiedendo servizi al server con il quale si collega) di norma ha lo stesso nome, ma senza la d finale; così il client FTP si chiama appunto ftp. I server (o daemon) sono programmi che di regola vengono lanciati automaticamente all avvio del sistema, e non terminano mai (se non all arresto del sistema). Per approfondimenti sulle reti di calcolatori e sulla programmazione TCP/IP secondo il paradigma client-server, si veda Tanenbaum [16] e Stevens [14]. 11 Mutt è un client molto potente del protocollo SMTP (Simple Mail Transfer Protocol), per la gestione della posta elettronica.
20 1.4. CLASSIFICAZIONE DEI SISTEMI OPERATIVI 7 caratteristiche dell utenza che essi devono soddisfare, più che le tecniche realizzative hardware e software. Com è ovvio, sono possibili più tipi di classificazioni, che riflettono differenti approcci di analisi. Una prima suddivisione, che tiene in conto sia aspetti funzionali che di cronologia evolutiva, distingue quattro tipi principali di Sistemi Operativi, chiamati rispettivamente: sistemi batch (o a lotti) si dividono a loro volta in sistemi batch monoprogrammati sistemi batch multiprogrammati sistemi interattivi (o a partizione di tempo; in inglese, time-sharing) sistemi personali sistemi dedicati divisibili anch essi in sistemi transazionali sistemi per il controllo di processi I Sistemi Operativi dei primi tre tipi offrono all utente la possibilità di scrivere, mettere a punto ed eseguire programmi arbitrari scritti in uno dei linguaggi di programmazione supportati dal sistema. L utente tipico di tali sistemi, che saranno descritti nel seguito del paragrafo, è quindi in molti casi un programmatore, che accede al calcolatore tramite un terminale. Il dialogo utente-sistema si svolge normalmente inviando comandi da terminale e ricevendo in risposta messaggi audiovisivi dal sistema. Come si vedrà in seguito, le funzioni del Sistema Operativo nell assistere il programmatore vanno ben oltre la semplice compilazione ed esecuzione controllata di programmi, per cui si parla comunemente di ambiente di sviluppo offerto dal Sistema Operativo, intendendo con tale termine l insieme di servizi offerti che consentono una maggiore facilità nella progettazione, nella stesura e nella verifica di nuovo software. Come meglio evidenziato nel seguito del capitolo, invece, i sistemi del quarto gruppo sono dedicati ad un tipo specifico di applicazione, e non permettono lo sviluppo di nuovi programmi Gestione a lotti Tralasciando le primissime implementazioni, che risalgono alla prima metà del secolo trascorso, la prima forma sufficientemente organizzata di Sistema Operativo vide la luce grazie all introduzione di lettori di schede perforate. Nacque così, all inizio degli anni 50, una modalità di colloquio chiamata gestione a lotti (batch processing). Le principali caratteristiche di tale forma di colloquio possono così essere sintetizzate: Le richieste degli utenti sono costituite da pacchi di schede perforate; ogni pacco, chiamato lavoro o job inizia con una scheda di identificazione dell utente e consiste in una sequenza di passi o step. Il job viene fisicamente
21 8 CAPITOLO 1. CLASSIFICAZIONE DEI SISTEMI OPERATIVI consegnato dall utente ad un operatore, che provvede, al tempo opportuno, a caricarlo sul lettore di schede perforate. Ogni step rappresenta una richiesta inoltrata dall utente al Sistema Operativo per ottenere un servizio elementare generalmente associato all esecuzione di un programma. Esempi di step sono la compilazione di un programma oppure l esecuzione di un programma già compilato in precedenza. La scheda di identificazione del lavoro e le altre schede che servono a definire i vari step sono chiamate schede di controllo. Anche i programmi e gli eventuali dati devono apparire una volta per tutte nel lavoro, che consiste quindi in una alternanza di schede di controllo e di schede dati; queste ultime possono contenere un programma da compilare oppure delle informazioni alfanumeriche che saranno usate durante l esecuzione del programma. Eseguito un lavoro, l utente ottiene dal Sistema Operativo un tabulato, dove sono stampati consecutivamente i risultati prodotti dai vari step eseguiti. Il Sistema Operativo deve registrare in tale tabulato, oltre agli eventuali risultati, tutti i messaggi e diagnostici che si sono verificati durante l esecuzione del lavoro. Il Sistema Operativo include un programma di controllo chiamato Batch Monitor, che svolge le seguenti funzioni: legge da lettore di schede il prossimo lavoro (funzionalità di loader o caricatore); se il lettore di schede è vuoto, rimane in attesa di un segnale dell operatore che indica che sono state collocate altre schede sul lettore; esegue successivamente gli step del lavoro letto (funzionalità di interprete di schede di controllo) e provvede, ad ogni step, a caricare da nastro magnetico o da disco l apposito programma di sistema (ad esempio un compilatore Fortran) per poi passargli il controllo (altro compito del loader); al termine dell esecuzione di un step, inizia lo step successivo; oppure, se lo step eseguito era l ultimo, inizia la decodifica della scheda di identificazione del job successivo e quindi l esecuzione del primo step del nuovo job. Usando lo schema appena descritto, ogni step di un job consiste nel leggere schede, eseguire istruzioni e stampare risultati. In effetti, i calcolatori dotati di questo tipo di Sistema Operativo erano composti essenzialmente di tre blocchi: lettore di schede, sistema di calcolo, stampante. L organizzazione della memoria del sistema è molto semplice, ed è mostrata nella Figura 1.2: il Batch Monitor risiede permanentemente in memoria, in una zona di indirizzamento fissa; il resto della memoria è dedicato, volta per volta, ad ospitare uno (ed un solo) programma da eseguire. Il maggiore inconveniente del tipo di sistema ora visto, chiamato gestione a lotti monoprogrammata, era il seguente: poiché il lettore di schede e la stampante sono
22 1.4. CLASSIFICAZIONE DEI SISTEMI OPERATIVI 9 Sistema Operativo Batch Monitor Area del Programma utente Figura 1.2: Organizzazione della memoria RAM per un sistema batch monoprogrammato. periferiche molto lente rispetto alla CPU 12, e poiché le fasi di I/O e di CPU non possono sovrapporsi, si verificava frequentemente, durante l esecuzione di uno step, che la CPU rimanesse bloccata in attesa di segnali del tipo fine lettura schede o fine scrittura riga di stampa. Il risultato era che il tempo medio tra la sottoposizione e la terminazione di un job, detto tempo di turnaround, era notevolmente alto, e il tasso di utilizzazione della CPU, che da sempre è una risorsa pregiatissima, era inaccettabilmente basso Gestione a lotti multiprogrammata Per palliare agli inconvenienti del batch processing monoprogrammato furono introdotti perfezionamenti nella gestione a lotti, resi possibili dall introduzione di nuove unità periferiche e di calcolatori dotati di più processori autonomi, per rimuovere il collo di bottiglia. In una prima fase, con l introduzione di lettori di nastro magnetico aventi un elevato tasso di trasferimento, furono introdotte architetture che prevedevano sistemi ausiliari per effettuare la conversione scheda-nastro e quella nastro-scheda. Il calcolatore principale non interagiva più con periferiche lente ma solo con unità a nastro, mentre i calcolatori ausiliari svolgevano in parallelo le attività di conversione, e ciò ha contribuito a velocizzare non poco il ciclo globale di lavoro. Le operazioni di lettura da scheda e di stampa venivano così effettuate off-line, e quindi non penalizzavano più l unità centrale di calcolo. Anche se serviva a rimuovere il collo di bottiglia citato in precedenza, l uso di elaboratori ausiliari poneva problemi di gestione: il numero di nastri da montare e smontare era notevole e gli errori degli operatori frequenti. 12 Il rapporto di velocità tra CPU e dispositivi di I/O (lettori di schede perforate) era di tre ordini di grandezza (10 3 ); oggi le velocità dei componenti sono enormemente superiori, ma il gap si è ulteriormente ampliato (è circa pari a ).
23 10 CAPITOLO 1. CLASSIFICAZIONE DEI SISTEMI OPERATIVI Il perfezionamento successivo si ebbe verso la fine degli anni 50, quando apparvero i primi calcolatori con canali di I/O autonomi nonché le prime memorie a disco di grande capacità: in pratica, venne ripresa l idea precedente di fare interagire la CPU prevalentemente con unità periferiche veloci, in questo caso i dischi. Tuttavia, grazie ai canali di I/O integrati nel calcolatore, non era più necessario ricorrere a calcolatori ausiliari in quanto tali canali sono dei processori specializzati in grado leggere schede, stampare dati o interagire coi dischi, interferendo in modo ridotto con le attività della CPU (esistono in realtà interferenze dovute agli accessi simultanei alla memoria da parte dei vari processori e al fatto che la sincronizzazione canale di I/O-CPU avviene tramite interruzioni che richiedono, a loro volta, l esecuzione sulla CPU di brevi programmi di gestione; comunque, tali interferenze possono essere alquanto contenute in un sistema ben dimensionato). La soluzione, tuttora adottata, ha portato all uso di dischi di spooling (Simultaneous Peripheral Operations On Line): il disco di spooling di ingresso contiene schede virtuali, ossia schede lette dal lettore e contenenti job non ancora eseguiti, che costituiscono un pool di job in attesa di accedere alla CPU; quello di spooling di uscita contiene invece tabulati virtuali, ossia tabulati già prodotti relativi a job eseguiti ma non ancora stampati. I canali di I/O provvedono a riempire o svuotare i dischi di spooling e a gestire i trasferimenti tra memoria e dischi mentre la CPU può essere quasi interamente dedicata ad eseguire programmi degli utenti o del Sistema Operativo. Si arriva così alla multiprogrammazione, cioè alla capacità di gestire più di un job simultaneamente; la fase di I/O di un job si sovrappone alle fasi CPU di altri job, e la CPU a questo punto, potendo commutare periodicamente da un job all altro in memoria, è quasi sempre attiva e dunque utilizzata molto meglio. Più in dettagli, lo scenario operativo in un sistema a lotti multiprogrammato è essenzialmente il seguente: il sistema ha in memoria un certo numero di job pronti ad eseguirsi (in generale, questi saranno un sottoinsieme proprio di quelli che compongono il pool di job, dato che la memoria primaria ha sempre capacità inferiore a quella dei dischi); il Sistema Operativo comincia ad eseguire uno di questi job; ad un certo punto, il programma che costituisce il job attivo si ferma in attesa di qualche risorsa, ad esempio il competamento di un operazione di I/O che lo riguarda; invece di bloccarsi e rimanere inattiva, come avviene in un sistema monoprogrammato, la CPU passa ad eseguire il programma di un altro job in memoria, nei confronti del quale si comporterà nello stesso modo: se deve bloccarsi in attesa, la CPU passa ancora ad un altro job, e così via. Quando uno dei job che si erano bloccati ha terminato l attesa (cioè quando la risorsa che aspettava si è resa disponibile), questo potrà riprendere la CPU e continuare ad eseguire il suo programma. Pertanto, finché ci sono job in memoria non bloccati, la CPU non rimane mai inattiva, come si è già detto. L organizzazione della memoria di un sistema batch multiprogrammato, com è facile capire da quanto detto, è già notevolmente più complessa del caso visto in precedenza, ed è mostrata nella Figura 1.3. Più in generale, la gestione tutta del sistema è più complessa: la multiprogrammazione, anche quando è fatta in modo semplice come nel caso ora visto, richiede infatti la messa in opera di diverse tecniche, tra cui le seguenti:
24 1.4. CLASSIFICAZIONE DEI SISTEMI OPERATIVI 11 Sistema Operativo Batch Monitor job 1 job 2 job 3 job 4 Figura 1.3: Organizzazione della memoria RAM per un sistema batch multiprogrammato. Job scheduling: il sistema deve scegliere, tra i vari elementi del pool di job, quale caricare in memoria; Memory management: occorre definire una strategia per l allocazione della memoria ai vari job che vengono caricati; CPU scheduling: il sistema sceglie, tra vari job in memoria pronti ad eseguirsi (cioè non bloccati da qualche risorsa), quale servire per primo, allocandogli la CPU; Meccanismi di protezione: Dato che in questi sistemi c è una pluralità di job che si eseguono a turno per porzioni di tempo, inframezzandosi tra loro, è necessario evitare che i diversi programmi accedano direttamente ai dispositivi del sistema, altrimenti si genererebbero inevitabilmente delle situazioni di conflitto; si pensi ad esempio alla fase di stampa dei risultati: se non ci fosse nessuna disciplina di accesso alla stampante, i dati di un job sarebbero stampati insieme a porzioni di dati di un altro job. Pertanto tutte le routines di I/O devono risiedere nel Sistema Operativo, che mette a disposizione dei job opportuni servizi per effettuare le operazioni di I/O. Verso la fine degli anni 60 la gestione a lotti subisce, grazie ai progressi conseguiti nel campo dell hardware e delle telecomunicazioni, una importante e radicale evoluzione consistente nell eliminare l ormai antiquato lettore di schede, consentendo agli utenti di sottomettere job da terminali remoti. L introduzione di circuiti integrati e la conseguente diminuzione dei costi dei componenti hardware fa sì che sia ormai possibile servirsi di terminali intelligenti, sia per editare un job da inviare successivamente ad un mainframe perché venga eseguito con le modalità della gestione a lotti, sia per visualizzare, archiviare o stampare i risultati del job eseguito. Lo sviluppo di reti di trasmissione
25 12 CAPITOLO 1. CLASSIFICAZIONE DEI SISTEMI OPERATIVI dati ha inoltre consentito di distribuire i punti di accesso al grande calcolatore, ampliando in questo modo l utenza potenziale. Attualmente, emergono due aree di applicazioni in cui la gestione a lotti si rivela più conveniente di quella interattiva (vedi sottoparagrafo successivo): in primo luogo, negli ambienti di produzione in cui sono utilizzate in modo periodico, e quindi prevedibile, procedure complesse che impegnano il calcolatore per delle ore consecutive, la gestione a lotti, oltre a consentire di sfruttare appieno la potenza di calcolo del sistema, consente anche di stabilire un calendario di esecuzione (schedule) dei job e dà quindi al gestore del centro di calcolo la certezza che un determinato job sarà eseguito entro una data prefissata. In secondo luogo, la gestione a lotti rimane la più adatta ogni qualvolta la risorsa di calcolo ha un costo molto elevato: questo si verifica per i supercalcolatori utilizzati per eseguire calcoli particolarmente impegnativi. In tale caso, il costo di simili macchine suggerisce di ottimizzarne l uso facendo in modo che i programmi degli utenti siano già messi a punto su macchine meno costose e ricorrendo alla gestione a lotti per sfruttare appieno la potenza del supercalcolatore. Algoritmi di Scheduling e metriche prestazionali È opportuno a questo punto fare una breve digressione, per evidenziare alcuni concetti fondamentali, che saranno ripresi più volte nel seguito. Il termine scheduling deriva dal verbo inglese to schedule, che significa predisporre un ordine, scadenzato nel tempo, di esecuzione di operazioni di vari tipi. È questo un concetto perfettamente generale, che si ritrova in molte parti del codice di un Sistema Operativo: nel Nucleo di un Sistema Operativo si ritrovano infatti diversi algoritmi di scheduling, applicati a varie operazioni, tra cui: Scheduling dei job (di cui si è accennato nel paragrafo precedente; tipico soprattutto dei sistemi batch); Scheduling della CPU (decidere a chi allocare la CPU dopo il job attuale); Scheduling del disco (in che ordine effettuare le operazioni di I/O su disco, tra le tante che sono state richieste e che si trovano in una coda di richieste pendenti). Naturalmente, per ogni problematica da affrontare, esiste una pluralità di soluzioni possibili, quindi il progettista di Sistemi Operativi deve scegliere, nell implementare una data funzionalità, tra più algoritmi adottabili; tale scelta, in generale, deve essere effettuata sulla scorta di opportuni criteri guida, che nel caso qui trattato, come in tutti i problemi di tipo ingegneristico, deve derivare dallo scegliere un aspetto delle prestazioni del sistema che si ritiene importante, e dal misurarlo per poterne valutare l effetto ottenuto. Ogni algoritmo di un Sistema Operativo, compresi i meccanismi di scheduling, va quindi sempre deciso in funzione di un opportuna metrica prestazionale del sistema o sottosistema in esame: la scelta dell algoritmo dipende dunque dall aspetto funzionale o dal tipo di prestazione che si ritiene maggiormente di interesse; ad esempio:
26 1.4. CLASSIFICAZIONE DEI SISTEMI OPERATIVI 13 Per lo scheduling della CPU, si può decidere di voler ottimizzare il tempo medio di risposta di un programma; oppure invece l uniformità del tempo di risposta. Per lo scheduling del disco, si può invece decidere di ottimizzare il tempo totale di completamento delle operazioni di I/O. La scelta di quale metrica adottare è un aspetto di politiche di implementazione, totalmente in mano al progettatore di sistema, e non sempre c è una soluzione univoca Sistemi a partizione di tempo È questo il tipo di Sistema Operativo concettualmente più importante. Per quanto efficiente, la gestione a lotti non è la più adatta in ambienti in cui lo sviluppo e la messa a punto di programmi sono considerate attività prioritarie: in questo caso, infatti, il dovere attendere che termini il job precedente (o addirittura un certo numeri di essi) prima di potere effettuare una nuova prova limita notevolmente la produttività dei programmatori. Per ovviare a tale inconveniente, fu sviluppato nel 1963 presso il Massachussetts Institute of Technology (MIT) il Sistema Operativo Compatible Time Sharing System (CTSS) basato su un elaboratore IBM 7090 con un banco aggiuntivo di memoria che può essere considerato il primo dei Sistemi Operativi di questo tipo. L idea di base dei sistemi a partizione di tempo (in inglese, time-sharing), detti anche sistemi interattivi o multitasking, è quella di assicurare, ad ogni utente collegato tramite un terminale, una frazione garantita delle risorse del calcolatore, a prescindere dalle richieste degli altri utenti collegati. Questo si ottiene tramite un multiplamento nel tempo della CPU tra i vari programmi associati ai terminali attivi: ogni programma ha diritto a impegnare la CPU per un quanto di tempo (tipicamente, si usano quanti di poche centinaia di millisecondi, anche se sarebbe più corretto misurare il quanto in termini di numero di istruzioni eseguibili durante un quanto). Se il programma non termina entro il quanto prefissato, esso viene posto in lista d attesa ed un nuovo programma riceve il quanto successivo. Se vi sono N terminali attivi, ogni utente ha la certezza di ottenere un quanto di tempo ogni N, qualunque siano i tempi di esecuzione degli altri programmi. Si tratta in pratica di un estensione logica della multiprogrammazione: lo scheduling della CPU viene reso abbastanza veloce e sofisticato da permettere ai task 13 di interagire con gli utenti simultaneamente. L utente ha così l illusione di essere il solo a interagire con la macchina, come se questa fosse dedicata a lui. Come già anticipato, l input dei programmi non avviene più da schede o da disco, ma dalla tastiera di un terminale: viene quindi fornito un meccanismo di comunicazione on-line tra sistema e utente. Il funzionamento di un sistema time-sharing è per molti versi analogo a quello di un sistema batch multiprogrammato, ma il tutto è gestito in modo estremamente più dinamico: la CPU è multiplexata tra vari task, ubicati in memoria RAM 13 È uso comune denominare task i programmi in esecuzione in un ambiente time-sharing; il termine job è invece tipico degli ambienti batch.
27 14 CAPITOLO 1. CLASSIFICAZIONE DEI SISTEMI OPERATIVI e su disco, e naturalmente viene allocata a un task solo se questo è in memoria, analogamente a quanto già accennato nel caso dei sistemi batch multiprogrammati. Ciascun task, nel corso della sua esecuzione, può venire swappato 14 dentro e fuori dalla memoria primaria, viene cioè copiato da memoria RAM a disco e viceversa per fare posto agli altri task; si parla in questi casi di algoritmi di swap-in e di swap-out, e di zona di swap (la partizione di un disco dedicata a fungere da memoria secondaria di appoggio per l esecuzione dei programmi). La Figura 1.4 schematizza (sia pur in modo molto semplificato) i possibili stati fondamentali dei task in un sistema a partizione di tempo: ad un istante dato, i task del sistema possono trovarsi nella zona di swap (dove, in contesa con altri processi, aspettano di essere ricaricati in memoria primaria dall algoritmo di swap-in) in memoria primaria (dove, in contesa con altri processi, aspettano di ottenere il controllo della CPU per poter avanzare nel proprio programma (algoritmo di scheduling della CPU ) in stato attivo (cioè in esecuzione), se l algoritmo di scheduling ha assegnato loro la CPU e stanno eseguendo il proprio codice; soltanto un processo alla volta può ovviamente trovarsi in questo stato 15. Il diagramma degli stati dei processi verrà ripreso in maggior dettaglio nel Capitolo 7 (si vedano in particolare le Figure 7.3 pag. 103 e 7.7 pag. 114). A prima vista, si potrebbe pensare che un sistema interattivo appaia agli N utenti collegati come un calcolatore N volte più lento di quello reale; in pratica, le prestazioni sono molto migliori poiché, durante l attività di messa a punto di programmi, l utente alterna fasi di editing del programmi nonché fasi di analisi dei risultati e ripensamenti (think time) a fasi di compilazione ed esecuzione, per cui una buona parte delle richieste degli utenti (editing, presentazione dei risultati) può essere esaudita prevalentemente dai canali di I/O con scarso impegno della CPU. I sistemi a partizione di tempo hanno avuto un notevole impatto nello sviluppo dei Sistemi Operativi: è stato necessario predisporre schemi hardware e software per proteggere i programmi e gli archivi degli utenti attivi nel sistema da danni causati da programmi non affidabili (o intenzionalmente indiscreti) di altri utenti. Allo stesso tempo, la gestione rigida della CPU basata sull assegnazione di quanti di tempo di lunghezza prefissata è evoluta per tenere conto del fatto che il Sistema Operativo non è in grado di conoscere a priori quanto tempo di CPU richiederà un programma. Per questo motivo, si preferisce usare una strategia flessibile: quando un programma inizia ad eseguire, esso è considerato di tipo interattivo, se dopo alcuni quanti di tempo, esso non ha ancora terminato, la sua priorità viene diminuita; infine, se oltrepassa un numero prefissato di quanti, esso viene declassato a lavoro di tipo differito e, in pratica, sarà eseguito come in un sistema di gestione a lotti assieme ad altri lavori dello stesso tipo quando non vi sono più richieste di utenti ai terminali. 14 Dall inglese to swap: scambiare, barattare. 15 Più esattamente, per una macchina multiprocessore con N CPU, si potranno avere N processi in esecuzione contemporaneamente.
28 1.4. CLASSIFICAZIONE DEI SISTEMI OPERATIVI 15 CPU scheduling (CPU) RAM swap out swap in SWAP (Disco) Figura 1.4: Stati dei task in un sistema a partizione di tempo Sistemi personali All inizio degli anni 80, appaiono i primi Personal Computer e con essi si afferma una nuova classe di Sistemi Operativi monoutente, orientati quindi a dare servizi ad una sola persona. In effetti l evoluzione tecnologica, e il conseguente enorme abbassamento dei costi sia hardware che software, consentono di produrre oggi a costi contenuti dei calcolatori che, pur avendo un ingombro limitato, hanno una potenza di calcolo paragonabile a quella dei mainframe degli anni 70. In simili condizioni, ha senso impostare un Sistema Operativo che serva un unico posto di lavoro, poiché l efficienza dell uso del calcolatore cessa di essere un obiettivo prioritario: la metrica prestazionale principale diventa la convenienza e prontezza d uso del sistema. Paradossalmente, questa nuova generazione di Sistemi Operativi offre all utente un interfaccia che risulta per alcuni aspetti superiore a quella offerta da sistemi interattivi sviluppati per mainframe. I dispositivi di I/O tipici di un sistema personale sono la tastiera, il mouse, lo schermo grafico, piccole stampanti, scanner, modem, scheda audio, e altro. In particolare, la disponibilità di uno schermo grafico ad alta risoluzione accoppiato all uso di microprocessori sempre più potenti ha reso possibile la realizzazione di software grafico interattivo che offre affascinanti possibilità di utilizzazione. Il Sistema Operativo MacOS della Apple
29 16 CAPITOLO 1. CLASSIFICAZIONE DEI SISTEMI OPERATIVI è uno dei capostipiti più noti di questa nuova generazione di Sistemi Operativi personali. I primi sistemi di questo tipo erano inizialmente sistemi mono-task, affatto privi di protezione di memoria: un errore di un programma utente poteva compromettere addirittura il Sistema Operativo. Un esempio ben noto è il sistema MS-DOS della Microsoft, che d altronde era stato concepito per funzionare su una macchina la cui CPU (Intel 8088, e poi 8086) era assolutamente priva, a livello hardware, di qualsiasi meccanismo di protezione. Oggi i sistemi personali sono molto evoluti da allora, e possono ormai adottare tecnologie sviluppate per grandi sistemi, anche se molte caratteristiche (quali ad esempio i meccanismi di protezione dei file) non sono strettamente necessarie, dato che l utente è uno solo. Gli esemplari più noti dei sistemi di questa categoria, oltre al già citato MacOS, sono indubbiamente i vari sistemi introdotti dalla Microsoft (da Windows 95 in poi), e le distribuzioni di tipo workstation del sistema Linux (in particolare Red Hat, Mandrake, SuSe) Sistemi transazionali I sistemi transazionali sono dei Sistemi Operativi disegnati attorno ad una specifica applicazione e quindi ad essa dedicati: l utente non è più libero di inventare nuovi programmi per poi richiederne l esecuzione ma può soltanto scegliere da un menu prefissato quale funzione intende eseguire; fatto ciò, egli deve in generale immettere i dati necessari per avviare la funzione. Esempi tipici di sistemi transazionali sono i sistemi di prenotazione posti, o i sistemi per la movimentazione dei conti correnti bancari. Le interazioni dell utente con il Sistema Operativo prendono il nome di transazioni: l annullamento di una prenotazione, oppure un versamento in conto corrente, sono esempi di transazioni Sistemi di controllo di processi Nei casi esaminati finora, si è sempre supposto che il Sistema Operativo svolga una funzione di interfaccia tra l hardware del calcolatore e una persona fisica davanti ad un terminale (utente od operatore che sia). Nel caso dei sistemi per il controllo di processi, tale ipotesi viene in parte o del tutto a cadere: l obiettivo principale di tali sistemi è quello di acquisire periodicamente dati provenienti da sensori e altre apparecchiature remote, di analizzare tali dati e di emettere quindi, in seguito all analisi, opportuni segnali ad apparecchiature di uscita. I sistemi di controllo si dividono a loro volta in due categorie: sistemi ad anello aperto: esiste un operatore che analizza i segnali ricevuti dall esterno in tempo reale e prende le opportune decisioni. sistemi ad anello chiuso: non è previsto alcun intervento umano; i segnali ricevuti dall esterno sono elaborati e danno luogo a comandi inviati ai vari dispositivi collegati.
30 1.5. UNA CLASSIFICAZIONE COMPLEMENTARE 17 Un sistema di controllo di traffico aereo è ad anello aperto in quanto il controllore di volo recepisce le immagini inviate dai radar sullo schermo e comunica via radio ai piloti le opportune istruzioni. I sistemi di controllo per centrali telefoniche elettroniche ed i sistemi di pilotaggio automatico sono ad anello chiuso in quanto non è previsto l intervento di operatori umani. In un certo senso, i sistemi per il controllo di processi si possono considerare sistemi transazionali con interfaccia sintetica e quindi, in generale, con vincoli più stringenti sui tempi di risposta agli eventi esterni. 1.5 Una classificazione complementare Si è già detto, al Paragrafo 1.4 pag. 6, che i Sistemi Operativi possono essere classificati in vari modi, a seconda del tipo di approccio di analisi che si intende perseguire. Viene presentata qui una seconda classificazione, indipendente da quella vista finora, che non ha la pretesa di essere onnicomprensiva ma che serve ad evidenziare altri aspetti dei Sistemi Operativi non messi sufficientemente in luce da quanto detto finora. Detta classificazione distingue i seguenti tipi di Sistema Operativo: Sistemi di sviluppo Sistemi paralleli e sistemi distribuiti Sistemi real-time Sistemi di sviluppo Nell ambito dei Sistemi Operativi che offrono ambienti di sviluppo, è doveroso citare anche i cosiddetti sistemi di sviluppo, ossia una classe di Sistemi Operativi specializzati che consentono di sviluppare in modo agevole programmi da trasferire successivamente nelle memorie a sola lettura (ROM - Read-Only Memory) di microprocessori, per il controllo di dispositivi di vario tipo (embedded applications). Si tratta di sistemi pseudo-dedicati, a metà tra sistema general-purpose e sistema dedicato. Possono interagire con dispositivi dedicati (ICE - In-Circuit Emulator) per la simulazione del sistema risultante finale e la messa a punto del software in ambiente operazionale reale Sistemi paralleli Si tratta di sistemi multiprocessore con più CPU, in stretta comunicazione tra loro. Le CPU condividono memoria e clock; la comunicazione tra le CPU avviene attraverso la memoria condivisa. Vantaggi:
31 18 CAPITOLO 1. CLASSIFICAZIONE DEI SISTEMI OPERATIVI CPU CPU CPU... Memoria Figura 1.5: Schema architetturale di un sistema SMP. maggiore throughput (numero di job gestiti nell unità di tempo); economia (periferiche, alimentatori etc. sono condivisi); maggior affidabilità: degradazione controllata (graceful degradation), sistemi tolleranti i guasti (fault-tolerant). Due grandi categorie: Multiprocessing simmetrico (SMP - Symmetric MultiProcessing): ogni CPU gira su una copia identica del Sistema Operativo; più processi possono girare contemporaneamente senza degradazione delle prestazioni. La maggior parte dei Sistema Operativo moderni (incluso Linux) supportano l SMP. La Figura 1.5 mostra l architettura minima di un sistema a multiprocessing simmetrico. Multiprocessing asimmetrico: ad ogni CPU è assegnato un compito specifico; un unico master processor coordina il tutto e alloca lavoro per i vari slave processor. Soluzione comune ai sistemi molto grandi (mainframe) Sistemi distribuiti Sono sistemi che distribuiscono il carico di lavoro (workload) tra più CPU. Diversamente dai sistemi paralleli, ogni CPU ha la propria memoria locale, e spesso propri dispositivi di I/O (si tratta in somma di macchine fisicamente distinte che cooperano). Le CPU comunicano tra loro tramite bus ad alta velocità, o linee di dati. Questi sistemi sono chiamati anche sistemi debolmente accoppiati, in contrapposizione ai sistemi strettamente accoppiati (cioè i sistemi paralleli). Vantaggi: condivisione di risorse;
32 1.6. SVILUPPI FUTURI 19 velocizzazione e parallelizzazione del calcolo (secondo il metodo divide et impera, noto dall ingegneria del software); condivisione del carico (i job sono spostabili ad una CPU meno carica). La comunicazione tra le CPU avviene in genere secondo i metodi e i protocolli propri delle reti di calcolatori, la cui trattazione esula però dagli scopi di questo testo Sistemi in tempo reale Includono (e talvolta con essi si identificano) i sistemi di controllo di processi. Si tratta di sistemi concepiti per applicazioni dedicate, con un limite massimo ai tempi di risposta ad eventi esterni; ad esempio: controllo di esperimenti scientifici, trattamento di immagini mediche, etc. Due categorie: Sistemi hard real-time tempi di risposta definiti rigorosamente; sono sistemi in antitesi ai sistemi time-sharing; non sono supportati dai Sistemi Operativi general-purpose. I dati sono memorizzati e gestiti in RAM o in ROM; assenza di dischi. Sistemi soft real-time i task con tempi di risposta critici hanno maggior priorità, e trattengono la CPU finché completano un operazione. Applicabilità: robotica, controllo di processi lenti; multimedia, realtà virtuale. 1.6 Sviluppi futuri È sempre difficile fare previsioni attendibili in settori in rapida evoluzione come quello informatico. Tuttavia, molti pensano che nel prossimo decennio (il primo del terzo millennio) l informatica sistemistica diverrà sempre più personale : si consoliderà quindi ulteriormente il già forte predominio dei Personal Computer, e in conseguenza i maggiori sforzi dei progettisti saranno rivolti alla progettazione di Sistemi Operativi per tale classe di macchine. Probabilmente i risultati più interessanti si conseguiranno nel settore dei sistemi dedicati in rete. Con tale termine si intende un sistema dedicato che consente non soltanto di utilizzare il Personal localmente ma anche di mantenerlo permanentemente collegato in rete (ad esempio, attraverso una rete pubblica di trasmissione dati quale Internet). In questo modo, l utente può operare localmente mentre, simultaneamente e con un minimo di interferenza, il Sistema Operativo provvede a inviare messaggi già preparati in precedenza a nodi remoti e, allo stesso tempo, riceve e memorizza su disco messaggi inviati da altri nodi della rete al Personal. Un altro promettente settore riguarda il collegamento tramite
33 20 CAPITOLO 1. CLASSIFICAZIONE DEI SISTEMI OPERATIVI linea dedicata veloce di un elevato numero di Personal Computer. Disponendo di una linea veloce di qualche Gigabyte/sec e delle opportune schede di interfaccia mediante le quali collegare i Personal Computer alla linea, l intero sistema risulta assimilabile ad un calcolatore parallelo con un rapporto prestazioni/costo molto interessante, tenuto conto delle economie di scala realizzate operando nel settore dei Personal Computer.
34 Capitolo 2 Struttura dei Sistemi Operativi 2.1 Introduzione Nel capitolo precedente si è data sostanzialmente una prima risposta, a mo di introduzione all argomento, alla domanda cosa sono i Sistemi Operativi. In questo capitolo, anch esso di taglio introduttivo, si esaminerà invece come sono strutturati i Sistemi Operativi, e che genere di servizi offrono ai programmi utente. Si darà quindi un primo sguardo interno a questi sistemi. 2.2 Standardizzazione e Sistemi Aperti Prima di iniziare, è opportuno fare una precisazione sugli standard. Primi anni 80 - avvento di Unix = primo Sistema Operativo indipendente dall hardware; da cui la tecnologia dei sistemi aperti (Open Systems). Tutti (o quasi) i venditori di calcolatori propongono una versione di Unix. Compatibilità software tra sistemi di marche diverse: il personale lavora su macchine, Sistemi Operativi, applicazioni diverse, ma con una preparazione tecnica unica. Benefici: integrazione delle applicazioni, portabilità, interoperabilità. POSIX è uno standard internazionale (ISO 1003 si veda [8]) per la definizione delle caratteristiche di un Sistema Operativo aperto (cioè interoperabile) - in pratica, una definizione generale delle caratteristiche di Unix. In POSIX viene precisata una standardizzazione dell interfaccia, non dell implementazione, che rimane interamente a discrezione del progettista e dei programmatori di sistema. 21
35 22 CAPITOLO 2. STRUTTURA DEI SISTEMI OPERATIVI Interfaccia = comandi, chiamate di sistema, servizi offerti etc. Tutti gli Unix moderni sono POSIX-compliant; molti Sistemi Operativi non- Unix hanno delle API POSIX-compliant. Linux è nato originalmente come un progetto per un Sistema Operativo POSIX-compliant. 2.3 Componenti di un Sistema Operativo Verranno esaminati i seguenti aspetti, che forniscono una panoramica abbastanza completa: Gestione dei processi e della CPU 1 Gestione della memoria primaria (RAM) 2 Gestione della memoria secondaria (dischi) 3 Gestione del sistema di I/O 4 Gestione dei file system 5 Sistema di protezione 6 Networking 7 Interprete di comandi (interfaccia utente) Gestione dei processi Un processo 9 è un programma in esecuzione, in un proprio ambiente di lavoro. Un processo ha bisogno di risorse per svolgere il compito assegnatogli, tra cui tempo CPU, memoria, file, dispositivi di I/O. Un Sistema Operativo dispone di algoritmi per multiplare nel tempo le capacità di calcolo della macchina, cioè per allocare opportunamente la CPU tra i vari processi attivi; inoltre dispone normalmente di chiamate di sistema che permettono a processi di creare altri (sotto)processi, che si eseguono in modo asincrono rispetto al processo padre: vivono cioè di vita propria e indipendente, e solo se il programma lo richiede possono eventualmente sincronizzarsi col processo padre o con altri processi. 1 Argomento trattato nei Capitoli 7 e 8. 2 Trattato nel Capitolo Trattato nel Capitolo Trattato nel Capitolo 13 5 Trattato nel Capitolo 5 6 Trattato in vari punti, tra cui i Paragrafi 5.3 pag. 67, pag. 100, 8.4 pag. 140, 10.6 pag. 163, 11.3 pag La gestione delle reti di calcolatori è un argomento che esula dagli scopi di questo testo, per cui se ne daranno soltanto brevissimi cenni in questo capitolo introduttivo. 8 Trattato nel Capitolo 6. 9 Il termine è pressoché intercambiabile con task (vedi Nota 13 pag. 13). Il concetto di processo verrà ripreso in maggior dettaglio nel Paragrafo 7.3 pag. 99.
36 2.3. COMPONENTI DI UN SISTEMA OPERATIVO 23 Risorse, ambiente di lavoro È opportuno a questo punto fare subito due precisazioni. La prima riguarda il significato attribuito alla parola risorsa in un contesto di Sistemi Operativi: si intende per risorsa qualunque oggetto (reale o astratto) che può condizionare l avanzamento dei processi, cioè l esecuzione dei loro programmi. Questo argomento sarà ripreso e approfondito nel Paragrafo pag. 104; si vedrà che questo concetto è molto utile nel descrivere il comportamento di un Sistema Operativo rispetto ai processi. La seconda riguarda invece una distinzione concettuale tra i termini programma e processo: il termine programma indica, ai fini della descrizione di un Sistema Operativo, un entità passiva, cioè semplicemente il contenuto di un file eseguibile; per contro il termine processo indica un entità attiva, che comprende di fatto un programma in esecuzione, con un proprio program counter (si veda il prossimo capitolo) ed un proprio ambiente di lavoro. Si veda il Paragrafo 7.3 pag. 99 per una discussione più approfondita dell argomento. Per quanto riguarda l ultimo elemento citato, si precisa che l ambiente di lavoro è tutto ciò che fa sì che uno stesso programma (codice eseguibile) finisca per comportarsi in modi diversi quando si esegue all interno di processi (e quindi di ambienti di lavoro) diversi. Alcuni esempi di aspetti che compongono l ambiente di lavoro, e che possono cambiare il comportamento di un programma: la cartella attiva (vedi Paragrafo pag. 74) comportamento di ls): chiamato senza argomenti, in una cartella scrive al terminale una certa lista di file, in un altra cartella scrive una lista diversa. l identificazione di utente (UID) (vedi pag. 77) comportamento di cat, o di qualunque programma che effettui una chiamata di sistema open() per accedere ad un file: a seconda dei diritti di accesso del file, con un UID di processo che corrisponde a quello del proprietario del file la open() verrà eseguita normalmente, con un UID diverso invece potrà ritornare un errore. etc. Come si vede, sia le risorse che l ambiente di lavoro influenzano il comportamento di un processo. Responsabilità del Sistema Operativo Il S.O. è responsabile delle seguenti attività per la gestione dei processi: Creazione e terminazione di processi; Sospensione e ripresa dell esecuzione dei programmi (CPU scheduling);
37 24 CAPITOLO 2. STRUTTURA DEI SISTEMI OPERATIVI Fornire meccanismi per la sincronizzazione dei processi e per la comunicazione tra processi Gestione della memoria primaria La memoria primaria (o RAM - Random Access Memory) è un grande array di byte, ciascuno col suo proprio indirizzo. È un deposito di dati ad accesso veloce, condiviso da CPU e periferiche. A differenza dei dischi, che mantengono permanentemente il loro contenuto di informazione, la memoria primaria è un dispositivo di memorizzazione volatile: in caso di caduta dell alimentazione, le informazioni contenute sono perse. Il Sistema Operativo è responsabile delle seguenti attività riguardo la gestione della memoria: Tener traccia di quali parti di RAM sono attualmente in uso e da chi; Decidere quali processi caricare in memoria, quando si crea disponibilità di spazio (job scheduling); Allocare e deallocare spazio di memoria quando necessario Gestione della memoria secondaria Memoria primaria è volatile, e troppo limitata per far spazio permanentemente a tutti i dati e programmi; per questi ad altri motivi, il calcolatore deve fornire della memoria secondaria a supporto della memoria primaria. I sistemi attuali utilizzano quasi tutti dei dischi come mezzo di memorizzazione on-line, per programmi e per dati. Il Sistema Operativo è responsabile delle seguenti attività riguardo la gestione dei dischi: Gestione dello spazio libero; Allocazione di spazio secondo le richieste; Decidere l ordine di esecuzione delle letture e scritture da/a disco (disk scheduling) Gestione del sistema di I/O Il sistema (o sottosistema) di I/O consta di Un sistema di bufferizzazione e caching dei dati
38 2.3. COMPONENTI DI UN SISTEMA OPERATIVO 25 Un interfaccia generica per i device driver Driver per specifiche periferiche hardware. In Unix, le peculiarità delle periferiche sono nascoste al Nucleo interno: sono note soltanto ai device driver. Le periferiche sono presentate dal sottosistema di I/O in una forma canonica : ad esempio, un disco è visto sempre come un array di blocchi di dimensione fissa, indipendentemente dalla reale geometria del disco (tracce, cilindri, etc. si veda il Paragrafo 3.8 pag. 45) e dalla dimensione dei settori fisici Gestione dei file system Il Sistema Operativo fornisce una vista logica uniforme della memorizzazione di informazioni; fa astrazione delle proprietà fisiche dei dischi e definisce un unità logica di raggruppamento delle informazioni, il file. Un file è una collezione di informazioni correlate definito dal suo creatore. Normalmente i file rappresentano programmi (in forma sorgente o in linguaggio macchina) e dati. I file sono generalmente organizzati in cartelle (in inglese, directory), per agevolarne l uso. Un insieme di file e cartelle residenti in un volume fisico costituisce un file system. Il Sistema Operativo è responsabile delle seguenti attività riguardo la gestione dei file system: Creazione e distruzione di file Creazione e distruzione di directory Supporto di primitive per la manipolazione di file e cartelle Allocazione di spazio a un file Mappatura dei file nella memoria secondaria Backup di file su supporti stabili (non volatili) e rimovibili (anche se quest ultima attività è spesso affidata a programmi che si eseguono a livello utente, senza servizi specifici del Nucleo) Sistema di protezione Per sistema di protezione si intende un meccanismo per disciplinare l accesso da programmi, processi, utenti alle risorse del sistema e degli utenti. In genere, il meccanismo di protezione deve: Distinguere tra uso autorizzato e non autorizzato
39 26 CAPITOLO 2. STRUTTURA DEI SISTEMI OPERATIVI Specificare i controlli imposti Fornire un sistema di autenticazione degli utenti Fornire un sistema di imposizione di questo meccanismo. Ad esempio, il sistema Unix fornisce diversi meccanismi di protezione, tra cui: UID, GID (User IDentity, Group Identity: identificazione di utenti e dei gruppi di appartenenza) Autenticazione degli utenti mediante password Bit di protezione per file e periferiche Bit di protezione per oggetti IPC (InterProcess Communication si veda il Paragrafo 8.4.1) Gestione dei segnali (interrupt software tra processi) basato sull UID Networking Un sistema distribuito, come già visto nel Paragrafo pag. 18, è un insieme di processori centrali (CPU) che non condividono memoria, periferiche, o clock. Ogni processore ha la propria memoria locale. I processori nel sistema complessivo sono connessi mediante una rete di comunicazione. Un sistema distribuito fornisce accesso per gli utenti a varie risorse del sistema globale, su tutta la rete. Il sottosistema di networking di un Sistema Operativo: Deve prendere in conto strategie di connessione e di instradamento dei dati (routing), e vari aspetti di sicurezza e contesa di risorse In molti casi generalizza l accesso alla rete sotto forma di accesso a file (dal punto di vista dei processi utente); i dettagli di networking (protocolli ed altro) sono nascosti nel codice dei device driver L interprete di comandi Questo argomento verrà ripreso in maggior dettaglio nel Capitolo 6. L interprete di comandi costituisce l interfaccia finale tra l utente e il Sistema Operativo. In origine era parte del Nucleo vero e proprio; poi (dapprima con il sistema MUL- TICS, e poi definitivamente con Unix) è stato implementato come un programma separato, che si esegue a livello utente. Molti comandi vengono dati al Sistema Operativo tramite istruzioni di controllo (o comandi di utente) che si occupano di Creazione e gestione di processi
40 2.4. SERVIZI E FUNZIONALITÀ DI UN SISTEMA OPERATIVO 27 Gestione dell I/O Gestione dello spazio in memoria secondaria Gestione dello spazio in memoria primaria Accesso ad un file system Protezione Networking Il programma che legge e interpreta le istruzioni di controllo è chiamato in vari modi: Interprete di schede di controllo (control-card interpreter) Interprete di riga di comando, o di linguaggio di comando (command line interpreter, o command language interpreter, o CLI) Shell (in Unix) La sua funzione è di prelevare il prossimo comando ed eseguirlo. All atto pratico, gli interpreti possono essere di vario tipo, spesso molto diversi tra loro: Basati su schede perforate, Con interfaccia a carattere, Con interfaccia grafica ad icone e mouse, e così via. Le interfacce grafiche a icone e mouse sono particolarmente ergonomiche e molto facili da usare, specialmente per utenti non specialisti; le interfacce a carattere, in particolare a riga di comando, sono molto potenti e versatili. 2.4 Servizi e funzionalità di un Sistema Operativo Con il termine servizi si indicano funzionalità messe a disposizione dell utente da parte del Sistema Operativo,, per aiutarlo e assisterlo nel suo lavoro. I servizi più importanti sono i seguenti: Esecuzione di programmi capacità del sistema di caricare un programma in memoria e farlo eseguire.
41 28 CAPITOLO 2. STRUTTURA DEI SISTEMI OPERATIVI Operazioni di I/O dato che i programmi utente non possono (e non debbono) eseguire operazioni di I/O direttamente, il Sistema Operativo deve fornire qualche mezzo per effettuare dell I/O, come già discusso brevemente nel Paragrafo pag. 9 a proposito dei sistemi multiprogrammati. Manipolazione di filesystem capacità di leggere, scrivere, creare, rimuovere, e rinominare dei file. Comunicazione scambio di informazioni tra processi che si eseguono sulla stessa macchina, o su sistemi differenti uniti insieme da una rete. Implementato tramite memoria condivisa (shared memory) o passaggio di messaggi (message passing). Rivelazione di errori assicurare un corretto comportamento di calcolo controllando errori nella CPU, RAM, dispositivi di I/O, o programmi d utente. Esistono poi altre funzionalità, concepite non tanto per aiutare e servire gli utenti, ma piuttosto per assicurare operatività ed efficienza al sistema; tra queste si citano: Allocazione di risorse allocare risorse a più utenti o a più processi che si eseguono contemporaneamente. Contabilizzazione (in inglese, accounting) tenere traccia e memorizzare quali utenti utilizzano quante risorse e di che tipo, per rifatturare i costi a terze parti o anche soltanto per accumulare statistiche d uso. Protezione assicurare che tutti gli accessi alle risorse di sistema siano controllate (file, regioni di memoria, periferiche, CPU, e così via) Panoramica delle chiamate di sistema Il Nucleo è definibile come la parte residente di un Sistema Operativo (cioè che è sempre in memoria e sempre in grado di eseguirsi), ma anche, più propriamente, come la parte che gestisce e coordina l avanzamento dei processi, notifica ad essi il verificarsi di eventi, e in generale fornisce loro una serie di funzionalità e servizi per permettere il corretto svolgimento dei programmi. Chiaramente, il Nucleo non è un processo esso stesso ma costituisce la parte più critica dell intero Sistema Operativo. Come si vedrà in un successivo capitolo, alcune parti (le meno critiche) del Sistema Operativo possono essere realizzate tramite appositi processi, mentre particolare attenzione va posta nel progettare un Nucleo affidabile ed efficiente. La comunicazione tra i processi ed il Nucleo avviene tramite un insieme predefinito di funzioni che prendono il nome di chiamate di sistema (in inglese, system call). L insieme di chiamate di sistema disponibili costituisce in un certo senso il biglietto da visita del Sistema Operativo: ogni servizio aggiuntivo offerto da un
42 2.4. SERVIZI E FUNZIONALITÀ DI UN SISTEMA OPERATIVO 29 Carica argomenti; trap 15 codice di servizio della chiamata n. 15 Programma utente; User mode Nucleo; Kernel mode Figura 2.1: Schema dell esecuzione di una chiamata di sistema mediante Trap. Sistema Operativo, ad esempio una gestione sofisticata di file oppure uno schema di protezione rivolto a gruppi di utenti, dà luogo ad apposite chiamate di sistema. Tali funzioni devono essere considerate come delle porte di accesso al Nucleo: un processo non può richiedere di eseguire un generico programma del Nucleo ma può soltanto richiedere di eseguire una delle chiamate di sistema esistenti. Le chiamate di sistema sono dunque il meccanismo principe tramite il quale il Sistema Operativo mette a disposizione dei processi utente i servizi e le funzionalità visti nel paragrafo precedente; esse vengono normalmente implementate mediante il meccanismo delle trap (interruzioni software), che trasferisce il controllo dal programma utente al Nucleo e viceversa. Tale meccanismo è illustrato brevemente nella Figura 2.1. Il Nucleo agisce come una routine di servizio dell interruzione, che al termine del suo compito ripassa il controllo al programma utente. La trap è un istruzione a livello linguaggio macchina, normalmente non disponibile da linguaggi di alto livello; pertanto il sistema fornisce sempre un corredo di funzioni intermedie di libreria, richiamabili da linguagio C o da altri linguaggi, che permettono l aggancio a queste funzionalità. Rimandando ad altri capitoli 10 la trattazione di dettaglio, è opportuno evidenziare che questo meccanismo, perfettamente analogo a quello di un interruzione hardware di un dispositivo, presenta il vantaggio essenziale di permettere alla CPU il passaggio da un modo di funzionamento controllato (User mode, in cui si esegue il codice dei programmi utente) a un modo privo di controlli hardware (System mode, nel quale la CPU può eseguire istruzioni privilegiate) e viceversa. Ciò, come si vedrà in seguito, è alla base dei meccanismi di protezione e di controllo del Nucleo sui processi utenti. Ciò detto, i principali tipi di chiamate di sistema possono così riassumersi: Controllo di processi create process, terminate process; load, execute program; abort (core dump) 10 Si vedano i Paragrafi 3.4 pag. 40 e 10.6 pag. 163.
43 30 CAPITOLO 2. STRUTTURA DEI SISTEMI OPERATIVI get/set process attributes wait for time; wait event, signal event allocate/free memory Manipolazione di file create, delete, rename file; open, close; read, write, reposition (seek) get/set file attributes Informazione sul sistema get/set time or date get/set system data (active processes, open files, etc.) get/set process, file, or device attributes Comunicazioni get host-id, process-id create, accept, delete communication connection send, receive messages transfer status information attach/detach remote devices Manipolazione di periferiche request device, release device read, write, reposition get/set device attributes logically attach/detach devices Chiamate di sistema e API Spesso, a fianco delle chiamate di sistema, viene introdotto un ulteriore strato di software tra i programmi degli utenti e quelli del Nucleo: si tratta di funzioni di tipo API (Application Programming Interface) contenute in apposite librerie di programmi. Tali funzioni, che rendono più agevole la richiesta di servizi al Nucleo, includono di norma una o più chiamate di sistema. La Figura 2.2 illustra le interazioni tra programma utente, API, chiamata di sistema e Nucleo: in tale figura si fa riferimento alla API malloc() usata dai programmatori C per ottenere memoria dinamica. Tale funzione non è una chiamata di sistema bensì una API, che utilizza la chiamata di sistema brk() per ottenere memoria dinamica dal Nucleo. In conclusione, il ruolo della libreria API è essenzialmente quello di facilitare la richiesta di servizi al Nucleo: le API possono essere considerate come delle chiamate di sistema ad alto livello che risultano più facili da utilizzare delle chiamate di sistema vere e proprie.
44 2.5. IMPLEMENTAZIONE DI UN SISTEMA OPERATIVO malloc() brk() libreria API NUCLEO programma applicativo Figura 2.2: API, chiamate di sistema e Nucleo. 2.5 Implementazione di un Sistema Operativo Al livello più alto, il progetto di un Sistema Operativo è influenzato da vari fattori, tra cui Scelta dell hardware: tipo di CPU adottata (Intel 80x86, Motorola 60xxx/88xxx, SUN Sparc/UltraSpark, MIPS, etc.); tipi di bus utilizzati (EISA, PCI, VMEbus, etc.); tipi di periferiche, e così via. Tipo di sistema: batch/time-sharing, single/multiuser, mono/multitask, distribuito, real-time, general-purpose. Nell iniziare a progettare il Sistema Operativo, i responsabili devono tener presente una serie di obiettivi, che spesso non sono tutti in armonia tra loro; ciò dipende dai differenti punti di vista che occorre considerare: Dal punto di vista dell utente, il Sistema Operativo deve essere pratico da usare, facile da imparare, affidabile, sicuro, e veloce. Dal punto di vista del sistemista, il Sistema Operativo deve essere facile da progettare, da implementare e da manutenere; deve essere inoltre flessibile, affidabile, privo di errori, ed efficiente. Dagli obiettivi scelti si derivano appropriate metriche prestazionali (vedi Paragrafo pag. 12), che verranno utilizzate per l individuazione degli algoritmi da adottare. Naturalmente, non esiste una soluzione unica al problema: questo è un compito creativo, affidato anche all esperienza pregressa, alla mentalità e ai gusti personali del progettista. L ingegneria del software (software engineering) può fornire dei principi generali a cui rifarsi, utili da applicare in questo compito. Tradizionalmente, i Sistemi Operativi erano scritti in linguaggio assembler. Oggi sono invece scritti di norma in linguaggi di alto livello, salvo alcune parti limitate;
45 32 CAPITOLO 2. STRUTTURA DEI SISTEMI OPERATIVI il Sistema Operativo Unix è stato il primo sistema ad adottare questa scelta 11, e negli anni ha aperto la strada a questa tendenza. Il codice scritto in un linguaggio di alto livello presenta un unico svantaggio, quello della maggior lentezza, ma molti vantaggi importanti: È molto più veloce da scrivere Può essere scritto da meno persone, quindi può essere più coerente (Unix, in origine, è stato scritto in tutto da due persone, Ken Thompson e Denis Ritchie dei Bell Laboratories). È più compatto È più facile da capire e da correggere (debugging). Inoltre, un Sistema Operativo scritto in linguaggio di alto livello è molto più facile da portare (cioè da adattare ad una nuova piattaforma hardware), poiché le uniche parti da riscrivere interamente sono quelle in assembler, che come si diceva rappresentano una minima parte del totale. A titolo di esempio, il Nucleo originale di Unix era composto in tutto da righe di codice in C, e in assembler; di quest ultima parte, 200 righe lo erano per motivi di efficienza, e 800 perché impossibili da scrivere in linguaggio C (gestione dell unità di Memory Management, il codice di avvio di un interrupt hardware, e poche altre cose). Quanto allo svantaggio accennato, la lentezza è ovviamente sempre meno un problema, dati i progressi enormi a cui si assiste continuamente nell evoluzione dell hardware; e inoltre è largamente provato dall esperienza che per rimediare a ciò occorre e basta ottimizzare solo piccolisime parti del codice (quelle effettivamente critiche dal punto di vista delle prestazioni), scrivendole in assembler o scrivendole dapprima in linguaggio di alto livello e poi adattando manualmente il codice assembler generato dal compilatore. 2.6 La programmazione di sistema Al termine di questa panoramica introduttiva sui Sistemi Operativi, è opportuno evidenziare come il loro studio sia la base per la cosiddetta programmazione di sistema, che rappresenta una categoria di programmazione di livello più sofisticato della normale programmazione di applicativi. In effetti, oltre alla programmazione più semplice che si impara in poche settimane, esistono altri tipi di programmazione che richiedono anni di duro impegno per poterli dominare correttamente. Questo fatto è evidenziato nel mondo produttivo da apposite qualificazioni professionali: il programmatore è nella gerarchia aziendale qualcosa di assai diverso dal programmatore di sistemi. Sorvolando su altri tipi di classificazione possibili, si può asserire che esistono tre grandi livelli di programmazione: 11 Gli autori di Unix hanno concepito e messo a punto il linguaggio C con lo scopo appunto di scrivere questo Sistema Operativo in linguaggio di alto livello.
46 2.6. LA PROGRAMMAZIONE DI SISTEMA indipendente dall hardware e dal Sistema Operativo 2. dipendente dal Sistema Operativo ma indipendente dall hardware 3. dipendente dal Sistema Operativo e dall hardware Programmi indipendenti dall hardware e dal Sistema Operativo Il primo livello è ovviamente quello che garantisce la massima portabilità dei programmi tra piattaforme hardware e software di tipo diverso. Le specifiche di molti linguaggi ad alto livello quali Fortran, C o C++ sono state rigidamente definite da appositi comitati di standardizzazione. In conseguenza, risulta possibile scrivere programmi che possono essere compilati ed eseguiti correttamente su calcolatori di tipo diverso e/o su calcolatori che usano Sistemi Operativi di tipo diverso. Un altro classico esempio di linguaggio che offre un ottima portabilità dei programmi è il linguaggio Java, oggi molto usato nel contesto Internet. In effetti molte delle pagine che si possono scaricare da un sito remoto Internet includono non solo informazioni grafiche nel formato html, ma anche programmi scritti in linguaggi quali Javascript o Java. Quando una di tali pagine viene scaricata da un utente sulla propria macchina client, il programma viene copiato dalla macchina remota ed eseguito sulla macchina locale. Per ragioni di portabilità e di sicurezza, i programmi Java subiscono questo trattamento: i programmi vengono dapprima compilati; a partire dal codice sorgente, il compilatore Java genera un codice proprio, detto bytecode, indipendente dalla macchina su cui il programma deve eseguirsi la forma compilata in bytecode viene trasferita sulla macchina che deve eseguire il programma (in questo caso, sulla macchina client dell utente) una volta giunti a destinazione, tali programmi in bytecode sono interpretati dalla macchina virtuale Java 12, ossia dal calcolatore dotato di un interprete Java. Pertanto la portabilità di un programma Java è vera non solo a livello sorgente, ma anche a livello di bytecode compilato. Programmi dipendenti dal Sistema Operativo ma indipendente dall hardware Per quanto versatili siano i linguaggi di programmazione sviluppati negli ultimi anni, essi non consentono di accedere direttamente ad informazioni gestite dal Sistema Operativo. Ciò viene fatto di proposito per rendere il linguaggio di programmazione indipendente dal Sistema Operativo. Vi sono tutttavia casi in cui il programmatore deve potere avere accesso ad informazioni gestite dal Sistema Operativo, ad esempio per leggere informazioni 12 In inglese: Java Virtual Machine, o JVM.
47 34 CAPITOLO 2. STRUTTURA DEI SISTEMI OPERATIVI riguardanti file, quali la data dell ultima modifica oppure la lunghezza di un file espressa in byte. Quando ciò si verifica, è necessario fare uso di apposite funzioni di interfaccia offerte dal Sistema Operativo. Questo tipo di programmazione, chiamata programmazione di sistema, consente di sviluppare programmi più sofisticati a scapito però della portabilità: un programma di sistema potrà essere ricompilato ed eseguito correttamente su macchine diverse, purché esse usino tutte lo stesso Sistema Operativo (Windows NT, Linux, AIX, ecc.) Programmi dipendenti dal Sistema Operativo e dall hardware Esiste un terzo livello di programmazione (riservato a pochi specialisti) a cui è necessario ricorrere quando la programmazione di sistema non è sufficiente. Ciò avviene quando si richiede di scrivere un nuovo Sistema Operativo, oppure quando si richiede di ampliare le funzionalità di un Sistema Operativo esistente. In tale caso, ovviamente, non è possibile avvalersi di apposite funzioni di interfaccia ma, al contrario, viene richiesto al programmatore di realizzarne delle nuove. Un caso classico è quello della realizzazione dei cosiddetti driver di I/O per nuove periferiche. Quando viene immessa sul mercato una nuova apparecchiatura periferica (masterizzatore, scheda audio, ecc.), è necessario aggiungere al Sistema Operativo prescelto un nuovo driver per la periferica, in modo che essa possa essere riconosciuta dal Sistema Operativo. Come vedremo alla fine del corso, i driver di I/O sono dei programmi inclusi nel Sistema Operativo che interagiscono con la scheda hardware di interfaccia del dispositivo di I/O. In questo senso, sono programmi aventi un grado di portabilità praticamente nullo, in quanto possono essere eseguiti soltanto su piattaforme hardware dello stesso tipo che fanno uso dello stesso Sistema Operativo.
48 Capitolo 3 Struttura dei Calcolatori Elettronici 3.1 Introduzione Una presentazione sistematica ed esauriente della struttura dei calcolatori elettronici e della loro organizzazione esula dagli scopi del presente testo. Questo capitolo pertanto non ha alcuna pretesa di completezza: si limita di fatto a dare una brevissima vista d insieme dell architettura hardware dei calcolatori, e a presentare soltanto alcuni concetti sul loro funzionamento, utili a comprendere appieno i meccanismi presentati nei capitoli seguenti a proposito dei Sistemi Operativi. Per maggiori dettagli, il lettore interessato è invitato a consultare un testo specializzato sull argomento, quale in particolare l ottimo testo di Tanenbaum [15]. 3.2 Architettura dei sistemi di calcolo L architettura generica di un sistema di calcolo è mostrata nella Figura 3.1. Si distinguono le seguenti parti principali: Un bus di sistema (system bus), che trasporta i segnali elettrici che governano il sistema hardware, e che mettono in comunicazione le varie parti del calcolatore. Tutte le parti seguenti sono interfacciate fisicamente con questo bus, che sostituisce pertanto l infrastruttura portante su cui il calcolatore è basato. La CPU (Central Processing Unit, Unità Centrale). È il cuore del sistema di calcolo; nei sistemi multiprocessore, ovviamente, vi sarà più di una CPU. Un insieme variabile di controllori di periferiche, o controllori di I/O (I/O controllers), interfacciati sul bus e connessi fisicamente alle relative periferiche (dischi, stampanti, linee seriali e così via). 35
49 36 CAPITOLO 3. STRUTTURA DEI CALCOLATORI ELETTRONICI dischi linee seriali stampante contr. disco contr. seriale contr. stampante bus contr. RAM RAM Figura 3.1: Architettura di un sistema di calcolo. Una quantità variabile di RAM Random-Access Memory (memoria ad accesso casuale); è la normale memoria di lettura/scrittura, indispensabile per tutte le operazioni dei calcolatori. È un dispositivo volatile, indirizzabile direttamente byte per byte da parte della CPU e dei controllori di I/O. La RAM contiene, nel corso dell esecuzione di un programma, sia le istruzioni (che vengono prelevate via via dalla CPU per essere eseguite) che i dati. Un controllore di memoria RAM (Memory controller), che interfaccia la RAM sul bus di sistema; il suo ruolo è tra l altro quello di risolvere contese di accesso alla RAM da parte della CPU e dei vari processori periferici, e sincronizzarne le operazioni. Una quantità (normalmente limitata a poche centinaia o migliaia di byte) di memoria ROM Read-Only Memory (memoria di sola lettura); si tratta di un dispositivo, indirizzabile come la memoria RAM, che contiene dati fissati una volta per tutte e quindi inalterabili; una porzione di ROM contiene normalmente, in un calcolatore, il codice da eseguire all accensione fisica del sistema (si veda Paragrafo 10.3 pag. 160) per inizializzare la macchina e accedere subito dopo ai programmi memorizzati sui dischi. Ciascuna CPU contiene due grandi blocchi al suo interno: La ALU (Arithmetical-Logical Unit, Unità Logico-Aritmetica), illustrata in Figura 3.2. È dotata a sua volta di una Unità Funzionale (Function Unit), che esegue le varie operazioni, e di alcuni registri di uso generale (il loro numero
50 3.2. ARCHITETTURA DEI SISTEMI DI CALCOLO operando 2. operando Registri Function Unit risultato Figura 3.2: Struttura della ALU. e ruolo varia considerevolmente a seconda delle architetture hardware), ed effettua operazioni elementari di trattamento dei dati (somme, moltiplicazioni, operazioni logiche bit a bit, etc.) sul contenuto dei registri (un operazione tipica prende uno o due registri come operandi, e scrive in un registro il risultato dell operazione). La CU (Control Unit, Unità di Controllo). È la parte che coordina le operazioni della CPU, e in particolare si occupa dell insieme di passi necessari per il prelievo (dalla RAM) della prossima istruzione da eseguire, e appunto per la sua esecuzione (tale ciclo basilare prende il nome di instruction fetch and execute cycle, ovvero ciclo di prelievo ed esecuzione di istruzioni). La CPU svolge il suo lavoro caricando valori dalla RAM in registri, effettuando su questi le operazioni specificate dalle istruzioni del programma, e salvando poi i risultati dai registri in memoria RAM. Ad esempio, si dà qui di seguito un possibile codice assembler corrispondente alla somma di due variabili: ; codice corrispondente all istruzione: a = b + c R0 ; copia il valore dell indirizzo "b" ; di memoria nel registro R0 R1 ; analogamente, copia "c" in R1 add R0, R1 ; effettua la somma, che e posta in R1 mov ; memorizza il risultato nell indirizzo "a" Tra i registri di uso generale, che si trovano nella ALU, va ricordato in particolare lo SP (Stack Pointer), che serve a gestire uno stack, cioè una porzione di memoria utilizzata secondo un meccanismo LIFO (Last-In First-Out) vedi l Appendice 3.9 al capitolo; i principali registri che si trovano nella CU sono invece i seguenti: PC (Program Counter), IR (Instruction Register), PSW
51 38 CAPITOLO 3. STRUTTURA DEI CALCOLATORI ELETTRONICI (Processor Status Word). Il primo contiene l indirizzo (in RAM) della prossima istruzione da eseguire, il secondo contiene l istruzione stessa, letta dalla RAM e da decodificare per l esecuzione; il PSW contiene vari bit, posizionati dopo ogni instruzione eseguita: Zero, Sign, Carry, Overflow e così via; questi bit, denominati condition code, vengono posizionati al termine di ogni istruzione, e sono utilizzati per istruzioni condizionali. Ad esempio, un istruzione del tipo jz (Jump if zero) provocherà il salto all istruzione specificata in argomento se e solo se il contenuto del registro interessato dall ultima istruzione eseguita è pari a 0 (in tal caso il bit Zero del PSW si troverà posizionato alto 1 ). Ciascun controllore di I/O è fisicamente una scheda con componenti elettronici a bordo. Il controllore dialoga da una parte con i dispositivi fisici a cui è connesso (un unica scheda può controllare più dispositivi dello stesso tipo; ad esempio, un unico controllore EIDE può gestire più dischi fisici, o un unico controllore di linee seriali può gestire tipicamente 8 o più linee), e dall altra con il resto del sistema, tramite il bus. Ciascun controllore è dotato tipicamente dei seguenti componenti, alloggiati a bordo della scheda: uno o più registri di interfaccia, indirizzabili dal bus di sistema; un proprio buffer locale di memoria di lavoro; tale memoria RAM è visibile e indirizzabile soltanto all interno della scheda stessa, e non è accessibile dal bus di sistema; un proprio microprocessore dedicato, che è responsabile della logica di funzionamento del controllore e gestisce i registri di interfaccia, la memoria locale, e il dialogo con i dispositivi fisici collegati. I microprocessori a bordo dei vari controllori di I/O verranno chiamati processori periferici, in contrapposizione alle CPU, che saranno dette processori centrali. Da quanto detto consegue che tutti i sistemi di calcolo moderni sono in realtà dei sistemi multiprocessore, con diversi processori che lavorano in parallelo: una o più CPU (processori centrali), che lavorano sulla memoria centrale; più processori periferici (uno per ogni controllore di I/O), che lavorano sulla memoria locale (privata) e sulla memoria centrale. Infatti i dispositivi di I/O (i processori periferici) e la o le CPU (i processori centrali) possono eseguire codice in modo concorrente: per un operazione di I/O, la CPU muove dati tra la memoria centrale e i registri di interfaccia del controllore, sul bus di sistema; contemporaneamente, il processore periferico muove i dati tra i registri di interfaccia sul bus e i buffer locali dei controllore; 1 Bit di questo genere sono sostanzialmente variabili booleane, e possono essere posizionati ad uno tra due valori possibili: 1 (corrispondente al valore vero) o 0 (valore falso); si parla in tal caso, rispettivamente, di bit posizionato alto (valore 1) e posizionato basso (valore 0).
52 3.3. INTERRUZIONI HARDWARE 39 l I/O vero e proprio avviene in un secondo tempo, tra la periferica fisica (ad esempio il disco fisico) e il buffer locale del controller. Il controller del dispositivo informa poi a tempo debito la CPU che ha finito la sua operazione; per far ciò provoca un interruzione. 3.3 Interruzioni hardware Le interruzioni (interrupt) sono procedure previste a livello hardware dalla circuiteria elettronica della CPU e da opportuni segnali del bus; sono provocate normalmente da un dispositivo esterno (come appunto un controllore di I/O), ed hanno per effetto il trasferimento del controllo, nello svolgimento delle istruzioni, ad una specifica routine di servizio dell interruzione, il cui indirizo di partenza viene memorizzato in precedenza in una locazione fissa di memoria. Poiché possono esistere più tipi di interruzione, ciascuno dei quali ha una routine di servizio dedicata, si utilizza di un vettore di interruzioni (interrupt vector), inizializzato all avvio del sistema, che contiene gli indirizzi di tutte le routine di servizio. Ciascun interrupt è contraddistinto da un numero di interrupt (interrupt number), che è in pratica una sorta di argomento dell interrupt stesso, e tale numero viene utilizzato per indicizzare il vettore di interrupt e trovare così l indirizzo della routine giusta. Un interrupt avviene in modo totalmente asincrono e imprevedibile per la CPU, che al momento dell interruzione sta eseguendo una qualunque istruzione. La circuiteria di interrupt (nella CPU) deve incaricarsi di salvare l indirizzo dell istruzione interrotta (che andrà ripresa dall inizio, al termine della routine di servizio dell interrupt) ed altre informazioni. Durante l esecuzione della routine di servizio, ulteriori richieste di interruzioni che arrivano alla CPU vengono disabilitate, per prevenire la perdita di informazioni 2. Si aggiunge ancora per completezza un altro aspetto importante, già brevemente anticipato nel Paragrafo pag. 28 e su cui si tornerà nei prossimi paragrafi. La CPU può lavorare in uno tra due modi distinti: un modo prilegiato, detto modo Sistema o System mode, nel quale non ci sono restrizioni hardware per le istruzioni eseguibili, e un modo di funzionamento controllato (non privilegiato), detto modo Utente o User mode, dove solo un sottoinsieme definito di istruzioni è eseguibile dal programma; all inizio di un interrupt, la CPU passa automaticamente a lavorare in System mode, conserva questo modo per tutta la durata dell esecuzione della routine di servizio, e alla fine ritorna a lavorare in User mode. Riassumendo, la cronologia degli eventi nello svolgimento di un interrupt hardware è la seguente: la circuiteria esterna, su richiesta di una periferica, asserisce un segnale di richiesta di interrupt; 2 Più precisamente, le interruzioni sono organizzate secondo una gerarchia di priorità; durante il servizio di un interrupt, vengono abilitate soltanto le interruzioni di priorità maggiore (che quindi potranno interrompere a loro volta, provocando un annidamento dell esecuzione delle routine di servizio), mentre quelle di priorità inferiore o uguale all interruzione attualmente in servizio vengono disabilitate.
53 40 CAPITOLO 3. STRUTTURA DEI CALCOLATORI ELETTRONICI la CPU interrompe l istruzione che stava eseguendo, e riscontra l interrupt; come parte di questa fase, che si svolge a livello hardware, la circuiteria esterna 3 inserisce sul bus il numero di interrupt, che viene letto dalla CPU; la CPU salva in uno stack di sistema il contenuto del PSW e di altri registri, e poi del PC (in questo modo viene salvato il cosiddetto contesto di esecuzione attuale, essenziale per poter riprendere successivamente l esecuzione del programma interrotto); la CPU pone se stessa in modo privilegiato, e altera la propria priorità in modo da non accettare altri interrupt di priorità inferiore o uguale a quella dell interrupt che si sta servendo; il numero di interrupt viene utilizzato per indicizzare il vettore di interrupt e trovare così l indirizzo di partenza del codice della routine di servizio dell interrupt; tale indirizzo viene caricato nel PC (questa sarà quindi la prossima istruzione eseguita); parte quindi l esecuzione del codice della routine di servizio dell interrupt; questo codice è di fatto parte del Nucleo del Sistema Operativo; al termine della routine, la CPU preleva dallo stack il valore originale del PC, del PSW e degli altri registri (recupero del contesto di esecuzione originale), pone se stessa nuovamente in modo non privilegiato, e posiziona nuovamente la propria priorità al valore che aveva al momento dell inizio dell interrupt 4 ; l istruzione che era stata interrotta al momento dell interrupt viene ripresa da capo dalla CPU. 3.4 Interruzioni software Il sistema delle interruzioni hardware visto nel paragrafo precedente permette tra l altro, come visto, di interrompere il normale flusso di istruzioni della CPU in modo non privilegiato (al servizio di un programma utente) per passare ad eseguire istruzioni in modo privilegiato (eseguendo codice del Nucleo del Sistema Operativo). Parallelamente al sistema visto, esiste un secondo sistema di interruzioni, basato però su apposite istruzioni software della CPU, di tipo trap n (o int n il nome specifico dipende dal tipo di CPU utilizzata), denominate genericamente trap. Una trap è un interrupt generato via software, causato o da un errore o da una richiesta esplicita di un programma utente. Anche una trap ha un numero di interrupt, che è in questo caso semplicemente l argomento n dell istruzione. Come si vedrà meglio nel Capitolo 10, un Sistema Operativo è interrupt driven (guidato ad interrupt), intendendo con ciò che il Nucleo è costituito di fatto da 3 Questa circuiteria, come si può capire dalla descrizione fatta, non è quindi tanto banale, poiché deve essere in grado di gestire i vari segnali del bus; in effetti si utilizzano dei chip dedicati, che prendono il nome di Programmable Interrupt Controller (PIC ). 4 Buona parte di quanto descritto in questa fase avviene tramite l esecuzione di un apposita istruzione macchina, di solito denominata rti Return from Interrupt.
54 3.4. INTERRUZIONI SOFTWARE 41 un insieme di routines che sono attivate soltanto quando si verificano interruzioni di un dato tipo, hardware o software. Le interruzioni software sono la base delle chiamate di sistema, che sono implementate dunque attraverso delle trap. Il motivo principale di tale strategia di funzionamento è la messa in opera di un sistema di sicurezza del Sistema Operativo. Trattandosi a tutti gli effetti di un interruzione, l esecuzione di un istruzione trap n fa passare la CPU dallo User mode al System mode; in questo modo è assicurata la protezione del codice del Nucleo e delle relative strutture di dati, poiché l unico modo di accedere ad esso da parte di un programma utente in esecuzione è tramite l esecuzione della suddetta istruzione trap n. La cronologia degli eventi nello svolgimento di un interrupt software è perfettamente analoga a quella già vista per un interrupt hardware, ed è qui riassunta, nel caso specifico di una chiamata di sistema: il programma utente deve chiedere un servizio specifico al Nucleo (ad esempio, la lettura di un blocco di dati da un file), e per far ciò esegue un istruzione trap n, con un argomento n (numero di interrupt) specifico per il tipo di chiamata di sistema (nel caso preso ad esempio, una chiamata read); prima di eseguire l istruzione trap, il programma provvede a memorizzare (ad esempio in alcuni registri della CPU) gli argomenti della chiamata di sistema 5 ; parte la procedura di interrupt; la CPU salva in uno stack di sistema (nel Nucleo) il contenuto del PSW e tutto il contesto di esecuzione attuale; la CPU pone se stessa in modo privilegiato (System mode), e altera la propria priorità (c è una priorità tipica del contesto esecuzione del codice di chiamata di sistema ); il numero di interrupt (l argomento dell istruzione trap) viene utilizzato per indicizzare il vettore di interrupt e trovare così l indirizzo di partenza del codice della routine di servizio della chiamata di sistema read; tale indirizzo viene caricato nel PC; parte quindi l esecuzione del codice della routine di servizio; questo codice, che è appunto parte del Nucleo del Sistema Operativo, inizia col controllare la validità degli argomenti passati alla chiamata di sistema, e in caso positivo serve la richiesta del processo utente; al termine della routine, la CPU preleva dallo stack i valori necessari per recuperare il contesto di esecuzione del processo utente, pone se stessa nuovamente in modo non privilegiato (User mode), e posiziona nuovamente la propria priorità al valore che aveva al momento dell inizio dell interrupt; per effetto del recupero del valore originale del PC, la prossima istruzione eseguita dalla CPU sarà quella seguente l istruzione trap, nel programma del processo utente, che prosegue quindi la sua esecuzione in modo sequenziale. 5 Nel caso in esempio, gli argomenti specificheranno l indicazione del file da cui prelevare i dati, quanti byte si richiede di leggere, e l indirizzo del buffer di memoria del processo utente dove si richiede che il Nucleo vada a depositare i dati letti.
55 42 CAPITOLO 3. STRUTTURA DEI CALCOLATORI ELETTRONICI Per quanto visto, l esecuzione di una routine di interrupt differisce da una normale chiamata a subroutine in almeno tre aspetti: l avvio della routine può essere asincrono rispetto all esecuzione delle istruzioni della CPU (nel caso di interruzione hardware); il codice della routine di servizio si esegue sempre in System mode; la procedura di chiamata prevede il salvataggio in uno stack (e il successivo recupero) non solo del valore attuale del PC, ma anche di altre informazioni, tra cui almeno il valore del PSW e la priorità della CPU. 3.5 I due modi della CPU La ragione di esistenza del sistema di sicurezza visto è abbastanza ovvia: il Sistema Operativo deve essere in grado di proteggere se stesso ed il resto del sistema dagli errori di un programma utente. C è in particolare la necessità di proteggere il Sistema Operativo, gli altri programmi in esecuzione, e tutte le risorse condivisibili, tra cui la RAM e i dispositivi di I/O. Si pensi al caso di un puntatore incrementato senza controllo, per un errore di logica di programmazione in un processo utente: il puntatore finirebbe per scrivere sul codice o sui dati di un altro programma caricato in memoria, o ancora del Nucleo stesso, e tutto il sistema finirebbe per corrompersi. In tali condizioni, nulla può essere garantito intermini di corretta esecuzione di un programma, e nemmono in termini di integrità dei dati custoditi su disco. La soluzione, come visto, consiste nel fornire un supporto hardware per distinguere almeno due modi diversi di operare (dual mode operation): User mode (modo Utente) la CPU esegue istruzioni per conto di un processo utente; System mode (modo Sistema; detto anche Monitor mode, o Supervisor mode) la CPU esegue istruzioni per conto del Sistema Operativo. La differenza tra i due modi è che alcune istruzioni del linguaggio macchina, dette istruzioni privilegiate, sono eseguibili soltanto in System mode. Il sistema, all accensione fisica, si avvia sempre in System mode, carica il Nucleo, quindi avvia i programmi utente in User mode. Il codice del Sistema Operativo è eseguito sempre in System mode. Per gestire questo meccanismo viene utilizzato un bit specifico, il bit di modo (della CPU), agggiunto all hardware del calcolatore per indicare il modo attuale: System (bit posizionato a 0) o User (posizionato a 1). Quando si verifica un interrupt o un trap, come visto, l hardware commuta automaticamente in System mode; all uscita del codice di servizio dell interruzione, il bit di modo viene riposizionato a 1 (User mode).
56 3.6. PROTEZIONI HARDWARE Protezioni hardware Sono basate sul meccanismo del dual mode (System mode User mode). Tre aree principali: protezione della CPU; protezione della RAM; protezione dell I/O Protezione della CPU Per il buon funzionamento del sistema di calcolo nella sua globalità, occorre far sì che nessun processo possa tenere la CPU indefinitamente in User mode. Lo strumento di base per la messa in opera di questa limitazione è un timer, cioè un dispositivo hardware che genera un interruzione dopo un periodo di tempo specificabile (tale periodo prende il nome di quanto di tempo, o time quantum, o timeslice). Ciò garantisce che il Sistema Operativo riesca a mantenere il controllo della situazione: il timer è inisializzato dal Nucleo prima di passare il controllo della CPU ad un processo utente; viene poi decrementato ad ogni clock tick (l intervallo minimo di tempo distinguibile dal calcolatore; di solito vale 1/100 di sec.); quando il valore arriva a 0, si ha un interrupt. Il Nucleo prende allora il controllo della CPU, ed esamina la situazione. Se necessario (se cioè il processo utente che ha attualmente la CPU ha utilizzato questa risorsa per un periodo sufficientemente lungo, ed altri processi devono poter accedere a questa risorsa), viene eseguito un cambio di contesto (context switch), e il controllo della CPU viene dato ad un altro processo. Questa tecnica è tipicamente usata nei sistemi time-sharing; il timer è usato inoltre anche per calcolare la data e ora attuale. Il caricamento di un timer, ovviamente, èun istruzione privilegiata, quindi eseguibile soltanto in System mode dal codice del Nucleo: se così non fosse, sarebbe possibile per un processo scavalcare questo meccanismo di sicurezza, attribuendo a se stesso un quanto di tempo altissimo Protezione della memoria Tale protezione è ovviamente necessaria per evitare che un programma errato possa scrivere nelle aree di memoria di altri processi e/o del Sistema Operativo, o ancora per evitare che un programma vada ad accedere a dati di un altro programma contemporaneamente in esecuzione, il che annullerebbe la privatezza della gestione dei dati di ciascun processo. Occorrono concettualmente almeno due registri dedicati, che memorizzino l intervallo di memoria fisica che un processo può accedere; ad esempio, un registro base (che contiene l indirizzo di partenza della zona permessa);
57 44 CAPITOLO 3. STRUTTURA DEI CALCOLATORI ELETTRONICI un registro limite (che contiene la dimensione in byte della zona permessa). Chiaramente i valori dei due registri sono diversi per ciascun processo caricato in memoria; sfruttando questo meccanismo, il Nucleo posiziona ai valori opportuni i due registri ogni volta che dà il controllo della CPU ad un processo, in modo che tutta la memoria al difuori dell intervallo permesso sia protetta. L hardware che gestisce questa protezione è denominato circuito di paginazione (in inglese, MMU Memory Management Unit), e può essere integrato fisicamente nella CPU o essere alloggiato in un chip fisicamente separato; esso fa sì che ogni tentativo di accesso alla memoria protetta, intenzionale o no, provochi automaticamente una trap di page fault (violazione di paginazione). Ciò provoca l intervento del Nucleo, in System mode, con la routine di servizio di questa trap: il Nucleo esamina la situazione, determina che il processo è responsabile (quindi c è un errore nel programma), e provvede alla terminazione forzata del processo. Le istruzioni di caricamento dei due registri di MMU sono istruzioni privilegiate. La spiegazione data è in realtà molto semplicistica, e serve solamente a far capire il tipo di meccanismo che il Nucleo di un Sistema Operativo può attivare; le cose in realtà sono alquanto più complesse, e verranno presentate in modo più dettagliato nel Capitolo Protezione dell I/O Infine, in un sistema multitasking occorre disciplinare l accesso alle periferiche, come già accennato, per evitare conflitti tra processi. Pertanto tutte le istruzioni di I/O sono istruzioni privilegiate; in tal modo un processo è obbligato a far ricorso ai servizi del Nucleo (tramite apposite choamate di sistema) per effettuare operazioni di I/O. Nel caso di periferiche che utilizzano la tecnica dell I/O mappato in memoria, occorrerà allora far ricorso alla protezione di memoria in modo da rendere sempre protetta, per tutti i processi utente, la zona (o le zone) di indirizzamento in cui i registri dei dispositivi sono mappati. Tra le precauzioni che è necessario prendere, per questo come per gli altri tipi di protezione visti, si ricorda che occorre ovviamente garantire che un programma utente non possa mai prendere il controllo del calcolatore in System mode. 3.7 Acceso diretto alla memoria (DMA) Il meccanismo degli interrupt hardware è adeguato nel caso di dispositivi a trasferimento relativamente lento, quale ad esempio una linea seriale, che genera un interrupt per ciascun carattere inviato o ricevuto; ad esempio ad ogni interrupt per l arrivo di un carattere la CPU preleva il carattere dai registri di interfaccia del controller, lo scrive in memoria RAM, inserendolo in un opportuna struttura di dati, quindi ritorna dall interrupt. Con una linea a bps (bit al secondo), si trasferiscono circa 1920 byte al secondo 6 ; si può quindi avere un byte letto 6 Con una linea asincrona (standard RS-232), un byte trasferito implica di norma 8 bit di dati + 1 start bit + 1 stop bit, cioè 10 bit in tutto.
58 3.8. DISPOSITIVI DI MEMORIZZAZIONE 45 o scritto al massimo ogni 1/1920 sec. = 520 µsec. la routine di interrupt dura qualche µsec o anche meno, quindi occupa ben meno dell 1% del tempo disponibile della CPU, anche a carico massimo. Il sistema si rivela invece inadeguato per dispositivi ad alta velocità, che trasferiscono dati a ritmi prossimi a quelli di velocità della memoria e del bus. Il caso tipico è quello dei dischi. Questi dispositivi utilizzano pertanto una tecnica più potente del semplice interrupt, denominata DMA (Direct Memory Access, o Accesso Diretto alla Memoria). Secondo questa tecnica, il controllore di I/O trasferisce blocchi di dati dal buffer locale direttamente nella RAM, senza intervento della CPU; viene generato solo un unico interrupt per tutto un blocco di dati, al termine del trasferimento, anziché uno per ogni byte. La cronologia degli eventi è essenzialmente la seguente: la CPU, nell ambito del servizio di una chiamata di sistema (si veda l esempio fatto in precedenza), deve richiedere la lettura di un blocco di dati da un disco; alloca allora un area di memoria, nello spazio di indirizzamento del Nucleo, destinata a contenere tali dati; la CPU invia ai registri di interfaccia del controllore del disco le informazioni su: blocco fisico del disco da leggere; quantità di byte da leggere dal disco e trasferire in memoria RAM; indirizzo di RAM a partire dal quale i dati dovranno essere depositati dal controllore. il controllore del disco esegue il compito ricevuto, controllando direttamente il bus e depositando i dati in RAM come richiesto; al termine dell operazione, un unico interrupt viene generato dal controllore, per informare la CPU che tuti i dati sono ora disponibili in RAM. In questo caso, i processori centrali (CPU) e i processori periferici sono in competizione tra loro per ottenere l accesso e il controllo del bus, e quindi della memoria RAM; è qui che entra in gioco il controller della RAM, che sincronizza le richieste ed evita i conflitti tra i vari processori (centrali e periferici). 3.8 Dispositivi di memorizzazione I dispositivi che servono alla memorizzazione di dati sono essenzialmente due: la memoria primaria e la memoria secondaria. La memoria primaria è costituita dalla RAM; è l unico dispositivo di memorizzazione che la CPU può accedere direttamente, e che può indirizzare byte per byte. I registri dei controller di dispositivo normalmente sono indirizzabili attraverso istruzioni macchina specifiche (ad esempio inb e outb, che permettono
59 46 CAPITOLO 3. STRUTTURA DEI CALCOLATORI ELETTRONICI traccia perno attuatore piatti testine di lett./scritt. cilindro settori rotazione Figura 3.3: Struttura fisica di un disco. rispettivamente di leggere o di scrivere un singolo byte da o verso un registro di I/O), e pertanto il loro indirizzamento è svincolato ed indipendente da quello della RAM; altre volte invece sono mappati in indirizzi di memoria primaria, in particolari ambiti di indirizzamento; in tal caso si parla di I/O mappato in memoria (memory-mapped I/O). La memoria secondaria è un estensione della memoria primaria, che fornisce una grande capacità di memorizzazione, di tipo non volatile (si tratta in pratica dei dischi). I dischi magnetici, mostrati in Figura 3.3, sono costituiti da piatti di metallo o vetro, concentrici e sovrapposti, ricoperti da materiale magnetico. La loro superficie è logicamente divisa in tracce, suddivise a loro volta in settori (zone di memorizzazione di capacità fissa, spesso pari a 512 byte); ogni traccia contiene normalmente un numero fisso di settori. I dischi sono posti in rotazione continua, e un insieme di testine magnetiche di lettura/scrittura 7 possono spostarsi lungo un loro raggio, per andare a leggere o a scrivere i settori di una data traccia. Un cilindro è l insieme delle tracce che corrispondono ad una posizione data delle testine. Il controller del disco pone in atto l interazione logica tra la periferica e il calcolatore. Il tempo di accesso ai dati di un disco è dato dalla somma di due componenti principali: il tempo di posizionamento (seek time) delle testine sulla traccia dove si trovano i dati ricercati; la latenza di rotazione (rotational latency), ovvero il tempo impiegato dal 7 La Figura 3.3 mostra, per semplicità, soltanto le testine relative alle superfici superiori di ciascun piatto, ma in realtà vengono utilizzate entrambe le facce, quindi ci sono testine di lettura/scrittura anche per le superfici inferiori dei piatti.
60 3.8. DISPOSITIVI DI MEMORIZZAZIONE 47 registri CPU cache della CPU memoria RAM dischi rigidi CD ROM floppy disk nastri magnetici Figura 3.4: Gerarchia dei dispositivi di memorizzazione. disco, ruotando, a posizionare il settore di interesse sotto le testine di lettura/scrittura. Valori attuali per il tempo di posizionamento medio sono dell ordine di alcuni millisecondi (tra 5 e 15); la latenza di rotazione è spesso trascurabile rispetto alla prima componente. I dispositivi di memorizzazione possono essere organizzati in una gerarchia, a seconda della loro velocità; altri possibili criteri di classificazione riguardano la volatilità dei dispositivi, e il loro prezzo per unità di immagazzinamento di dati 8. La Figura 3.4 illustra quanto detto; come si vede, i dispositivi più veloci sono i registri della CPU, mentre i più lenti sono generalmente i nastri magnetici (o i floppy disk). Tra i registri della CPU e la memoria RAM c è un dispositivo intermedio, presente tipicamente nei chip delle CPU moderne: la memoria di cache Il caching Il meccanismo di caching è un concetto perfettamente generale, che si ritrova sotto vari aspetti nei sistemi di calcolo; consiste nel copiare (e memorizzare per 8 Misurato ad esempio in Euro per Megabyte, cioè il costo medio sul mercato di un dato dispositivo diviso per la sua capacità in Megabyte. A titolo informativo si danno i seguenti dati, relativi all anno 2000, derivati da Silberschatz et al. [13]: memoria RAM 74.5 Eurocent/Mbyte; nastro magnetico 2.33 Eurocent/Mbyte; disco rigido magnetico Eurocent/Mbyte.
61 48 CAPITOLO 3. STRUTTURA DEI CALCOLATORI ELETTRONICI un certo tempo) dati in un dispositivo più veloce, in modo da poterlo prelevare altre volte in minor tempo. Ad esempio, un area di memoria RAM che contiene una copia di un blocco di un disco è una cache: ogni volta che si ha bisogno di un dato di quel blocco, lo si preleva dalla RAM (molto più velocemente) piuttosto che dal disco. L algoritmo fondamentale di qualsiasi sistema di cache è il seguente: cerca il dato nella cache if (dato in cache) restituisci il dato dalla cache else cerca uno slot libero nella cache if(trovato) scegli quello slot else scegli uno slot occupato, secondo un opportuno criterio, e liberalo (perdendo il suo contenuto precedente) (algoritmo di sostituzione) endif leggi il dato dalla memoria lenta copialo nello slot della cache che e stato scelto restituisci il dato endif La dimensione della cache e l algoritmo di sostituzione degli elementi memorizzati influenzano lo hit rate, definito come la probabilità p di trovare il dato in cache, ovvero come il rapporto tra il numero di hit (accesso ad un dato trovato nella cache) e numero di accessi in totale. In effetti la situazione ideale si ottiene con una memoria di cache più estesa possibile (ma normalmente è molto più limitata della memoria che deve velocizzare), e con un algoritmo di sostituzione che vada sempre a scegliere gli slot di elementi di memoria che non saranno mai più richiesti, o richiesti nuovamente più tardi possibile. Sia T m il tempo di accesso ad una data memoria (ad esempio la RAM del sistema), e T c il tempo di accesso della memoria di cache, con T c << T m ; se p è lo hit rate, il tempo di accesso effettivo al dato in memoria (EAT - Effective Access Time) è dato in media dalla EAT = p T c + (1 p) (T m + T c ) Un esempio numerico può mostrare facilmente i benefici di una memoria di cache: sia una memoria RAM con tempo di accesso T m = 120 nsec, e una cache primaria nella CPU con tempo di accesso T c = 20 nsec; se lo hit rate è pari all 85% (p = 0.85) si avrà EAT = 0.85 x x ( ) = 38 nsec. È cruciale, come si vede, avere uno hit rate alto; ciò dipende da vari fattori, di cui i principali sono dimensione della cache;
62 3.9. APPENDICE - GESTIONE DELLO STACK 49 algoritmo di sostituzione degli elementi della cache; ripetitività degli accessi ai dati da parte della CPU. Con una oculata scelta della dimensione della cache e dell algoritmo di sostituzione, si possono ottenere tipicamente valori dello hit rate in un sistema di calcolo che vanno dall 80% al 98%. 3.9 Appendice - Gestione dello Stack Questo paragrafo fornisce, per il lettore che ne fosse sprovvisto, le conoscenze di base sul meccanismo di gestione degli stack. La presentazione degli argomenti è in forma molto semplificata. Uno stack è un area di memoria utilizzata in primo luogo per la corretta gestione delle chiamate a subroutine all interno di un programma. Si consideri il seguente scenario: una routine routinea di un dato programma chiama una routine routineb la routine routineb chiama a sua volta una routine routinec Lo pseudo-codice corrispondente può essere il seguente: routinea() {... call routineb(); } routineb() {... call routinec();... return; } routinec() {... return; } Le istruzioni call routineb() (in routinea) e return (in routineb) provocano quanto segue, a livello di codice assembler:
63 50 CAPITOLO 3. STRUTTURA DEI CALCOLATORI ELETTRONICI chiamata di routineb: routinea memorizza il valore attuale del PC (Program Counter), che contiene l indirizzo della prossima istruzione da eseguire (quella seguente l istruzione call routineb()); il PC viene impostato al valore della prima istruzione del codice di routineb; in tal modo, la prossima istruzione eseguita è l inizio di routineb; ritorno a routinea: routineb recupera il valore del PC memorizzato precedentemente da routinea, e reimposta il PC a questo valore; in tal modo, la prossima istruzione eseguita, dopo la fine di routineb, sarà quella di routinea che segue la chiamata a subroutine. Come si vede, la memorizzazione del valore del PC serve a ricordare dove si era rimasti nell esecuzione della routine chiamante, e a riprendere l esecuzione da questo punto dopo il completamento della routine chiamata. Tale memorizzazione deve avvenire in un area globale, accessibile sia dalla routine chiamante che da quella chiamata. Un modo per mettere in opera questo meccanismo è la gestione di uno stack: l area di memoria ad esso associata viene utilizzata tramite un registro, lo SP (Stack Pointer), che punta inizialmente all estremo superiore della memoria (si veda la Figura 3.5 (a)) e che viene utilizzato tramite due primitive: push e pop, rispettivamente per l inserimento e l estrazione di un valore dallo stack. push r (dove r è un qualsiasi registro della CPU) provoca la memorizzazione del valore di r nell area puntata dallo SP; SP viene poi decrementato di 4 9 (Figura 3.5 (b)); pop r provoca l incremento di 4 del valore dello SP, quindi la lettura del valore puntato da SP e la sua memorizzazione in r (Figura 3.5 (c)). È facile convincersi che tale sistema permette di memorizzare e poi recuperare i corretti valori di ritorno anche nel caso di più routine annidate l una nell altra, come nell esempio visto all inizio del paragrafo; la Figura 3.5 (d) mostra la situazione durante l esecuzione di routinec, ove reta e retb sono rispettivamente i valori di PC memorizzati al momento della prima e della seconda chiamata (sono quindi i valori che occorrerà impostare nel PC al momento del ritorno a routineb (retb) e poi del ritorno a routinea (reta). Uno stack implementa dunque un meccanismo di memorizzazione di tipo LIFO (Last-In First-Out), nel senso che l ultimo dato memorizzato (con un istruzione push) è il primo ad essere recuperato (con un istruzione pop). Uno stack viene utilizzato in pratica anche per salvare e recuperare altri valori, in particolare per la gestione degli argomenti delle subroutine e per quella di variabili locali. Si consideri il seguente pseudo-codice, che è un evoluzione del precedente: 9 Si supponga per semplicità, qui come nel resto di questo paragrafo, di avere a che fare esclusivamente con quantità (registri, indirizzi di memoria etc.) da 32 bit, quindi contenute in 4 byte.
64 3.9. APPENDICE - GESTIONE DELLO STACK 51 int a, b; /* due variabili globali, di tipo intero */ routinea() {... call routineb(a, b); } routineb(x, y) {... call routinec(x);... return; } routinec(x) { int z; /* una variabile locale, di tipo intero */... z = 2*x;... return; } Le istruzioni provocano ora quanto segue: chiamata di routineb: routinea memorizza nell ordine, con altrettanti push: il valore della variabile b; il valore della variabile a; il valore attuale del PC (Program Counter) avvio di routineb: il codice di questa routine sa di avere a disposizione i due argomenti, a e b, a partire dal valore di SP con offset rispettivamente 8 e 12 (Figura 3.5 (e)); ritorno a routinea: routineb recupera (con un pop) il vecchio valore del PC e lo reimposta; successivamente (prime istruzioni di routinea dopo la call routineb) la funzione chiamante provvede a eliminare (con due pop) gli argomenti a e b della chiamata appena terminata: lo stack è ora nuovamente vuoto; chiamata di routinec 10 : routineb memorizza nell ordine, con altrettanti push: 10 Si noti che questa chiamata avviene prima di quanto descritto al punto precedente; l ordine di presentazione è spostato solo per motivi di complessità crescente degli argomenti introdotti.
65 52 CAPITOLO 3. STRUTTURA DEI CALCOLATORI ELETTRONICI il valore della variabile x; il valore attuale del PC (Program Counter) avvio di routinec: questa routine provvede dapprima a fare spazio, nello stack, per la variabile locale z (con un push); a questo punto il codice della routine sa di avere a disposizione argomento e variabile locale ai seguenti offset a partire dal valore di SP: x offset 12; z offset 4 (Figura 3.5 (f)); Ciò che avviene al ritorno di routinec è perfettamente analogo a quanto già descritto, con l unica aggiunta dell eliminazione dallo stack, da parte di routinec prima di uscire, della variabile locale z.
66 3.9. APPENDICE - GESTIONE DELLO STACK 53 SP val1 SP (a) stack vuoto (b) stack con un valore (dopo un push) SP val1 reta retb SP (c) stack con 0 valori (dopo un pop) (d) stack durante l esecuzione di routinec, primo esempio SP a b reta (e) stack durante l esecuzione di routineb, secondo esempio a b reta x retb z SP (f) stack durante l esecuzione di routinec, secondo esempio Figura 3.5: Organizzazione di uno stack.
67 Capitolo 4 Software di Base 4.1 Introduzione In questo capitolo si descrivono le caratteristiche principali di alcuni importanti programmi di utilità (viene usato, come noto, il termine software di base), orientati alla messa a punto di programmi sviluppati dagli utenti e ad altre attività correlate, che costituiscono un elemento essenziale di ogni distribuzione di un Sistema Operativo. Come si vedrà nella prossime pagine, tali programmi risultano indispensabili ai programmatori, agli utenti generici e agli amministratori di sistema. 4.2 Messa a punto di programmi È opportuno presentare dapprima gli strumenti che storicamente sono stati introdotti per primi, ed il cui obiettivo è quello di facilitare l utente nel realizzare programmi Assemblatori, compilatori e interpreti E stato ribadito, a proposito del software di base, che l unico linguaggio riconosciuto dal calcolatore è il linguaggio macchina binario. Poiché è estremamente disagevole scrivere programmi servendosi di tale linguaggio, sono stati sviluppati programmi traduttori il cui compito è quello di trasformare programmi scritti in uno dei vari linguaggi di programmazione esistenti (il cosiddetto linguaggio sorgente) in programmi scritti in linguaggio macchina. Il processo di traduzione ha caratteristiche diverse, a seconda del linguaggio sorgente e delle modalità di realizzazione. In generale, la traduzione precede l esecuzione del programma. In questo caso, se le frasi eseguibili del linguaggio sorgente corrispondono alle istruzioni del calcolatore, il traduttore prende il nome di assemblatore. Nel caso contrario, i programmi traduttori sono chiamati compilatori. 54
68 4.2. MESSA A PUNTO DI PROGRAMMI 55 Il linguaggio assemblativo per i microprocessori Intel 80x86, ad esempio, è un linguaggio sorgente simile al linguaggio macchina di tali microprocessori; esso prevede frasi eseguibili del tipo add oppure mov corrispondenti al repertorio di istruzioni riconosciuto da tali calcolatori, per cui il traduttore di tale linguaggio è chiamato assemblatore. Viceversa, il linguaggio C è un linguaggio sorgente più potente per cui ogni frase eseguibile deve essere tradotta dal compilatore in gruppi di istruzioni in linguaggio macchina (si pensi, ad esempio, alle frasi C che includono espressioni aritmetiche di arbitraria complessità). In altri casi, la traduzione può andare di pari passo con l esecuzione del programma: traduttori di tale tipo sono chiamati interpreti. Un programma interpretato ha tempi di esecuzione molto maggiori di uno compilato, proprio perché ogni frase del linguaggio sorgente ed ogni variabile può essere ripetutamente tradotta e riesaminata dall interprete. In compenso, gli interpreti occupano spesso meno memoria dei compilatori, i relativi linguaggi sono molto più concisi e potenti, e soprattutto offrono un ottima diagnostica durante l esecuzione, e quindi una produttività nettamente superiore nella fase di messa a punto di un programma, per cui sono molto usati in piccoli sistemi e nello sviluppo di linguaggi specializzati. Tra i linguaggi interpretati più noti citiamo Basic Lisp Java Perl Python Shell La Tabella 4.1, adattata da Kernighan e Pike [10], mostra alcuni aspetti interessanti di confronto tra linguaggi interpretati e linguaggi compilati. La tabella mostra, per un certo algoritmo preso ad esempio, il numero di righe di codice necessarie per tradurre questo in un programma nel linguaggio dato, e il relativo tempo di esecuzione (su due diverse macchine e Sistemi Operativi) per uno stesso insieme di dati di ingresso. Come si può vedere, il linguaggio C (linguaggio compilato) è il più veloce come tempi di esecuzione, ma richiede il maggior numero di righe di codice; all estremo opposto, il linguaggio Java (interpretato) è fino a 30 volte più lento (questo rapporto può andare anche fino a 1:50). Un caso particolarmente brillante è quello del linguaggio Perl, che come si può vedere è estremamente conciso (solo 18 righe di codice, il che lo rende evidentemente molto più semplice concettualmente), ed ha tempi di esecuzione molto prossimi a quelli di un linguaggio compilato. Dal punto di vista del Sistema Operativo, gli assemblatori e i compilatori sono dei programmi eseguibili scritti in linguaggio macchina che richiedono per la loro esecuzione un file di ingresso, uno di uscita e alcuni file ausiliari temporanei, ossia eliminabili dopo che è terminata l esecuzione del programma compilatore. Il file d ingresso contiene il programma sorgente preparato in precedenza servendosi di un opportuno editor. Il file di uscita contiene invece un programma
69 56 CAPITOLO 4. SOFTWARE DI BASE Unix, 250 MHz Windows NT, 400 MHz righe di codice C 0.36 sec 0.30 sec 150 C Java Perl Tabella 4.1: Confronto tra linguaggi compilati e interpretati. rilocabile (sono anche usati i termini: modulo oggetto e programma oggetto). Tale programma è scritto in linguaggio macchina ma non è ancora eseguibile in quanto, solitamente, fa riferimento ad altri sottoprogrammi e dati esterni, ossia non inclusi nel programma sorgente, che il compilatore non è in grado di tradurre. Alcuni compilatori più sofisticati prevedono più fasi consecutive. Il GNU C compiler, ad esempio, prevede tre fasi distinte: 1. preprocessamento: frasi del tipo: #include <nomefile> oppure: #ifdef variabile compilazione... #endif sono tradotte nelle opportune frasi C; 2. compilazione: il programma C viene tradotto nel linguaggio assemblativo del processore utilizzato; 3. assemblaggio: il programma assemblativo viene tradotto in linguaggio macchina dando luogo ad un modulo oggetto. Questo approccio consente al programmatore di inserire sequenze di istruzioni in linguaggio assemblativo all interno del programma sorgente. Il GNU C compiler, ad esempio, prevede la frase C asm(); per inserire frasi in linguaggio assemblativo all interno del programma C. Tale caratteristica risulta fondamentale per scrivere il codice di un Sistema Operativo, e più particolarmente di un Nucleo: per pilotare i dispositivi di I/O, infatti, è necessario agire sulle porte di I/O mediante istruzioni in linguaggio assemblativo. Per fortuna, solo una limitata parte del Nucleo deve necessariamente essere codificata in linguaggio assemblativo. La rimanente parte, circa il 90% del codice, può essere scritta in un linguaggio ad alto livello quale il C Linkaggio di programmi In effetti, l uso di programmi rilocabili si rivela indispensabile già in applicazioni di media complessità, dove è facile superare le migliaia di frasi: in tali casi, non è conveniente prevedere un unico programma ma è di gran lunga preferibile decomporre l applicazione in varie unità di programma o moduli compilabili
70 4.2. MESSA A PUNTO DI PROGRAMMI b00000 intrn: --- extrn: prog1 prog0 call dati0 intrn: prog1 extrn: prog prog call b00000 b00000 dati1 intrn: prog2 extrn: --- prog2 dati2 modulo A modulo B modulo C Figura 4.1: Un esempio di riferimenti tra moduli. separatamente. La nozione di modulo dipende dal linguaggio sorgente utilizzato: in un linguaggio quale il C, non solo la funzione main() che corrisponde al programma principale, ma anche una qualsiasi funzione (o gruppi di funzione e/o strutture di dati) può essere definito come modulo. In genere, gli standard di programmazione esistenti suggeriscono di usare moduli di lunghezza non superiore alle centinaia di frasi. A questo punto, il quadro risulta più complesso: ogni modulo è compilato separatamente e dà luogo ad un corrispondente modulo oggetto. Questi ultimi non sono tuttavia logicamente indipendenti: il modulo A può includere una chiamata ad una funzione contenuta in un altro modulo B, ignoto al momento della compilazione di A. Allo stesso modo, i moduli interagiscono su variabili o strutture di dati comuni contenute in moduli diversi da quello attualmente compilato. Una conseguenza della decomposizione in moduli è quindi che il compilatore non è in grado di associare un indirizzo rilocabile ad ogni funzione e ad ogni variabile contenuta in un modulo: esso esegue una traduzione parziale, creando quindi una tabella di riferimenti esterni per tutti quei nomi che non ha trovato all interno del modulo. La Figura 4.1 illustra un semplice esempio di collegamenti tra tre moduli: il modulo A include una chiamata alla funzione prog1() inclusa nel modulo B. Il modulo B, a sua volta, include una chiamata alla funzione prog2() inclusa nel modulo C. Quando viene compilato il modulo A, il compilatore non è in grado di risolvere l etichetta prog1, anche se la dichiarazione del tipo extern int prog1(...) presente nel codice lo aiuta nello stabilire che prog1 è effettivamente un nome esterno. Il compilatore inserisce quindi nella tabella dei simboli esterni extern sia l etichetta prog1 che l indirizzo (o gli indirizzi) in cui tale etichetta appare. Lo stesso avviene durante la compilazione del modulo B. La tabella intern presente in ogni modulo elenca tutti i simboli definiti nel modulo insieme ai corrispondenti indirizzi in cui essi appaiono nel modulo. Si rende quindi necessaria una nuova funzione del Sistema Operativo, il linker 1, per terminare la compilazione dei vari moduli. Servendosi delle indicazioni 1 Linker viene tradotto da alcuni con le parole correlatore o collegatore, ma in pratica è invalso l uso di utilizzare anche in italiano il termine originale inglese.
71 58 CAPITOLO 4. SOFTWARE DI BASE b000 00e000 b00000 bc0000 be0000 testata file eseguibile call 00bd00 call 00e600 prog0 prog1 prog2 dati0 dati1 dati2 Figura 4.2: Un esempio di collegamento tra moduli. contenute nelle tabelle dei riferimenti esterni presenti in ogni modulo oggetto, il linker fonde i vari moduli oggetto in un unico file e completa all interno dei moduli la compilazione inserendo gli indirizzi mancanti nelle varie parti del codice. Il risultato è un file eseguibile pronto ad essere caricato in memoria e quindi eseguito. Come si vedrà nel Capitolo 9, dopo la funzione di linking che produce un file eseguibile, viene eseguita un altra funzione chiamata loading che precede immediatamente l esecuzione del programma. Riprendendo l esempio precedente, possiamo vedere nella Figura 4.2 come risulta il file eseguibile prodotto dal linker in corrispondenza ai tre moduli A, B e C. Le frecce tra etichette esterne e programmi nella Figura 4.1 indicano le cuciture effettuate dal linker: ad esempio, accanto all etichetta prog2 nel modulo B, il linker ha inserito l indirizzo iniziale di prog2 nel modulo linkato. Durante l esecuzione del programma, prog2 potrà essere correttamente invocato tramite indirizzamento indiretto Librerie statiche e dinamiche L esistenza del linker offre ai programmatori la possibilità di ricorrere a sottoprogrammi 2, di uso più o meno generale, già messi a punto da altri: sono le cosiddette librerie di programmi che hanno oggi un ruolo fondamentale nella messa a punto delle applicazioni. Tutti i linguaggi di programmazione fanno un ampio uso di tali librerie, per cui non è addirittura possibile eseguire alcun programma in un qualsiasi linguaggio se non lo si correla con numerosi altri sottoprogrammi inclusi in una o più librerie. Come è ovvio, ogni linguaggio di programmazione ha le sue apposite librerie, 2 Sono di uso comune i termini programma, sottoprogramma, subroutine, funzione ed altri ancora, che vengono utilizzati in modo più o meno intercambiabile. Nel testo ci si riferirà di norma al termine programma o talvolta sottoprogramma.
72 4.2. MESSA A PUNTO DI PROGRAMMI 59 per cui vi saranno librerie di programmi per il linguaggio C, per il C++, per il Pascal e così via. Nell effettuare i collegamenti tra le funzioni C scritte dall utente e quelle già presenti in apposite librerie, il collegatore o linker può fare uso di due tipi di librerie, ognuna delle quali presenta vantaggi e svantaggi. libreria statica: contiene il codice oggetto di funzioni; il collegatore aggiunge al codice oggetto corrispondente alla compilazione dei programmi dell utente il codice di tutte le funzioni di libreria utilizzate. Il programma eseguibile corrispondente è autosufficiente, nel senso che tutti i moduli oggetto indirizzati sono contenuti nello stesso file eseguibile. Lo svantaggio di tale approccio è che la stessa funzione di libreria viene duplicata in vari file eseguibili, per cui lo spazio su disco viene sfruttato in modo poco efficace. Il compito del loader risulta alquanto semplice, proprio perché il file eseguibile è autosufficiente. libreria dinamica: il collegatore si limita ad inserire nei file oggetto corrispondenti alla compilazione dei programmi dell utente i nomi delle funzioni di libreria utilizzate e gli indirizzi nel codice in cui compaiono chiamate a tali funzioni. Durante l esecuzione del programma, i programmi inclusi nelle librerie vengono caricati dinamicamente in una apposita area di memoria (a meno che non siano già state caricate in precedenza) e viene effettuato un collegamento dinamico tra le istruzioni del programma utente e gli indirizzi delle funzioni richieste. Come si vedrà più avanti nel Paragrafo 4.3, il compito del caricatore risulta più complesso. È importante notare come uno stesso programma possa risultare di dimensioni notevolmente diverse nel caso di collegamento con librerie statiche o con librerie dinamiche: si consideri il semplicissimo programma qui riportato /* programma prova.c */ int main(void) { printf("ciao ciao...\n"); return 0; } Compilandolo sotto Linux dapprima con liberie dinamiche (situazione di default per il compilatore gcc) e poi con librerie statiche (ciò che si ottiene specificando il flag -static), si ottiene quanto segue 3 $ gcc -o prova.dyn prova.c 3 Il programma size(1) stampa, per ciascuno dei file eseguibili dati in argomento, la dimensione in memoria del relativo programma; le colonne text, data, bss, dec e hex indicano rispettivamente la dimensione in byte delle regioni di codice, di dati inizializzati e di dati non inizializzati, e poi la dimensione totale di queste tre regioni, espressa dapprima in decimale e poi in esadecimale. Per maggiori dettagli sull argomento, in particolare sul significato dei termini regione e bss, si veda il Paragrafo 9.2 pag. 148.
73 60 CAPITOLO 4. SOFTWARE DI BASE $ gcc -static -o prova.sta prova.c $ ls -l prova.dyn prova.sta -rwxr-xr-x 1 luca ax Nov 30 17:59 prova.dyn -rwxr-xr-x 1 luca ax Nov 30 18:00 prova.sta $ size prova.dyn prova.sta text data bss dec hex filename b prova.dyn a41 prova.sta $ Come si vede, nel caso dinamico il file eseguibile è lungo circa 13 Kbyte, e il programma caricato in memoria occupa 1323 byte tra codice e dati; il grosso del programma, cioè l implementazione della funzione printf(), risiede nella libreria dinamica libc.so. Invece nel caso statico, dato che il codice e i dati della printf() (e delle funzioni da questa chiamate) viene inserito direttamente nell eseguibile, si ottiene un file eseguibile lungo circa 1.6 Mbyte, e un programma che occupa in memoria byte. È evidente come ciò possa in pratica costituire un notevole spreco di spazio, sia su disco (per memorizzare i vari file eseguibili), sia in memoria primaria (una stessa funzione di libreria, come la printf(), sarà caricata inutilmente più volte in memoria RAM da parte di programmi che la eseguono) Il problema più importante dal punto di vista pratico è tuttavia un altro, ed è a sfavore delle librerie dinamiche: le librerie di programma evolvono nel tempo ed i parametri usati da alcune funzioni della libreria possono cambiare da una versione all altra. Quando ciò avviene, può succedere in alcuni Sistemi Operativi che l applicazione selezionata dall utente non possa essere eseguita su un determinato sistema poiché esso fa uso di una versione della libreria incompatibile con quella richiesta dall applicazione. Nel file system di Unix le librerie di sottoprogrammi di uso comune nel sistema sono di norma inserite nelle cartelle /lib o /usr/lib; tutte le librerie in Unix hanno un nome che inizia con lib, seguito da una stringa (di una o più lettere) che identifica la libreria stessa; le librerie statiche hanno estensione.a, mentre quelle dinamiche hanno estensione.so. Ad esempio, la libreria di base del linguaggio C (funzioni di I/O standard, supporto alle chiamate di sistema etc.) ha nome libc, mentre quella che contiene i sottoprogrammi di tipo matematico (calcolo di logaritmi, potenze etc.) ha nome libm; in un sistema Linux, nella directory /usr/lib si trovano i seguenti file, dove si identificano facilmente le versioni statiche e dinamiche delle due librerie dette: /usr/lib/libc.a /usr/lib/libc.so /usr/lib/libm.a /usr/lib/libm.so Ulteriori librerie specializzate possono essere inserite al di sotto della cartella /usr, anche se non esiste ancora uno standard che specifica, per ogni tipo di libreria, la corretta collocazione nell albero delle cartelle. Un semplice esempio può servire ad illustrare l affermazione precedente.
74 4.2. MESSA A PUNTO DI PROGRAMMI 61 Nelle distribuzioni di Linux, la libreria di funzioni per l interfaccia grafica X Windows ha il nome di percorso /usr/x11r6/lib. Altre librerie specializzate, quali quella che contiene le funzioni attinenti alla rappresentazione di immagini nel formato jpeg, sono incluse nella cartella /usr/local/lib. In altri casi ancora, diversi programmi applicativi codificati in modo modulare fanno uso di librerie proprie aggiuntive che devono essere inserite in apposite cartelle. L applicazione netscape, ad esempio, richiede una propria libreria di funzioni che deve essere inserita nella cartella /usr/local/netscape. In generale, quella che può sembrare una eccessiva rigidità dell applicazione, ossia il dovere inserire una libreria di funzioni con un nome di percorso prefissato, si traduce in un notevole beneficio per l utente. Grazie a tale approccio, è possibile inserire nei programmi applicativi linkati in modo dinamico appositi test per verificare se le librerie dinamiche installate nel sistema sono compatibili con l applicazione da installare, segnalando eventuali incompatibilità all utente. Poiché ogni libreria ha un apposito nome di percorso, risulta inoltre possibile, fare convivere diverse versioni della stessa libreria: questo può risultare indispensabile per potere eseguire sia applicazioni più antiche che fanno uso di versioni precedenti di una libreria che applicazioni più recenti che invece richiedono una versione più recente della stessa libreria ed incompatibile con quella precedente Immissione di programmi Un programma è un documento immesso dall utente tramite tastiera che consiste in una stringa di caratteri. La funzione editor del Sistema Operativo assiste l utente nel creare nuovi programmi oppure nel modificare programmi esistenti. A tale scopo, visualizza sullo schermo i caratteri appena digitati consentendo così di rilevare eventuali errori di battitura; inoltre, consente all utente di correggere parti di documento tramite operazioni di inserimento e cancellazioni di caratteri. Infine, l editor si serve del sistema di archiviazione, sia per memorizzare in appositi archivi su disco nuovi programmi immessi dagli utenti, sia per reperire l archivio contenente il programma su cui operare ulteriori modifiche. Da vari anni sono ormai disponibili editor specializzati chiamati editor guidati dalla sintassi per mettere a punto programmi scritti in uno specifico linguaggio di programmazione. Tali editor si avvalgono della sintassi del linguaggio per riconoscere le parole chiavi contenute nel programma, per evidenziarle tipograficamente e, sopra tutto, per effettuare un riconoscimento automatico di eventuali errori di sintassi. Il loro uso migliora sensibilmente la produttività del programmatore Caricamento in memoria di un file eseguibile Il caricamento di un file eseguibile in memoria RAM precede immediatamente la sua esecuzione e viene effettuato automaticamente dal Sistema Operativo in seguito alla richiesta di esecuzione del programma da parte dell utente. Le attività associate al caricamento dipendono dallo schema di gestione della memoria utilizzato e dal formato del file eseguibile. La Figura 4.3 illustra le fasi
75 62 CAPITOLO 4. SOFTWARE DI BASE utente al terminale programma sorgente EDITING COMPILAZIONE modulo oggetto altri moduli oggetto file eseguibile CARICAMENTO LINKING ESECUZIONE programma caricato in memoria Figura 4.3: Fasi di messa a punto di un programma. di messa a punto di un programma utilizzando le varie funzioni del Sistema Operativo appena descritte. Ritorneremo su questo argomento nel Capitolo Supporto durante l esecuzione Si è discusso in precedenza delle varie fasi attraverso le quali passa un programma fino a iniziare l esecuzione. Supponiamo ora che l utente al terminale richieda l esecuzione di un programma. Inizia a questo punto da parte del Sistema Operativo, una fase di controllo del programma utente che si protrae fino alla sua terminazione. Un primo tipo di controllo è quello relativo alla terminazione corretta del programma. Quando ciò si verifica, il Nucleo provvede ad inviare un apposito messaggio al processo di sistema associato al terminale (vedi Capitolo 6) che può quindi rilasciare l area di memoria e le altre risorse impegnate dal processo utente e prepararsi ad accettare il prossimo comando dell utente. Un altro tipo di controllo è quello relativo alla terminazione erronea del programma. Grazie ad esso, il Sistema Operativo è capace di riprendere il controllo anche in seguito a terminazioni anomale di un processo utente. Le terminazioni anomale controllabili sono quelle che inducono la CPU a generare un apposito segnale di interruzione software (una trap) quando si verificano. In effetti, esistono specifici segnali di interruzione software in grado di rilevare i seguenti eventi:
76 4.4. SALVATAGGIO/RIPRISTINO DI DATI 63 esecuzione da parte della CPU di una istruzione erronea (codice operativo non valido o indirizzo erroneo) indirizzo non valido (il programma cerca di indirizzare un cella di RAM inesistente) diritti d accesso insufficienti (il programma cerca di indirizzare un area protetta senza avere i privilegi necessari) trabocco (overflow) numerico durante una operazione aritmetica L effetto di una trap di questo tipo, come nei casi visti nel Capitolo 3, è normalmente quello di far abortire il processo utente. Quando il programma viene linkato in modo dinamico anziché statico, il Sistema Operativo deve provvedere un altro supporto fondamentale: un caricatore/linker dinamico di programmi inclusi nelle librerie dinamiche. Il ruolo di tale supporto è di ritardare il linking dei programmi di libreria richiesti dal programma in esecuzione. In alcuni casi, il traduttore prevede di collegare al programma utente un modulo di supporto all esecuzione che effettua ulteriori controlli, oltre a quelli svolti dall hardware del calcolatore. Il linguaggio Pascal, ad esempio, prevede un apposito modulo che svolge varie funzioni, tra cui quella filtrare ogni indirizzamento di variabili di tipo array per verificare se i valori degli indici rientrano nell intervallo dichiarato. Nel caso opposto, il modulo interrompe l esecuzione del programma e prepara un messaggio di diagnostica specificando il nome dell array e i valori degli indici al momento dell errore. Un altro prezioso strumento per la messa a punto di programmi è il debugger simbolico. Tale funzione richiamabile tramite appositi parametri in fase di compilazione e collegamento, dà luogo al caricamento in memoria di un modulo aggiuntivo che interagisce con l utente al terminale e serve a controllare l esecuzione del programma. Mediante tale modulo è possibile, ad esempio, richiedere l esecuzione del programma fino ad arrivare ad una frase specifica del programma sorgente e leggere quindi il valore attuale di alcune variabili. In tale ottica, gli interpreti si possono considerare come i moduli di controllo più completi poiché essi controllano ogni singola frase del programma anziché un limitato sottoinsieme. Come è già stato osservato, il prezzo da pagare, però, per tale ricchezza di controlli è quello di avere tempi di esecuzione del programma interpretato molto più elevati di quelli del programma compilato. 4.4 Salvataggio/ripristino di dati Anche se gli attuali file system sono alquanto affidabili, non si può escludere a priori il verificarsi di malfunzionamenti hardware dei dischi che potrebbero causare la perdita di tutte le informazioni in essi contenute. Per questo motivo, è consigliabile effettuare periodicamente il backup, ossia il salvataggio su supporti rimovibili del contenuto dei dischi del sistema. A tale scopo sono stati messi a punto programmi di utilità che consentono di svolgere tale attività in modo alquanto semplice. Le caratteristiche salienti di tali programmi sono le seguenti:
77 64 CAPITOLO 4. SOFTWARE DI BASE salvataggio/ripristino su una serie di volumi rimovibili opportunamente etichettati mediante un apposito programma di utilità che prevede una apposita interfaccia per guidare l utente nella fase di montaggio/smontaggio di volumi; possibilità di comprimere/decomprimere gli archivi riducendo così la quantità di memoria complessiva richiesta per il backup; possibilità di effettuare un backup selettivo basato sulla data dell ultimo aggiornamento di ogni archivio. In tale modo vengono salvati periodicamente i soli archivi che sono stati modificati dopo l ultimo backup effettuato. 4.5 Amministrazione del sistema Il termine amministrazione del sistema sta ad indicare una serie di funzioni attinenti all uso corretto delle risorse del sistema multiprogrammato da parte degli utenti. In particolare, sono stati messi a punto programmi di utilità per risolvere i seguenti problemi: identificazione dell utente: Oggi la soluzione più adottata consiste nell identificare ogni utente tramite un nome utente (username) ed una parola chiave (password). L utente deve, per potere accedere alle risorse del sistema, identificarsi e digitare la propria parola chiave. Solo dopo che l identificazione è stata portata a termine con successo, l utente può accedere al sistema. E opportuno individuare un responsabile, l amministratore del sistema, il quale è l unico abilitato a creare nuovi utenti od a rimuovere utenti già esistenti. conteggio delle risorse utilizzate e relativo addebito: In molte aziende i costi del sistema informativo sono distribuiti tra i reparti che usufruiscono del servizio. E quindi importante misurare l uso da parte dei vari utenti delle principali risorse del sistema quali tempo di CPU, il prodotto del tempo per lo spazio su disco utilizzato, i volumi di stampa, ecc.. Le statistiche raccolte sull uso delle risorse sono anche molto utili per individuare comportamenti anomali nonché eventuali colli di bottiglia. Potrebbe risultare, ad esempio, dalle statistiche di utilizzo della stampante veloce che essa causa un ritardo nella terminazione dei job per via delle code di stampa sempre piene. autorizzazione alla condivisione delle risorse: Risulta spesso necessario, nel caso di progetti che prevedono gruppi di programmatori ed utenti, prevedere una condivisione controllata delle cartelle e degli archivi di uso comune. Come nel caso della identificazione degli utenti, è opportuno concentrare nelle mani dell amministratore del sistema anche le autorizzazioni o revoche di condivisione dell informazione in modo da avere un responsabile unico per tale attività che può risultare alquanto critica.
78 Capitolo 5 Il File System 5.1 Introduzione Si è già discusso nel Capitolo 3 come le memorie di massa, quali i dischi e i nastri magnetici, abbiano caratteristiche diverse da quelle della memoria RAM. In primo luogo, sono di tipo non volatile e consentono quindi di conservare a basso costo e per un periodo prolungato di tempo informazioni degli utenti. In secondo luogo, tali memorie hanno dei tempi di indirizzamento variabili che dipendono sia dall indirizzo richiesto che dallo stato attuale del circuito di indirizzamento. Nel caso di un nastro magnetico, ad esempio, il tempo di indirizzamento dipenderà dalla distanza tra la porzione di nastro dove andrà eseguita l operazione di I/O e quella attualmente posizionata dalla testina di lettura/scrittura. Nel caso di un disco a braccio mobile, invece, il tempo di indirizzamento dipenderà dalla distanza tra il cilindro contenente la traccia richiesta e quello attualmente posizionato dal braccio mobile dell unità a disco. In conseguenza, diventa importante collocare l informazione nelle memorie di massa in modo da ridurre, per quanto possibile, i tempi di indirizzamento. In terzo luogo, infine, le informazioni contenute in tali memorie non sono direttamente accessibili alla CPU: a differenza della memoria principale, non esistono, ad esempio, istruzioni che consentono alla CPU di trasferire informazioni tra registri interni e aree di disco. La stessa funzione è invece ottenuta utilizzando processori specializzati chiamati processori di I/O ed eseguendo le seguenti istruzioni: 1. istruzioni per preparare i parametri (indirizzo del disco, indirizzo della traccia, indirizzo del settore, numero di byte da trasferire, indirizzo in memoria) da trasferire al processore di I/O al quale è collegato il disco. Nei calcolatori di tipo mainframe tali istruzioni prendono il nome di programma di canale; 2. istruzione di tipo start i/o per attivare il processore di I/O. Eseguita l ultima istruzione, il programma si pone in attesa del segnale di interruzione di fine I/O da disco che indicherà il completamento dell operazione. 65
79 66 CAPITOLO 5. IL FILE SYSTEM Come si è appena visto, il reperimento e il trasferimento di informazioni contenute in memoria di massa risulta alquanto complesso. Nasce quindi l esigenza di includere nei Sistemi Operativi appositi moduli, chiamati sistemi di archiviazione (in inglese, file system), che rendano più agevole l accesso alle informazioni contenute in tali memorie. 5.2 Ruolo di un file system Il file system svolge anzitutto i seguenti compiti: semplificare gli accessi al disco: grazie al file system, il programmatore che intende leggere o scrivere dati su disco non deve specificare gli indirizzi fisici dei settori da trasferire; il disco appare invece come un contenitore di file e cartelle e sono previste apposite frasi nei linguaggi di programmazione che consentono di creare, modificare e cancellare file e cartelle; consentire al programmatore di organizzare i suoi dati servendosi di una delle strutture logiche di dati previste ed offrire per ognuna gli operatori necessari ad operare su di essa; gestire i vari dischi collegati al sistema, assegnando o rilasciando aree di memoria secondaria in base alle richieste dei programmi che utilizzano il file system. Le cartelle (in inglese, directory) sono state introdotte per consentire agli utenti di raggruppare file tra loro affini nella stessa cartella. Suddividendo i numerosi file contenuti in un disco in cartelle diverse le quali, a loro volta, possono includere altre cartelle oltre che file, risulta semplificato il reperimento dei singoli file e, come si vedrà più avanti, risulta possibile fare uso di schemi di protezione differenziati. Inoltre, il Sistema Operativo è responsabile delle seguenti attività riguardo la gestione dei file system: Creazione e distruzione di file; Creazione e distruzione di cartelle; Supporto di primitive per la manipolazione di file e cartelle; Allocazione di spazio a un file; Mappatura dei file nella memoria secondaria; Fornire utility per il crash recovery (recupero dai guasti).
80 5.3. PROTEZIONE DELLE INFORMAZIONI Protezione delle informazioni Oltre a conservare nel tempo informazioni raggruppate in file, i file system offrono in generale alcuni schemi di protezione con finalità diverse. Si accenna soltanto, per motivi di spazio, a due di essi: gestione automatica di copie di riserva o backup. Ogni volta che viene modificato un file, il file system crea automaticamente un file contenente la versione precedente. Questo consente all utente di non perdere i suoi dati nel caso di operazioni erronee. In alcuni file system, anziché una sola copia di backup viene creata una serie storica delle versioni precedenti indicizzata, appunto, sul numero di versione (versione 1 per il file appena creato, 2 per la prima modifica, ecc.). In tali file system viene lasciato all utente il compito di cancellare le versioni considerate superflue. accesso riservato a cartelle o file. In un sistema multiprogrammato, i file di utenti diversi sono registrati in dischi comuni ed è quindi necessario garantire uno schema di protezione che impedisca ad utenti non autorizzati di accedere ai file di altri utenti. A tale scopo, si rivela molto utile il concetto di cartella, poiché in tale modo risulta alquanto agevole raggruppare tutti i file di un utente in una stessa cartella facendo vedere all utente la porzione di disco corrispondente alla sua cartella (si veda quanto detto più avanti, nel Paragrafo 5.6.6, a proposito del caso specifico di Unix). In pratica, un simile approccio deve essere in qualche modo mitigato per consentire la condivisione controllata di informazioni. 5.4 Strutturazione di un file system Struttura dei file Dal punto di vista del Sistema Operativo: nulla sequenza di byte (approccio Unix) struttura a record, semplice: righe (di testo) lunghezza fissa lunghezza variabile struttura complessa file eseguibile
81 68 CAPITOLO 5. IL FILE SYSTEM archivio di dati (generato da programmi di sistema quali tar(1), cpio(1), ed altri) Vi sono ovviamente vantaggi e svantaggi nell approccio neutro di Unix: da un lato si ottiene indubbiamente una maggior elasticità, dato che tutto è decidibile dai singoli programmi utente (se è il Nucleo a decidere, il tutto è necessariamente fisso e inamovibile); d altro lato, il prezzo di questa elasticità è, almeno potenzialmente, una minore ottimizzazione di alcune operazioni a livello Nucleo Struttura dei nomi di file Dal punto di vista del Sistema Operativo: Nulla sequenza di caratteri arbitrari, salvo / e \0s (approccio Unix) Struttura a nome +. + estensione (ad esempio, command.com, ping.exe, autoexec.bat). Il discorso è analogo al precedente: se è il Sistema Operativo a decidere, si deve fare necessariamente in quel modo. Nel Nucleo di Unix non esiste il concetto di estensione; esiste però nei programmi a livello utente; ad esempio, il compilatore C di GNU/Linux, gcc, capisce varie estensioni di file e attribuice loro significati precisi; tra questi si citano:.c file sorgente in linguaggio C.s file sorgente in assembler.o file oggetto rilocabile.a libreria statica Attributi dei file In generale, oltre ai dati che contiene, ogni file ha una serie di attributi; queste informazioni, utilizzate dal Nucleo per gestire il file stesso, sono normalmente memorizzate nella cartella che lo contiene, oppure in appositi blocchi di controllo del file system: Nome è la sola informazione memorizzata in forma leggibile dall uomo Tipo del file se supportato dal Sistema Operativo Ubicazione informazioni su come accedere ai blocchi di dati che compongono il file
82 5.4. STRUTTURAZIONE DI UN FILE SYSTEM 69 Dimensione lunghezza del file, normalmente espressa in byte Protezione informazioni di controllo per specificare nel caso di un file: chi può leggere, scrivere, eseguire il file e così via; nel caso di una cartella: chi può consultarne il contenuto, chi può distruggere file nella cartella, e così via Identificazione dell utente proprietario (se supportato dal Sistema Operativo), di varie date e ore (di creazione e/o di ultima modifica del file e così via) per i meccanismi di protezione e sicurezza Struttura e implementazione delle cartelle Differenti metodi di strutturazione sono possibili; il più diffuso è il modello ad albero rovesciato (directory-tree structure), che sarà descritto nel seguito a proposito del file system di Unix. Per quanto riguarda le tecniche di implementazione, i metodi utilizzati sono sostanzialmente due: lista lineare di coppie nome di file + puntatore a blocco di controllo. È un metodo semplice da programmare, ma può risultare lento nell effettuare la ricerca in cartelle che comprendono un gran numero di file (cosa comunque da evitare). Questo metodo è usato da Unix. tabella di hashing (in inglese, hash table). Questa tecnica è descritta nell Appendice 5.8 al capitolo. Permette di accelerare notevolmente il tempo di ricerca; d altro lato, è relativamente complessa e potenzialmente lenta da programmare per il Sistema Operativo, nell inserzione e rimozione di file Operazioni su file e cartelle Un file system deve normalmente supportare almeno le seguenti operazioni sui file: creazione, apertura, chiusura di un file; lettura, scrittura di dati; riposizionamento all interno del file (seek); rimozione di un file; troncamento (alterazione della lunghezza di un file). Mentre le operazioni sulle cartelle sono le seguenti:
83 70 CAPITOLO 5. IL FILE SYSTEM ricerca di un elemento al suo interno (un file o sottocartella); creazione, rimozione di un elemento lista del contenuto della cartella cambio di nome di un elemento attraversamento della cartella (ricerca di sottocartelle), e quindi dell intero file system. 5.5 Implementazione dei file system Un file system risiede in un disco, o una partizione di un disco; in generale, in un disco logico, cioè appunto una parte di un disco fisico vista dal Nucleo come un insieme autocontenuto di blocchi di dati 1. Gli aspetti principali dell implementazione di un file system sono i seguenti: scelta della dimensione dei blocchi per il file system; algoritmi di allocazione dei blocchi ai file; algoritmi di gestione dello spazio libero; strategie per l ottimizzazione delle prestazioni Dimensione dei blocchi I valori usuali oscillano tra 512 byte e 8 Kbyte; valori maggiori velocizzano il file system, perchè aumentano la bufferizzazione a livello dell interazione tra la CPU e il controller del disco, ma aumentano la frammentazione interna, cioè il fenomeno dello spazio che rimane inutilizzato a causa di blocchi usati solo in parte. Ad esempio, dato un file di 9000 byte, se il file system ha blocchi da 8 Kbyte l occupazione sarà di 2 x 8Kbyte, ossia byte, quindi si hanno 7384 byte inutilizzati; se invece si hanno blocchi da 1 Kbyte l occupazione sarà di 9 x 1Kbyte, ossia 9216 byte, quindi soltanto 216 byte sono inutilizzati. Sono possibili accorgimenti per attenuare questo problema: ad esempio il file system di Unix 4.2BSD, per diminuire la frammentazione esterna, ha introdotto una tecnica di implementazione che prevede l uso di due diverse dimensioni di blocco: il file system definisce un block size, ad esempio pari a 8 Kbyte, e un fragment size, ad esempio di 1 Kbyte. Tutti i blocchi di un file sono grandi block size, salvo l ultimo, che è un multiplo di fragment size, per completare il file. Ad 1 Nel seguito del capitolo, con il termine disco si intenderà sempre un unità logica (disco logico), così come è vista da tutto il Sistema Operativo; questa di solito non coincide con l intero disco fisico, che è di norma diviso in più partizioni, ciascuna delle quali costituisce un disco logico.
84 5.5. IMPLEMENTAZIONE DEI FILE SYSTEM 71 esempio, un file di byte sarà composto da 2 blocchi da 8 Kbyte più un frammento da 2 Kbyte (non riempito completamente). Il file system ext2fs di Linux introduce altri tipi di ottimizzazione; si potrebbero portare diversi altri esempi Allocazione di blocchi ai file Anche in questo campo esistono diverse tecniche, ciascuna preferibile alle altre a seconda delle metriche prestazionali di interesse per un dato tipo di file system: Allocazione contigua. Vengono allocati a un file dei blocchi fisicamente succcessivi nel disco. È una tecnica veloce per l accesso aleatorio nel file (in particolare, è particolarmente adeguata per sistemi real-time); occorre però prevedere a priori (al momento della creazione) la dimensione del file; esiste inoltre, per questo tipo di allocazione, il problema della frammentazione esterna, cioè la presenza di zone di blocchi contigui non utilizzati, sparse nel file system; alla creazione di un nuovo file, ci si può trovare nella condizione di avere sufficiente spazio libero in tutto il disco, ma frammentato in modo che il file da creare non entri in nessuna zona disponibile. Allocazione collegata. Il file è costituito da una lista collegata di blocchi. Ogni blocco contiene un puntatore al prossimo blocco; il metodo è lento per un accesso aleatorio (occorre traversare tutto il file); non c è frammentazione esterna, ma c è uno spreco di spazio perchè in ogni blocco vengono sacrificati alcuni byte per il puntatore al blocco seguente. Per inciso, il metodo FAT (File Allocation Table), utilizzato dai Sistemi Operativi MS-DOS e OS/2, è una variante di questa tecnica. Allocazione indicizzata. Ad ogni file è associato un blocco indice, che punta a tutti i blocchi di dati. Il metodo è abbastanza veloce per l accesso aleatorio, e soffre di frammentazione esterna; esiste tuttavia il problema dello spreco di spazio per il blocco indice, che cresce col crescere del file (si tratta quindi in pratica di un insieme di blocchi indice). Il file system di Unix (descritto oltre, Paragrafo 5.6) utilizza una tecnica combinata, che prevede un allocazione indicizzata insieme all uso di liste collegate di blocchi Gestione dello spazio libero Inizialmente un file system è vuoto, quindi tutti i suoi blocchi di dati sono disponibili in un unico pool per essere allocati ai file che verranno creati via via. Per gestire correttamente file che vengono creati, o che crescono di dimensione, è necessario disporre di algoritmi per l allocazione di blocchi a un file, da prelevare
85 72 CAPITOLO 5. IL FILE SYSTEM dal pool di blocchi liberi. D altro canto, un file che diminuisce di dimensione o che viene rimosso implica la restituzione dei suoi blocchi al file system, cioè al pool di blocchi liberi. Le principali tecniche a disposizione sono le seguenti: Mappa di bit (o vettore di bit; bit-map, o bit-vector). Viene utilizzata una parte dello spazio del disco per gestire una mappa di tutti i blocchi del volume; ogni blocco è rappresentato da un bit: se il bit è a 0 il blocco è libero, se è a 1 è occupato. L algoritmo è semplice e veloce per trovare il prossimo blocco libero; presenta un certo spreco di spazio per la bitmap; tra i vantaggi offerti presenta la facilità di ottenere file con blocchi contigui. Lista collegata. I blocchi liberi sono organizzati in una lista, e puntano ciascuno al prossimo. Non c è spreco di spazio, ma è meno facile (benchè non impossibile) ottenere file con blocchi contigui. Lista collegata con raggruppamento. Si tratta di una variante dello schema precedente: esiste anche qui una lista collegata di blocchi liberi che puntano ciascuno al successivo, ma ciascun blocco della lista contiene gli indirizzi di N altri blocchi liberi. Questo è il metodo utilizzato da Unix: la lista dei blocchi liberi parte dal super-block (la struttura di controllo di tutto il file system di veda oltre, Paragrafo 5.6.1). L algoritmo di allocazione/deallocazione di blocchi può ottenere spazio contiguo Ottimizzazione delle prestazioni Al di là degli algoritmi scelti, sono possibili vari accorgimenti e strategie per migliorare le prestazioni di un file system; si accenna qui alle principali. Utilizzazione di una disk cache, cioè una parte di memoria RAM dedicata al mapping di alcuni blocchi disco. Vale a tal proposito quanto già detto in generale sul caching (si veda il Paragrafo pag. 47). L algoritmo di sostituzione più usato in questo contesto è un algoritmo LRU (Least-Recently Used algorithm): vengono sostituite prima le pagine di memoria che si riferiscono a blocchi disco che non sono stati usati da più tempo. La ricerca di elementi viene accelerata con le tecniche di hashing si veda l Appendice 5.8 al capitolo). Il caching viene applicato in pratica anche ai blocchi indice e/o strutture di controllo dei file (nel caso di Unix, agli inode si veda oltre). Per il miglioramento dell accesso sequenziale (che rimane comunque il caso più frequente che si verifica in pratica) vengono usate, con il caching, delle tecniche di lettura anticipata (read-ahead) e rilascio indietro (free-behind): La prima tecnica consiste nel rimuovere un blocco dalla cache appena si verifica la richiesta del blocco successivo; la logica è che i blocchi precedenti probabilmente non saranno più utilizzati, quindi può essere opportuno rimuoverli immediatamente per far posto ad altri blocchi che hanno maggior probabilità di essere utilizzati più a lungo.
86 5.6. IL FILE SYSTEM DI UNIX 73 La seconda tecnica consiste viceversa nel leggere e mettere in cache, alla richiesta di un blocco, il blocco stesso e il blocco successivo (o vari altri blocchi successivi); la logica è che è probabile, per un accesso sequenziale a un file, che i blocchi seguenti vengano richiesti subito dopo. Infine, un altro terreno importante è quello che mira ad ottimizzare (cioè a minimizzare) i movimenti delle testine dei dischi, che come già detto altrove (Paragrafo 3.8 pag. 45) determinano il tempo di posizionamento, ovvero la componente più importante del tempo di accesso ai dati di un disco. Questo argomento prende il nome di disk scheduling, e sarà trattato nel Capitolo 12; si tratta di effettuare un riordinamento ottimale delle richieste pendenti di lettura/scrittura a un dato disco. Si tenga presente che il gap di prestazioni tra CPU e dischi è tale che ha perfettamente senso aggiungere dalle migliaia alle centinaia di migliaia di istruzioni per diminuire il movimento delle testine. 5.6 Il file system di Unix L intero Sistema Operativo Unix è basato su un file system che rimane tuttora valido, pur essendo stato introdotto un quarto di secolo fa; è opportuno quindi iniziare la descrizione di Unix partendo da tale file system. Esistono oggi svariate realizzazioni di file system per Unix che differiscono anche in modo notevole l una dall altra, ma in pratica sono tutte più o meno delle varianti, con l apporto di varie ottimizzazioni e migliorìe, di un unica implementazione originale 2, che è quella che qui si va a descrivere. In questo capitolo verranno discussi soltanto alcuni aspetti dell implementazione del file system di Unix; per una trattazione più completa e dettagliata si rimanda a Asta [3], Paragrafo 10 (Organizzazione dei file system in Unix) Strutturazione del file system Un file system viene creato in Unix tramite il comando mkfs(8), ovviamente riservato all amministratore di sistema; questa operazione organizza il disco logico in 4 (o talvolta 5) regioni autoidentificate, illustrate nella Figura 5.1. La prima regione (costituita dal solo blocco numero 0) non è utilizzata dal file system: questo blocco è semplicemente messo da parte per le procedure di bootstrap. La seconda regione (costituita dal solo blocco numero 1) contiene il superblock, un blocco che contiene una struttura di dati di base, con tutto ciò che serve per gestire un file system; tra le altre cose, contiene la lunghezza del file system stesso e le frontiere delle altre regioni 3. 2 Si tratta per la precisione dell implementazione adottata in Unix V.7. 3 L integrità del superblock è ovviamente critica per l intero file system, nel senso che se viene corrotto il superblock, c è rischio di perdere il contenuto di tutti i files. Per questo molte implementazioni più recenti, incluso il file system ext2fs di Linux, mantengono copie del superblock in vari altri blocchi sparsi nel disco.
87 74 CAPITOLO 5. IL FILE SYSTEM n n+1 inutilizzato (boot) superblock i list inode inode inode inode inode inode inode... i number = 6 inode dati m m+1 inutilizzato (swap) z Figura 5.1: Organizzazione di un file system di Unix. La terza regione è denominata i-list, e contiene una lista di strutture di controllo dei files. Ognuna di queste è una struttura di dati lunga 64 bytes, chiamata inode. L indice di un inode particolare all interno della i-list è chiamato il suo i-number 4. Dopo la i-list e fino alla fine del disco, viene la quarta regione, che comprende i blocchi di dati che sono disponibili per il contenuto dei files. Qualora la dimensione totale del filesystem (specificata in argomento a mkfs) sia inferiore alla dimensione totale del disco logico, può infine esistere una quinta regione, che si estende dalla fine del filesystem fino alla fine del disco; tale regione può essere utilizzata come zona di swap del sistema 5 (si veda il Paragrafo pag. 13) File e cartelle Un file Unix è un contenitore di informazioni strutturato come una sequenza di byte. Il Nucleo Unix non interpreta il contenuto di un file. Sono disponibili diverse librerie di programmi che realizzano astrazioni ad un livello più elevato: 4 La combinazione del device number del disco logico e dello i-number serve a identificare univocamente un file. Si veda il Capitolo Più comunemente, alla zona di swap si dedica un intera partizione del disco.
88 5.6. IL FILE SYSTEM DI UNIX 75 / var bin sbin usr etc dev tmp home... fd0... rossi bianchi verdi... prog1 documentii... Figura 5.2: Collocazione dei file nello spazio dei nomi. facendo uso dei programmi inclusi in tali librerie, è possibile ad esempio considerare un file come composto da una sequenza di record, ognuno dei quali è suddiviso in campi; è inoltre possibile caratterizzare uno o più campi di un record come campi chiave ed effettuare indirizzamenti veloci basati sull uso di campi chiave. Tutti i programmi inclusi in tali librerie fanno uso tuttavia delle chiamate di sistema standard del Nucleo Unix. Dal punto di vista dell utente, i file sono collocati in uno spazio dei nomi organizzato ad albero rovesciato, come indicato nella Figura 5.2. Tutti i nodi dell albero, tranne i nodi foglia, denotano nomi di cartelle. Ogni nodo di tipo cartella specifica quali nomi di file e quali nomi di cartelle sono contenuti in esso. Un nome di file o cartella consiste in una sequenza di caratteri ASCII, con l eccezione del carattere / e del carattere nullo \0 usato come terminatore di stringhe. Ogni file system Unix pone un limite alla lunghezza massima di un nome di file, tipicamente 255 caratteri. La cartella associata alla radice dell albero è chiamata cartella radice (in inglese, root directory). Per convenzione, il suo nome è rappresentato col carattere /. I nomi di file all interno della stessa cartella devono essere distinti tra loro, anche se lo stesso nome di file può essere usato in cartelle diverse. Unix associa una cartella attiva (o cartella di lavoro; in inglese, current working directory) ad ogni processo; essa fa parte del contesto di esecuzione del processo ed identifica la cartella attualmente usata dal processo. Per identificare un file, il programmatore fa uso di un nome di percorso (in inglese, pathname) che consiste in una alternanza di caratteri / e di nomi di cartella. L ultimo componente del nome di percorso può essere il nome di una cartella, oppure il nome di un file, a seconda che l oggetto da identificare sia una cartella oppure un file. Se il primo componente di un nome di percorso è uno /, allora il nome di percorso è di tipo assoluto, poiché la ricerca deve iniziare a partire dalla cartella radice. Nel caso opposto il nome di percorso è relativo, poiché la ricerca inizia dalla cartella attiva del processo in esecuzione.
89 76 CAPITOLO 5. IL FILE SYSTEM Nel costruire nomi di percorso, sono anche usati i simboli. e... Essi indicano, rispettivamente, la cartella attiva e la cartella che contiene la cartella attiva. Se la cartella attiva è la cartella radice, il significato dei simboli. e.. coincide Link hard e soft Ogni nome di file incluso in una cartella è chiamato hard link o più semplicemente link. Lo stesso file può avere più link inclusi nella stessa cartella, o in cartelle diverse, e quindi può avere più nomi. Il comando Unix ln file1 file2 è usato per creare un nuovo hard link avente nome di percorso file2 relativo al file avente nome di percorso file1. Gli hard link hanno due limiti: non è possibile creare un hard link relativo ad una cartella poiché questo potrebbe trasformare l albero delle cartelle in un reticolo con cicli, rendendo con ciò impossibile la identificazione di un file in base al suo nome di percorso; un nuovo hard link file2 può essere creato soltanto nel file system che contiene il file file1; questo è un limite in quanto i sistemi Unix più recenti consentono di installare più file system di tipo diverso in più dischi o partizioni. I suddetti limiti sono superati facendo uso di soft link (sono anche chiamati link simbolici). Essi consistono in appositi file di dimensione ridotta che contengono il nome di percorso di un file. Tale nome di percorso è arbitrario; esso può identificare un file inserito in un qualsiasi file system, o perfino un file inesistente. Il comando Unix ln -s file1 file2 è usato per creare un nuovo soft link avente nome di percorso file2 relativo al file avente nome di percorso file1. Quando questo comando viene eseguito, il file system crea un file di tipo link simbolico (si veda il prossimo paragrafo) e scrive in esso il nome di percorso file1; inserisce quindi nella cartella opportuna una nuova voce contenente l ultimo nome del nome di percorso file2. In questo modo, diventa possibile tradurre automaticamente ogni riferimento ad file2 in un riferimento ad file1.
90 5.6. IL FILE SYSTEM DI UNIX Tipi di file I file Unix sono divisi nei seguenti tipi: file regolari; cartelle; link simbolici; device file (special file) orientati a blocchi; device file (special file) orientati a caratteri; pipe con nome (named pipe) o FIFO; socket I primi tre tipi di file sono i componenti del file system Unix: l informazione (dati o programmi) è contenuta nei file regolari; l uso di cartelle consente di realizzare uno spazio dei nomi di file organizzato ad albero, e quindi di fare uso di nomi di percorso; i link simbolici, infine, offrono una maggiore flessibilità di gestione. Una cartella, all atto pratico, non è altro che un file come gli altri, che contiene però i nomi dei file in essa contenuti e informazioni per accedere ai blocchi di controllo dei file stessi (inode). Un processo non può scrivere direttamente in una cartella, ma lo fa implicitamente quando crea, distrugge, o cambia nome ad uno dei file in essa contenuti. I device file, generalmente denominati special file nella documentazione e manualistica Unix, identificano dispositivi di ingresso/uscita quali dischi, tastiera o mouse. Essi sono stati introdotti da Unix per realizzare uno standard di programmazione C in base al quale le stesse chiamate di sistema che gestiscono i file ordinari (open(), close(), read(), write() etc. si veda oltre) devono potere essere utilizzate per gestire dati contenuti in un qualsiasi dispositivo di ingresso/uscita. Le pipe con nome o FIFO sono usate come un meccanismo veloce per la comunicazione tra processi. In pratica, il file di tipo pipe serve soltanto a memorizzare il nome della pipe, mentre lo scambio di dati tra processi avviene in RAM senza fare uso del file system. I socket sono utilizzati sia come strumento di sincronizzazione locale che per la connessione in rete Inode, i-number, descrittore di file Unix effettua una distinzione netta tra file e blocco di controllo di un file. Tutti i tipi di file tranne i device file e le FIFO sono considerati come contenitori di sequenze, eventualmente nulle, di caratteri. Pertanto un file non include al suo interno alcuna informazione che ne descriva la tipologia o la struttura, quale ad
91 78 CAPITOLO 5. IL FILE SYSTEM esempio la lunghezza del file oppure un delimitatore di tipo End-Of-File (EOF) che segnali la fine del file stesso. Tutte le informazioni necessarie al file system per gestire un file sono contenute invece in un apposito blocco di controllo, ovvero una struttura di dati chiamata inode. Ogni file possiede un proprio inode ed il file system usa al suo interno il numero dell inode, detto i-number, come identificatore del file. Tutti gli inode di un dato file system sono memorizzati in una zona del file system stesso, distinta dalla zona dati (quella che contiene i blocchi da assegnare ai file) e denominata i- list. Un programma utente invece, come si vedrà nel prossimo Paragrafo 5.7.1, fa riferimento a un file sempre attraverso un numero intero, denominato descrittore di file (o file descriptor), restituito dalle chiamate di sistema che aprono o creano i file. Questo file descriptor serve al Nucleo per referenziare correttamente il relativo inode, caricato in memoria RAM al momento dell apertura del file. Come già detto, esistono svariate realizzazioni di file system per Unix; in ogni caso, ognuna di esse deve rispettare lo standard POSIX, che richiede la presenza dei seguenti attributi tra quelli che caratterizzano un inode: tipo di file (vedi sopra); numero di hard link associati al file; lunghezza del file in byte; identificatore del dispositivo che include il file; numero dell inode associato al file; User ID (UID) dell utente proprietario del file; Group ID (GID) del file; alcune date e ore che specificano, ad esempio, quando l inode è stato indirizzato per ultimo e quando è stato modificato per ultimo; i diritti d accesso ed il file mode Diritti d accesso e file mode I potenziali utenti di un file sono divisi in tre classi: l utente proprietario del file: quando un file è creato da un programma, lo User ID e il Group ID dell utente per conto del quale tale programma viene eseguito vengono usati per caratterizzare il file; gli utenti che appartengono allo stesso gruppo (che hanno lo stesso stesso Group ID) a cui appartiene il file; i rimanenti utenti (others).
92 5.6. IL FILE SYSTEM DI UNIX 79 Tre diversi tipi di diritti d accesso chiamati Read (diritto di lettura), Write (diritto di scrittura) ed execute (diritto di esecuzione del codice) sono previsti per ognuna di queste tre classi. L insieme di diritti d accesso associati ad un file consiste quindi in nove indicatori (flag) binari. Altri tre indicatori chiamati suid (Set User ID), sgid (Set Group ID) e sticky definiscono il file mode. Si descrive qui di seguito il loro significato nel caso in cui il file sia un file contenente un programma eseguibile: un processo che esegue un file conserva solitamente lo User ID dell utente proprietario del processo; se però il file eseguibile ha il flag suid alzato (impostato a uno), allora il processo riceve lo User ID del proprietario del file; un processo che esegue un file conserva solitamente il Group ID associato all utente proprietario del processo; se però il file eseguibile ha il flag sgid alzato, allora il processo riceve il Group ID associato al file; se il file eseguibile ha il flag sticky alzato, il Nucleo deve cercare di mantenere in memoria il programma anche dopo che è stato eseguito 6. Quanto detto vale per i file ordinari; nel caso delle cartelle, i flag hanno un significato diverso, ma perfettamente coerente con la logica globale del file system quando si rifletta a quanto detto poc anzi (una cartella contiene i nomi dei file in essa contenuti, e i puntatori ai relativi inode): i flag Read indicano il diritto di consultare il contenuto della cartella, quindi ottenere la lista dei file in essa compresi; i flag Write indicano il diritto di modificare la cartella, quindi sono necessari per le seguenti operazioni: rimuovere un file dalla cartella; creare un nuovo file nella cartella; cambiare di nome a un file; i flag execute indicano invece il diritto di attraversamento della cartella, vale a dire la possibilità di passare dalla cartella ad una sottocartella in essa contenuta, nella ricerca di un file a partire da un nome di percorso; il flag sticky indica che nessun utente può distruggere un file all interno della cartella salvo il suo proprietario, e ciò vale anche se la cartella è di uso pubblico (se cioè ha i flag Write alzati). Si noti che da quanto detto deriva che per eliminare un file non è necessario nessun diritto particolare sul file stesso: basta avere i diritti di scrittura sulla 6 Il flag sticky non è più molto usato per i file eseguibili poiché esistono tecniche di gestione della memoria basate sul Demand Paging (si veda il Paragrafo 11.6 pag. 184) che lo rendono superfluo; è invece molto usato per file di tipo cartella, come precisato nel seguito del paragrafo.
93 80 CAPITOLO 5. IL FILE SYSTEM cartella che lo contiene (fatto salvo quanto appena detto sul flag sticky). Questa impostazione può apparire singolare a prima vista, ma in realtà risponde bene a criteri di sicurezza più che validi: se quasiasi utente, foss anche l amministratore del sistema (utente root), deposita (intenzionalmente o no) un file in una cartella di un altro utente, quest ultimo può forse non avere nemmeno il diritto di vederne il contenuto (se i flag di rettura non sono posizionati), ma ha comunque almeno il diritto di espellerlo da casa sua, rimuovendolo dalla propria cartella. D altra parte, il controllo del flag Write su una cartella garantisce l inamovibilità dei file in essa contenuti, e il controllo sui flag execute permette di rendere inaccessibile ad utenti estranei tutta l arborescenza di file e cartelle sottostante. L uso del flag sticky sulle cartelle permette tipicamente di gestire in modo sicuro cartelle di uso pubblico, per la creazione di file temporanei. Queste cartelle hanno i nove indicatori Read, Write e execute tutti alzati, e quindi chiunque può creare liberamente file e sottocartelle in esse; la presenza del flag sticky evita però che un utente vada a rimuovere, intenzionalmente o no, i file temporanei di un altro utente. 5.7 Chiamate di sistema per la gestione di file Quando un utente indirizza il contenuto di un file ordinario, oppure quello di una cartella, egli indirizza in pratica dei dati registrati in un block device, ossia in un dispositivo di ingresso/uscita di tipo disco. In questo senso, un file system può essere considerato come una astrazione a livello utente della organizzazione fisica di una qualche partizione di disco. Poiché l utente non è abilitato, per motivi di sicurezza, ad interagire direttamente col disco, ogni operazione attinente ad un file deve essere svolta in System mode. Per questo motivo, Unix prevede un insieme di chiamate di sistema per la gestione dei file; quando un processo intende interagire con uno specifico file, esso invoca la chiamata di sistema opportuna passando il nome di percorso del file come parametro. Si esamineranno ora brevemente tali chiamate di sistema. Si tenga presente che tutte queste chiamate, così come quelle che verranno introdotte nei prossimi capitoli, restituiscono invariabilmente il valore 1 in caso di errore; tale circostanza in linea di massima non verrà più rammentata, ma il lettore deve tenerla presente in tutti gli esempi di codice che verranno presentati Apertura di un file Per potere indirizzare un file, esso deve essere stato aperto in precedenza (oppure creato ex-novo e poi aperto). A tale scopo, il processo esegue la seguente chiamata di sistema: fd = open(path, flag, mode) I tre parametri usati hanno il seguente significato:
94 5.7. CHIAMATE DI SISTEMA PER LA GESTIONE DI FILE 81 path : Nome del percorso (assoluto o relativo) del file da aprire. flag : Specifica come il file deve essere aperto: solo lettura, solo scrittura, lettura/scrittura; specifica anche alcune richieste addizionali, quali la scrittura in fondo al file (modo append ) e, nel caso in cui non esista alcun file avente nome di percorso path, se esso debba essere automaticamente creato. mode : Specifica i diritti d accesso, cioè in particolare i flag Read, Write ed execute (nel solo caso in cui il file venga creato). Il Nucleo gestisce tale chiamata di sistema istanziando un oggetto file aperto e restituendo al processo un identificatore chiamato descrittore di file, già introdotto in precedenza (un intero positivo o nullo). Un oggetto file aperto contiene: alcune strutture di dati per la gestione del file, quali un campo per l identificazione del file system da cui proviene il file, un campo mode che indica le modalità in cui è stato aperto il file, un campo offset che indica l indice all interno del file dell ultimo carattere indirizzato (tale campo è chiamato anche puntatore di I/O del file) e così via; alcuni puntatori a funzioni (i metodi dell oggetto) che il processo è autorizzato ad invocare; tale insieme dipende dal valore del parametro flag. Si danno ora alcune ulteriori informazioni relative all interazione tra file e processo, che si evincono dalla semantica POSIX. Un descrittore di file rappresenta l interazione tra un processo ed un file aperto, mentre un oggetto file aperto contiene dati relativi a tale interazione. Lo stesso oggetto file aperto può essere identificato da più descrittori di file. Più processi possono aprire lo stesso file in modo concorrente: in tale caso, il file system assegna ad ognuno di essi un apposito descrittore di file ed un apposito oggetto file aperto. Quando ciò si verifica, il file system di Unix non tenta in alcun modo di sincronizzare le diverse operazioni di ingresso/uscita eseguite da più processi sullo stesso file. È comunque disponibile la chiamata di sistema flock() che consente a più processi di sincronizzarsi sull intero file o su una porzione di esso. Per creare un nuovo file, è anche possibile fare uso della chiamata di sistema creat(), che è gestita dal Nucleo come un caso particolare di una open() Indirizzamento di un file Dopo averlo aperto in precedenza, è possibile indirizzare un file regolare Unix in modo sequenziale oppure casuale (random); i device file e le FIFO sono solitamente indirizzati in modo sequenziale. In entrambi i tipi di accesso, il Nucleo aggiorna il valore del campo offset dell oggetto file aperto, cioè il puntatore di I/O del file, in modo che esso punti al prossimo carattere da leggere o da sovrascrivere all interno del file.
95 82 CAPITOLO 5. IL FILE SYSTEM Il file system assume di norma che il nuovo indirizzamento è sequenziale rispetto a quello effettuato per ultimo: le chiamate di sistema read() e write() fanno sempre riferimento all attuale valore del puntatore di I/O dell oggetto file aperto, nel senso che: ogni lettura o scrittura inizia implicitamente dalla posizione nel file indicata dal puntatore di I/O; dopo ogni lettura o scrittura, il puntatore viene automaticamente incrementato del numero di byte letti o scritti. Per modificare esplicitamente tale valore, ed effettuare quindi letture o scritture di tipo casuale, è necessario invocare la chiamata di sistema lseek(), che imposta un nuovo valore del puntatore. Quando un file viene aperto, il Nucleo imposta il suo puntatore di I/O alla posizione del primo byte nel file, ossia lo pone uguale a 0 7. La chiamata di sistema lseek() opera sui seguenti parametri: newpos = lseek(fd, offset, whence); che hanno i seguenti significati: fd : Descrittore di file del file aperto; offset : Specifica un intero usato per calcolare il nuovo valore del puntatore di I/O; whence : Specifica se il nuovo valore del puntatore di I/O debba essere calcolato come il valore di offset (offset dall inizio del file), la somma (algebrica) di offset più l attuale valore del puntatore di I/O (offset, positivo o negativo, dalla posizione attuale), o infine la differenza tra l indice dell ultimo byte nel file e offset (offset dalla fine del file). La chiamata di sistema lseek() restituisce il nuovo valore del puntatore di I/O. La chiamata di sistema read() fa uso dei seguenti parametri: nread = read(fd, buf, count); che hanno i seguenti significati: fd : Descrittore di file del file aperto che si intende leggere; buf : Indirizzo dell area di memoria nello spazio degli indirizzi del processo dove trasferire i dati letti; count : Numero di byte da leggere. 7 Salvo quando il file viene aperto in modo append, di cui si è detto; in tal caso il puntatore viene posizionato alla fine del file, cioè viene posto uguale alla lunghezza del file.
96 5.7. CHIAMATE DI SISTEMA PER LA GESTIONE DI FILE 83 Nel gestire tale chiamata di sistema il Nucleo cerca di leggere count byte dal file identificato da fd, a partire dal byte individuato dal puntatore di I/O nel descrittore dell oggetto file aperto. In alcuni casi (fine del file, pipe vuota, ecc.), il Nucleo non riesce a leggere tutti i byte richiesti. Il valore nread restituito dalla chiamata di sistema specifica il numero di byte effettivamente letti, che sarà pertanto sempre inferiore o uguale a count. Inoltre tale valore di ritorno sarà uguale a 0 se e solo se la lettura è partita dalla fine del file, e non c era quindi più niente da leggere. Riassumendo, nread può valere: un valore positivo, inferiore o uguale a count, pari al numero di byte effettivamente letti; 0 in caso di end of file; -1 in caso di errore. Il nuovo valore del puntatore di I/O è comunque ottenuto sommando il valore precedente a nread (salvo in caso di errore). I parametri della chiamata di sistema write() sono simili a quelli della read(). Il Nucleo esegue le chiamate di sistema read(), write() e lseek() in modo atomico: due chiamate di sistema non possono essere eseguite simultaneamente sullo stesso file Chiusura di un file Quando un processo non ha più necessità di indirizzare un file, esso invoca la chiamata di sistema: res = close(fd); che rilascia l oggetto file aperto relativo al descrittore di file fd. Il valore di ritorno res è semplicemente 0 in caso di successo, e -1 come d abitudine in caso di errore. Quando un processo termina, il Nucleo chiude tutti i file ancora aperti del processo Cambiamento di nome e cancellazione di file Per potere cambiare nome oppure cancellare un file, un processo non ha bisogno di aprire il file. In effetti, tali operazioni non agiscono sul contenuto del file bensì sul contenuto di una o più cartelle dove inserire o rimuovere il nome del file. La chiamata di sistema: res = rename(oldpath, newpath); cambia il nome di un link al file, mentre la chiamata di sistema: res = unlink(pathname); decrementa di uno il contatore di hard link al file. Il file viene cancellato dal file system quando tale contatore assume il valore 0.
97 84 CAPITOLO 5. IL FILE SYSTEM Esempi Gli esempi successivi illustrano alcune semplici applicazioni delle chiamate di sistema viste. Copyfile Il primo esempio è una routine che copia un file in un altro (si tratta essenzialmente di una routine equivalente al comando Unix cp(1), ancorché in versione semplificata); si noti l uso caratteristico del loop nell effettuare letture e scritture ripetute. Ad ogni iterazione, vengono scritti nread byte, tanti quanti ne sono stati letti (e non quanti si è richiesto di leggere); il loop termina correttamente quando la read() ritorna 0 byte letti, cioè come detto quando si raggiunge la fine del file. Pseudo-codice: copyfile(infile, outfile) { ifd = open(infile, read-only); ofd = open(outfile, write-only+crea se necessario, read-write per tutti); if(open error) return ERROR; } loop (nread = read(ifd, buffer, 4096); finche nread diverso da 0) { if(read error) return ERROR; write(ofd, buffer, nread); } close(ifd); close(ofd); return SUCCESS; Codice reale: #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #define SIZE 4096 /* copia il file infile nel file outfile. infile e outfile * sono i nomi dei due file. * retval: 0 se OK, -1 se errore */
98 5.7. CHIAMATE DI SISTEMA PER LA GESTIONE DI FILE 85 int copyfile(char *infile, char *outfile) { int ifd, ofd; int nread; char buf[size]; ifd = open(infile, O_RDONLY, 0); ofd = open(outfile, O_WRONLY O_CREAT O_TRUNC, 0666); if(ifd == -1 ofd == -1) return -1; /* errore */ } while(nread = read(ifd, buf, SIZE)) { if(nread == -1) { close(ofd); close(ifd); return -1; /* errore */ } write(ofd, buf, nread); } close(ofd); close(ifd); return 0; CopyFileSection Il secondo esempio è un evoluzione del primo, rispetto al quale prende un terzo argomento; la routine copia il primo file nel secondo, ma soltanto a partire da un certo offset. Per questo viene ovviamente utilizzata la chiamata di sistema lseek(), che permette di spostarsi all offset desiderato nel primo file; l operazione di copia continua poi normalmente, fino alla fine del file d ingresso. Pseudo-codice: copyfilesection(infile, outfile, offset) { ifd = open(infile, read-only); ofd = open(outfile, write-only+crea se necessario, read-write per tutti); if(open error) return ERROR; lseek(ifd, offset, da inizio file); loop (nread = read(ifd, buffer, 4096); finche nread diverso da 0) {
99 86 CAPITOLO 5. IL FILE SYSTEM } if(read error) return ERROR; write(ofd, buffer, nread); } close(ifd); close(ofd); return SUCCESS; Codice reale: #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #define SIZE 4096 /* copia il file infile nel file outfile. infile e outfile * sono i nomi dei due file; offset e l offset iniziale * da cui copiare. * retval: 0 se OK, -1 se errore */ int copyfilesection(char *infile, char *outfile, off_t offset) { int ifd, ofd; int nread; char buf[size]; ifd = open(infile, O_RDONLY, 0); ofd = open(outfile, O_WRONLY O_CREAT O_TRUNC, 0666); if(ifd == -1 ofd == -1) return -1; /* errore */ lseek(ifd, offset, SEEK_SET); } while(nread = read(ifd, buf, SIZE)) { if(nread == -1) { close(ofd); close(ifd); return -1; /* errore */ } write(ofd, buf, nread); } close(ofd); close(ifd); return 0;
100 5.8. APPENDICE - FUNZIONI DI HASHING Appendice - Funzioni di hashing L uso di funzioni di hashing è una tecnica generale di programmazione, utilizzata in molte parti di un Sistema Operativo e in altri campi dell informatica. In questo capitolo sono stati citati due esempi importanti di utilizzo di questa tecnica: l implementazione interna delle cartelle e la gestione dei blocchi della memoria di cache (nella RAM) dei dischi. Lo scopo della tecnica è di fornire un algoritmo veloce per la ricerca di elementi in una lista non ordinata, evitando la ricerca lineare (esaustiva) su tutta la lista; lo scopo è raggiunto dividendo la lista in un numero molto più piccolo di sottoliste (viene cioè operata una partizione dell insieme degli elementi); l algoritmo permette di identificare immediatamente la sottolista a cui appartiene l elemento cercato (se esiste), dopodichè la ricerca all interno della sottolista è lineare ma molto veloce, data la sua ridotta dimensione. Il meccanismo è essenzialmente il seguente: Si definisce una funzione di hashing, h(), ad N valori finiti (ad esempio, da 0 a 63). Spesso si sceglie per N una potenza di 2 8. Si raggruppano gli elementi in N sottoliste lineari, ove ciascuna sottolista contiene tutti e soli gli elementi con identico valore della funzione di hashing h() La ricerca di un elemento avviene secondo i seguenti passi: si calcola la funzione di hashing dell elemento ricercato; si ricerca l elemento (se c è) nella sola sottolista corrispondente al valore della funzione h() La generica situazione è illustrata nella Figura 5.3: esiste un array di N puntatori ad elemento, ciascuno dei quali rappresenta l inizio di ciascuna sottolista; gli elementi delle sottoliste sono collegati tra di loro per mezzo di puntatori al prossimo elemento (lista collegata semplice). Calcolata la funzione di hashing, l array viene indicizzato direttamente per trovare l inizio della sottolista di interesse, e da qui parte la ricerca lineare. Perché la tecnica sia efficace, cioè dia buoni risultati in termini di velocizzazione della funzione di ricerca di un generico elemento, occorre soprattutto trovare un valore ottimo di N, e una buona funzione di hashing, cioè una funzione che sia semplice (e veloce) da calcolare, e che disperda uniformemente gli elementi su tutte le sottoliste. 8 Nel caso dei nomi di file in una cartella, ad esempio, una scelta possibile è la seguente: h() = {somma dei caratteri del nome del file} modulo N Nel caso della memoria di cache di blocchi di un disco, in Unix si sceglie di solito una funzione del genere: h() = {device number + block number} modulo N Per la definizione di device number, si veda il Capitolo 13.
101 88 CAPITOLO 5. IL FILE SYSTEM sottoliste valori di hash elementi Figura 5.3: Tecnica delle tabelle di hashing.
102 Capitolo 6 Interfaccia con l Utente 6.1 Introduzione In qualunque Sistema Operativo, e segnatamente in quelli di tipo interattivo, ogni utente collegato al sistema deve essere in grado, servendosi dei diversi dispositivi di ingresso previsti, di specificare in modo agevole quale tra i vari servizi riconosciuti ed eseguibili dal sistema egli intende ottenere. In conseguenza, il Sistema Operativo deve includere appositi programmi per riconoscere i segnali di ingresso e i comandi inviati dall utente; tali programmi costituiscono complessivamente una interfaccia tra l utente da una parte, e il resto del Sistema Operativo e l hardware dall altra, o in molti casi, più esattamente, tra l utente da un lato e il software di base del Sistema Operativo dall altra. Si insiste sul fatto che il software di base, più che le chiamate di sistema, è ciò che l utente vede e giudica di un Sistema Operativo, come già evidenziato nel Paragrafo 1.3 pag. 5, e pertanto detto software, e l interfaccia utente che permette di accedervi, svolge un ruolo fondamentale nell uso pratico di un Sistema Operativo. Le interfacce dei Sistemi Operativi possono avere caratteristiche alquanto diverse, anche se il loro compito rimane sempre quello di identificare la richiesta effettuata dall utente nonché gli eventuali parametri ad essa associati. Si possono distinguere tre approcci possibili basati, rispettivamente, sull uso di menu, comandi ed icone. 6.2 Interfaccia a menu Nelle interfacce basate sull uso di menu, l utente non deve ricordare i nomi dei diversi programmi che intende eseguire poiché ogni menu che appare sullo schermo include un elenco di funzioni (o classi di funzioni) eseguibili e, per ognuna di esse, è fornita una breve descrizione nonché il nome del tasto da premere per richiederne l esecuzione. In generale, data la limitata capacità dello schermo, è necessario ricorrere a una gerarchia di menu per potere descrivere tutte le funzioni offerte dal Sistema Operativo. Il pregio di tali interfacce, tuttora utilizzate in sistemi transazionali ed all interno di vari programmi applicativi, sta nella 89
103 90 CAPITOLO 6. INTERFACCIA CON L UTENTE loro semplicità: in primo luogo, esse non richiedono l uso di un terminale grafico ma possono operare anche su semplici terminali alfanumerici. In secondo luogo, i programmi di gestione di tali interfacce sono alquanto semplici e compatti. Le interfacce a menu si rivelano tuttavia troppo rudimentali per potere consentire ad un utente di interagire con un moderno Sistema Operativo. 6.3 Interfaccia a comandi In tale interfaccia l utente interagisce col sistema inviando ad essi dei comandi. Ogni comando è costituito da una sequenza di caratteri alfanumerici seguita dal tasto di <INVIO>. I comandi hanno una loro sintassi ben definita che è usata da un modulo del Sistema Operativo chiamato genericamente interprete di comandi (command language interpreter, o CLI si veda il Paragrafo pag. 26); tale modulo analizza ogni comando ed esegue le azioni richieste. Tipicamente il comando inizia col nome seguito da una serie di parametri ed opzioni. In realtà, è più corretto parlare negli attuali Sistemi Operativi di linguaggi di comandi anziché di semplici comandi. In effetti, è possibile creare file contenenti sequenze di comandi: quando l interprete di comandi riceve il nome di tale file come programma da eseguire, passa ad interpretare i vari comandi contenuti nel file. I linguaggi di comandi prevedono l uso di variabili locali, variabili di sistema e frasi condizionali, per cui rappresentano, in diversi casi, una valida alternativa ai linguaggi compilati. I programmi scritti in un linguaggio di comandi prendono il nome di script. Al solito, la scelta se realizzare una applicazione facendo uso di uno script oppure di un programma compilato (ad esempio, dal compilatore di linguaggio C) dipende dai tempi di risposta richiesti: l esecuzione di uno script risulta ovviamente più lenta poiché ogni comando è interpretato dall interprete di comandi. D altra parte, i Sistemi Operativi includono solitamente biblioteche molto ampie di comandi già predisposti che possono essere opportunamente combinati in uno script, per cui il tempo di messa a punto dell applicazione risulta molto inferiore quando si ricorre ad uno script Lo shell di Unix L interfaccia tra l utente ed il Sistema Operativo Unix è una interfaccia a comandi. Questa interfaccia è qui descritta succintamente, evidenziando solo alcune delle caratteristiche più interessanti; il lettore è invitato a consultare Asta [3] per una trattazione completa dell argomento. I comandi Unix sono alquanto potenti per due motivi diversi. Non esiste un insieme predefinito di comandi: ogni programma eseguibile o interpretabile tramite uno degli interpreti inclusi nel Sistema Operativo è considerato come un comando. L utente può quindi creare comandi personalizzati e richiederne l esecuzione semplicemente digitando il nome del
104 6.3. INTERFACCIA A COMANDI 91 nuovo comando seguito dagli opportuni parametri e premendo il tasto di <INVIO>. Unix consente di creare comandi composti da comandi più semplici. A tale scopo, Unix include un apposito linguaggio di comandi che si distingue da altri simili linguaggi per la sua ricchezza e flessibilità. Esso è un vero e proprio linguaggio di programmazione dotato di frasi di controllo e chiamate di procedura. A differenza dei linguaggi di programmazione convenzionali quali il C oppure il Fortran, i comandi scritti in tale linguaggio non devono essere compilati da un apposito compilatore ma vengono immediatamente letti, interpretati ed eseguiti da un interprete di comandi chiamato shell (guscio). Esistono diversi shell (Bourne shell, C shell, Korn shell, bash shell, ecc.) che differiscono leggermente l uno dall altro. Viene inoltre offerta all utente la possibilità di definire il proprio shell da usare in applicazioni particolari. Vediamo ora quali sono le caratteristiche più interessanti di tale linguaggio di comandi che, secondo una consuetudine invalsa, chiameremo shell. Un comando Unix è una frase composta dal nome del comando seguito eventualmente da parametri separati da uno spazio. Dopo aver digitato il comando, è necessario digitare il tasto di <INVIO> per indicare il completamento del comando stesso. Nell eseguire, ad esempio, il comando: cp file1 file2 lo shell effettua una copia del file chiamato file1 ed assegna a tale copia il nome file2 (se esiste già un file avente tale nome, il suo contenuto viene distrutto e sostituito dal nuovo). I caratteri immessi da tastiera e quelli visualizzati sullo schermo del terminale sono considerati dallo shell come due file speciali chiamati, rispettivamente, standard input e standard output (abbreviati di norma in stdin e stdout) 1. In assenza di altre informazioni, lo shell assume che il file stdin (file di ingresso) sia associato alla tastiera e quello stdout (file di uscita) sia associato al video. Nell eseguire, ad esempio, il comando: date lo shell visualizza la data e l ora attuale sul video. La ridefinizione dell input e dell ouput è una delle varie caratteristiche che rendono lo shell molto flessibile. Usando gli operatori > per ridefinire l output e < per ridefinire l input, è possibile specificare file di ingresso e di uscita diversi da quelli standard. Il comando: date > file1 1 Esiste un terzo file (o canale di I/O) importante, denominato standard error o stderr; si veda Asta [3].
105 92 CAPITOLO 6. INTERFACCIA CON L UTENTE scrive, ad esempio la data e l ora attuale nel file file1 anziché sul video. Servendosi di tale linguaggio, è possibile specificare mediante frasi di controllo del tipo if/then/else e while, l ordine mediante il quale eseguire la sequenza di comandi contenuta nel programma e controllare tale esecuzione in funzione dei parametri ricevuti e delle condizioni di terminazione dei vari comandi eseguiti. In Asta [4] si possono trovare diversi esempi di shell script Unix, presentati in ordine di complessità crescente. Si suggerisce comunque al lettore di provare il maggior numero possibile di comandi tra quelli documentati tramite il comando man Struttura dello shell L interprete di comandi o shell è un programma memorizzato in un apposito file eseguibile del file system. Durante l inizializzazione del sistema, Unix lancia uno shell per ognuno dei terminali installati; ognuno di tali shell visualizza un messaggio di login sul proprio terminale e rimane in attesa fino a quando l utente non inizia una sessione comunicando il suo nome e la relativa parola d ordine. Se il login risulta corretto, lo shell responsabile del terminale crea uno shell secondario per gestire le richieste del nuovo utente e si pone in attesa della terminazione (o del passaggio allo stato di background) dello shell appena creato. Quando l utente al terminale conclude, mediante il comando logout, la sessione di lavoro, lo shell elimina tutti gli eventuali shell secondari creati durante la sessione. La distinzione tra shell principale e shell secondari può risultare oscura a prima vista: perché non fare interpretare direttamente dallo shell principale i comandi digitati dall utente anziché lanciare shell secondari? La risposta è semplice: la gerarchia di shell consente una flessibilità operativa molto maggiore agli utenti più smaliziati. Diamo un paio di esempi per giustificare questa affermazione: facendo seguire ad un comando il carattere &, il comando diventa un comando di background e lo shell ridiventa immediatamente attivo senza aspettare la terminazione del comando di background lanciato. L utente può, ad esempio, lanciare una compilazione di un grosso progetto software (ridirigendo lo stdout in un file di log), che richiede un tempo notevole, tramite un comando di background, e continuare ad usare il terminale mentre la compilazione è in atto; in alcuni sistemi Unix, tra cui Linux, è possibile creare più console e commutare da una all altra. In Linux, digitando <CTRL><ALT>F2,..., <CTRL><ALT>F7, si possono lanciare altre console oltre a quella iniziale che condividono tutte lo stesso monitor. Vediamo ora come lo shell associa al nome di un comando il file contenente il programma o lo script da eseguire. In teoria, si potrebbe richiedere di digitare ogni volta l intero pathname ma tale soluzione non sarebbe gradita dall utente in quanto lo costringerebbe a digitare lunghe sequenze di caratteri, ad esempio
106 6.3. INTERFACCIA A COMANDI 93 /usr/bin/man anziché man. Il problema viene risolto introducendo una apposita variabile shell chiamata PATH che contiene la sequenza delle directory, separate dal carattere : dove cercare il file avente un nome prescelto. Il contenuto di PATH può essere personalizzato inserendo una nuova definizione, diversa da quella di default, nel file.profile contenuto nella home directory (cartella attiva iniziale) dell utente. Possiamo usare il comando 2 : echo $PATH per visualizzare sullo schermo il valore della variabile PATH, ossia i nomi di tutte le directory che verranno esaminate dallo shell per cercare un file il cui nome sia uguale a quello del comando. In risposta a tale comando, appare sul monitor una sequenza di pathname del tipo: /usr/local/bin:/usr/bin:/bin:/usr/x11r6/bin:/sbin:. Se il comando digitato dall utente non può essere eseguito, lo shell segnala una condizione di errore. Questo avviene per due possibili motivi: il file system non contiene un file avente il nome del comando; il file avente il nome del comando non è né un file eseguibile né un file di comandi per il quale sia disponibile un interprete. Lo shell è in grado di distinguere un file eseguibile (un comando) da un file contenente comandi da eseguire tramite un opportuno interprete (tale tipo di file è chiamato comando indiretto), leggendo i primi byte del file: essi contengono ciò che viene denominato un numero magico (o magic number), ossia un identificatore che specifica qual è il programma richiesto per eseguire il file (caricatore o specifico interprete). Non esiste infatti un solo interprete: esistono diversi interpreti per gli shell Unix ed esistono inoltre interpreti del tutto diversi per linguaggi interpretati quali Java, Perl etc. Come è ovvio, se il file contiene un numero magico ignoto al Sistema Operativo, lo shell ritorna un messaggio di errore del tipo: command not found Se invece lo shell lanciato per eseguire il comando va in esecuzione, lo shell che lo ha lanciato si mette in attesa della sua terminazione. Il suddetto schema è 2 Lo shell permette di assegnare valori (stringhe alfanumeriche) a variabili arbitrarie tramite l operatore =: VARIABLE=string e di sostuire ad una variabile il suo valore tramite l operatore $: echo $VARIABLE
107 94 CAPITOLO 6. INTERFACCIA CON L UTENTE ricorsivo: se uno dei comandi è a sua volta un comando indiretto, un terzo shell passa a interpretarlo e così via. Concludiamo questa presentazione sommaria degli shell Unix con una osservazione importante. Oltre ad essere digitati da utenti al terminale, i comandi possono essere lanciati da un qualsiasi programma in esecuzione tramite una chiamata di sistema del tipo execve() (vedi Capitolo 8), o una API del tipo system(). È quindi possibile scrivere un programma che simula la sequenza di azioni svolte da un utente al terminale. 6.4 Interfacce grafiche Le interfacce basate sull uso di icone sono state introdotte per rispondere a particolari esigenze di facilità d uso e di apprendimento e sono quindi rivolte in primo luogo ad utenti non necessariamente esperti. A differenza dei due tipi di interfacce descritte in precedenza che richiedono soltanto un terminale alfanumerico ed una tastiera, le interfacce grafiche richiedono invece l uso di un terminale grafico, di una tastiera e di un apposito dispositivo d ingresso chiamato mouse mediante il quale è possibile spostare il cursore grafico sullo schermo ed inviare segnali al sistema premendo uno dei pulsanti collocati in cima ad esso. L idea di base è quella di offrire all utente un menu grafico composto da ideogrammi chiamati icone. Il primo Sistema Operativo commerciale con interfaccia grafica è stato messo a punto per i Personal Computer Macintosh della Apple negli anni 80. In tale sistema, il menu iniziale includeva una serie di icone corrispondenti ai file dell utente, a quelli contenenti programmi del Sistema Operativo e ad altri file di servizio come la cartella vuota e il cestino. Per cancellare, ad esempio, un file, ad esempio il file di nome A, in tale sistema è sufficiente: 1. posizionare il cursore dentro all icona A (questo si ottiene spostando il mouse sulla scrivania finché la freccia del cursore non sia contenuta nell icona); 2. spostare, tenendo premuto il pulsante del mouse, l icona A in quella denominata cestino rilasciando quindi il pulsante. Per aprire un documento gestito da una apposita applicazione, è sufficiente effettuare un doppio clic sull icona corrispondente. In alcuni casi, però, l uso della tastiera rimane fondamentale: ogni qualvolta si intende creare una nuova cartella, oppure cambiare il nome di un file, è necessario trasmettere l informazione al file system facendo uso della tastiera. Oggi le interfacce grafiche sono diventate uno standard per i Sistemi Operativi dei Personal Computer e sono molto diffuse nei Sistemi Operativi MacOs della Apple, in quelli di tipo Unix/Linux ed in quelli della famiglia Microsoft Windows quali Windows 98 o Windows 2000.
108 6.5. INTERFACCIA GRAFICA O A COMANDI Interfacce grafiche per Unix A differenza di altri Sistemi Operativi quali MacOS della Apple, oppure Windows NT della Microsoft, che incorporano l interfaccia grafica all interno del Nucleo, l interfaccia grafica per Unix è sempre realizzata tramite una applicazione di tipo client/server. In effetti, Unix è nato prima che esistessero i terminali grafici necessari per realizzare tale tipo di interfaccia. Il più noto programma applicativo Unix che realizza una interfaccia grafica si chiama X windows. Esso è stato sviluppato inizialmente presso lo MIT ed è oggi lo standard de facto di interfaccia grafica per sistemi Unix. Sono stati sviluppati numerosi client specializzati per X windows, ad esempio per emulare terminali alfanumerici (xterm), per visualizzare documentazione Unix all interno di una finestra (xman), per visualizzare in forma grafica la struttura del file system e delle sue cartelle (xfm), per rappresentare e collocare finestre (fvwm, ecc.), e così via. 6.5 Confronto tra interfaccia grafica e interfaccia a comandi Le interfacce a comandi continuano ad essere le interfacce preferite dai programmatori professionisti per la loro estrema flessibilità d uso, anche se possono risultare poco gradite ad un utente occasionale il quale non realizza perché sia necessario fare uso di comandi alquanto complessi per svolgere semplici funzioni. L evoluzione di Windows NT è significativa a tale rispetto: le prime versioni di Windows NT offrivano soltanto una finestra terminale alfanumerico di tipo emulatore MS-DOS, ossia lo shell di MS-DOS che è alquanto rudimentale e certamente non all altezza del resto del Sistema Operativo. Versioni successive di Windows NT hanno poi introdotto un gestore di linguaggi di comandi chiamato WHS (Windows Scripting Host); esso è in grado di controllare l esecuzione in modalità interpretativa di script, scritti usualmente in linguaggio VBscript o altri linguaggi analoghi. WSH consente all utente di eseguire script in due modi possibili: direttamente dal desktop, cliccando sull icona che rappresenta lo script, oppure digitando una riga di comando nell apposita finestra alfanumerica chiamata command console.
109 Capitolo 7 Gestione dei Processi e della CPU 7.1 Introduzione Pur tenendo conto della continua diminuzione del costo dell hardware, i processori (circuiti in grado di eseguire istruzioni programmabili registrate in memoria RAM) continuano ad essere considerati risorse hardware pregiate il cui uso deve essere ottimizzato. Per questo motivo, gli attuali Sistemi Operativi sfruttano il potenziale parallelismo hardware dell elaboratore avviando e controllando l esecuzione di più programmi o sequenze di istruzioni sui processori disponibili. Viene in tale modo aumentata la produttività o throughput del sistema, ossia il numero di programmi eseguiti per unità di tempo. Per conseguire tale risultato, è necessario fare uso di opportune tecniche di gestione dei programmi in esecuzione. 7.2 Multitasking L idea di base comune a queste tecniche è dunque quella di caricare simultaneamente in memoria più programmi e di eseguirne alcuni in parallelo, utilizzando i diversi processori a disposizione. Tale impostazione è nota come multitasking. Prima di entrare nei dettagli, è interessante osservare in via preliminare come sia possibile realizzare sistemi multitasking con ottime prestazioni anche per calcolatori con un architettura hardware minima, dotati di due soli processori: una CPU (processore centrale) e un processore di I/O (processore periferico). Questa configurazione, mostrata nella Figura 7.1, rappresenta in pratica il caso minimo di una configurazione reale, dato che da alcuni decenni ormai i calcolatori di uso generico (dai grandi elaboratori fino ai più piccoli Personal Computer) non sono mai dotati di un solo processore, ma ne contengono sempre più di uno: c è sempre almeno una unità centrale (CPU) ed uno o più processori nelle schede dei controller delle varie periferiche, come già evidenziato nel Paragrafo 3.2 pag. 96
110 7.2. MULTITASKING 97 CPU I/O proc. disco RAM controller RAM Figura 7.1: Architettura minima di riferimento per un sistema concorrente. 35. Questi processori condividono una memoria comune (la RAM centrale), e devono pertanto essere considerati come sistemi concorrenti in grado di svolgere più attività indipendenti allo stesso tempo. Come si può osservare nella figura, nel modello minimo esistono dunque almeno due processori autonomi, ossia una CPU (processore centrale) e un processore di ingresso/uscita (processore periferico). Nei sistemi mainframe e nei server aventi elevate prestazioni, sono solitamente presenti più CPU e più processori di I/O. I diversi processori del sistema accedono ad una memoria comune ed è presente un apposito circuito hardware (RAM Controller) che risolve eventuali conflitti derivanti dal fatto che più processori richiedono di accedere alla memoria simultaneamente: grazie a tale circuito, ogni processore può essere programmato come se fosse l unico a accedere alle informazioni contenute in memoria. Esiste quindi nei sistemi concorrenti un parallelismo potenziale di operazioni a livello hardware, nel senso che è possibile fare in modo che processori distinti eseguano allo stesso tempo istruzioni appartenenti a programmi diversi. Nel resto del capitolo si vedrà come deve essere organizzato un Sistema Operativo per sfruttare al meglio i processori tra loro indipendenti presenti nell elaboratore Cicli di uso della CPU e di I/O Ciò detto, è esperienza comune che l esecuzione di un programma sia invariabilmente costituita da una successione di fasi di elaborazione sulla CPU (detti cicli di uso della CPU, o CPU burst cycle) e fasi di attesa per il completamento di operazioni di I/O che impegnano il processore (o i processori) di I/O, lasciando logicamente inattiva la CPU (questi sono detti cicli di I/O, o I/O burst
111 98 CAPITOLO 7. GESTIONE DEI PROCESSI E DELLA CPU a. A I/O A A B I/O B B I/O B B C I/O C C (msec.) A B C A B C B (msec.) Esecuzione nella CPU b. I/O A I/O B I/O C I/O B (msec.) Esecuzione nel processore di I/O Richiesta di I/O al processore periferico Interrupt di fine I/O Figura 7.2: Un esempio di esecuzione multitasking di programmi. cycle). Questo comportamento dei programmmi è noto come CPU-I/O burst cycle. Durante le fasi di attesa di I/O, il Sistema Operativo multitasking può mandare in esecuzione sulla CPU altri programmi tra quelli caricati in memoria, migliorando così la produttività del sistema. La Figura 7.2 illustra in modo semplificato questa alternanza di cicli di uso della CPU e di I/O per tre programmi, chiamati A, B e C, e mostra l aumento di produttività derivante dalla esecuzione concorrente (caso b.) rispetto alla esecuzione sequenziale in ambiente uniprogrammato (caso a.). Nella figura si suppone per semplicità che i programmi abbiano dei cicli di CPU (fasi di calcolo) tutti della durata di 20 msec., alternati con cicli di I/O della durata di 35 msec.; il programma B richiede due cicli di I/O, mentre gli altri uno solo. Ogni volta che è necessario effettuare un ciclo di operazioni di I/O per conto di un programma, questo viene sottoposto a scheduling e la relativa richiesta viene inviata al processore di I/O, che si occupa di eseguirlo non appena possibile; al termine di ciascun ciclo di I/O, il processore periferico provvede ad informare la CPU mediante un interrupt di fine I/O (o fine DMA si veda il Paragrafo 3.7 pag. 44), e il prossimo ciclo di uso della CPU per quel programma viene successivamente eseguito non appena possibile. La figura mostra, come anticipato, che l esecuzione multitasking di più programmi distinti consente, in generale, di aumentare sensibilmente la produttività del sistema. Nel caso preso ad esempio i tre programmi si completano in 280 msec. se non c è processore di I/O, altrimenti in 180 msec.; la Tabella 7.1 mostra la differenza di produttività ottenuta nei due casi, che passa da a programmi al secondo, con un aumento quindi del 55.5%. Oggi la gestione multitasking è utilizzata in quasi tutti i tipi di Sistemi Operativi (gestione a lotti, sistemi interattivi, sistemi transazionali, controllo di processi). Vedremo successivamente nel capitolo come i diversi obiettivi che tali sistemi devono conseguire si riflettano in diverse tecniche di gestione dei processori (scheduling della CPU).
112 7.3. PROCESSI 99 Caso Tempo (msec.) Produttività Aum. di prod. (risp. al caso a.) a = progr./sec. b = progr./sec % Tabella 7.1: Differenza di produttività indotta dal multitasking. 7.3 Processi La nozione di processo (in inglese process o task) è emersa all inizio degli anni 60 insieme allo sviluppo dei primi sistemi multitasking. In tali sistemi è infatti necessario distinguere l attività svolta da un processore dalla esecuzione di un programma: infatti, in un intervallo di tempo prefissato, un processore può alternativamente eseguire sequenze di istruzioni appartenenti a programmi diversi mentre, durante lo stesso intervallo, l esecuzione di un programma su quel processore o su altri può essere ripresa e sospesa più volte. Come già precisato nel Paragrafo pag. 22, per contro il termine processo indica un entità attiva, che comprende di fatto un programma in esecuzione in un proprio specifico ambiente di lavoro. Si rimanda al citato paragrafo per una discussione dell ambiente di lavoro, come entità capace di influenzare le modalità di svolgimento di un programma e quindi di alterare il comportamento di un processo. Un processo include principalmente: un Program Counter (PC) una Sezione Testo (codice del programma - read-only, shared) uno Stack (nella fase utente) una Sezione Dati (dati gestiti dal programma nel suo ciclo di vita) 1 varie risorse, allocategli di norma dal Nucleo. informazioni di sistema, gestite dal Nucleo, sullo stato del processo (v. oltre) e su altri elementi utili alla gestione del processo stesso. Uno dei compiti principali del Sistema Operativo è quello di controllare l avanzamento di un gruppo di processi secondo vari obiettivi, tra cui quelli di soddisfare le richieste degli utenti e di mantenere impegnati i diversi processori. Per avanzamento di un processo si intende qui l avanzamento nell esecuzione delle istruzioni del programma associato. 1 Nel caso dei sistemi Unix/Linux, nella sezione dati si distinguono sempre i dati inizializzati da quelli non inizializzati; questi ultimi formano la sezione bss del processo. Si veda il Capitolo 9.
113 100 CAPITOLO 7. GESTIONE DEI PROCESSI E DELLA CPU Operazioni su processi Le operazioni fondamentali che un Nucleo svolge sui processi, o svolte da processi su altri processi, riguardano essenzialmente cinque categorie: creazione di nuovi processi, terminazione di processi, sincronizzazione tra processi o col Nucleo, comunicazione di dati, segnalazione. Si ritornerà con maggior dettaglio su queste operazioni nel seguito del capitolo e nel capitolo successivo. Creazione di processi In un Sistema Operativo, dall avvio fino allo spegnimento, processi padri creano nuovi processi figli e così via, dando quindi vita ad un albero di processi (un grafo orientato aciclico), secondo una relazione padre-figli. Ad esempio in Unix il Nucleo provvede ad avviare un primo processo al bootstrap della macchina, denominato init, che inizia a generare tuti i processi di sistema (server, comandi per il login sui terminali, e così via), che a loro volta proliferano generando sottoprocessi. In molti casi, al terminare di uno di questi processi suoi figli, init ne rilancia un altro, in un ciclo senza fine. Allo shutdown del sistema, init provvede a provocare la terminazione di tutti i processi da lui generati e tuttora in vita, provocando così l arresto di tutte le attività. I principali aspetti caratterizzanti di questa categoria di operazioni, in cui l implementazione, le modalità d uso e le funzionalità offerte possono variare sensibilmente da un Sistema Operativo a un altro, sono i seguenti: Risorse. Le risorse acquisite dal processo padre al momento della generazione del figlio possono essere ereditate dal figlio; nel seguito dell esecuzione dei due processi, possono essere totalmente condivise tra padre e figlio (nel senso ad esempio che cambiamenti, in uno dei due processi, nello stato di possesso di una risorsa, possono riflettersi anche sull altro processo), condivise solo parzialmente, oppure può non esserci affatto condivisione. Esempi di risorse condivisibili o no sono i file (o canali di comunicazione) aperti, aree di memoria condivisa, socket (per la comunicazione tra processi attraverso reti di calcolatori), e così via. In Unix, le risorse sono ereditate integralmente dal processo figlio, ma poi non c è normalmente alcuna condivisione: ciascun processo procede indipendentemente per la sua strada, senza influenzare l altro 2. Esecuzione. Questo elemento implica due aspetti distinti: vincoli eventuali di sincronizzazione nell esecuzione dei programmi del processo padre e del figlio, e modalità di esecuzione dei programmi stessi. Per il primo aspetto, i processi padre e figlio possono eseguirsi concorrentemente (in modo asincrono tra di loro) oppure no, nel qual caso in particolare il padre può essere obbligatoriamente sincronizzato sulla terminazione del figlio oppure no. 2 Ciò è vero per processi classici, generati attraverso la chiamata di sistema fork(); non è necessariamente vero per thread, generati attraverso la chiamata di sistema clone() di Linux. Le due chiamate di sistema qui menzionate sono trattate più in là in questo e nel prossimo capitolo.
114 7.3. PROCESSI 101 Per il secondo aspetto, il figlio può eseguire un programma precisato dal padre (la cui specifica quindi fa parte degli argomenti della primitiva per la creazione del processo figlio), oppure lo stesso programma del padre; in tal caso esiste normalmente un altra primitiva di sistema che permette ad un processo (il processo figlio, che inizialmente si ritrova ad eseguire obbligatoriamente il programma del padre) di passare all esecuzione di un altro programma. Questo è il caso di Unix, come dettagliato nel Capitolo 8, con le chiamate fork() per creare un processo ed execve() per l esecuzione di un nuovo programma. Terminazione La terminazione di un processo, con la conseguente deallocazione delle varie risorse ad esso allocate dal Nucleo, può avvenire in seguito ad una chiamata di sistema esplicita (come exit() in Unix), chiamabile in qualunque punto del programma, oppure implicita (per esecuzione dell ultima istruzione del programma). La terminazione può essere accompagnata dalla trasmissione di un codice di uscita (che notifichi almeno una tra le condizioni di successo o di errore ), recuperato poi dal Nucleo oppure dal processo padre. Infine, la terminazione di un processo può essere eventualmente forzata dal Nucleo, in caso di errore del programma (procedura abort); un esempio già discusso di questo tipo riguarda il caso di un programma che, per un errore di programmazione, commetta un tentativo di violazione di protezione della memoria andando a leggere o a scrivere in indirizzi a lui non allocati. Sincronizzazione La sincronizzazione riguarda la disciplina delle modalità di avanzamento dei vari processi, che possono essere mutuamente condizionati; può essere di due tipi: tra processi, su base cooperativa o no; oppure nei confronti di eventi determinati dal Nucleo, come il caso già contemplato di sincronizzazione sulla terminazione di un processo. Un esempio del primo tipo è il caso dell accesso ad un file di un data base: per assicurare la consistenza dei contenuti, i processi che accedono ai record fanno in modo da assicurare ad essi un accesso esclusivo, il che significa che qualunque processo che debba acquisire il controllo di un record per accedervi deve attendere che un eventuale altro processo abbia rilasciato detto controllo. Un altro caso del secondo tipo è quello discusso all inizio del capitolo (si veda la Figura 7.2): il processo A, ad esempio, una volta lanciata la richiesta di I/O che lo riguarda, si sincronizza sul suo completamento, e chiede implicitamente al Nucleo di riprendere l esecuzione del programma una volta che i relativi dati siano resi disponibili. Comunicazione Tale categoria di operazioni, che permette a processi di scambiare dati, richiede sempre implicitamente la sincronizzazione; si basa quindi sulla categoria precedente. La comunicazione di dati tra processi può svolgersi in varie modalità:
115 102 CAPITOLO 7. GESTIONE DEI PROCESSI E DELLA CPU one-to-one (un processo che invia dati ad uno specifico altro processo destinatario), one-to-many (un unico generatore di dati, molti processi che possono riceverli), many-to-many, many-to-one. La comunicazione può ovviamente avvenire internamente alla macchina o tra processi su macchine diverse, attraverso una rete di calcolatori. Segnalazione Quest ultima categoria riguarda la comunicazione tra processi non di dati, ma bensì di eventi asincroni; viene implementata di norma con degli interrupt software (dei trap, oppure simulati a livello Nucleo durante il cambio di contesto dello scheduler vedi oltre). Il processo mittente provoca un interruzione nel processo destinatario, e ciò fornisce a quest ultimo l informazione che qualcosa è successo. Unix dispone di una chiamata di sistema specifica, kill(), per permettere a un processo di inviare segnali ad altri processi (o a se stesso). Operazioni di segnalazione sono utilizzabili per provocare la terminazione di altri processi (ad esempio, un padre che termina un processo figlio o viceversa, oppure altre situazioni analoghe), o ancora per informare un processo che un certo limite di tempo è giunto a espirazione, e così via Stati di un processo La modellizzazione dell entità processo necessita di ulteriori importanti dettagli; uno di questi riflette la necessità di evidenziare le diverse situazioni in cui un programma può trovarsi durante la sua esecuzione. Così nella Figura 7.2, con riferimento al caso b., se si concentra l attenzione su uno specifico processo (sia questo il processo A) e sull avanzamento dell esecuzione delle istruzioni da parte del processore centrale, appare chiaro che il processo in esame passa attraverso diverse situazioni: nel periodo di tempo 0-20 msec., dispone della CPU ed è in grado di eseguire istruzioni del suo programma; sta quindi portando avanti il proprio avanzamento; nel periodo da 20 a 55 msec., è in uno stato di attesa di evento : sta aspettando che l operazione di I/O da lui richiesta venga portata a termine da parte di un processore periferico, in modo da poter disporre poi dei dati relativi all I/O 3 ; nel periodo da 55 a 60 msec., il processo è pronto ad avanzare nell esecuzione delle istruzioni (dato che l I/O è ormai terminato), ma non ha la CPU (che è invece allocata al processo C); deve quindi attendere per ottenere nuovamente la CPU; 3 Questa situazione di attesa d evento non coincide necessariamente con il periodo di tempo di esecuzione di istruzioni per conto del processo su un processore di I/O; così ad esempio per il processo B questo stato si estende sul periodo di tempo da 40 a 90 msec., mentre l esecuzione di istruzioni di I/O per lui va soltanto da 55 a 90 msec.
116 7.3. PROCESSI 103 New ammesso interrupt/trap uscita Terminated rilascio CPU Ready Running dispatch verificarsi di evento (completamento di I/O) Waiting attesa di evento (o di I/O) Figura 7.3: Stati di un processo e relative transizioni. nel periodo da 60 a 80 msec., la CPU è di nuovo a sua disposizione e il processo si ritrova nuovamente nello stesso stato in cui si trovava nei primi 20 msec., cioè in fase di avanzamento. Si può formalizzare quanto deriva dalle osservazioni fatte introducendo il concetto di stato di un processo. Durante il suo ciclo di vita un processo cambia stato; il suo avanzamento è quindi caratterizzato da una serie di transizioni da uno stato all altro. Il numero e tipo di stati possibili dipende dalla modellizzazione che si adotta (e dalla specifica implementazione in un dato Sistema Operativo), ma si può dire senza perdita di generalità che gli stati fondamentali possibili sono i seguenti: New: il processo è in fase di creazione; Running: il processo ha il controllo della CPU (cioè del processore centrale), e sta eseguendo istruzioni del suo programma; Waiting: il processo sta aspettando il verificarsi di qualche evento (o l acquisizione di qualche risorsa) che ne blocca l esecuzione; più formalmente, si dirà che un processo P è in attesa dell evento E (ovvero è bloccato sull evento E) quando, per potere procedere nel suo avanzamento, deve attendere il verificarsi di tale evento; Ready: il processo è in grado di eseguirsi (non ci sono cause che bloccano il suo avanzamento), ma è in attesa di avere il controllo della CPU; Terminated: il processo ha terminato l esecuzione del programma, ed è in fase di terminazione. Lo stato Waiting è anche detto Asleep. Quest ultimo termine (che significa addormentato ) si giustifica con l uso, largamente invalso nella letteratura tecnica,
117 104 CAPITOLO 7. GESTIONE DEI PROCESSI E DELLA CPU di parlare di processi addormentati : quando un processo si blocca in attesa di un evento, si dice che il processo va a dormire (in attesa di quell evento), e che sarà risvegliato (awakened) al verificarsi dell evento che ne ha provocato il blocco. La Figura 7.3 illustra gli stati ora introdotti e le transizioni possibili, accennando anche alle cause che le determinano. Come già anticipato nel Paragrafo pag. 13, in generale in una macchina con un solo processore centrale vi sarà un solo processo alla volta in stato Running (in una macchina multi-cpu vi saranno invece N processi in tale stato, tanti quante sono le CPU), e molti processi in stato Ready, che si contendono l uso del processore centrale. Il Nucleo può decidere di fare passare un processo dallo stato Ready a quello Running; la funzione che sceglie tra i vari processi quello da mettere in esecuzione è chiamata scheduling a breve termine, e verrà discussa alla fine di questo capitolo. Per ciò che concerne lo stato Waiting, vi sono numerosi motivi, tra loro diversi, per cui un processo viene a trovarsi in tale stato; ogni specifico cambiamento di stato del sistema e/o ogni evento esterno al sistema potrà servire, di norma, a sbloccare solo alcuni tra i vari processi bloccati Eventi e risorse Procedendo nella formalizzazione del modello, si osserva che i processi, come gruppo di entità autonome, non sono sufficienti a rappresentare il funzionamento di un sistema concorrente: in realtà, i processi interagiscono tra di loro durante l avanzamento, riflettendo in questo modo il fatto che i processori (centrali e periferici) comunicano tra di loro e si sincronizzano tramite svariati meccanismi. Nei sistemi concorrenti non si può escludere, anzi è un caso frequente, che due o più processi richiedano simultaneamente l uso dello stesso processore, oppure che essi indirizzino lo stesso lettore di disco. Per consentire di trattare in maniera uniforme l assegnazione ai processi di componenti hardware e software del sistema, si introduce il concetto di risorsa, già anticipato nel Paragrafo pag. 22 e utilizzato finora informalmente nel corso del capitolo: questa è definita come qualunque oggetto (reale o astratto) che i processi usano e che può condizionare il loro avanzamento. In base a tale definizione, l avanzamento di un processo è condizionato dalla disponibilità di risorse di vario tipo. Uno dei compiti tipici dei Sistemi Operativi multitasking è quello di controllare l uso di risorse da parte dei processi in modo da risolvere eventuali conflitti. Il concetto di risorsa è strettamente collegato con quello di evento, e ne rappresenta in certo qual modo una formalizzazione: in particolare, si può sempre definire un evento come l avvenuta disponibità di una corrispondente risorsa. Così un processo P che attende il verificarsi di un certo evento E (ad esempio, la disponibilità in memoria RAM di un blocco di dati B di cui il processo ha richiesto la lettura da disco) può essere formalmente definito come un processo
118 7.3. PROCESSI 105 che ha bisogno di acquisire una risorsa R (una zona di memoria RAM contenente i dati del blocco B); il processo, come detto poc anzi, è bloccato sull evento E, o in modo equivalente, è in attesa della risorsa R. L avvenuta disponibilità della risorsa corrispondente rappresenta il verificarsi dell evento. Si introducono ora alcuni attributi importanti che servono a meglio caratterizzare le varie risorse gestite. A tale scopo, si classificano le risorse in: risorse permanenti, dette anche risorse riusabili: risorse di questo tipo possono essere utilizzate ripetutamente da più processi senza che il loro stato venga modificato. Le interazioni tra un processo P e una risorsa R avvengono secondo il seguente schema: 1. richiesta di R da parte di P ; 2. assegnazione da parte del Nucleo di R a P ; 3. utilizzo di R da parte di P ; 4. rilascio di R da parte di P. Una risorsa permanente è seriale se può essere utilizzata da un solo processo alla volta, è condivisa (shared) nel caso opposto. Un area di memoria contenente dati usati da un solo processo è una risorsa permanente seriale. Viceversa, un area di memoria contenente dati usati da più processi, ad esempio una libreria di programmi, è una risorsa permanente condivisa. risorse consumabili: sono oggetti transienti, gestiti dal Sistema Operativo e con tempo di vita limitato, dall istante in cui sono prodotte a quello in cui sono consumate. Esse possono essere prodotte da processi o dal Nucleo stesso, mentre possono essere consumate parimenti da altri processi oppure dal Nucleo. Lo scenario delle interazioni è il seguente: P c (processo consumatore) richiede una risorsa R; P c va eventualmente a dormire (se la risorsa non è immediatamente disponibile); P p (processo produttore) produce la risorsa R; P p risveglia P c, se questo era andato a dormire; P c acquisisce R e la consuma Tale scenario tipicamente si ripete in loop. Nel caso dell ultimo scenario si dice che P p e P c sono sincronizzati, nel senso che il secondo rimane bloccato fino a quando il primo non avrà eseguito una determinata azione. Un esempio tipico e particolarmente semplice è quello di due programmi che comunicano dati attraverso una pipe: in un comando shell come il seguente ls *.[ch] wc -l
119 106 CAPITOLO 7. GESTIONE DEI PROCESSI E DELLA CPU il processo produttore è chiaramente quello che esegue il programma ls con i suoi argomenti; l output prodotto da questo programma (la lista dei file con estensione.c o.h nella cartella attiva) viene inviato, attraverso la pipe, al programma wc, il cui processo è quello consumatore: infatti wc consuma i dati che trova nella pipe (le varie risorse consumabili), leggendoli e prelevandoli se e quando ne trova (e andando a dormire se non ce ne sono), e ne effettua poi un trattamento (il computo del numero di righe, quindi dei file). Tali aspetti verranno trattati più diffusamente quando si affronterà il problema della sincronizzazione tra processi, più avanti nel capitolo Descrittore di processo Per gestire convenientemente le svariate risorse (hardware o software, reali o astratte) presenti nel sistema, il Nucleo utilizza dei descrittori delle risorse stesse; questi sono in generale delle strutture di dati (diverse per ciascun tipo di risorsa 4 ) che riflettono adeguatamente la natura e le caratteristiche di una risorsa, nonchè le operazioni che possono effettuarsi su di essa. Esempi informali di risorse, oltre a quando già visto, includono i processi stessi, aree di memoria, blocchi di disco, pacchetti di dati da o verso le interfaccie di rete, e così via. Per quanto riguarda i processi, esiste dunque una struttura dati opportuna, detta descrittore di processo (in inglese process descriptor, abbreviato in PD), che contiene informazioni associate a ciascun processo, usate dal Nucleo. Tale descrittore è usato da tutti gli algoritmi di scheduling applicati ai processi (che verranno visti nel seguito del capitolo), e serve in particolare a ricostituire il contesto operativo di un processo ogni volta che gli viene riattribuito il controllo della CPU; un PD comprende i seguenti elementi (la lista non è esaustiva): identificatore del processo; stato del processo; valori salvati del contenuto del Program Counter e dei registri CPU (compresi i condition code del PSW si veda Paragrafo 3.2 pag. 35); puntatore allo stack e valore dello Stack Pointer; informazioni per lo scheduling della CPU, come la priorità del processo e altro; informazioni (descrittori di risorse) per la gestione della memoria RAM associata al processo: valori dei registri dell MMU (Memory Management Unit si veda il Paragrafo pag. 43), limiti di indirizzi di memoria, e altro; 4 Esiste un numero sorprendentemente alto di risorse e di corrispondenti descrittori in un Nucleo: in Linux, ad esempio, sono svariate centinaia, di cui una parte decisamente alta riguarda le problematiche di networking e la gestione dei protocolli associati. Ciascuno di questi descrittori richiede la programmazione di algoritmi corrispondenti per la gestione e spesso lo scheduling delle risorse; da ciò si può avere un idea della complessità del lavoro di implementazione di un Sistema Operativo moderno.
120 7.3. PROCESSI 107 informazioni per la contabilizzazione delle risorse utilizzate dal processo, quali tempo CPU utilizzato, user/system time 5, e altro; informazioni (descrittori di risorse) sui canali di I/O utilizzati, come file aperti, dispositivi allocati in uso, e altro; in generale, descrittori di risorse (o puntatori ad essi) per qualunque tipo di risorsa associata al processo Cambio di contesto Ogni volta che viene effettuata una commutazione di processi, cioè ogni volta che il controllo della CPU viene passato a un nuovo processo, il Nucleo deve salvare tutte le informazioni richieste nel descrittore del processo precedentemente in esecuzione e prelevare dal descrittore del nuovo processo le corrispondenti informazioni per registrarle nei registri del processore e nelle varie strutture di dati del Sistema Operativo. È necessario quindi provvedere al salvataggio del contesto del vecchio processo e al ripristino del contesto del nuovo; dal punto di vista di un singolo processo, ad ogni passaggio da stato Running a stato Ready o Waiting, il contesto operativo del processo viene salvato nel relativo descrittore di processo; ad ogni passaggio inverso (ritorno allo stato Running e riattribuzione del controllo della CPU) i dati di contesto precedentemente salvati vengono recuperati dal descrittore, e il programma riprende la sua esecuzione esattamente dal punto dove l aveva lasciata in precedenza. L insieme di queste operazioni, illustrate nella Figura 7.4, è chiamato cambio di contesto (context switch), ed è parte fondamentale dello scheduling dei processi. Il tempo di context switch è tutto overhead, cioè tempo perso dal punto di vista dell operatività dei singoli programmi: non c è nessun lavoro utile durante la commutazione, quindi tale tempo va minimizzato con ogni metodo possibile, altrimenti rischia di diventare un collo di bottiglia per il sistema. Questo tempo dipende molto dal supporto hardware offerto dalla CPU (in termini di istruzioni speciali, di eventuali insiemi intercambiabili di registri e altro), ed è generalmente dell ordine dei microsecondi anche se appunto molto variabile (può andare da 1 a 10 3 µsec) Thread L uso del modello dei processi, pur essendo una utile astrazione che consente di strutturare in modo ottimale il Nucleo di un sistema multitasking, presenta alcuni inconvenienti dovuti alla ricchezza di informazioni associata ad ogni descrittore di processo: in effetti il cambio di contesto può risultare, specialmente nel caso di sistemi in tempo reale, eccessivamente costoso in termini di tempo di esecuzione. Per questo motivo alcuni Sistemi Operativi fanno uso di un modello di esecuzione più evoluto di quello considerato finora. 5 Lo user time e il system time sono rispettivamente il tempo di CPU utilizzato da un processo in User mode e in System mode; si veda time(1), times(2).
121 108 CAPITOLO 7. GESTIONE DEI PROCESSI E DELLA CPU P1 Sistema Operativo (Nucleo) P2 in esecuzione inattivo Salva contesto in PD1 overhead Ripristina contesto da PD2 inattivo in esecuzione Salva contesto in PD2 overhead Ripristina contesto da PD1 inattivo in esecuzione Figura 7.4: Commutazione e cambio di contesto tra due processi. In tale modello, ogni processo è composto da uno o più thread. Un thread è definibile come una unità di esecuzione del programma associato a un processo; all interno di uno stesso processo possono coesistere più thread (detti peer thread), che eseguono contemporaneamente parti del codice del programma associato al processo. Un thread pertanto comprende almeno un proprio Program Counter, un proprio stack e il relativo Stack Pointer, oltre a un insieme di registri della CPU. Tutti i peer thread di un dato processo condividono tra loro le aree di memoria assegnate al processo nonché i file da esso aperti: essi agiscono sugli stessi dati, a differenza di quanto accade tra due processi, anche in relazione padre-figlio, che eseguono lo stesso programma. Pertanto un thread condivide con i suoi peer thread: sezione testo sezione dati risorse del Sistema Operativo. Il descrittore di un singolo thread si limita quindi al cosiddetto contesto di esecuzione, ossia al contenuto dei registri del processore ed a quello dello stack usato dal thread. Lo scheduling (vedi Paragrafo 7.4 pag. 110) non viene più
122 7.3. PROCESSI 109 effettuato tra processi, bensì tra thread e, in generale, il Nucleo tratta in modo paritetico i thread di uno stesso processo e quelli appartenenti a processi diversi quando deve selezionare il thread più adatto a cui assegnare la CPU. Dal punto di vista realizzativo, per potere gestire thread è necessario introdurre appositi descrittori che si affiancano ai descrittori di processo; tali descrittori includono pochi campi, tra cui un identificatore di thread, un riferimento al processo a cui appartengono, un puntatore a un area di stack, ed un area di salvataggio dei registri della CPU. Per questo motivo i thread sono anche detti processi leggeri (inglese: lightweight process, o LWP); un processo tradizionale, così come introdotto nei paragrafi precedenti, equivale a un processo (del nuovo modello) con soltanto un thread, ed è talvolta detto per contrasto processo pesante (heavyweight process). Il tempo richiesto dal Nucleo per effettuare la commutazione tra due thread risulta solitamente ben minore di quello richiesto per effettuare la commutazione tra due processi, proprio per la maggior leggerezza del corrispondente descrittore. I thread rappresentano quindi una soluzione elegante al problema di minimizzare l overhead dello scheduler dovuto al context switch. Inoltre i thread rappresentano spesso una valida scelta per realizzare applicazioni che condividono strutture di dati comuni, o che devono avere un elevato grado di interattività con l utente; si considerino, a titolo illustrativo, i seguenti esempi di uso dei thread: Un server (ad esempio per un applicazione di rete) può essere implementato come un processo a più thread, in cui un thread principale è bloccato in attesa perenne di connessioni da client remoti, mentre altri thread possono eseguirsi, in corrispondenza di altrettante connessioni attive con client. In applicazioni che richiedono un buffer comune (ad esempio del tipo produttore-consumatore), l uso di più thread può velocizzare notevolmente il programma. In un sistema WYSIWYG 6 di trattamento di testo, dopo l inserzione di un paragrafo, un thread può provvedere a riformattare tutto il testo (ciò che può richiedere un certo tempo, se il documento è di grandi dimensioni), mentre un altro thread è pronto immediatamente ad accettare nuovi comandi dall utente. Per quanto riguarda gli aspetti di implementazione dei thread, esistono due grandi varianti: Thread a livello Nucleo (Kernel-level thread, o KLT ): i thread sono supportati direttamente nel Nucleo, con chiamate di sistema specifiche ( è questo il caso, tra l altro dei Sistemi Operativi Mach, OS/2, Linux). 6 WYSIWYG è l acronimo di what you see is what you get, e indica una categoria di programmi, solitamente di automazione d uffico, in cui l utente crea o modifica documenti e contemporaneamente vede sullo schermo il risultato delle sue modifiche, così come apparirà ad una stampa del documento stesso. Tutte le applicazioni di Microsoft Office o di Open Office sono di tipo WYSIWYG.
123 110 CAPITOLO 7. GESTIONE DEI PROCESSI E DELLA CPU Thread a livello utente (User-level thread, o ULT ): il Nucleo non è al corrente dell esistenza dei thread, che sono implementati con un insieme di chiamate a routine di libreria che lavorano a livello utente; l esempio più importante è la libreria Pthread, che fa parte delle specifiche di POSIX (POSIX.1c). Sono possibili anche soluzioni miste, in cui le due implementazioni convivono (è questo il caso del Sistema Operativo Solaris della SUN Microsystems). I due approcci hanno naturalmente alcuni pro e contro: Gli ULT sono più leggeri e più veloci da commutare rispetto ai KLT; Tutti i peer ULT di un dato processo sono visti come un processo unico dal Nucleo (che come detto non è al corrente di questa suddivisione interna), quindi lo scheduler potrebbe penalizzarli: tra due processi, uno con un solo thread e l altro con cinque, anzichè dividere equamente la risorsa CPU tra sei unità di esecuzione, il Nucleo la divide tra due (i due processi che lui vede); una di queste due parti poi viene poi divisa (a livello utente) tra i cinque thread di un processo. Una chiamata di sistema bloccante (ad esempio la lettura di una riga di dati dallo standard input) blocca tutto un processo, e quindi tutti i suoi ULT. L argomento dei thread verrà ripreso ulteriormente in questo e nel prossimo capitolo. 7.4 Scheduling di processi Uno dei problemi maggiori del Nucleo di un Sistema Operativo, per quanto riguarda la gestine dei processi, è quello di determinare in che ordine debbano essere soddisfatte le richieste dei processi per risorse di vario tipo. La soluzione di tale problema, chiamata scheduling e già introdotta nel Paragrafo pag. 12, è essenzialmente una strategia che tenendo conto di diversi obiettivi, ovvero di certe metriche prestazionali da ottimizzare, consente al Nucleo di scegliere il prossimo processo a cui assegnare una delle risorse gestite, non appena essa è nuovamente disponibile. Poiché tali risorse hanno caratteristiche tra loro molto diverse, un Sistema Operativo include, in generale, più funzioni di scheduling. Come è stato già evidenziato più volte, lo scheduling è un operazione fondamentale in molte parti di un Sistema Operativo; In particolare, quasi tutte le risorse hardware di un calcolatore sono sottoposte a scheduling prima dell uso (CPU, dischi ed altro). Si attira l attenzione sul fatto che in assenza di metriche prestazionali esplicite qualunque strategia avrebbe lo stesso valore, per cui potrebbe risultare soddisfacente, in tale contesto, usare funzioni di scheduling che determinino in modo del tutto casuale l ordine secondo il quale soddisfare le richieste dei processi.
124 7.4. SCHEDULING DI PROCESSI Code di scheduling Per gestire convenientemente lo scheduling delle risorse richieste dai vari processi, il Nucleo provvede a gestire varie code, dette code di scheduling dei processi; le code sono in numero di una per ogni tipo di risorsa, e i loro elementi sono i descrittori di processo (DP) dei vari processi interessati. Le code sono implementate con liste collegate (in modo semplice o doppio) di strutture di dati (i descrittori dei vari processi), che contengono pertanto dei puntatori al prossimo elemento ed eventualmente all elemento precedente. Durante la loro esecuzione, i processi migrano continuamente tra le varie code di scheduling. Queste sono alcune delle code di scheduling dei processi (la lista non è esaustiva): Job queue - è la coda che contiene l insieme di tutti i processi attivi nel sistema. Ready queue - contiene l insieme di tutti i processi in stato Ready, pronti ad eseguirsi e in attesa della CPU. Device queue - si tratta di varie code, una per ogni tipo di periferica, che comprendono l insieme di tutti i processi in attesa del completamento di un operazione di I/O da parte di una data periferica; tutti i processi in queste code sono pertanto in stato Waiting. Event queue - è un estensione della categoria precedente: si tratta anche qui di varie code, che radunano l insieme di tutti i processi in attesa di qualche evento o risorsa (di tipo hardware o software, come ad esempio un processo in attesa della terminazione di un processo figlio); anche i processi di queste code sono tutti in stato Waiting. Le Figure 7.5 e 7.6 illustrano quanto detto: la prima mostra la situazione in un dato istante di tempo, con i processi distribuiti sulle varie code, mentre la seconda evidenzia il processo di migrazione tra le code, in seguito a richieste dei programmi (richiesta di I/O su una data periferica, attesa di una risorsa consumabile, etc.) o ad interventi del Nucleo (esaurimento del quanto di tempo di CPU assegnato ad un processo, etc.) Tipi di scheduler Tra le varie funzioni di scheduling che riguardano i processi, le più importanti riguardano le fasi fondamentali del ciclo di vita dei processi, e cioè (si veda la Figura 1.4 pag. 15): il caricamento di un processo da memoria secondaria in memoria primaria, e viceversa; la selezione di un processo in memoria primaria per allocargli l uso della CPU, e viceversa (deselezione del processo).
125 112 CAPITOLO 7. GESTIONE DEI PROCESSI E DELLA CPU Ready q. PD3 PD13 PD9 PD18 Disk q. PD16 PD2 Tty q. Ethernet q. PD41 PD5 PD22... Sfw event q. PD8 PD15 PD11 PD21 PD22 Figura 7.5: Code di scheduling dei processi. I moduli del Sistema Operativo che implementano tali funzioni sono appunto detti scheduler. Si distinguono due tipi fondamentali di scheduler: scheduler a lungo termine (o job scheduler); tipico dei sistemi batch, questo scheduler si occupa di selezionare un certo numero di job (tanti quanti ne permette la memoria primaria disponibile) che attendono di essere eseguiti, e li carica (una volta per tutte) da disco in RAM per l esecuzione. scheduler a breve termine (o scheduler della CPU ); è una parte fondamentale di ogni sistema time-sharing, si occupa di selezionare qual è il prossimo processo da eseguire, e gli alloca la CPU. Esiste poi una terza categoria: nei sistemi time-sharing lo scheduling a lungo termine è assente, ed è sostituito da uno scheduler a medio termine, che si occupa dello swapping dei processi tra la RAM e il disco (zona di swap). Questo swapping può essere di due tipi: swapping parziale, pagina per pagina, secondo la tecnica della paginazione (swap-in, swap-out); swapping totale di tutto il processo (total swap-out), se ci sono troppi processi in esecuzione e la RAM è insufficiente. Quest ultimo scheduler verrà esaminato in seguito, quando si parlerà della gestione della memoria (Capitolo 11). È opportuno evidenziare subito alcune differenze fondamentali tra gli scheduler ora introdotti. Lo scheduler a breve termine è chiamato molto di frequente, ad intervalli dell ordine di millisecondi; è pertanto fondamentale che l algoritmo adottato sia veloce. Lo scheduler a medio o lungo termine, per contro, viene
126 7.4. SCHEDULING DI PROCESSI 113 Ready queue CPU I/O I/O queue(s) Richiesta di I/O Quanto di tempo esaurito Figlio avviato Softw. event queue Fork processo figlio Risorsa disponibile Softw. event queue Attesa di risorsa consumabile Figura 7.6: Migrazione dei processi tra le code di scheduling. chiamato di rado (ad intervalli di secondi, o di minuti); pertanto può anche essere un algoritmo lento. Gli scheduler a medio e lungo termine, infine, controllano il grado di multiprogrammazione, cioè il numero di processi concorrentemente in memoria. Nel resto del capitolo si concentrerà l attenzione soprattutto sullo scheduling a breve termine; in conclusione verrà discusso un esempio di scheduler a lungo termine. La Figura 7.7 mostra un affinamento del diagramma degli stati di un processo per un sistema time-sharing, con l evidenziazione del ruolo dello scheduler a medio e a breve termine Criteri di scheduling Ogni Sistema Operativo è progettato per conseguire, sia pure con enfasi diverse, almeno due obiettivi macroscopici principali: la prevedibilità dei tempi d esecuzione dei processi e l utilizzazione ottimale delle risorse. Sono quindi questi i due punti di riferimento principali per fissare delle metriche prestazionali, in particolare per lo scheduling della CPU, e determinare dunque dei criteri sulla base dei quali giudicare della bontà o meno delle possibili soluzioni. Prevedibilità dei tempi d esecuzione. Tale obiettivo si riferisce al fatto che, sia nei sistemi batch che in quelli interattivi, è importante garantire all utente un livello di servizio minimo, qualunque sia l occupazione del sistema al momento in cui è eseguito il job o il comando. Metriche prestazionali di tale tipo possono essere definite in vari modi. Nei sistemi a lotti (batch), si usano misure complesse come il tasso di servizio, ossia il numero di unità di servizio ricevute dal job per unità di tempo. Tali metriche tengono conto sia del tempo di CPU che del numero di Kbyte
127 114 CAPITOLO 7. GESTIONE DEI PROCESSI E DELLA CPU CPU Running Terminated Ready scheduler di CPU RAM swap in Scheduler a medio termine Asleep swap out, total swap out SWAP (Disco) Ready, swapped out Asleep, swapped out New Figura 7.7: Diagramma degli stati dei processi e ruolo degli scheduler. per ora (area di memoria RAM per unità di tempo) ottenute dal job da quando è stato immesso nel sistema. Utilizzazione delle risorse. Questo obiettivo è considerato prioritario nei sistemi di grandi dimensioni e, in particolare, nei sistemi di gestione a lotti. Data la diversità delle risorse gestite, il Sistema Operativo stabilisce una gerarchia di priorità: le risorse più pregiate sono i processori, seguiti dalla memoria, dalla memoria secondaria e, infine, dalle unità a nastro e a disco che consentono di montare volumi rimovibili e dalle stampanti. È utile sottolineare che mentre è relativamente agevole utilizzare in modo ottimale una singola risorsa, risulta invece impossibile, date le caratteristiche dei job eseguiti in multiprogrammazione, determinare uno scheduling che ottimizzi tutte le risorse allo stesso tempo. Una buona utilizzazione delle risorse, ottenibile tramite una messa a punto (tuning) dei parametri associati alle varie funzioni di scheduling, consente di aumentare la produttività del sistema e quindi, indirettamente, di migliorare la prevedibilità dei tempi di risposta a parità di carico di lavoro. Ciò detto, nel caso dei sistemi time-sharing, le metriche prestazionali più in uso sono le seguenti: Utilizzo di CPU. È una metrica che si iscrive chiaramente nel caso appena visto di utilizzazione delle risorse, e la CPU è la risorsa più pregiata di un
128 7.4. SCHEDULING DI PROCESSI 115 sistema di calcolo, quindi deve essere più attiva possibile. In pratica tale parametro può variare dal 40% (sistema poco carico) al 90-95% (sistema molto carico). Throughput, o produttività, del sistema. Già menzionato all inizio del capitolo, è definito come il numero di programmi terminati nell unità di tempo. Tempo di turnaround. Già introdotto nel Paragrafo pag. 7, è definito come l intervallo di tempo che intercorre tra l avvio e la terminazione di un processo. Tempo di attesa. Definito come la somma dei tempi passati in stato Ready, in attesa della CPU. In effetti l algoritmo di scheduling non influisce sul tempo impiegato per l esecuzione del programma, ma sul tempo perso dal processo in attesa di accedere alla CPU. Naturalmente, a parità di programmi, maggiore è il tempo di attesa e maggiore sarà il tempo di turnaround. Tempo di risposta. In un sistema interattivo questo parametro può essere il più importante dal punto di vista dell utente; è definito come l intervallo di tempo tra la sottomissione di una richiesta al programma e l inizio della risposta, cioè il tempo che l utente vede intercorrere tra il momento in cui chiede qualcosa al programma e quello in cui comincia a vedere una reazione da parte del programma stesso. In termini più formali è definibile anche come il tempo speso in stato Ready fino alla prima transizione in stato Running. Com è ovvio, alcuni di questi parametri devono essere massimizzati (utilizzo della CPU, throughput), mentre altri devono essere minimizzati (tempo di turnaround, tempo di attesa, tempo di risposta). Di solito si cerca di ottimizzare i valori medi; in alcuni casi invece conviene ottimizzare i valori minimi, o massimi. Infine, per sistemi time-sharing può essere opportuno minimizzare la varianza del tempo di risposta, anzichè minimizzare il tempo medio: ciò può garantire la migliore equità possibile di trattamento di tutti gli utenti, e inoltre dare garanzie di prevedibilità delle prestazioni del sistema Scheduling a breve termine Lo scheduling a breve termine, o scheduling della CPU, è utilizzato per gestire le risorse hardware più pregiate del sistema, ossia i processori. Si indichi con P il processo in esecuzione su un processore. Tale funzione di scheduling può essere eseguita nei seguenti casi, come evidenziato nella Figura 7.8: 1. P passa dallo stato Running allo stato Waiting su un evento E, per cui rilascia la CPU che può essere riassegnata a qualche altro processo; 2. P viene fatto passare dallo stato Runnig allo stato Ready, a causa di un interrupt o di un trap: ciò attiva lo scheduler, il quale decide d autorità di sospendere l avanzamento di P (benchè questo possa continuare), per dare
129 116 CAPITOLO 7. GESTIONE DEI PROCESSI E DELLA CPU New ammesso Ready 2 interrupt/trap rilascio CPU 3 uscita Running 5 Terminated 4 verificarsi di evento (completamento di I/O) dispatch Waiting 1 attesa di evento (o di I/O) Figura 7.8: Punti di intervento dello scheduler di CPU. la CPU a un altro processo Q ritenuto a questo punto più prioritario di P ; si noti che l interrupt può essere provocato in particolare dal timer di sistema, quando P ha esaurito il suo timeslice (si veda il Paragrafo 3.6.1); 3. P rilascia volontariamente la CPU e passa dallo stato Running allo stato Ready, eseguendo una specifica chiamata di sistema (si veda, nel caso Linux, la chiamata di sistema sched yield(), descritta nel Capitolo 8); 4. un processo Q diverso da P passa dallo stato Waiting a quello Ready; lo scheduler effettua allora un controllo per decidere se non sia più conveniente mettere in esecuzione Q, o comunque un altro processo in stato Ready, anziché continuare l esecuzione di P ; 5. P passa nello stato Terminated ed esce. Uno scheduler che prende decisioni solo nei casi 1, 3 e 5 è detto scheduler senza prelazione (nonpreemptive scheduler); ogni altro tipo di scheduler è detto scheduler con prelazione (preemptive scheduler). La differenza sta nell eventuale proattività dello scheduler: nei casi 1, 3 e 5 infatti una decisione deve comunque essere presa, quindi l intervento dello scheduler e obbligatorio; non così nei rimanenti casi 2 e 4. In particolare, uno scheduler con prelazione può intervenire d autorità in qualunque circostanza, come già sottolineato, e pertanto è in grado di proteggere la risorsa CPU dall uso indiscriminato di un singolo processo. Lo scheduling senza prelazione è molto più semplice da implementare, e non necessita di particolare supporto hardware (come il timer di sistema discusso nel Paragrafo 3.6.1), ma è meno efficiente e sicuro: deve infatti far affidamento sulla cooperatività di tutti i processi per funzionare correttamente (tutto funziona bene se i processi provvedono a passarsi la mano reciprocamente per il controllo della CPU; se per contro un processo non rilascia mai la CPU, rischia di monopolizzare l intero sistema finchè non si bloccherà per una richiesta di I/O o di
130 7.4. SCHEDULING DI PROCESSI 117 altra risorsa). Per questi motivi lo scheduling senza prelazione può essere adatto a semplici sistemi di tipo personale (Personal Computer), ma è assolutamente inadeguato per un sistema multiutente time-sharing Algoritmi di scheduling della CPU Si passerà ora a descrivere le caratteristiche di alcuni tra i principali algoritmi disponibili per lo scheduling di CPU, e si faranno valutazioni sulla loro bontà in base ad alcuni dei criteri di scheduling visti. Scheduling FCFS Il primo algoritmo è concettualmente il più semplice possibile; consiste nel dare la CPU ai vari processi, nell ordine in cui la richiedono, senza nessun altro accorgimento; prende il nome di algoritmo First-Come, First Served (FCFS). La coda dei processi è trattata come un FIFO (First-in, first-out): quando un processo entra nella Ready queue, viene posto in fondo alla coda; la CPU viene data sempre al processo in testa alla coda. Per valutare le prestazioni dello scheduling FCFS si consideri il seguente esempio: siano tre processi, che arrivano tutti al tempo 0, nell ordine P 1, P 2, P 3 e con tempi di burst di CPU rispettivamente pari a 3, 3 e 24 msec. La Figura 7.9 mostra la situazione con un diagramma di Gantt. P1 P2 P Figura 7.9: Scheduling FCFS, primo caso. I tempi di attesa dei vari processi sono: P 1-0 msec; P 2-3 msec; P 3-6 msec; il tempo medio di attesa è dunque pari a ( )/3 = 3 msec. Se ora si suppone invece che i processi arrivino nell ordine P 3, P 1, P 2, la situazione diventa quella di Figura P3 P1 P Figura 7.10: Scheduling FCFS, secondo caso. I tempi di attesa sono ora: P 1-24 msec; P 2-27 msec; P 3-0 msec; il tempo medio di attesa è pari allora a ( )/3 = 15 msec, cioè enormemente superiore al caso precedente. Pertanto FCFS in generale non dà alcuna garanzia di tempo di risposta minima (è anzi fortemente variabile a seconda dei casi), e soffre inoltre di un effetto
131 118 CAPITOLO 7. GESTIONE DEI PROCESSI E DELLA CPU indesiderato, chiamato effetto convoglio: molti processi brevi che rischiano di attendere in coda un unico processo con un lungo burst di CPU. FCFS è un algoritmo senza prelazione (una volta che un processo ottiene la CPU, la mantiene fino alla fine del suo burst cycle), quindi è senz altro inadeguato a sistemi time-sharing. Scheduling SJF Il secondo algoritmo è denominato scheduling SJF (Shortest Job First), ed è basato sulla seguente strategia: si associa ad ogni processo la lunghezza del suo prossimo burst di CPU, e si seleziona il processo con la minima lunghezza. Esistono due varianti: SJF senza prelazione - una volta data la CPU a un processo, questo non può essere prelazionato fino al completamento del suo burst di CPU. SJF con prelazione - se si presenta un nuovo processo con burst di CPU inferiore al tempo rimanente per il processo in esecuzione, quest ultimo viene prelazionato. Questo schema è anche detto, più propriamente, scheduling SRTF (Shortest-Remaining-Time-First), dato che seleziona comunque il processo il cui tempo rimanente (del burst attuale) è minore. Per valutare le prestazioni dello scheduling SJF si considerino quattro processi, con tempi di arrivo e burst time dati dalla tabella 7.2: Processo Tempo di arrivo (msec.) Burst time P P P P Tabella 7.2: Esempio di processi per l algoritmo di scheduling SJF. Nel caso senza prelazione il diagramma di Gantt è mostrato nella Figura Tempi di arrivo P1 P2 P3 P4 P1 P3 P2 P Figura 7.11: Scheduling FCFS, senza prelazione. I tempi di attesa sono: P 1 0 msec; P = 6 msec; P = 3 msec; P = 7 msec. Il tempo medio di attesa è dunque pari a ( )/4 = 4 msec.
132 7.4. SCHEDULING DI PROCESSI 119 Tempi di arrivo P1 P2 P3 P4 P1 P2 P3 P2 P4 P Figura 7.12: Scheduling FCFS, con prelazione. Nel caso con prelazione il diagramma di Gantt è mostrato nella Figura I tempi di attesa ora sono: P (11-2) = 9 msec; P (5-4) = 1 msec; P 3 0 msec; P 4 (7-5) = 2 msec. Il tempo medio di attesa è dunque pari a ( )/4 = 3 msec. Si vede anche in questo caso che l algoritmo con prelazione è migliore rispetto a quello senza prelazione, com era ovvio aspettarsi. Si può dimostrare che SJF è un algoritmo ottimale, nel senso che per un dato insieme di processi, fornisce sempre il minimo tempo medio di attesa possibile. Questo algoritmo ha però un problema evidente: si basa su un dato (la lunghezza del prossimo burst di CPU) che in generale non è noto a priori, almeno per Sistemi Operativi di tipo time-sharing. Pertanto è inadeguato, di per sè, per sistemi di scheduling a breve termine, a meno di non introdurre approssimazioni; esistono a tal proposito metodi per stimare la lunghezza probabile del prossimo burst di CPU, usando algoritmi di predizione lineare o medie esponenziali, che tengono conto della storia recente di ciascun processo (cioè degli ultimi valori della lunghezza di burst); questi metodi non saranno qui trattati. L algoritmo SJF è invece usato spesso per lo scheduling a lungo termine nei sistemi batch, basandosi sul limite di tempo del processo, che è un parametro che viene specificato alla sottomissione dei job. È anche usato, infine, come parametro di valutazione di altri metodi, dato che, essendo un algoritmo ottimale, rappresenta un limite massimo teorico. Scheduling con priorità Il termine scheduling con priorità (priority scheduling) indica genericamente una classe di algoritmi di scheduling basati sull assegnazione ad ogni processo di una priorità (normalmente un numero intero, per velocità di calcolo), secondo opportuni criteri, che dovrà variare dinamicamente; la CPU viene data ogni volta al processo con la maggiore priorità. In effetti l algoritmo SJF rientra in questa categoria, ove la priorità di ciascun processo è data dalla lunghezza (stimata) del prossimo burst di CPU (e con la convenzione, in questo caso, che valori numerici bassi indichino priorità alte 7 ). Le strategie di assegnazione delle priorità possono essere le più varie, e si basano di solito su criteri quali l ingombro in memoria primaria, il tempo di CPU 7 In effetti molti algoritmi di questa classe adottano questa convenzione; è il caso ad esempio di Unix System V. Linux invece adotta la convenzione inversa, cioè quella più naturale (valori numericamente maggiori indicano priorità più elevate).
133 120 CAPITOLO 7. GESTIONE DEI PROCESSI E DELLA CPU utilizzato finora, il numero di file aperti, e così via. Conformemente al caso particolare visto nel paragrafo precedente, in generale gli algoritmi con priorità possono essere di due tipi: senza prelazione o con prelazione. Il problema che si presenta in generale con questo tipo di algoritmi è quello del blocco indefinito (starvation 8 ): se non si prendono adeguati accorgimenti, alcuni processi a bassa priorità potrebbero continuare a rimanere tali, e quindi in definitiva non essere mai eseguiti, poichè ci potrebbe essere sempre qualche processo a priorità maggiore che prenderà la CPU al loro posto. Una soluzione al problema detto è la tecnica dell invecchiamento (aging): si mette in atto un sistema di aumento graduale dei processi che attendono la CPU senza ottenerla, in modo che al passare del tempo questi finiscano per diventare i più prioritari. In particolare, un implementazione pratica molto diffusa negli scheduler time-sharing consiste nel calcolare la priorità p P di ciascun processo P con una funzione di questo tipo: p P = f( T CP U T eff ) Ove T CP U è il tempo di CPU usato nel tempo recente (ad esempio negli ultimi 5 secondi), e T eff è il tempo fisico effettivamente trascorso. In altre parole, la priorità è tanto minore numericamente (e quindi anche in questo caso tanto più elevata) quanto minore è il tempo CPU utilizzato nell unità di tempo trascorso. In tal modo si inserisce un meccanismo di feedback che stabilizza il sistema: più un processo ottiene la CPU, più la sua priorità si abbassa; più un processo attende senza avere CPU, più la priorità si alza, finchè non sarà tale da fargli ottenere la CPU. Inoltre, uno degli effetti collaterali benefici di questo meccanismo è quello di privilegiare i processi che richiedono poco tempo di CPU, garantendo comunque l avanzamento dei processi CPU-bound, che fanno un forte uso della CPU. I processi del primo tipo sono tipicamente quelli che eseguono programmi interattivi: ad esempio un editor di testo passa la gran parte del suo tempo in attesa d evento, aspettando il prossimo carattere di input da parte dell utente. Quando questo input è disponibile, il processo viene risvegliato e passa da stato Waiting a stato Ready; non appena ciò avviene, il rapporto tra tempo CPU utilizzato e tempo trascorso è talmente basso (per la lunga attesa) che il processo si trova ad altissima priorità, ed ottiene quindi subito la CPU. Così il tempo medio di risposta viene mantenuto basso, e l utente ha l impressione di avere a che fare con un sistema sempre pronto ai suoi comandi, anche in condizioni di carico relativamente elevate. Scheduling RR L algoritmo di scheduling RR Round Robin è particolarmente adatto a sistemi time-sharing (in certi contesti) e a sistemi real-time. Si tratta in sintesi di un algoritmo simile al FCFS ma con prelazione, in modo da facilitare la commutazione tra i processi. L approccio è il seguente: viene data ad ogni processo una piccola quantità di tempo CPU (detto quanto di tempo, o time quantum, o timeslice si veda il Paragrafo pag. 43). Esaurito questo tempo, il processo 8 Dall inglese to starve, morire di fame.
134 7.4. SCHEDULING DI PROCESSI 121 è prelazionato e aggiunto in coda alla Ready queue, che lavora quindi come una coda circolare: lo scheduler alloca in circolo ad ogni processo la CPU per un intervallo di tempo non superiore a un quanto di tempo alla volta. Se ci sono N processi nella Ready queue, e il timeslice è q, ogni processo ottiene 1/N del tempo CPU, in frazioni lunghe al massimo q unità di tempo alla volta 9 ; inoltre, nessun processo aspetta mai più di (N 1)q unità di tempo. Il comportamento dell algoritmo RR dipende molto dal valore del quanto di tempo q utilizzato. Si possono distinguere facilmente due casi limite: se q è molto grande, non c è mai prelazione e l algoritmo RR tende a comportarsi come FCFS (coda di tipo FIFO), con i limiti e i difetti già visti; questo caso è quindi da evitare; se q è molto piccolo, il comportamento dello scheduler può essere definito come una condivisione di processore (o processor sharing): ogni processo ha l impressione di disporre, solo per se stesso, di un processore dedicato di velocità pari a 1/N del processore reale. Quanto detto a proposito del secondo caso è però soltanto teorico. Infatti q deve essere comunque grande rispetto al tempo di context switch, altrimenti la frazione di tempo sprecata per commutare tra processi è troppo alta: se ad esempio il tempo di context switch è pari al 10% di q, ciò comporta che circa il 10% del tempo CPU è sprecato. Pertanto per q eccessivamente piccolo, la frazione di tempo di CPU dedicato a ciascun processo è ben inferiore a 1/N come detto prima, e il sistema tende al limite a passare più tempo nella commutazione dei processi che all avanzamento dei processi stessi. Da quanto detto emerge che il quanto di tempo q deve essere giudiziosamente scelto, in modo da non essere nè troppo piccolo (per evitare lo spreco di tempo CPU dovuto ad un eccessiva quantità di commutazioni tra processi) nè troppo grande (per evitare il comportamento FCFS); la regola pratica che spesso si segue è di fare in modo che l 80% dei burst di CPU sia minore di q, e i valori tipicamente adottati variano di norma tra i 10 e i 100 msec. Le prestazioni di questo algoritmo possono essere valutate confrontandole con quelle dell algoritmo SJF. Si considerino cinque processi, arrivati tutti contemporaneamente al tempo 0, con burst time dati dalla tabella 7.3: Processo Burst time (msec.) P P 2 60 P P P 4 40 Tabella 7.3: Esempio di processi per gli algoritmi di scheduling SJF e RR. 9 Detta frazione può ovviamente essere minore di q se il processo, prima di esaurire il suo quanto di tempo, passa in stato Waiting perchè deve attendere il competamento di un operazione di I/O o l allocazione di una risorsa.
135 122 CAPITOLO 7. GESTIONE DEI PROCESSI E DELLA CPU Se si utilizza l algoritmo SJF 10 si ha la situazione mostrata nel diagramma di Gantt di Figura P P P P P Figura 7.13: Scheduling SJF, confronto con RR. Il tempo medio di attesa risulta essere pari a ( )/5 = 156 msec. Il tempo medio di risposta 11 è ovviamente anch esso pari a 156 msec. Se si utilizza invece l algoritmo RR con un quanto di tempo pari a 70 msec. la situazione diventa quella mostrata nel diagramma di Gantt di Figura Durata del timeslice P 1 P 2 P 3 P 4 P 5 P 1 P 3 P 4 P 1 P 3 P Figura 7.14: Scheduling RR. I tempi di attesa sono i seguenti: P (310-70) + ( ) = 370 msec.; P 2 70 msec.; P ( ) + ( ) = 410 msec.; P ( ) = 380 msec.; P msec. Il tempo medio di attesa è così pari a ( )/5 = 300 msec. I tempi di risposta invece sono i seguenti: P 1 0 msec., P 2 70 msec., P msec., P msec., P msec. Il tempo medio di risposta è pari a ( )/5 = 134 msec. L esempio visto mette in luce le caratteristiche prestazionali di questo algoritmo: tipicamente con RR si ottengono tempi di attesa (e quindi, a parità di programmi, tempi di turnaround) più alti di quelli ottenibili con l algoritmo SJF, ma tempi di risposta molto migliori. Si noti che al diminuire del quanto di tempo, il tempo medio di attesa varia ma rimane pressochè invariato, mentre diminuisce il tempo di risposta; nell esempio visto, con un quanto di tempo pari a 40 msec. si ha un tempo medio di risposta pari a 80 msec, ciò che evidenzia ancor più il risultato detto. Contesti particolari 10 Trattandosi di processi con identico tempo di arrivo, la distinzione tra SJF con o senza prelazione può essere ignorata, dato che i due casi coincidono. 11 Si ricorda che il tempo di risposta è definibile come l intervallo di tempo trascorso da un processo nella ready queue, fino alla sua prima transizione in stato Running (si veda il Paragrafo pag. 113).
136 7.4. SCHEDULING DI PROCESSI 123 Si accenna qui brevemente, senza trattare l argomento in dettaglio, a come le situazioni viste possano variare sensibilmente in alcuni contesti particolari. I sistemi multi-cpu richiedono in generale algoritmi più complessi; si distinguono due categorie: multiprocessing simmetrico: il sistema gestisce un unica ready queue, in modo da realizzare un meccanismo di bilanciamento del carico (load sharing) tra i vari processori centrali; occorre adottare algoritmi che proteggano il sistema da situazioni di conflitto nell accedere alle strutture di dati del Nucleo; multiprocessing asimmetrico: una sola CPU può accedere alle strutture di dati del kernel; pertanto la condivisione dei dati è più semplice, e non ci sono pericoli di conflitti del tipo sopra accennato. I sistemi in tempo reale richiedono a loro volta algoritmi di scheduling specifici; come già detto nel Paragrafo pag. 19, detti sistemi si dividono in due grandi categorie, sistemi hard real-time e sistemi soft real-time. Nei primi, i task critici hanno limiti massimi di tempo di risposta; in tali sistemi non si può usare paginazione di memoria né dischi (poiché i tempi di esecuzione avrebbero variazioni imprevedibili, come si vedrà nei Capitoli 11 e 12), quindi si deve utilizzare solo software specifico su hardware dedicato. Gli algoritmi di scheduling della CPU sono anch essi specifici a questa categoria. Nei sistemi del secondo tipo invece, i task critici hanno sistematicamente maggior priorità degli altri, ma per il resto il sistema è di tipo convenzionale. Pertanto essi possono essere compatibili con sistemi time-sharing classici, ivi compresi gli algoritmi di scheduling. I requisiti importanti per questi sistemi sono soprattutto due: disporre di un meccanismo di scheduling con priorità, e avere una bassa latenza di dispatch; quest ultimo aspetto sarà trattato più avanti nel testo Scheduling a lungo termine A conclusione della panoramica sullo scheduling dei processi si danno qui alcuni dettagli sullo scheduling a lungo termine. Tale tipo di scheduling avviene solo nei Sistemi Operativi di gestione a lotti dotati di un batch monitor in grado di controllare l esecuzione dei vari job nel sistema (vedi Paragrafo pag. 7). In tali sistemi, i vari job da eseguire hanno due importanti caratteristiche: l utente dichiara tramite apposite richieste nella scheda iniziale del job e/o nelle schede iniziali dei vari step le risorse richieste dall intero job, oppure dal singolo step; le richieste si riferiscono spesso a risorse che richiedono un lungo periodo di inizializzazione prima di poter essere utilizzate (risorse con elevato tempo di setup). Molti job richiedono, ad esempio, di montare volumi di dischi o nastri prefissati, oppure di inserire sulla stampante carta di tipo speciale, ecc. In ognuno di questi esempi è richiesto l intervento dell operatore per montare e smontare volumi o per cambiare carta e la durata di tali interventi è dell ordine dei minuti.
137 124 CAPITOLO 7. GESTIONE DEI PROCESSI E DELLA CPU Le funzioni che realizzano lo scheduling a lungo termine sono eseguite solo quando inizia o termina un lavoro (job) di un utente. Distinguiamo due tipi di situazioni: Lo scheduling di job per quanto riguarda l uso di unità periferiche dedicate (stampanti, unità a nastro, plotter, ecc.) è solitamente effettuato dall operatore del sistema: usando opportuni comandi privilegiati della console del sistema, egli può assegnare unità periferiche a un job e iniziarne l esecuzione. Lo scheduling di altre risorse, ad esempio la memoria principale o lo spazio su disco, è effettuato da processi di sistema (processi specializzati che svolgono funzioni del Sistema Operativo, vedi Capitolo 10) chiamati Initiator: essi corrispondono ai processi shell in un sistema interattivo e cercano di ottenere tutte le risorse necessarie per eseguire uno step di un job. In alcuni casi, può essere effettuata la prelazione di uno o più step di un job per consentire ad un altro step di un nuovo job di andare in esecuzione immediatamente. Se, ad esempio, l Initiator di un nuovo job che dà luogo a un processo con priorità massima non trova la memoria necessaria per caricare il programma richiesto, il Nucleo può disporre la prelazione di altri processi meno prioritari, con conseguente liberazione delle aree di memoria ad essi assegnate. In tal caso, il Nucleo dovrà salvare su disco tutte le aree di memoria usate dai processi prelazionati in modo da essere in grado di rimettere successivamente tali processi in esecuzione.
138 Capitolo 8 Processi Concorrenti 8.1 Introduzione Rispettando l approccio top down seguito finora, si illustra ora il supporto offerto dal Sistema Operativo nel realizzare programmi concorrenti, ossia programmi in grado di sfruttare nel modo più efficiente possibile i diversi processori presenti nel sistema. Si rimanda invece ad un capitolo successivo la descrizione delle principali azioni svolte dal Nucleo nel trattare le chiamate di sistema attinenti alla programmazione concorrente. 8.2 Programmi concorrenti I programmi si possono classificare in sequenziali e concorrenti: nel primo caso, l esecuzione di un programma dà luogo ad un singolo processo; nel secondo caso, dà luogo a più processi che competono tra loro per l uso delle risorse del sistema. Da un punto di vista funzionale, non vi è differenza tra i due tipi di programmazione: tutto quello che può essere realizzato da un programma concorrente può anche essere realizzato da un programma sequenziale. Dal punto di vista della produttività, invece, i programmi concorrenti sono, in generale, più efficienti di quelli sequenziali, specialmente su macchine dotate di più CPU. Esistono numerose applicazioni, ad esempio nel campo del calcolo numerico ed in quello della elaborazione delle immagini, in cui l algoritmo utilizzato si presta ad essere codificato come un gruppo di programmi autonomi che possono essere eseguiti indipendentemente uno dall altro. Oltre a dare luogo a più processi, i programmi concorrenti hanno un altra importante caratteristica: devono fare in modo che i vari processi si sincronizzino tra loro in modo corretto. Si è visto in precedenza come l interprete di comandi crei un nuovo processo (processo figlio) per mandare in esecuzione il file eseguibile specificato dall utente, per poi porsi in attesa della terminazione di tale processo. Già da questo 125
139 126 CAPITOLO 8. PROCESSI CONCORRENTI semplice esempio, appare evidente la necessità di introdurre appositi vincoli di sincronizzazione tra processi: lo shell (processo padre) riprenderà la sua esecuzione soltanto quando il processo figlio lo avrà avvertito in qualche modo che ha svolto il compito assegnatogli. Un altro tipico esempio di sincronizzazione tra processi è illustrato nella Figura 8.1. Il processo principale P 0 crea quattro processi figli P 1,..., P 4 che possono operare in parallelo su dati diversi. P 0 invia dati ai quattro processi generati, utilizzando uno dei meccanismi di comunicazione tra processi (IPC Inter-Process Communication) disponibili, quindi si pone in attesa. Soltanto quando tutti i processi figli hanno terminato il loro lavoro, P 0 può riprendere l esecuzione. In questo caso, esistono i seguenti tipi di vincoli di sincronizzazione/comunicazione: P 0 : genera iterativamente dati per P 1 e inviali al relativo meccanismo IPC P 1 : preleva iterativamente dati provenienti da P 0 leggendoli dal relativo meccanismo IPC (stessi due vincoli per P 2, P 3, P 4 ) P 0 : aspetta la terminazione di P 1 e di P 2 e di P 3 e di P 4. Il modo più semplice per scrivere un programma concorrente è quello di servirsi di apposite librerie di programma quali PVM od altre, che rendono la programmazione concorrente alquanto agevole. Un altro modo, più a basso livello, consiste nell usare un linguaggio di programmazione convenzionale arricchito da apposite primitive (API o chiamate di sistema) per: creare nuovi processi; terminare se stesso (come processo) o altri processi esistenti; imporre vincoli di sincronizzazione tra processi concorrenti; fornire meccanismi di comunicazione e di segnalazione tra processi concorrenti. È importante ricordare che le suddette API o chiamate di sistema sono soltanto degli strumenti per realizzare programmi concorrenti e non costituiscono di per sé una soluzione ad uno specifico problema di interazione. In tale ottica, si può affermare che un qualsiasi linguaggio di programmazione sequenziale come il linguaggio C, C++, Pascal o Cobol, arrichito con apposite primitive, si trasforma in un linguaggio concorrente mediante il quale diventa possibile realizzare gli schemi di interazione richiesti.
140 8.2. PROGRAMMI CONCORRENTI 127 P0 P1 P2 P3 P4 P0 Sincronizzazione sulla terminazione Comunicazione dati Figura 8.1: Un esempio di sincronizzazione tra processi Controllo di processi in Unix Il ruolo e le funzionalità generiche delle primitive anzi elencate sono già stati introdotti nel capitolo precedente, al Paragrafo pag Per concretezza, la discussione relativa all uso di queste primitive verrà focalizzata sul Sistema Operativo Unix, preso come caso di esempio. È facile constatare che Unix dispone di chiamate di sistema per implementare tutte le primitive necessarie alla programmazione concorrente. Creazione di processi La chiamata di sistema pid = fork(); è utilizzata per creare un processo figlio che è una copia conforme del processo padre. Come evidenziato dalla assenza di parametri, il processo figlio ottiene una
141 128 CAPITOLO 8. PROCESSI CONCORRENTI copia di tutte le risorse possedute dal padre, e quindi una copia del programma da eseguire. Vi è un unica differenza che può essere sfruttata dai programmatori per distinguere il processo figlio dal padre, ed è il valore di ritorno pid della fork(). Infatti questa primitiva ritorna due valori distinti ai due processi: al processo padre ritorna un valore sempre diverso da 0 e positivo, che è l identificativo di processo (PID) del processo figlio così generato; al processo figlio ritorna sempre 0. Infine ritorna -1 (al solo processo padre) in caso di errore. Pertanto la chiamata è normalmente sempre seguita da un istruzione if, che permette ai due processi (padre e figlio) di seguire due strade diverse nel codice, pur eseguendo lo stesso programma; ad esempio: pid = fork(); if(pid!= 0) { /* codice eseguito dal processo padre */... } else { /* codice eseguito dal processo figlio */... } La chiamata di sistema execve{path, args, env}; è utilizzata a complemento della fork() nella creazione di processi; sostituisce, nel processo corrente, un nuovo programma al posto di quello attuale, e lo esegue. La combinazione di una chiamata fork(), seguita nel processo figlio da una chiamata execve(), porta al caso generale della generazione di un nuovo processo che esegue un nuovo programma. Si confronti con quanto detto nel Paragrafo pag Esistono diverse varianti della execve(), più agevoli da utilizzare; esse sono in realtà tutte funzioni di libreria (API) che conducono a un unica vera chiamata di sistema, che è appunto la execve(); tuttavia questo argomento non verrà ulteriormente approfondito in questa sede. Una di queste varianti, su cui per semplicità si concentrerà questa presentazione, è la execl(path, name, arg1, arg2,..., NULL); I suoi argomenti sono i seguenti: path è il percorso (relativo o assoluto) del file che contiene il programma eseguibile;
142 8.2. PROGRAMMI CONCORRENTI 129 name è il nome con cui viene invocato il programma 1 ; arg1 è il primo argomento da passare al programma, e così via con gli altri argomenti, che possono essere in numero variabile; la lista è terminata con un argomento pari a zero (un puntatore nullo) 2. Si noti che il processo rimane lo stesso, quindi ovviamente tutto l ambiente è inalterato. La chiamata a questa funzione è una sorta di viaggio senza ritorno, dato che il codice del programma attuale viene semplicemente deallocato, e sostituito con quello del nuovo programma. Pertanto questa chiamata normalmente non ritorna mai, e se lo fa, il suo valore di ritorno è sicuramente -1 (errore: impossibile eseguire il nuovo programma). Affinando lo schema di base visto prima, questo diventa ora il seguente: pid = fork(); if(pid!= 0) { /* codice eseguito dal processo padre */... } else { /* codice eseguito dal processo figlio: si va ad eseguire un nuovo programma */ execl(file, prog, arg1, arg2, NULL); } Terminazione di processi La chiamata di sistema exit(code); consente ad un processo di terminare; il Sistema Operativo provvede a terminare ogni operazione di I/O rimasta pendente, dealloca ogni risorsa associata al processo, e infine lo termina, recuperando anche la memoria RAM ad esso associata. La exit() non ritorna mai al programma chiamante. Il valore di code, che è un intero, costituisce il codice d uscita (exit code) del processo; per convenzione, tale codice è pari a 0 nel caso di terminazione normale del programma; è diverso da 0 nel caso di terminazione anormale (il programma è stato invocato con argomenti errati, o ha riscontrato una situazione di errore nel corso della sua esecuzione). I diversi valori possibili possono riflettere diverse condizioni di errore (ciò è a discrezione di chi scrive il programma). 1 Questo di solito coincide con il primo argomento path, o con l ultima componente di questo; in casi particolari tuttavia è possibile invocare un programma con un nome che non corrisponde al file eseguibile che lo contiene. 2 La costante NULL in linguaggio C è definita e utilizzata come un puntatore nullo, cioè un oggetto costante di tipo puntatore che vale 0.
143 130 CAPITOLO 8. PROCESSI CONCORRENTI Sincronizzazione sulla terminazione di processi La chiamata di sistema pid = wait(status_address); consente ad un processo di sincronizzarsi sulla terminazione di un suo processo figlio; il processo che la invoca viene posto in stato Waiting in attesa dell evento terminazione di uno qualunque dei processi figli. Al risveglio, la wait() ritorna il PID del processo figlio che ha terminato; inoltre, se l argomento status address è un indirizzo non nullo di un intero, in questo intero viene depositato lo stato d uscita (exit status) del processo che ha terminato, che contiene l exit code del processo che ha terminato ed altre informazioni. In tal modo un processo padre, se lo vuole, può recuperare il codice di uscita di un processo suo figlio. A questo punto lo schema di base completo diventa il seguente: pid = fork(); if(pid diverso da 0) { /* processo padre */ if(pid uguale a -1) error_exit("cannot fork");... wait(status_address); excode = (estrai codice da status); exit(excode); /* esce col codice del figlio */ } else { /* processo figlio */ /* eventuale ridirezione dell I/O */ execl("/usr/local/bin/program", "program", "-x", "file", NULL); error_exit("cannot exec program"); } Il seguente pseudo-codice mostra il comportamento di base dell interprete di comandi di Unix, lo shell: shell() { loop { write(1, "$ ", 2); /* prompt - 2 caratteri a stdout */ read(0, line, SIZE); /* leggi riga da stdin */ parse(line); /* separa i token; applica i vari operatori ($, *,?, [ ], etc.) */
144 8.2. PROGRAMMI CONCORRENTI 131 } } pid = fork(); if(pid non uguale a 0) { /* processo padre: la shell originaria */ if( line non termina con & ) { wait(status_address); excode = (estrai codice d uscita da status); /* tieni conto di excode per istruzioni di controllo della shell: if, while, etc. */ } } else { /* processo figlio */ redirect(); /* applica ridirezioni di stdin, stdout etc. (operatori <, >, >> etc.) */ execl(file, program, args); /* esegui il comando nel processo figlio */ } Segnalazione tra processi La chiamata di sistema kill(pid, sig); consente ad un processo di inviare un segnale (cioè la comunicazione di un evento asincrono, o interrupt software) sig ad un processo pid. Valgono ovvie restrizioni di sicurezza: il processo mittente deve avere lo stesso UID (User- Id, identificativo d utente) del processo ricevente (salvo se il mittente è root, il super-user). Un segnale provoca un interrupt software nel processo ricevente, che viene regolarmente servito da una routine di servizio dell interrupt, detta l azione associata al segnale. L azione di default per ogni segnale può essere normalmente di due tipi: a) exit; b) core-dump + exit. Principali segnali: SIGINT (2) - interrupt character da terminale SIGQUIT (3) - quit character da terminale SIGKILL (9) - terminazione forzata (non intercettabile)
145 132 CAPITOLO 8. PROCESSI CONCORRENTI SIGTERM (15) - terminazione software (default per kill(1)) La chiamata di sistema signal(sig, func), che qui non verrà trattata in dettaglio, permette di predisporre un azione specifica (consistente nella chiamata alla funzione func) al ricevimento del segnale sig. Esempio di codice L esempio successivo illustra una applicazione delle chiamate fork(), exit(), kill(). Il processo padre esegue quattro azioni distinte: crea il processo figlio; esegue l istruzione sleep(10) per autosospendersi durante 10 secondi 3 ; quando ritorna in esecuzione, provoca la terminazione del processo figlio eseguendo la chiamata di sistema kill(). infine, provoca la propria terminazione eseguendo la chiamata di sistema exit(). Il processo figlio esegue un ciclo senza fine durante il quale invia un messaggio di stampa ogni secondo. Pseudo-codice: main() { pid = fork(); if(pid non uguale a 0) { /* processo padre */ print("pid processo figlio = ", pid); print("attesa 10 secondi"); sleep(10); /* attendi 10 sec. senza fare nulla */ print("terminazione del processo figlio"); kill(pid, SIGTERM); /* segnale di terminazione al processo "pid" */ exit(0); } else { /* processo figlio */ print("processo figlio inizia"); pid = getpid(); /* ottengo il mio proprio identificativo */ loop /* ciclo continuo senza fine */ 3 La funzione sleep() non è una chiamata di sistema in Unix, ma una API di libreria, a sua volta implementata con alcune chiamate di sistema di più basso livello.
146 8.2. PROGRAMMI CONCORRENTI 133 } } { } print("processo figlio", pid, "lavora"); sleep(1); Codice reale: #include <signal.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> int main() { pid_t pid; int retcode; printf("inizio prova\n"); /* crea processo figlio */ pid = fork(); if (pid!= 0) { /* programma eseguito da processo padre */ printf("pid processo figlio= %d\n", (int)pid); printf("processo padre inizia attesa di 10 secondi\n"); sleep(10); printf("processo padre elimina processo figlio\n"); kill(pid, SIGTERM); printf("processo figlio terminato\n"); exit(0); } else { /* programma eseguito da processo figlio */ printf("processo figlio inizia\n"); pid = getpid(); while (1) { printf("processo figlio %d lavora\n", (int)pid); sleep(1); } } } Si osservi come, nella parte di codice eseguita dal processo figlio, si fa uso della chiamata di sistema getpid() per ottenere il proprio PID, poiché il valore restituito dalla fork() (al processo figlio) come detto è pari a 0, e quindi non riflette nessun identificativo di processo.
147 134 CAPITOLO 8. PROCESSI CONCORRENTI Si osservi infine che non soltanto le variabili locali ma anche quelle globali si riferiscono ad un singolo processo. Infatti la chiamata di sistema fork() crea il processo figlio facendo una copia dello spazio di istruzioni e di dati del processo padre; pertanto da quel momento in poi ciascun processo (padre e figlio) lavora su una propria istanza, strettamente privata, di variabili e strutture di dati. Ne consegue che processi distinti non possono scambiarsi dati tramite variabili globali. Comunicazione tra processi La chiamata di sistema pipe(fdc); è il più semplice (ma anche il più limitato) meccanismo di comunicazione di dati con sincronizzazione tra processi. Consente ad un processo di creare una pipe, cioè un canale di comunicazione tra processi, che può essere condiviso con i processi figli (che ereditano i canali aperti). Il canale è unidirezionale, quindi non permette la comunicazione simultanea di dati da e verso due o più processi, ma soltanto la comunicazione di dati da un processo scrittore a un processo lettore 4. L argomento fdc è un vettore di due interi, fdc[0] e fdc[1], i cui valori vengono posizionati dal Nucleo al ritorno dalla chiamata; si tratta di due file descriptor: fdc[0] serve per leggere dalla pipe, fdc[1] per scrivere in essa. La pipe è implementata come un canale di trasmissione di tipo FIFO, di dimensione finita (di solito pari a 4 Kbyte); alla pipe è associato sempre un meccanismo di sincronizzazione automatica di processi scrittori e lettori, che vengono bloccati in caso di pipe piena o vuota: se un processo lettore effettua una lettura di dati mentre la pipe è vuota, questo processo viene automaticamente sospeso, finchè un processo scrittore non avrà inserito alcuni dati; se un processo scrittore effettua una scrittura di dati mentre la pipe è piena, questo processo viene automaticamente sospeso, finchè un processo lettore non avrà estratto alcuni dati, facendo così posto per nuovi dati nella pipe. Infine, la condizione di EOF (End of File - 0 byte letti) viene ritornata dalla read() al processo lettore se la pipe è stata chiusa in scrittura; ciò significa che quando un processo scrittore ha terminato di trasmettere dati, chiude la pipe (effettua una close() del relativo file descriptor); in tal modo, quando il processo lettore ha finito di leggere gli ultimi dati in essa contenuti, alla read() seguente troverà EOF 5. Quando un processo crea una pipe invocando la relativa chiamata di sistema, il Nucleo inizialmente alloca il canale di comunicazione e lo apre due volte, 4 È possibile mettere in opera altri usi più complessi di una pipe, ma questi sono comunque limitati e generalmente di uso non comune. 5 La presentazione qui fatta della chiamata di sistema pipe() è incompleta per un uso reale.
148 8.2. PROGRAMMI CONCORRENTI 135 fdc[1] Padre fdc[0] a. Prima della fork() fdc[1] fdc[1] Padre Figlio fdc[0] b. Dopo la fork() fdc[0] Figura 8.2: Creazione di una pipe e condivisione con un processo figlio. una in lettura e l altra in scrittura; entrambi i canali di comunicazione sono aperti per un unico processo, quello che ha effettuato la chiamata. Tuttavia se il processo fa successivamente una fork(), il processo figlio eredita tutti i canali di comunicazine aperti, quindi anche quelli che riguardano la pipe, che da questo punto in poi potrà essere effettivamente utilizzata per far comunicare due processi tra loro. La Figura 8.2 illustra quanto detto. Esempio di codice L esempio successivo illustra una applicazione delle chiamate fork(), execl(), exit(), wait(), pipe(). Pseudo-codice: sel(file, pattern) { pipe(fdc); pid = fork(); if(pid diverso da 0) { /* padre - produttore */
149 136 CAPITOLO 8. PROCESSI CONCORRENTI ifd = open(file, read-only); loop(nread = read(ifd, buffer, 1024); finche nread diverso da 0) write(fdc[1], buffer, nread); } close(fdc[1]); /* cosi il figlio trovera EOF dopo ultimo svuotamento pipe */ close(ifd); wait(status_address); code = (estrai exit code da status); exit(code); } else { /* figlio - consumatore */ redirect(fdc[0] sullo stdin); execl("/bin/grep", "grep", pattern, NULL); } Codice reale: #include <stdio.h> #include <string.h> #include <fcntl.h> #include <wait.h> void fatal_error(char *); int main(int argc, char *argv[]) { int fdc[2]; /* pipe file descriptors */ pid_t pid; char *file, *pattern; /* controllo invocazione */ if(argc!= 3) fatal_error("usage: sel file pattern\n"); file = argv[1]; pattern = argv[2]; /* crea la pipe */ if(pipe(fdc)) fatal_error("non posso creare la pipe\n"); if(pid = fork()) { /* codice del padre (produttore) */
150 8.2. PROGRAMMI CONCORRENTI 137 char buf[bufsiz]; int nread, ifd, status; /* check fork was o.k. */ if(pid == (pid_t)-1) fatal_error("errore fork\n"); /* orienta la pipe */ close(fdc[0]); /* apri il file, in lettura */ if((ifd = open(file, O_RDONLY)) == -1) fatal_error("errore apertura file\n"); /* leggi i dati dal file, e mandali al processo figlio, attraverso la pipe */ while(nread = read(ifd, buf, BUFSIZ)) { if(nread == -1) fatal_error("errore lettura da file\n"); write(fdc[1], buf, nread); } /* tutto OK, tutto finito: chiudi la pipe (cosi il processo figlio trovera EOF e sapra che e finita) */ close(fdc[1]); close(ifd); /* sincronizzati sulla terminazione del figlio */ wait(&status); exit(wexitstatus(status)); } else { /* codice del figlio (consumatore) */ /* orienta la pipe */ close(fdc[1]); /* duplica il file descriptor 0 (stdin) dall output della pipe, cosi grep avra la lettura da pipe come stdin */ if(dup2(fdc[0], 0) == -1) fatal_error( "errore ridirezione pipe a stdin di grep\n"); close(fdc[0]); /* ora esegui grep */ execlp("grep", "grep", pattern, NULL); /* si arriva qui se exec(2) ha fallito */ fatal_error("cannot exec grep\n");
151 138 CAPITOLO 8. PROCESSI CONCORRENTI } } void fatal_error(char *msg) { write(2, msg, strlen(msg)); exit(1); } Si noti l uso di dup2(). 8.3 Sincronizzazione tra processi Il modello risorse/eventi, introdotto nel capitolo precedente (si veda il Paragrafo pag. 104), può essere utilizzato per descrivere i vincoli di sincronizzazione tra i processi. In generale, due o più processi interagiscono tra loro quando si contendono l uso di risorse, permanenti o consumabili. Esistono molti schemi di interazione possibili ma essi sono tutti riconducibili a due paradigmi fondamentali, chiamati mutua esclusione e produttore/consumatore Ruolo delle risorse consumabili Tutte le diverse forme di sincronizzazione tra processi tipiche della programmazione concorrente sono descrivibili, e implementabili, tramite opportune produzioni e consumi di risorse consumabili di vario tipo. Le risorse consumabili sono divise in classi e corrispondono a classi di eventi dello stesso tipo che si verificano in istanti diversi. Un qualsiasi vincolo di sincronizzazione tra due processi P 1 e P 2 può essere espresso molto semplicemente tramite risorse consumabili affermando che P 1 è il produttore di una risorsa consumabile R(E) associata ad eventi di tipo E e che P 2 è il consumatore di R(E). I vari costrutti di programmazione utilizzati per descrivere la sincronizzazione tra processi implicano tutti risorse consumabili di tipo software create e consumate dal Nucleo e dai processi Paradigma della mutua esclusione Il problema della mutua esclusione di una risorsa permanente R contesa da più processi P 1,..., P n è quello di garantire che, ad ogni istante, vi sia al più un processo P i che occupi R e che ogni processo richiedente ottenga l uso di R entro un intervallo limitato di tempo.
152 8.3. SINCRONIZZAZIONE TRA PROCESSI 139 Ricordando quanto affermato in precedenza (nel citato Paragrafo 7.3.3), si osserva che si presenta un problema di mutua esclusione ogni qualvolta si intende rendere seriale una qualche risorsa permanente del sistema. In effetti, risorse permanenti hardware come le aree di memoria o i lettori di disco non sono di per sé seriali, anche se è necessario, per un corretto avanzamento dei processi che le utilizzano, che esse vengano trattate come tali. In modo analogo, risorse permanenti software come una tabella di dati, una lista o un file non sono di per sé seriali, anche se, per mantenere la coerenza dei dati in esse contenute, è necessario che gli aggiornamenti siano eseguiti da un processo alla volta. Esistono varianti al problema appena citato: in alcuni casi, i processi che richiedono la risorsa possiedono ognuno una priorità, ossia un numero che esprime il tipo di privilegio che essi hanno rispetto alle risorse. Quando si libera una risorsa, essa è assegnata al processo con priorità massima e, nel caso ve ne sia più di uno, a quello che ha atteso più a lungo. Un altra variante prevede, oltre alle priorità, l uso del rilascio anticipato della risorsa per soddisfare immediatamente le richieste di processi prioritari. In questo caso, non appena giunge una richiesta da parte di un processo avente priorità sufficientemente elevata, il Nucleo prelaziona il processo che usa attualmente la risorsa ponendolo in stato Ready (vedi Paragrafo 7.3 pag. 99) ed assegna quindi la risorsa al nuovo processo. In ogni caso, come dettagliato nel seguito, la mutua esclusione di una risorsa permanente R può ottenersi associando a R delle risorse consumabili X R, e facendo in modo che ogni processo acquisisca una X R prima di utilizzare R e rilasci la X R dopo aver utilizzato R. Questo tipo di risorse fittizie, create dal Nucleo col solo scopo di regolare il traffico di processi che in alternanza affluiscono verso la risorsa permanente R e che se ne distaccano, è chiamato, con evidente analogia, semaforo Paradigma produttore/consumatore Questo secondo schema di interazione è alla base di tutti gli scambi di informazione tra processi tra loro interagenti. Si distingue il processo produttore di informazioni da quello consumatore. Si noti come, a differenza della mutua esclusione che si applica anche a processi logicamente indipendenti (processi operanti su dati diversi), lo schema del produttore/consumatore implica che due o più processi si scambino informazioni, ossia che operino su dati comuni. Si presentano alcune importanti varianti del problema che schematizzano problemi di sincronizzazione comuni ai Sistemi Operativi multitasking. un produttore/un consumatore: l unico produttore P p produce in istanti imprevedibili informazioni che devono essere raccolte e trattate dall unico consumatore P c. P c non può procedere se l informazione richiesta non è disponibile; viceversa, P p può produrre, anche se P c non ha ancora elaborato l informazione ricevuta in precedenza. Se questo si verifica, si può avere una perdita di informazioni. Un esempio di questo genere di interazione è quello, già citato nel capitolo precedente, di due processi che comunicano dati tra di loro attraverso una
153 140 CAPITOLO 8. PROCESSI CONCORRENTI pipe. Un altro caso è quello che si verifica tra la tastiera di un terminale e il processo P c incaricato di raccogliere i caratteri generati: ogni tasto premuto dà luogo ad un carattere che è trasferito dal canale di I/O in un byte di memoria. Ogni carattere può essere considerato come una risorsa consumabile R prodotta da tastiera e consumata da P c. Quando è pronto a ricevere un nuovo carattere da tastiera, P c chiede di consumare una unità di R; se R non è disponibile, il Nucleo bloccherà P c sull evento nuova produzione di una unità di R ; altrimenti, P c è autorizzato dal Nucleo a consumare R. più produttori/un consumatore: la funzione di spooling di uscita descritta nel Paragrafo pag. 9 è un programma concorrente che prevede una interazione di tipo produttore/consumatore tra i vari processi utenti che producono file di stampa e il processo Output Writer che li consuma trasferendoli sulla stampante. più produttori/più consumatori: per aumentare le prestazioni del programma concorrente (posto che esistano le opportune risorse hardware) può essere opportuno dedicare un gruppo di processi per svolgere in multiprogrammazione, e quindi con maggiore efficienza, la stessa funzione. Nei sistemi per la gestione a lotti, ad esempio, i lavori sono suddivisi in classi in base alla priorità e alle risorse richieste. Ad ogni classe è associato almeno un processo del Sistema Operativo chiamato Initiator; tale processo esamina le richieste del lavoro e dei suoi passi e provvede a soddisfarle, oltre che a controllare l esecuzione di ogni passo. Allo stesso tempo, è realizzato uno spooling di entrata che consente a più unità di input locali o remote di inviare simultaneamente lavori. A tale scopo, ogni unità di input è gestita da un apposito processo di sistema chiamato Input Reader; tale processo smista i lavori ricevuti, copiandoli in file diversi a seconda della loro classe e notifica quindi uno degli Initiator associati alla classe del lavoro appena ricevuto. La Figura 8.3 illustra le principali interazioni tra i processi di sistema in un sistema per la gestione a lotti. Come appare dalla figura, sono presenti due schemi di interazione del tipo più produttori/più consumatori: il primo è relativo all immissione di dati nel sistema; le risorse consumabili sono i lavori prodotti da più Input Reader e consumati da più Initiator. Il secondo è relativo alla emissione di dati nel caso in cui siano presenti più stampanti veloci omogenee; le risorse consumabili sono gli file di stampa prodotti da processi in esecuzione e consumati da più Output Writer. 8.4 Primitive di sincronizzazione Esistono numerose varianti, sia per quanto riguarda i nomi, sia per quanto riguarda la definizione delle chiamate di sistema che riguardano la sincronizzazione tra processi. Tali chiamate di sistema prendono il nome di primitive di sincronizzazione. Per brevità, se ne descrivono soltanto alcune.
154 8.4. PRIMITIVE DI SINCRONIZZAZIONE 141 unità di ingresso 1 unità di ingresso n input reader 1 input reader n disco di spooling di ingresso initiator 1 initiator n processo 1 processo n disco di spooling di uscita output writer 1 output writer k stampante 1 stampante k Figura 8.3: Interazione tra processi di sistema in un sistema per la gestione a lotti. lock()/unlock() : sono usate per risolvere problemi di mutua esclusione su una qualunque risorsa. Quando un processo P esegue lock(x), il Nucleo verifica se è disponibile la relativa unità di risorsa consumabile X. Nel caso negativo, P è bloccato dal Nucleo fino a quando un altro processo non crei una unità di X. Non appena questa è disponibile (ciò che può anche avvenire immediatamente), viene eliminata (consumata) e P è rimesso in esecuzione. Quando un processo P esegue unlock(x), il Nucleo crea una unità di X, dopodiché verifica se esiste almeno un processo bloccato sulla risorsa X (cioè che è rimasto bloccato nell esecuzione di una precedente lock()), e nel caso affermativo sblocca il primo di essi (facendogli consumare immediatamente la risorsa appena prodotta). In ogni caso, rimette P in esecuzione. Come accennato in precedenza, la serializzazione di una risorsa permanen-
155 142 CAPITOLO 8. PROCESSI CONCORRENTI te R contesa da più processi si ottiene associando a R delle risorse consumabili X R di tipo semaforo e facendo in modo che ogni processo esegua lock(x R ) prima di utilizzare R e unlock(x R ) dopo averla utilizzata. Spesso, per motivi di efficienza, il Nucleo offre un insieme prefissato di risorse consumabili X 1,..., X n alle quali è possibile attribuire un significato arbitrario. Anche semplici problemi del tipo produttore/consumatore possono essere risolti con le suddette primitive, serializzando tramite un apposita risorsa consumabile X i una variabile N i che rappresenta il numero di oggetti di tipo i prodotti e non ancora consumati. send()/receive(): consentono a processi diversi di scambiarsi informazioni, garantendo allo stesso tempo la protezione degli spazi degli indirizzi di ogni processo. La risorsa consumabile è il messaggio, ossia una sequenza di caratteri spesso di lunghezza fissa, per motivi di efficienza. Il contenuto di un messaggio è arbitrario ed è stabilito dal processo mittente. Servendosi della primitiva send(dest,m), il processo mittente invia ad un altro processo dest il messaggio m. Un altra primitiva del tipo receive(mitt,m) consente ad un processo destinatario di richiedere un nuovo messaggio m da un altro processo mitt. In pratica, ad ogni processo è associata una coda di messaggi pendenti, ossia trasmessi e non ancora ricevuti dai destinatari. A seconda del tipo di implementazione, la send() può bloccare o no il processo che la esegue; nel primo caso il canale di comunicazione è concettualmente a capacità limitata, nel secondo caso è invece a capacità illimitata. Il blocco avviene quando il canale, che ha capacità limitata, si trova pieno di messaggi non ancora consegnati ai processi destinatari. Analogamente, se un processo P esegue una receive() e il Nucleo verifica che non vi sono messaggi pendenti destinati a P, esso blocca P fino a quando un altro processo non esegua una send() ad esso destinata (so noti che ciò avviene con qualunque tipo di canale, a capacità limitata o illimitata). Le primitive send() e receive() sono utilizzate per risolvere problemi di sincronizzazione del tipo produttore/consumatore: nel caso del sistema di spooling di uscita, ad esempio, il processo che chiude un file di stampa invia al processo Output Writer un messaggio contenente informazioni circa il file di stampa appena prodotto. Un altro esempio classico è quello di due processi, in un Sistema Operativo Unix, che comunicano tra di loro tramite una pipe (si veda l esempio presentato nel Paragrafo 8.2.1), ed è facile riconoscere che si tratta di un meccanismo con canale di capacità limitata Primitive di sincronizzazione in Unix Unix include diverse primitive di sincronizzazione. In particolare, le funzioni incluse nel pacchetto chiamato System V InterProcess Communication (IPC) consentono di operare su semafori, di inviare o ricevere messaggi, e di condividere aree di memoria tra processi. Purtroppo le chiamate di sistema IPC sono piuttosto macchinose da utilizzare; per semplicità si introdurranno qui due funzioni Unix che corrispondono alle primitive di sincronizzazione lock() e unlock() descritte in precedenza.
156 8.4. PRIMITIVE DI SINCRONIZZAZIONE 143 L idea di base per realizzare dei semafori in Unix è quella di sfruttare le caratteristiche del file system Unix, ed in particolare delle chiamate di sistema open() con i relativi parametri. Se infatti un file viene aperto da un processo con i parametri O CREAT (crea se necessario) e O EXCL (apertura esclusiva), l operazione riesce soltanto se il file non esiste (e viene quindi creato ora ex-novo) 6. Possiamo quindi usare i file come semafori: se il file esiste, il semaforo corrispondente è impegnato, altrimenti risulta libero. La funzione lock(p) illustrata appresso opera su un singolo parametro p che è un puntatore al nome del file, ossia al nome del semaforo che si intende utilizzare; tale nome può essere sia un pathname assoluto che relativo (vedi Paragrafo pag. 74). Pseudo-codice: lock(p) { loop { fd = open(p, O_CREAT+O_EXCL); if(errore: il file esiste gi\ a) sched_yield(); else if(altro tipo di errore) return errore; else return fd; /* file creato in modo esclusivo = risorsa acquisita; restituisci descrittore del semaforo */ } } Codice reale: int lock(const char *p) { int fd, retcode; while(1) { fd = open(p, O_RDWR O_CREAT O_EXCL, 0666); if ((fd < 0) && (errno == EEXIST)) retcode = sched_yield(); else if (fd < 0) { printf("lock open error\n"); retcode = -1; 6 Inoltre, nessun altro processo può aprire il file finché esso non viene chiuso dal processo che lo ha aperto in precedenza. Questa seconda caratteristica è comunque ridondante ai fini del corretto funzionamento delle funzioni lock() e unlock() qui presentate.
157 144 CAPITOLO 8. PROCESSI CONCORRENTI } break; } else { retcode = fd; break; } } return retcode; Come si può osservare dal codice, la funzione lock() tenta di creare (flag O CREAT) ed aprire il file con modalità esclusiva (flag O EXCL): se un file con lo stesso nome già esiste, l operazione dà errore, e lock() rilascia temporaneamente (e volontariamente) la CPU eseguendo la chiamata di sistema sched yield(), già introdotta nel Paragrafo pag. 115, che pone il processo in ultima posizione tra quelli in stato Ready. Successivamente, quando lo stesso processo andrà nuovamente in esecuzione, verrà rieseguita la open() finché l apertura del file riesce (ciclo while). Quando il file è stato creato e quindi aperto, lock() termina restituendo il file descriptor fd del file corrispondente al semaforo ottenuto. La funzione unlock(p, fd) illustrata appresso opera su due parametri: p che è un puntatore al nome del semaforo ottenuto in precedenza e fd che è il descrittore (file descriptor) associato al file aperto. Pseudo-codice: unlock(p, fd) { close(fd); if(errore) return errore; else { unlink(p); } } return OK; /* file rimosso = risorsa rilasciata; da questo momento pu essere ricreato tramite lock() */ Codice reale: int unlock(const char *p, int fd) { int retcode; retcode = close(fd);
158 8.5. STALLO TRA PROCESSI 145 } if (retcode < 0) { printf("unlock close error\n"); return(retcode); } else { unlink(p); /* cancella il file */ return(0); } Come si può osservare dal codice, la funzione unlock() inizia col chiudere il file corrispondente al semaforo. Se tale file non esiste (uso improprio del semaforo), segnala una condizione di errore tramite il codice di ritorno. Altrimenti, la funzione provvede ad eliminare il file del semaforo invocando la chiamata di sistema unlink(). Poiché esattamente un processo aveva aperto il file, e questo non ha altri nomi (altri hard link), il contatore d uso del file verrà posto a 0 in seguito alla unlink(), ed il Nucleo provvederà quindi a cancellare il file dal disco. Da questo momento in poi, una open() eseguita da un altro processo (nella lock()) si concluderà con successo. Si osservi il ruolo del primo parametro p: esso punta al nome del semaforo, ed è necessario per potere invocare la chiamata di sistema unlink(). Non esiste infatti in Unix una funzione che consenta di derivare dal file descriptor il corrispondente nome del file. Infine, è molto importante osservare che il codice visto è ben ottimizzato per sincronizzazioni veloci tra processi, ma dà buoni risultati soltanto nel caso in cui le risorse (cioè i semafori) vengano acquisite e poi rilasciate in tempi brevi; se così non è, si può avere uno spreco anche notevole del tempo di CPU. Ciò è dovuto al loop con la chiamata alla primitiva sched yield(), che in pratica passa il controllo della CPU ad altri processi ma poi lo riprende alla prima occasione utile; se la risorsa che si cerca di acquisire risulta occupata da un altro processo per un tempo lungo (ad esempio dell ordine dei secondi), il loop del processo che cerca continuamente di acquisire la risorsa provocherà lo spreco di CPU di cui si è detto. 8.5 Stallo tra processi Non si può parlare di programmazione concorrente senza accennare agli errori di sincronizzazione, ossia ad errori di programmazione causati dall uso improprio delle primitive di sincronizzazione. L errore di interazione più vistoso e più noto prende il nome di stallo o deadlock: durante l avanzamento del gruppo di processi tra loro interagenti, uno o più processi vengono posti in stato di attesa e vi rimangono per un tempo indeterminato. La Figura 8.4 illustra un classico caso di programma concorrente che può dare luogo, in determinate circostanze, ad uno stallo tra due processi.
159 146 CAPITOLO 8. PROCESSI CONCORRENTI... lock(sem_r1)... lock(sem_r2) unlock(sem_r1)... unlock(sem_r2) lock(sem_r2)... lock(sem_r1) unlock(sem_r2)... unlock(sem_r1)... processo A processo B Figura 8.4: Esempio di stallo tra due processi. Tale errore è causato da un uso improprio delle primitive di sincronizzazione lock() e unlock() da parte dei processi: i due processi A e B fanno uso entrambi della stessa coppia di risorse che chiameremo R1 ed R2. Per garantire l accesso seriale ad ognuna di esse i processi fanno uso di due semafori chiamati sem R1 e sem R2 e delle relative primitive di sincronizzazione lock() e unlock() descritte nel paragrafo precedente. Il processo A esegue, ad esempio, una lock(sem R1) prima di accedere ad R1 ed una unlock(sem R1, fd) per rilasciare R1. Il problema consiste nel fatto che A richiede prima il semaforo sem R1 e poi quello sem R2, mentre il processo B richiede prima il semaforo sem R2 e poi quello sem R1. Può quindi succedere che, in seguito allo scheduling deciso dal Nucleo per A e B, A ottenga il semaforo sem R1 e subito dopo B ottenga il semaforo sem R2. A questo punto si determina una condizione di stallo poiché né A né B sono in grado di continuare la loro esecuzione: ognuno aspetta un semaforo posseduto dall altro. Come si evitano errori di interazione di tipo stallo in un programma concorrente? Una risposta possibile sta nell esempio appena illustrato: occorre obbligare i processi a richiedere semafori in un ordine prefissato, ad esempio in un ordine crescente. È compito del programmatore, e non del Sistema Operativo, scrivere programmi concorrenti che non contengano errori di interazione.
160 8.6. PROGRAMMAZIONE IN TEMPO REALE Programmazione in tempo reale A conclusione di questo capitolo sulla programmazione concorrente si danno alcuni cenni su una variante di programma concorrente che è molto utilizzata nell ambito del controllo di processi. Un programma in tempo reale è un programma concorrente che deve soddisfare dei vincoli relativi ai tempi d esecuzione di almeno uno dei sottoprogrammi che lo compongono. Programmi di tale tipo sono anche chiamati programmi dipendenti dal tempo. Un tipico esempio di programma in tempo reale è quello per il controllo di un processo industriale: se il sistema riceve dall esterno segnali corrispondenti a una situazione che richiede un intervento tempestivo, esso non deve soltanto riconoscere tale situazione ma anche produrre le informazioni richieste entro e non oltre un intervallo prefissato di tempo. Un altro esempio è quello di un programma per le previsioni metereologiche: in tale caso, il tempo richiesto per eseguire la previsione deve essere inferiore all inverso della frequenza di rilevazione dei dati metereologici. Un caso perfettamente analogo, ma nettamente più stringente dal punto di vista dei tempi di reazione disponibili, è quello delle applicazioni audio, ad esempio per l acquisizione (lettura da convertitore analogico-digitale e registrazione su disco rigido) e la restituzione (lettura da disco e invio ad un convertitore digitale-analogico) di un segnale musicale: qui il tempo disponibile è pari all inverso della frequenza di campionamento del segnale (nello standard di codifica dei Compact Disks, tale frequenza vale 44.1 khz, quindi il tempo a disposizione è pari a 22.7 µsec). Anche i Sistemi Operativi sono, sia pure in misura diversa, dei programmi in tempo reale. I due estremi della gamma possono essere rappresentati, da un lato da un semplice sistema interattivo senza particolari vincoli sui tempi di risposta all utente e, dall altro, da un sistema dedicato per il controllo di una centrale telefonica elettronica. Nel primo caso, l unica parte dipendente dal tempo sono i sottoprogrammi che gestiscono le diverse interruzioni. Nel secondo caso, invece, quasi tutti i sottoprogrammi sono dipendenti dal tempo in quanto ogni ritardo nel trattare una coppia di linee tra cui è in svolgimento una comunicazione si traduce in un malfunzionamento del sistema. In generale, i Sistemi Operativi general purpose non si prestano ad eseguire correttamente programmi in tempo reale e si preferisce fare uso di Sistemi Operativi semplificati di tipo real time in grado di assicurare tempi di risposta brevi ai processi che lo richiedono.
161 Capitolo 9 Spazio degli Indirizzi di un Processo 9.1 Introduzione Ancora una volta, l approccio top down ci forza a considerare la problematica della gestione della memoria in modo diverso da quello tradizionale. Anziché cominciare dal Nucleo e descrivere le varie tecniche da esso utilizzate per gestire la memoria RAM, partiamo dai processi ed esaminiamo i motivi per cui essi richiedono o rilasciano memoria; in particolare, studiamo: gli eventi più significativi che inducono un processo a richiedere memoria; i vari modi, diretti o indiretti, tramite i quali un processo effettua richieste di memoria al Nucleo. Rimandiamo invece al Capitolo 11 la discussione di come il Nucleo assegna effettivamente RAM ai processi. 9.2 Indirizzi e regioni Ci riferiamo in questo contesto a file eseguibili contenenti programmi codificati in linguaggio macchina e pronti ad essere eseguiti dalla CPU. Chiamiamo indirizzo logico un gruppo di bit che serve a identificare: l indirizzo della prossima istruzione da eseguire; oppure: l indirizzo di un operando contenuto in RAM (solo nel caso in cui l istruzione debba accedere ad operandi contenuti nella RAM). 148
162 9.2. INDIRIZZI E REGIONI 149 Per semplicità, supponiamo che l indirizzo logico consista in un gruppo di 32 bit 1. Negli esempi successivi, rappresenteremo quindi gli indirizzi logici come numeri inclusi nell intervallo compreso tra 0x e 0xffffffff. Chiamiamo invece indirizzo fisico un gruppo di bit usati dal bus per indirizzare una cella di RAM. La maggior parte dei microprocessori fanno uso oggi di indirizzi fisici da 32 bit, e la singola cella (unità minima indirizzabile di RAM) è un byte (un gruppo di 8 bit). Come vedremo nel Capitolo 11, un compito fondamentale del Nucleo consiste nel definire un mapping tra indirizzi logici ed indirizzi fisici, per cui durante l esecuzione di una istruzione, ogni indirizzo logico viene tradotto in un opportuno indirizzo fisico. Il programmatore può in qualche modo determinare gli indirizzi logici usati dal suo programma ma ignora quali saranno gli indirizzi fisici assegnati al programma durante l esecuzione. In modo analogo, un compilatore, un assemblatore o un linker possono impostare gli indirizzi logici dei programmi ma non quelli fisici. Iniziamo col descrivere come il linker (vedi Paragrafo pag. 56) assegni indirizzi logici al programma da eseguire. Procediamo per gradi e supponiamo dapprima che il file eseguibile sia stato linkato in modo statico, ossia che non vengano utilizzate librerie dinamiche. In questo caso, il linker assegna al processo che dovrà eseguire il programma un gruppo di regioni di memoria, ossia intervalli di indirizzi logici, caratterizzate da un indirizzo iniziale e da una lunghezza. Ad un file eseguibile corrispondono quindi diverse regioni di memoria. Per motivi di flessibilità, le regioni di memoria assegnate ad un processo non sono tra loro contigue. L insieme di indirizzi logici racchiuso nelle regioni di memoria di un processo prende il nome di spazio degli indirizzi del processo: esso include tutti e soli gli indirizzi logici che un processo è autorizzato ad usare durante la sua esecuzione. Ogni indirizzamento effettuato dal processo al di fuori dal suo spazio degli indirizzi viene considerato dal Nucleo come un indirizzamento non valido dovuto ad un errore di programmazione; il processo responsabile dell errore viene normalmente abortito, come già descritto nel Paragrafo pag. 43. Una eccezione a tale regola vale per la regione di memoria che contiene lo stack: in tale caso, è possibile che, in seguito ad una serie di push, il processo effettui un indirizzamento immediatamente prima dell inizio della regione (lo stack cresce tradizionalmente per indirizzi decrescenti). Tale eventualità è ovviamente considerata normale dal Nucleo, per cui anziché abortire il processo, viene semplicemente allocata una porzione di memoria aggiuntiva allo stack, estendendo così la regione, e il programma può continuare normalmente il suo lavoro; tutto ciò avviene in modo trasparente per il programma in esecuzione. La Figura 9.1 illustra un semplice esempio di spazio degli indirizzi di un processo. Come si osserva dalla figura, il linker ha assegnato al programma 4 regioni di memoria distinte tra loro non contigue: 1 In realtà, diversi microprocessori tra cui quelli Intel 80x86, fanno uso di indirizzi logici più complessi composti da due componenti: un segmento da 16 bit ed un offset da 32 bit, l indirizzo risultante chiamato indirizzo lineare è comunque un indirizzo da 32 bit.
163 150 CAPITOLO 9. SPAZIO DEGLI INDIRIZZI DI UN PROCESSO 0x regione codice 0x00c00000 regione dati inizializzati 0x00f00000 spazio degli indirizzi del processo regione dati non inizializzati regione stack 0xbfffffff Figura 9.1: Spazio degli indirizzi di un processo. regione codice: contiene le istruzioni del programma, è lunga 64 Kbyte ed inizia all indirizzo logico 0x ; regione dati inizializzati: contiene costanti, è lunga 64 Kbyte ed inizia all indirizzo logico 0x00c00000 regione dati non inizializzati 2 : contiene lo heap (memoria dinamica ottenibile tramite la funzione malloc()), è lunga 128 Kbyte ed inizia all indirizzo logico 0x00f00000 regione stack: contiene lo stack, inizia al massimo indirizzo logico possibile, ossia 0xffffffff e si espande all ingiù. Come fa il loader a assegnare specifiche regioni di memoria alle varie parti del programma? In realtà il loader fa ben poco in quanto le regioni sono già state identificate dal compilatore ed assegnate dal linker 3. Approfondiamo la discussione iniziata nel Paragrafo pag. 56 dove non si faceva ancora riferimento a regioni di memoria ma soltanto a variabili esterne. Riferiamoci alla Figura 4.1 pag. 57: il compilatore ha assegnato al modulo A due sezioni distinte, una per il codice (prog0) ed una per le variabili globali (dati0) 2 Nei sistemi Unix questa regione viene chiamata bss; tale nome deriva in realtà da una vecchia direttiva assembler di alcune macchine IBM, che era l acronimo di Block Started by Symbol. 3 Vedremo più avanti in questo capitolo che altre regioni possono essere assegnate dinamicamente al processo.
164 9.2. INDIRIZZI E REGIONI 151 ed ha assegnato a tali sezioni gli indirizzi logici e 00b In modo analogo, quando è stato compilato il modulo B, il compilatore ha assegnato alla sezione prog1 l indirizzo logico e alla sezione dati1 l indirizzo logico 00b Gli stessi valori sono stati assegnati dal compilatore alle sezioni prog2 e dati2 del modulo C. Dopo il linkaggio dei tre moduli in un unico file eseguibile, le tre sezioni di codice sono state fuse in un unica regione codice avente indirizzo iniziale e, analogamente, le tre sezioni di dati sono state fuse in un unica regione dati avente indirizzo iniziale 00b00000 (vedi Figura 4.2 pag. 58). Le azioni svolte dal loader dipendono dal tipo di regione e dai registri di indirizzamento presenti nella CPU. Si danno qui alcuni esempi, senza alcuna pretesa di completezza. Nel caso di regione codice, è sufficiente impostare nel registro Program Counter (contatore programma, registro eip nell architettura Intel 80x86) il primo indirizzo logico della regione codice. Questo assicura che quando il processo andrà in esecuzione, la CPU eseguirà la prima istruzione contenuta nella regione codice. Nel caso di regione stack (usata dal compilatore, tra l altro, per contenere le variabili locali), si imposta nel registro Stack Pointer (puntatore allo stack, registro esp nell architettura Intel 80x86) il primo indirizzo logico (il più alto) della regione stack. Infine, nel caso di regione dati il compilatore ha lasciato non risolto (indirizzo esterno) l indirizzo della sezione data contenente le variabili globali ed ha effettuato indirizzamenti all interno di tale sezione in modo rilocabile, avvalendosi di un registro base (registro ebx nell architettura Intel 80x86) opportunamente inizializzato tramite una istruzione del tipo: lea.data, %ebx Durante il linkaggio, il valore corrispondente a.data verrà opportunamente impostato. Da questa discussione preliminare, emerge quindi che quando viene caricato un file eseguibile e viene creato il processo destinato ad eseguire le istruzioni contenute nel file eseguibile, tale processo riceve uno spazio degli indirizzi preparato dal linker secondo varie modalità. Vediamo ora come lo spazio degli indirizzi iniziale di un processo possa cambiare durante l esecuzione del processo: il processo può acquisire nuove regioni di memoria, allargare o restringere la dimensione di regioni di memoria esistenti, o rilasciare regioni di memoria. Ancora una volta, ci interessiamo alle regioni di memoria e quindi agli indirizzi logici posseduti da un processo e non al come il Nucleo assegna indirizzi fisici ai vari indirizzi logici.
165 152 CAPITOLO 9. SPAZIO DEGLI INDIRIZZI DI UN PROCESSO 9.3 Modifiche dello spazio degli indirizzi La regione codice e quella dati inizializzati rimangono solitamente immutate durante l esecuzione del programma. Viceversa, la regione dati non inizializzati può crescere, ma solo in seguito a esplicite richieste di memoria dinamica da parte del processo. In un sistema Unix, la chiamata di sistema per richiedere una variazione (positiva o negativa) di questa regione è la brk(end_data); che fissa direttamente il nuovo valore end data dell indirizzo logico finale di tale regione, che potrà essere maggiore (richiesta al Nucleo di allocazione di nuova memoria) o anche minore (restituzione al Nucleo di memoria non più necessaria) del valore precedente. In pratica, la brk() non viene mai chiamata direttamente, ma si fa ricorso ad API più comode da utilizzare, come le funzioni malloc(3) (richiesta di memoria) e free(3) (restituzione di memoria) 4. In modo analogo al caso ora visto, la regione stack può crescere durante l esecuzione del programma, come già detto (ad esempio, a causa dell annidamento di chiamate di funzioni o dell allocazione di spazio per variabili locali), ma a differenza del caso precedente tale crescita è assicurata trasparentemente e automaticamente dal Nucleo, senza intervento da parte del processo (si veda il Paragafo 9.2). Si consideri ad esempio la seguente funzione in linguaggio C: int function() { int i, j; char buffer[10000]; char *p; i = 30; p = malloc(5000); subfunc1(p, i); j = subfunc2(buffer); } free(p); return j; In essa lo spazio di indirizzi del processo viene ampliato in due distinte regioni: 4 Tra i vari vantaggi dell uso di queste API si menziona che la brk() di fatto deve richiedere memoria al Nucleo in multipli di una pagina (si veda il Capitolo 11), cioè ad esempio per blocchi da 4 Kbyte, mentre la malloc() (che internamente chiama brk(), nascondendone le problematiche d uso) può fornire memoria al programma in blocchi di dimensione arbitraria (anche un byte alla volta).
166 9.3. MODIFICHE DELLO SPAZIO DEGLI INDIRIZZI 153 nella regione stack : la dichiarazione di variabili locali alla funzione (i, j, buffer, p) fa sì che venga allocato automaticamente spazio nello stack per far posto a queste variabili; assumendo che variabili intere e puntatori occupino entrambi 4 byte (32 bit), lo stack pointer viene decrementato di 2 x 4 byte (interi i e j), poi di byte (array buffer), poi ancora di 4 byte (puntatore p); il Nucleo tipicamente assegna una dimensione iniziale alla regione stack (ad esempio 4 Kbyte) e aggiunge automaticamente, quando se ne verifica la necessità, una o più unità di incremento (ad esempio da 2 Kbyte). Nel caso in esame, supponendo che lo stack sia inizialmente pressochè vuoto, il Nucleo aggiunge automaticamente ai 4 Kbyte iniziali altri 6 Kbyte (corrispondenti a 3 unità di incremento da 2 Kbyte), per arrivare ad una dimensione di 10 Kbyte, sufficienti a contenere i byte occupati dalle variabili allocate. nella regione dati non inizializzati : la chiamata esplicita alla malloc() fa richiedere 5000 byte in più per questa regione;s se la dimensione di pagina è di 4 Kbyte, ciò fa sì che il processo, attraverso la brk() chiamata internamente alla malloc(), chieda al Nucleo 2 pagine, cioè 8 Kbyte (di questi, 5000 byte vengono dati in uso al programma, e i rimanenti potranno essere utilizzati in successive chiamate a malloc(), che cercherà di gestire queste richieste senza dover richiedere altra memoria al Nucleo). Quanto visto finora riguarda, come detto, variazioni di dimensione di regioni del processo, che però rimangono sempre le stesse. Esiste un caso diverso dai precedenti, in cui il processo rilascia tutte le regioni di memoria da esso possedute, incluse la regione codice e quella dati inizializzati, ed ottiene un nuovo gruppo di regioni affatto diverse da quelle ottenute in precedenza. Ciò avviene quando il processo effettua una chiamata di sistema per caricare dinamicamente un file eseguibile, quindi quando invoca, nel caso Unix, execve() (o le sue varianti). Si consideri, ad esempio, il seguente programma: Pseudo-codice: main() { print("lista dei file nella cartella attiva:"); } execl("/bin/ls", "ls", NULL); print("errore in execl()"); exit(1); Codice reale: #include <stdio.h> #include <unistd.h> #include <string.h>
167 154 CAPITOLO 9. SPAZIO DEGLI INDIRIZZI DI UN PROCESSO int main(void) { printf("lista dei file nella cartella attiva:\n\n"); } execl("/bin/ls", "ls", NULL); fprintf(stderr, "errore in execl()\n"); exit(1); Il programa in esecuzione richiede il caricamento del programma ls contenuto nella directory /bin. In seguito a tale chiamata di sistema, il Nucleo dealloca dalla RAM tutte le regioni del programma iniziale (che sparisce), carica in memoria il file eseguibile ls ed assegna al processo un nuovo gruppo di regioni di memoria. Dal punto di vista della programmazione, la execve() corrisponde ovviamente ad un salto senza ritorno, per cui la stampa del messaggio d errore (così come la chiamata alla exit() che segue) viene effettuata solo nel caso in cui la chiamata di sistema non possa essere eseguita correttamente dal Nucleo. 9.4 Linking dinamico Si supponga ora che il file eseguibile sia stato linkato a librerie dinamiche. In questo caso, viene caricato non solo il programma da eseguire ma anche un particolare modulo del linker che provvede automaticamente a caricare funzioni di libreria prima di iniziarne l esecuzione nel programma. In seguito al linkaggio dinamico effettuato, il file eseguibile include nella sua testata (inglese header) una serie di voci che si riferiscono a funzioni esterne contenute in qualche libreria; ogni voce consiste nella tripla: dove: <func> <path> <offlist> <func > è il nome della funzione esterna; <path > è il percorso del modulo che definisce la funzione; <offlist > è la lista di offset nel modulo in corrispondenza a chiamate alla funzione esterna. Basandosi sulle informazioni contenute nella suddetta testata, il program interpreter esegue le seguenti azioni: 1. assegna una nuova regione di memoria per ogni libreria dinamica richiesta dal programma; 2. determina l indirizzo logico della funzione di libreria richiesta sommando all indirizzo iniziale della regione l offset all interno della libreria (ogni libreria include decine o centinaia di funzioni diverse);
168 9.4. LINKING DINAMICO completa nel codice tutti i riferimenti a funzioni di libreria sostituendo il valore 0 con l indirizzo logico appropriato (vedi Figure 4.1 pag. 57 e 4.2 pag. 58). Dal punto di vista delle regioni di memoria ciò implica: un gruppo di regioni di memoria per consentire al linker caricato insieme al programma di poter operare; una nuova regione di memoria per ogni libreria di funzioni utilizzata. Potrebbe sembrare superfluo continuare a riservare regioni di memoria al program interpreter dopo che è iniziata l esecuzione del programma. In effetti il program interpreter deve continuare ad esistere per consentire al programma di effettuare linking dinamici ad altre funzioni. Nei sistemi Unix, ciò si realizza mediante la chiamata di sistema dlopen(): ogni volta che il programma invoca tale chiamata di sistema, entra in funzione il program interpreter per linkare dinamicamente una nuova funzione. Si noti come, a differenza della execve(), la dlopen() corrisponde ad una chiamata di funzione con ritorno al programma chiamante. Il seguente programma usa la chiamata di sistema dlopen() per caricare dinamicamente dalla libreria matematica /lib/libm.so.6 del linguaggio C la funzione radice quadrata sqrt() e la applica sul parametro x. #include <stdio.h> #include <dlfcn.h> int main(int argc, char **argv) { void *handle; double (*sqrt)(double); double x, y; } x = ; handle = dlopen ("/lib/libm.so.6", RTLD_LAZY); sqrt = dlsym(handle, "sqrt"); y = (*sqrt)(x); printf ("%f\n", y); dlclose(handle); Si noti che per essere linkato correttamente, bisogna specificare tra i file da linkare la libreria dinamica /usr/lib/libdl.so, per cui il comando gcc deve essere del tipo 5 : 5 Si è visto nel Paragrafo pag. 58 che tutte le librerie, sotto Unix, si trovano normalmente in /lib o /usr/lib, ed hanno un nome nome del tipo libxxx ed estensione.a o.so, dove xxx è una stringa che identifica la libreria. Il compilatore gcc accetta, per comodità dell utente, l opzione -lxxx, e la interpreta come indicazione di linkare appunto il programma con la libreria libxxx.so (o libxxx.a nel caso di compilazione statica), cercando la libreria in una delle due directory dette. Pertanto l opzione -ldl è l indicazione di linkare con la libreria libdl.so.
169 156 CAPITOLO 9. SPAZIO DEGLI INDIRIZZI DI UN PROCESSO gcc -o prova prova.c -ldl 9.5 Mapping di file in memoria Il concetto di regione di memoria rischia di rimanere alquanto vago se non si specifica in che modo gli indirizzi logici di una regione sono collegati alle varie parti del file eseguibile. Questo è precisamente il ruolo del cosiddetto mapping di file in memoria: associare ad una regione di memoria un file (o parte di esso) residente su disco. L allocazione e deallocazione di porzioni di regione di memoria si effettuano in Unix tramite le chiamate di sistema mmap() e munmap(), che operano sui seguenti parametri: pointer = mmap(start, length, prot, flags, fd, offset); retval = munmap(start, length); Senza entrare nel dettaglio, la prima chiamata permette di richiedere al Nucleo di mappare una porzione di un file (o di altro tipo di oggetto), identificato dal file descriptor fd, lunga length byte a partire dalla posizione offset nel file stesso, in un area di memoria in RAM, il cui indirizzo logico viene restituito come valore di ritorno. La seconda chiamata ovviamente rimuove la mappatura. Come si vede, la mmap() richiede quindi tra i suoi parametri un descrittore, che identifica un file (o altro oggetto) aperto in precedenza. A questo punto, la distinzione tra spazio degli indirizzi e RAM assegnata al processo dovrebbe risultare chiara: a mano a mano che il processo esegue istruzioni, esso fa uso di nuovi indirizzi logici ed è compito del Nucleo provvedere sia ad assegnare nuova RAM al processo, sia a leggere da disco nell area di RAM ottenuta i dati associati agli indirizzi logici. Tutto ciò avviene in modo trasparente rispetto al programma: come detto in precedenza, il programmatore non ha alcun controllo sugli indirizzzi fisici usati dal suo programma ed è cura del Nucleo assegnare (o rilasciare) aree di RAM al processo che esegue il programma. Come vedremo nel prossimo capitolo, la strategia seguita dal Nucleo consiste nell allocare RAM al processo il più tardi possibile, ossia quando il processo effettua indirizzamenti in una zona di regione di memoria non ancora caricata in memoria. Questo approccio è di solito efficace in quanto molti processi eseguono soltanto un sottoinsieme delle istruzioni contenute nel programma Altri tipi di mapping Oltre al mapping di file eseguibili utilizzato dal loader per caricare un programma, la chiamata di sistema mmap() è anche usata per realizzare due altri tipi di mapping chiamati mapping anonimo e mapping di file di dati. Entrambi contribuiscono ad ampliare lo spazio degli indirizzi di un processo.
170 9.5. MAPPING DI FILE IN MEMORIA 157 Mapping anonimo Due delle quattro regioni di memoria illustrate nella Figura 9.1 pag. 150 sono regioni di memoria particolari che non hanno una immagine su disco, ossia un file di riferimento, da cui estrarre le informazioni richieste: la regione stack e quella usata dallo heap. In entrambi i casi, le regioni nascono vuote ed è il processo a dovere scrivere dati in esse prima di poterli rileggere: lo stack di un processo è inizialmente vuoto e si riempie a mano a mano che il processo invoca funzioni le quali a loro volta invocano altre funzioni (i compilatori C collocano tutte le variabili locali ad una funzione in cima allo stack). In modo analogo, l area di memoria dinamica ottenuta tramite la API malloc() non è inizializzata. In questi casi, si parla di mapping anonimo per sottolineare il fatto che la regione di memoria corrispondente non ha un file di riferimento su disco. La distinzione tra mapping standard e mapping anonimo è importante dal punto di vista del Nucleo: quando un processo effettua una scrittura in una delle sue regioni di memoria, le azioni svolte dal Nucleo sono diverse a seconda del tipo di mapping. Mapping di un file di dati Esiste un altro tipo di mapping non illustrato nella Figura 9.1 pag. 150 che può essere usato dai programmatori: quello relativo al file di dati. Tale approccio consente di accedere ai dati di un file (tutto il file per intero oppure solo una porzione di esso) come se fossero mappati dentro ad un vettore di dati in memoria, anziché essere contenuti all interno del file. Nel caso di indirizzamenti casuali del file, non è quindi necessario fare uso della chiamata di sistema lseek() ma è sufficiente indirizzare l elemento richiesto dell array. Anche tutte le letture e scritture dal file sono implicite, per cui non c è bisogno nemmeno delle chiamate read() e write(). Il seguente esempio illustra il mapping di un file di dati chiamato /tmp/file0 da parte di un programma. Pseudo-codice: main() { fd = open("/tmp/file0", lettura-scrittura); p = mmap(lunghezza 1 Mbyte, fd, offset 0); loop(per i da 0 a 1 Mbyte) { if(p[i] diverso da Newline) p[i] = nuovo valore; } close(fd); munmap(p, 1 Mbyte); exit(0); } Codice reale:
171 158 CAPITOLO 9. SPAZIO DEGLI INDIRIZZI DI UN PROCESSO /* leggi il file di testo esistente "/tmp/file0" da 1 MB e modificalo tramite letture e scritture casuali. Il file "/tmp/file0" deve essere stato creato in precedenza */ #include <stdlib.h> #include <unistd.h> #include <stdio.h> #include <fcntl.h> #include <linux/mman.h> #define MBYTE (1024*1024) #define FNAME "/tmp/file0" int main(void) { int fd; int i; char *p; fd = open(fname, O_RDWR); p = (char *)mmap(null, MBYTE, PROT_READ PROT_WRITE, MAP_SHARED, fd, 0L); for (i = 0; i < MBYTE; ++i) if (p[i]!= \n ) { int i1, i2; } i1 = (i*5) % MBYTE; i2 = (i*7) % MBYTE; p[i] = (p[i1] + p[i2]) / 2; } close(fd); munmap(p, MBYTE); exit(0);
172 Capitolo 10 Struttura Interna del Nucleo 10.1 Introduzione Dagli anni 60, quando apparvero i primi Sistemi Operativi multiprogrammati, ad oggi le metodologie di progetto si sono affinate fino a giungere ad un buon livello di standardizzazione per quanto riguarda la realizzazione di Nuclei per architetture a memoria centralizzata condivisa. Sono invece tuttora in corso ricerche e approfondimenti per quanto riguarda la progettazione di Sistemi Operativi per architetture multiprocessore (sistemi con più CPU) nonché per architetture distribuite (reti di calcolatori, basi di dati distribuite, ecc.). In questo capitolo si esaminano brevemente alcune delle problematiche più significative attinenti al progetto di Nuclei di Sistemi Operativi multiprogrammati mentre, nei capitoli successivi, si descrivono le funzioni svolte dai principali moduli inclusi nel Nucleo Nucleo e processi L uso della combinazione Nucleo/processi discusso nel Pararagrafo 7.3 pag. 99 risulta determinante per realizzare in modo semplice ed elegante Sistemi Operativi di tipo multitasking. Come detto in precedenza, il Nucleo deve essere considerato come uno strato intermedio di software tra i programmi più complessi del Sistema Operativo (interprete di comandi, compilatori, ecc.) e l hardware dell elaboratore. In base a questa impostazione modulare, i rimanenti programmi del Sistema Operativo vedono la combinazione hardware + Nucleo come una macchina virtuale più facile da programmare della macchina reale. Per questo motivo, il Nucleo è talvolta considerato come una estensione diretta dell hardware. 159
173 160 CAPITOLO 10. STRUTTURA INTERNA DEL NUCLEO Va tuttavia messo in evidenza che esistono alcuni compiti fondamentali, quelli che garantiscono appunto la corretta gestione dei processi, che devono necessariamente essere realizzati dal Nucleo in quanto coordinatore dell avanzamento dei processi. In particolare, citiamo: la creazione e terminazione di processi; la commutazione di processi (in inglese, task switching). Tale commutazione consiste nel porre in un opportuno stato di attesa il processo in esecuzione e porre quindi in esecuzione il processo selezionato dalla funzione di scheduling; la gestione di tipo time sharing del tempo di CPU con i relativi programmi che realizzano l algoritmo di scheduling; la realizzazione di opportuni schemi di interazione tra processi (vedi Paragrafo 7.3 pag. 99); Oltre a creare processi, coordinarne l avanzamento ed eliminarli quando necessario, il Nucleo deve svolgere numerose altre attività quali: la inizializzazione del Sistema Operativo con relativa impostazione delle strutture di dati necessarie e la creazione dei primi processi di sistema; la gestione dei segnali d interruzione provenienti sia dalla CPU che da dispositivi esterni; la chiusura ordinata del Sistema Operativo che precede lo spegnimento dell elaboratore con il rilascio di tutte le risorse possedute dai processi e la loro eliminazione. Accenneremo nel prossimo capitolo al modo in cui Nucleo svolge i diversi compiti ad esso assegnati. Per ora, limitiamoci a studiare che cosa rende il programma Nucleo diverso dagli altri programmi finora considerati Che cosa è il Nucleo Come i normali programmi, il Nucleo deve essere opportunamente compilato e linkato dando luogo ad un file eseguibile. Tale file eseguibile non può tuttavia essere caricato da un loader poiché quest ultimo presuppone l esistenza di un Nucleo. Il file eseguibile contenente il Nucleo viene quindi caricato in RAM tramite una tecnica chiamata bootstrapping: le prime istruzioni eseguite servono a trasferire in RAM settori di disco che contengono altre istruzioni le quali, a loro volta, provvedono a leggere la rimanente parte del codice. Fatto ciò il Nucleo inizializza le proprie strutture di dati e crea alcuni processi di servizio, tra cui i processi di tipo login shell in grado di accettare comandi dagli utenti. Al termine di tale fase di inizializzazione, il Nucleo diventa operativo. Quando il Nucleo è operativo, esso è pronto sia a soddisfare richieste da parte dei processi in esecuzione, sia a registrare nelle proprie strutture di dati il verificarsi
174 10.3. CHE COSA È IL NUCLEO 161 entry point entry point entry point entry point NUCLEO entry point entry point entry point Figura 10.1: Struttura interrupt driven del Nucleo. di eventi causati da dispositivi di I/O esterni alla CPU. In altre parole, il Nucleo è in grado di rispondere in tempi brevi a richieste di servizio provenienti sia dalla CPU che da altri dispositivi hardware. Per capire come ciò sia possibile, è necessario fare riferimento ai segnali di interruzione e al supporto hardware offerto dal calcolatore per realizzare il modello Nucleo/processi. Si veda quanto già detto in proposito nel Paragrafo 3.4 pag. 40. Come illustrato in Figura 10.1, il Nucleo è un programma di tipo interrupt driven, ossia un gruppo di programmi in qualche modo indipendenti che sono attivati soltanto quando si verificano interruzioni di uno specifico tipo. Ognuno di tali programmi indipendenti possiede un proprio entry point, ossia l indirizzo della prima istruzione del programma. Poiché i moderni calcolatori prevedono decine di interruzioni diverse, vi sono decine di entry point tramite i quali possono essere eseguiti i corrispondenti programmi del Nucleo. Quando l unità di controllo della CPU rileva un segnale di interruzione, essa: 1. identifica il numero d ordine n dell interruzione; 2. passa in System mode ed usa lo stack del Nucleo per salvare il contenuto di alcuni registri del processo sospeso; 3. usa la tabella dei descrittori delle interruzioni (interrupt vector) ed il numero n per derivare l indirizzo o entry point del programma del Nucleo atto a trattare interruzioni di tipo n;
175 162 CAPITOLO 10. STRUTTURA INTERNA DEL NUCLEO 4. imposta nel registro Program Counter l indirizzo derivato al passo precedente. Al termine delle quattro azioni svolte dall unità di controllo, la CPU passa ad eseguire la prima istruzione del programma del modulo atto a trattare interruzioni di tipo n. In altre parole, viene eseguito il programma del Nucleo avente l entry point corrispondente alla interruzione che si è appena verificata. Per motivi di flessibilità, sia l indirizzo di base della tabella dei descrittori delle interruzioni, sia i valori degli entry point possono essere impostati dal Nucleo durante la fase di installazione, ossia appena prima di avere abilitato le interruzioni. L architettura Intel 80x86 include, ad esempio, un registro idtr che punta alla base della tabella dei descrittori delle interruzioni. Ogni descrittore, lungo 8 byte, contiene l indirizzo logico dell entry point nonché altri flag per la protezione (vedi Manuali Intel [7] per ulteriori dettagli) Ruolo dei segnali di interruzione I segnali di interruzione sono generati sia dall unità di controllo della CPU, sia da dispositivi esterni alla CPU 1, per rappresentare eventi tra loro molto diversi quali: segnalazione da parte di un dispositivo esterno della fine di una operazione di ingresso/uscita; segnalazione da parte del chip interval timer: terminazione di un intervallo di tempo prefissato; segnalazione da parte dell unità di controllo della CPU di una condizione anomala verificatasi nell eseguire una istruzione: codice operativo non valido, indirizzo operando non valido, indirizzo istruzione non valido, ecc.; esecuzione di una apposita istruzione il cui effetto è quello di generare un segnale di interruzione (vedi Paragrafo 10.6); segnalazione di un evento ad una o più CPU (solo nei sistemi multiprocessore) Gestori delle interruzioni I programmi del Nucleo che devono trattare specifiche interruzioni prendono il nome di gestori delle interruzioni (in inglese, interrupt handlers). In generale, ogni segnale di interruzione richiede un suo apposito gestore: il gestore delle interruzioni emesse periodicamente dal circuito hardware interval 1 La Intel denota come exception un segnale di interruzione prodotto dalla CPU e come interrupt un segnale prodotto da un dispositivo diverso dalla CPU.
176 10.6. IMPLEMENTAZIONE DELLE CHIAMATE DI SISTEMA 163 timer, ad esempio, non ha niente in comune con quello che gestisce le interruzioni provenienti da tastiera. Come vedremo nel Capitolo 13 su specifici esempi, il verificarsi di un determinato segnale di interruzione può causare cambiamenti di stato tra i processi in vita nel sistema. Un classico caso è quello delle operazioni di I/O bloccanti, ossia operazioni che devono essere eseguite prima che il processo possa continuare ad eseguire istruzioni. Se, ad esempio, il processo effettua una chiamata di sistema read() per leggere dati da disco, esso dovrà essere bloccato dal Nucleo fino a quando la lettura dei dati non sarà avvenuta poiché la semantica delle varie forme di read() presenti nei linguaggi di programmazione specifica che i dati letti sono immediatamente disponibili per altre elaborazioni da parte del programma. In questo specifico caso, il segnale di interruzione emesso dal disco per segnalare l avvenuta lettura dei dati è l evento atteso dal processo bloccato, per cui tale segnale può essere considerato come una risorsa consumabile prodotta dal controllore del disco e consumata dal processo bloccato sull operazione di read(). In generale, però, non esiste sempre un processo bloccato per ogni segnale di interruzione che si verifica. Nel caso dei segnali di interruzione emessi periodicamente dall interval timer, ad esempio, non vi è alcun processo bloccato in attesa di tale evento Implementazione delle chiamate di sistema La criticità delle funzioni svolte fa sì che il passaggio di controllo ad un qualsiasi programma del Nucleo non è realizzato come una normale chiamata di procedura, bensì tramite una tecnica speciale che consente non soltanto di passare il controllo ma anche di assicurare la protezione dei programmi e dei dati contenuti nel Nucleo. Per distinguerle dalle semplici chiamate di procedura, le chiamate a programmi del Nucleo prendono il nome di chiamate di sistema. Come visto nel Capitolo 3, in molte architetture, incluse quelle dei microprocessori più recenti, le chiamate di sistema sono realizzate tramite il meccanismo delle trap, che fa passare la CPU dallo User mode al System mode, generando un apposita interruzione software. Il parametro dell istruzione trap n identifica la chiamata di sistema (tipicamente viene riservato un byte per tale parametro, per cui il Nucleo è in grado di gestire fino a 256 chiamate di sistema distinte). Quando si verifica una interruzione software, la CPU passa automaticamente ad eseguire le istruzioni del gestore delle chiamate di sistema, ossia dell apposito programma del Nucleo abilitato a trattare simili eventi. Il gestore verifica il tipo di chiamata, gli eventuali parametri associati alla chiamata e, nel caso in cui tutto sia regolare, passa il controllo al programma che gestisce la chiamata di sistema avente numero n. Quando il Nucleo ha eseguito le azioni richieste, esso esegue una apposita istruzione di tipo rti (return from interrupt), il cui effetto è quello di ripristinare il
177 164 CAPITOLO 10. STRUTTURA INTERNA DEL NUCLEO A (User Mode) A (User Mode) NUCLEO tratta la richiesta di A (Kernel Mode) Figura 10.2: Esecuzione di una chiamata di sistema da parte del Nucleo. contesto in User mode del processo che aveva effettuato la chiamata di sistema e di saltare all indirizzo depositato in precedenza sullo stack di sistema del processo. Dopo avere eseguito la rti, la CPU passa quindi ad eseguire l istruzione successiva alla trap. La Figura 10.2 illustra il caso più semplice in cui un processo richiede un servizio al Nucleo, lo ottiene e riprende quindi la sua esecuzione Descrittori di risorse Per svolgere le sue attività, il Nucleo fa uso di numerosi descrittori di risorse, ossia di strutture di dati di vario tipo quali liste, tabelle, vettori di tabelle, ecc. Esempi classici di descrittori si riferiscono alle seguenti risorse: processo: ogni processo gestito dal Nucleo possiede un proprio descrittore che deve essere aggiornato ogni qualvolta il processo cambia stato. memoria RAM : ogni blocco (page frame) di memoria possiede un descrittore che specifica l eventuale uso da parte di uno o più processi (nel caso di blocchi condivisi). spazio degli indirizzi di un processo: ogni processo è abilitato a fare uso di uno specifico insieme di indirizzi lineari, a prescindere dal fatto che le pagine di dati associate a tali indirizzi siano effettivamente presenti in RAM. Come abbiamo visto nel Paragrafo 9.2 pag. 148, il Nucleo fa uso di un apposito descrittore per ogni regione di memoria assegnata ad un processo. memoria secondaria: il Nucleo mantiene per ogni file system montato alcuni descrittori che rappresentano il contenuto del disco; anche se tali descrittori sono già presenti sul disco stesso, risulta molto più efficiente farne una copia in memoria per ridurre il numero di accessi al disco. Nei file system Unix, tali descrittori prendono il nome di superblocco, descrittore di gruppo di blocchi e inode.
178 10.8. INTERROMPIBILITÀ DEL NUCLEO 165 file aperti da un processo: l interazione tra un processo ed un file richiede l uso di altri descrittori che contengono diverse informazioni quali il modo in cui è stato aperto il file, il byte del file attualmente scandito dal processo, ecc. Nei file system Unix, tali descrittori prendono il nome di file object. A prescindere dal modo in cui sono realizzati, i descrittori di risorsa hanno alcune importanti proprietà in comune. ogni descrittore è memorizzato in RAM in un area di memoria riservata al Nucleo: per motivi di sicurezza, i processi non devono potere accedere direttamente ad un descrittore di risorsa. ogni descrittore utilizzato dal Nucleo deve essere opportunamente inizializzato prima di poterlo utilizzare. In generale, durante l inizializzazione del Sistema Operativo, vengono riservate le aree di memoria destinate a contenere i vari descrittori e vengono inizializzati opportunamente i campi di ogni descrittore. i descrittori sono frequentemente aggiornati dai vari programmi del Nucleo: di norma, ognuno di essi è considerato come una risorsa seriale che deve essere aggiornata in modo non interrompibile da parte del Nucleo (vedi paragrafo successivo). Un esempio può servire a rendere più intuitivo l ultimo punto: il modo più semplice per codificare lo stato di un processo è di fare uso di apposite liste di descrittori di processo, una per ognuno dei vari stati riconosciuti dal Nucleo. Consideriamo ad esempio la lista dei processi eseguibili, ossia pronti ad essere eseguiti dalla CPU. Ogni qualvolta la funzione di fine quanto di tempo viene invocata, essa scandisce tale lista per inserire nella posizione appropriata il processo che ha terminato il suo quanto di tempo. Durante tale fase, il Nucleo non deve essere interrotto finché il descrittore di processo non sia stato inserito. Nel caso opposto, infatti, la seconda attivazione del Nucleo potrebbe richiedere l esecuzione di un programma che consulta la stessa lista e vi è la possibilità che essa sia stata lasciata in uno stato non coerente dalla precedente attivazione: ad esempio alcuni dei puntatori utilizzati per realizzare la lista potrebbero non essere stati ancora aggiornati Interrompibilità del Nucleo Per motivi di semplicità, l interazione processo-nucleo è stata presentata come una attività sequenziale consistente nei seguenti tre passi: 1. il processo attiva il Nucleo tramite int; 2. la CPU passa in stato Kernel ed il Nucleo esegue gli opportuni programmi; 3. al termine, il Nucleo rilascia l uso della CPU ed il processo riprende l esecuzione in stato User.
179 166 CAPITOLO 10. STRUTTURA INTERNA DEL NUCLEO A è interrotto e sostituito da B riprende l esecuzione di B che termina riprende l esecuzione di A che termina A1 B1 C B2 A2 inizio esecuzione di A B è interrotto e sostituito da C che termina senza essere a sua volta interrotto Figura 10.3: Esecuzione annidata di programmi del Nucleo. La realtà è molto più complessa poiché il Nucleo non deve soltanto soddisfare richieste emesse dal processo in esecuzione, ma anche gestire interruzioni di vario tipo quali quelle emesse dai dispositivi di I/O e quelle emesse dal chip interval timer. In effetti, il Nucleo non ha una struttura sequenziale come molti programmi, bensì parallela: ogni segnale d interruzione riconosciuto dall hardware e gestito dal Sistema Operativo causa l attivazione di un apposito programma del Nucleo. Tale struttura parallela assicura a sua volta l interrompibilità (o prelazionabilità) del Nucleo, ossia la capacità di rispondere ad un segnale di interruzione mentre sta già trattando un altro segnale di interruzione (sia pure di tipo diverso). Ogni Nucleo interrompibile deve quindi essere in grado di eseguire in modo annidato le interruzioni, ossia deve essere in grado di sospendere l esecuzione di un programma di gestione di una interruzione per passare ad eseguire un altro programma di gestione di una interruzione di tipo diverso; terminata l esecuzione del secondo programma, il Nucleo deve riprendere l esecuzione del primo programma. La Figura 10.3 illustra un esempio di esecuzione annidata di tre programmi distinti del Nucleo. In effetti, l attività più complessa nella progettazione di un Nucleo consiste nel renderlo il più possibile interrompibile: in realtà, non sarà mai possibile renderlo totalmente interrompibile in quanto alcune operazioni critiche devono essere eseguite con le interruzioni disabilitate e con la certezza che il Nucleo non verrà interrotto mentre esegue tali operazioni (vedi esempio precedente relativo all inserimento di un descrittore di processo nella lista dei processi eseguibili). Per esemplificare quanto appena detto, diamo alcuni esempi dei casi che possono verificarsi in seguito alla attivazione del Nucleo tramite una qualche interruzione. Nel caso più semplice, il programma del Nucleo termina e riprende in stato User l esecuzione del processo che ha eseguito la int (vedi Figura 10.2 pag. 164).
180 10.8. INTERROMPIBILITÀ DEL NUCLEO 167 A (User Mode) B (User Mode) NUCLEO inizia a trattare la richiesta di A (Kernel Mode) A chiede una risorsa non disponibile e viene sospeso NUCLEO seleziona B Figura 10.4: Chiamata di sistema che pone il processo in uno stato di attesa. A (User Mode) B (User Mode) NUCLEO inizia a trattare la richiesta di A (Kernel Mode) NUCLEO sospende A e seleziona B (Kernel Mode) segnale d interruzione Figura 10.5: Esecuzione di un processo prioritario con gestione interruzione incompiuta. In altri casi, il programma del Nucleo può richiedere una risorsa attualmente non disponibile, ad esempio, la lettura di un carattere da tastiera che non è stato ancora digitato dall utente. Quando ciò si verifica, il Nucleo provvede a porre il processo in stato di attesa ed a selezionare un altro processo da porre nello stato di esecuzione (vedi Figura 10.4). In altri casi ancora, un processo in stato User può essere interrotto per gestire una interruzione di I/O relativa ad un altro processo; inoltre, mentre Nucleo gestisce tale interruzione, si verifica un altra interruzione di I/O di tipo diverso relativa ad un terzo processo avente una elevata priorità e tale interruzione fa passare il terzo processo nello stato di eseguibile: in questo caso, il Nucleo può decidere di rimettere in esecuzione il terzo processo lasciando incompiuto il trattamento della prima interruzione (vedi Figura 10.5). Non tutti i Nuclei tra quelli più diffusi sono in grado di effettuare tale tipo di preemption ; quelli in grado di effettuarla sono chiamati preemptive kernels.
181 168 CAPITOLO 10. STRUTTURA INTERNA DEL NUCLEO Gli esempi illustrati non sono in alcun modo esaurienti. Sono molto numerose infatti le possibili combinazioni di interazioni che devono essere considerate nel progettare un Nucleo interrompibile e non è possibile elencarle tutte Microkernel e kernel monolitici Abbiamo elencato nel Paragrafo 10.2 pag. 159 alcune funzioni che debbono necessariamente essere realizzate tramite programmi del Nucleo ma non abbiamo specificato se altre importanti funzioni del Sistema Operativo debbano essere realizzate all interno del Nucleo, oppure tramite appositi processi. In effetti, esistono a tale riguardo due approcci diversi di implementazione che portano a classificare i Nuclei in due grandi categorie, rispettivamente: micro-nuclei (in inglese, microkernels) Nuclei monolitici (monolithic kernels). Il primo approccio privilegia l uso di un gran numero di processi di sistema per realizzare le varie funzioni possibili del Sistema Operativo, facendo uso, come supporto di base, di un Nucleo ridotto chiamato micro-nucleo che svolge pochi compiti essenziali quali la commutazione di processi ed una gestione semplificata delle interruzioni: nella maggior parte dei casi, il micro-nucleo riceve il segnale di interruzione ed invia un opportuno messaggio al processo interessato. Le principali funzioni del Sistema Operativo vengono svolte da processi specializzati: esiste, ad esempio, un processo per lo scheduling, un processo per la gestione del file system, un processo per il controllo dei diritti d accesso dei processi utente, un processo per la gestione delle aree di memoria RAM e così via. Seguendo tale approccio, quando un processo utente intende richiedere un qualche servizio da parte del Sistema Operativo, esso non esegue una chiamata di sistema al Nucleo ma invia un opportuno messaggio al processo di sistema gestore del servizio richiesto. In particolare, il processo richiedente esegue i seguenti passi: 1. esegue una chiamata di sistema di tipo send() (vedi Paragrafo 8.4 pag. 140) al processo gestore specificando nel messaggio i parametri associati alla richiesta; 2. si pone in attesa del messaggio di terminazione tramite una receive(). I processi di sistema hanno una struttura ciclica: usano una primitiva di sincronizzazione di tipo receive() per verificare se vi sono messaggi, ossia richieste di servizio; se vi è almeno un messaggio, passa ad eseguire la richiesta, invia un messaggio di terminazione (vedi Figura 10.6) al processo richiedente e torna ad eseguire una receive(). In pratica, viene realizzata una architettura software di tipo client/server dove la maggior parte delle funzioni del Sistema Operativo
182 10.9. MICROKERNEL E KERNEL MONOLITICI 169 receive()... (tratta la richiesta)... invia richiesta... send() receive()... send() ottieni risposta server client Figura 10.6: Interazione client/server tra processo utente e processo di sistema. sono svolte da processi di sistema di tipo server. Da quanto detto emerge chiaramente come il compito principale del micro-nucleo sia essenzialmente quello di un gestore di messaggi (implementazione delle primitive send() e receive()). I vantaggi derivanti da tale impostazione sono numerosi: il Sistema Operativo risulta modulare e facile da mantenere ed aggiornare; la struttura del Nucleo risulta estremamente semplificata; in particolare, scompare la necessità di rendere il Nucleo interrompibile poiché esso è in grado di trattare ogni richiesta in tempi ridotti; l uso di processi di sistema rende più agevole i controlli sulla validità delle richieste; se la richiesta è di tipo gravoso, il server può decidere di creare più processi figli per poterla trattare in parallelo. Purtroppo, esiste una grossa limitazione a tale approccio che è quello delle prestazioni: l introduzione di numerosi processi di sistema fa aumentare notevolmente il numero di commutazioni di processo ed inoltre la comunicazione tra processi realizzata tramite chiamate di sistema di tipo send() e receive() richiede frequenti interventi del Nucleo. I risultati sono quindi deludenti; oggi esistono pochi Sistemi Operativi commerciali basati sull uso di micro-nuclei: il più noto è il sistema Next derivato dal prototipo Mach realizzato presso la Carnegie Mellon University. Passiamo ora considerare un approccio diametralmente opposto a quello precedente: quello del Nucleo monolitico che include la maggior parte delle funzionalità del Sistema Operativo all interno del Nucleo. In base a tale approccio, il Nucleo include un elevato numero di programmi in grado di trattare decine o centinaia di chiamate di sistema diverse, che corrispondono ai diversi servizi offerti. Esistono, ad esempio, chiamate per la gestione del file system, chiamate per la sincronizzazione di processi, chiamate per la richiesta di aree di memoria e così
183 170 CAPITOLO 10. STRUTTURA INTERNA DEL NUCLEO via. Alcune chiamate di sistema possono richiedere tempi di esecuzione elevati, per cui è indispensabile progettare il Nucleo monolitico in modo interrompibile. Non esistono molti punti a favore del Nucleo monolitico se non quello cruciale dell efficienza. Molti programmi del Sistema Operativo devono essere considerati alla stregua di programmi in tempo reale: possono essere eseguiti anche migliaia di volte al secondo ed il modo in cui vengono realizzati condiziona le prestazioni dell intero sistema. Oggi, i più noti Sistemi Operativi sono realizzati facendo uso di Nuclei monolitici. In alcuni casi, sono utilizzati un numero limitato di processi di sistema per alleggerire in parte la struttura del Nucleo ma esso rimane comunque il componente più complesso del Sistema Operativo Implementazione dei processi in Linux Linux supporta i thread per ridurre il costo della commutazione di processi, ma utilizza un approccio alquanto diverso: nel Nucleo di Linux non si fa differenza tra processo e thread (nel codice sorgente si parla genericamente di task), la creazione di un processo (in senso generico) avviene mediante la chiamata di sistema (propria di Linux) clone(), che permette, a differenza della fork() (che è comune a tutti gli Unix), di specificare quali risorse vanno condivise tra il processo padre e il figlio, e quali invece vanno mantenute separate (creandone una copia privata per il processo figlio). Ciò è a totale discrezione del programmatore. In tal modo i processi figli possono condividere buona parte delle risorse possedute dal processo padre, tra cui lo spazio degli indirizzi e i descrittori di file aperti. Più precisamente, la chiamata di sistema clone() opera sui seguenti parametri: pid = clone(func, child_stack, flags, arg); che hanno i seguenti significati: func : l indirizzo della funzione che deve essere eseguita dal nuovo processo al suo avvio; child stack : l indirizzo dell area di memoria che dovrà essere utilizzata per lo stack del nuovo processo; flags : argomento intero che specifica quali risorse debbano essere condivise col processo padre (vedi oltre); args : puntatore ad un array di argomenti da passare alla funzione func. Il valore di ritorno è il PID del processo figlio, o -1 in caso di errore. Come si vede, il nuovo processo viene sostanzialmente specificato come un thread, ad ha sempre un proprio stack (dato che va ad eseguire autonomamente un altra porzione di codice rispetto al processo padre). L argomento flags può contenere uno o più dei seguenti specificatori, che precisano appunto le risorse condivise:
184 IMPLEMENTAZIONE DEI PROCESSI IN LINUX 171 CLONE VM condivisione dello spazio di memoria (sostanzialmente, la regione dati) 2 ; CLONE FS condivisione del contesto di file system (cartella radice, cartella attiva, maschera di creazione di nuovi file etc.); CLONE FILES condivisione dei file aperti; CLONE SIGHAND condivisione dei gestori di segnali (signal handlers). Se la clone() non specifica nessuna condivisione di risorse, si ricade sostanzialmente nel caso abituale della fork() (che è quindi vista come un caso particolare di clone()), mentre se si specifica di condividere tutto, si ha un thread vero e proprio. La creazione di un processo Linux è più rapida di quella di un processo classico, poiché è sufficiente copiare buona parte dei campi di tipo puntatore a risorse del processo padre in quelli del processo figlio. Per questi motivi, i processi di questo tipo sono anche chiamati processi leggeri (in inglese, lightweight processes) 3. La struttura del Nucleo di Linux non risulta pertanto appesantita da un ulteriore tipo di descrittore, ed è possibile realizzare applicazioni parallele basate su processi leggeri che non hanno nulla da invidiare, in termini di efficienza, a quelle basate sui thread. I seguenti due esempi mostrano l uso pratico della chiamata di sistema clone(), e la differenza, nella condivisione di dati tra processo padre e figlio, nell uso di fork() e di clone(). I due programmi qui appresso riportati fanno entrambi quanto segue: viene inizializzata a 10 una variabile globale (global var); viene creato un processo figlio; il primo programma usa clone(), il secondo invece fork(); il processo creato esegue un unica funzione, che incrementa la variabile globale per tre volte, ad intervalli di un secondo, e poi esce; il processo padre attende la terminazione del figlio, e poi esce anch esso; prima di uscire, entrambi i processi stampano il valore della variabile globale. Nel caso di utilizzazione di clone(), la variabile globale, incrementata nel processo figlio, risulta alla fine pari a 13 sia nel processo padre che nel processo figlio (dato che si tratta della stessa unica variabile, condivisa in entrambi i processi); nel caso invece di utilizzazione di fork(), la variabile globale risulta pari a 13 2 la regione testo (istruzioni) è comunque condivisa, dato che i due processi eseguono lo stesso programma; la regione stack è comunque separata, come già detto; resta quindi la regione dati, inizializzati e non. 3 Si noti che alcuni testi utilizzano il termine lightweight process come generico sinonimo di thread.
185 172 CAPITOLO 10. STRUTTURA INTERNA DEL NUCLEO nel processo figlio, mentre è rimasta invariata (cioè pari a 10) nel processo padre (infatti la variabile è stata copiata, col suo valore 10, al momento della fork(), e da quel momento in poi si tratta di due variabili distinte che vivono in spazi separati). Il primo programma usa clone() per condividere lo spazio dei dati (CLONE VM ) e i file aperti (CLONE FILES ciò è necessario perché il figlio possa continuare a scrivere messaggi allo stdout, senza doverlo riaprire esplicitamente). Pseudo-codice: Codice reale: /* creazione di un processo leggero tramite clone(2) */ #include <unistd.h> #include <wait.h> /* definizioni di WCLONE e WALL */ #include <sched.h> /* definizioni di CLONE_VM e CLONE_FILES */ #include <sys/types.h> #define STACKSIZ 1024 int global_var = 0; int child_func(void *unused) { int i; } printf("processo clone inizia - global_var = %d\n", global_var); for (i = 0; i < 3; ++i) { ++global_var; printf("processo clone lavora\n"); sleep(1); } printf("processo clone esce - global_var = %d\n", global_var); return 0; int main(void) { pid_t pid; int stackclone[stacksiz]; global_var = 10; printf("processo padre inizia - global_var = %d\n", global_var); /* crea processo clone */
186 IMPLEMENTAZIONE DEI PROCESSI IN LINUX 173 pid = clone(child_func, &stackclone[stacksiz-1], CLONE_VM CLONE_FILES, NULL); if (pid < 0) { printf("bad clone()\n"); exit(1); } printf("processo padre aspetta terminazione clone\n"); waitpid(pid, NULL, WCLONE); } printf("clone terminato\n"); printf("processo padre esce - global_var = %d\n", global_var); exit(0); Il secondo programma usa fork(), quindi tutte le risorse sono copiate dal processo padre al processo figlio e vivono poi ciascuna di vita propria. Il codice eseguito dal processo figlio è strutturato in modo da replicare esattamente quanto avviene nel caso della clone() nell esempio precedente (si va cioè ad eseguire la stessa funzione child func()). Pseudo-codice: Codice reale: /* creazione di un processo figlio tramite fork(2) */ #include <unistd.h> #include <wait.h> /* include definizioni di WCLONE e WALL */ #include <sys/types.h> int global_var = 0; int child_func(void *unused) { int i; printf("processo figlio inizia - global_var = %d\n", global_var); for (i = 0; i < 3; ++i) { ++global_var; printf("processo figlio lavora\n"); sleep(1); } printf("processo figlio esce - global_var = %d\n", global_var); return 0;
187 174 CAPITOLO 10. STRUTTURA INTERNA DEL NUCLEO } int main(void) { pid_t pid; global_var = 10; printf("processo padre inizia - global_var = %d\n", global_var); /* crea processo figlio */ if((pid = fork()) == 0) { /* codice del processo figlio */ int retval; retval = child_func(null); exit(retval); } if (pid < 0) { printf("bad fork()\n"); exit(1); } printf("processo padre aspetta terminazione figlio\n"); waitpid(pid, NULL, 0); printf("figlio terminato\n"); printf("processo padre esce - global_var = %d\n", global_var); exit(0); L output prodotto dal primo programma è il seguente: processo padre inizia - global_var = 10 processo padre aspetta terminazione clone processo clone inizia - global_var = 10 processo clone lavora processo clone lavora processo clone lavora processo clone esce - global_var = 13 clone terminato processo padre esce - global_var = 13 Mentre quello prodotto dal secondo è il seguente: processo padre inizia - global_var = 10 processo figlio inizia - global_var = 10 processo figlio lavora
188 SCELTA DEL LINGUAGGIO DI PROGRAMMAZIONE 175 processo figlio lavora processo figlio lavora processo figlio esce - global_var = 13 processo padre inizia - global_var = 10 processo padre aspetta terminazione figlio figlio terminato processo padre esce - global_var = 10 Il confronto tra i due output evidenzia bene il comportamento già descritto poc anzi Scelta del linguaggio di programmazione La scelta dei linguaggi di programmazione utilizzati per realizzare il Sistema Operativo ha una sua importanza in quanto condiziona l efficienza dell intero sistema. Alcuni linguaggi di programmazione, infatti, includono dei meccanismi per la chiamata di procedure e per il controllo automatico degli indirizzamenti che rendono il codice macchina generato poco efficiente. Come già sottolineato in precedenza, la parte critica del Sistema Operativo è costituita dal Nucleo, mentre per le altre parti la scelta del linguaggio di programmazione non è determinante ai fini dell efficienza del sistema. Per quanto riguarda la programmazione del Nucleo, si assiste ad una evoluzione molto lenta verso linguaggi ad alto livello. Fino agli anni 70, tutti i Nuclei erano codificati in linguaggio assemblativo: ad esempio, i Nuclei dei Sistemi Operativi OS/360 e successori usati per i mainframe IBM e quello del Sistema Operativo VAX/VMS della Digital Equipment sono stati codificati interamente in linguaggio assemblativo. È opportuno precisare che una piccola parte del Nucleo, quella che riguarda le interazioni con l hardware, deve necessariamente essere codificata nel linguaggio assemblativo per potere utilizzare le apposite istruzioni che operano sui registri della macchina. Quando, ad esempio, il Nucleo effettua una commutazione di processi, deve salvare il contenuto dei registri utilizzati dal processo sospeso in una apposita area di memoria (tipicamente, nel descrittore di processo e nello stack di sistema del processo). Per fare ciò, serve un linguaggio di programmazione in grado di indirizzare specifici registri hardware della CPU, ossia serve il linguaggio assemblativo. Si può stimare tra il 5 e il 10% la percentuale di codice del Nucleo basata sull architettura del calcolatore. La rimanente parte del Nucleo tuttavia può essere scritta in un linguaggio ad alto livello, purché efficiente. Unix è stato il primo Sistema Operativo che ha fatto un uso intensivo di un linguaggio ad alto livello per codificare la maggior parte dei programmi del Nucleo; in effetti, i progettisti di Unix hanno messo a punto un apposito linguaggio, il linguaggio C, per svolgere tale compito.
189 176 CAPITOLO 10. STRUTTURA INTERNA DEL NUCLEO Dopo alcuni decenni, la situazione non è molto cambiata: molti Nuclei quali quelli realizzati per i nuovi sistemi Unix continuano ad essere realizzati nel linguaggio C con alcune parti in linguaggio assemblativo. Una innovazione significativa rispetto ai sistemi precedenti consiste nel separare in modo netto le parti del Nucleo indipendenti dall architettura da quelle dipendenti da essa. In tale modo risulta molto facilitata la portabilità del Nucleo da una piattaforma hardware ad un altra. Perfino un concetto innovativo come quello della programmazione ad oggetti che ha avuto un notevole impatto nella realizzazione di programmi applicativi ha incontrato un limitato successo tra i progettisti di Nuclei, anche se alcuni Sistemi Operativi quali Windows NT fanno uso del linguaggio C++ per codificare alcune funzioni meno critiche del Sistema Operativo. Il motivo è sempre lo stesso, ossia l inefficienza dei compilatori per linguaggi orientati agli oggetti quale il C++ rispetto ai compilatori per linguaggi procedurali tradizionali. L unica piccola innovazione dal punto di vista delle tecniche di programmazione è consistita nel fare uso di un limitato numero di oggetti realizzati in C tramite strutture che contengono sia dati che puntatori a funzioni. Un discorso a parte vale per quelle parti del Sistema Operativo realizzate tramite processi: poiché esse sono meno critiche dal punto di vista delle prestazioni, l uso di linguaggi orientati agli oggetti può essere giustificato.
190 Capitolo 11 Gestione della Memoria Primaria 11.1 Introduzione Un compito fondamentale del Nucleo è quello di gestire le aree di memoria presenti nel calcolatore. Queste sono spesso divise in tre grandi categorie: memoria primaria (o core): spazio nella RAM volatile, veloce, indirizzabile direttamente byte a byte; memoria secondaria: spazio su dischi fissi non volatile, lento, indirizzabile a blocchi; memoria terziaria: spazio su supporti amovibili, quali floppy disk, CDROM, nastri magnetici e simili molto lenta rispetto alla memoria secondaria, ma con volumi sostituibili e trasportabili. Dal punto di vista che qui interessa, non vi sono grandi differenze tra la seconda e la terza categoria, per cui si distinguerà soltanto tra memoria primaria, oggetto del presente capitolo, e memoria secondaria, che verrà trattata nel capitolo successivo. Le tecniche di gestione della memoria primaria sono alquanto diverse da quelle di gestione della memoria secondaria, e ciò per almeno due ovvie ragioni: i meccanismi fisici alla base dei due tipi di memoria sono differenti, perché diverse sono le loro caratteristiche (come già esposto nel Paragrafo 5.1 pag. 65); la gestione della RAM è più critica di quella delle aree di disco. Inoltre, le richieste di RAM da parte di funzioni del Nucleo sono considerate prioritarie rispetto alle richieste di RAM effettuate da un processo in User mode. 177
191 178 CAPITOLO 11. GESTIONE DELLA MEMORIA PRIMARIA 11.2 Indirizzamento della RAM È utile sottolieare che durante il tempo di vita di un processo la memoria primaria, come qualunque altra risorsa, deve essere allocata al processo e deallocata da questo. Una premessa è d obbligo prima di iniziare la discussione: gli accessi alla RAM da parte di un processo sono numerosissimi, dato che ogni nuova istruzione da eseguire va letta dalla RAM e che, in alcuni casi, l esecuzione dell istruzione richiede ulteriori accessi alla RAM per leggere o scrivere operandi. Per questo motivo, non è possibile pensare a schemi di gestione interamente software ma è necessario ricorrere ad appositi circuiti hardware che consentano determinati tipi di gestione senza aumentare in modo significativo il tempo d accesso alla RAM che è oggi dell ordine di poche decine di nanosecondi. Un altra importante considerazione è che la gestione multitasking, più volte citata in precedenza, richiede l uso di una tecnica di suddivisione della memoria RAM. Infatti, tale tipo di gestione presuppone che, ad ogni istante, vi siano più programmi indipendenti caricati in memoria. Poiché i programmi hanno tempi di esecuzione diversi e lunghezze diverse, nasce il problema di definire tecniche di indirizzamento della RAM che consentano da un lato di caricare agevolmente nuovi programmi in memoria e, dall altro, di utilizzare efficientemente la memoria a disposizione Paginazione La paginazione è una tecnica di indirizzamento della RAM che fa uso di sofisticati circuiti di traduzione degli indirizzi. Ogni indirizzo logico 1 è tradotto dal circuito di paginazione (MMU si veda il Paragrafo pag. 43) in un indirizzo fisico. Come si vedrà in seguito, indirizzi logici contigui possono essere tradotti in indirizzi fisici non contigui. La memoria RAM disponibile è suddivisa in N blocchi (in inglese, page frame) di lunghezza fissa, tipicamente 4096 byte (sono anche usati talvolta blocchi di dimensioni maggiori). L informazione contenuta in uno di tali blocchi prende il nome di pagina. La maggior parte delle attuali CPU utilizzano indirizzi logici da 32 bit. In tale caso, ogni indirizzo logico è decomposto in un numero di pagina (i 20 bit più significativi) e un indirizzo relativo nella pagina (i rimanenti 12 bit). Infatti ogni pagina comprende come detto 4096 = 2 12 byte, pertanto i 12 bit meno significativi devono essere usati per indirizzare le singole celle di una pagina; i rimanenti 20 bit più significativi numerano ovviamente tutte le possibili pagine. Per ridurre la dimensione delle tabelle, i 20 bit che esprimono il numero di pagina sono decomposti ulteriormente in due gruppi di 10 bit: il primo gruppo indica la posizione nella tabella principale chiamata Page Directory, mentre il secondo gruppo indica la posizione in una delle tabelle secondarie chiamate Page Table. 1 Per la definizione di indirizzi logici e indirizzi fisici si rimanda al Paragrafo 9.2 pag. 148.
192 11.3. PAGINAZIONE 179 indirizzo logico Page Directory indirizzo fisico Page Table page frame Figura 11.1: Trasformazione di un indirizzo logico in un indirizzo fisico. Si tenga presente che per ciascun processo attivo esistono una (e una sola) Page Directory, e più Page Table (una per ogni voce della Page Directory). La trasformazione dell indirizzo logico in un indirizzo fisico è effettuata dal circuito di paginazione nel seguente modo (vedi Figura 11.1): 1. ottieni da un apposito registro della CPU l indirizzo della Page Directory del processo in esecuzione (nei microprocessori Intel 80x86 tale registro, memorizzato ovviamente nel descrittore di processo, è chiamato cr3); 2. usa i 10 bit più significativi dell indirizzo logico per selezionare la voce opportuna nella Page Directory; 3. leggi il contenuto della voce selezionata al passo precedente per indirizzare la Page Table contenente la pagina da indirizzare; 4. usa i 10 bit intermedi dell indirizzo logico per selezionare la voce opportuna della Page Table; 5. somma i 12 bit meno significativi dell indirizzo logico all indirizzo fisico della page frame di RAM ottenuta al passo precedente, ottenendo così l indirizzo fisico desiderato. Tutte le Page Table e le Page Directory contengono fino a 1024 voci (che infatti sono identificate da un indice a 10 bit); ogni voce di una Page Table occupa 32 bit, di cui: 20 bit contengono l indirizzo fisico di una page frame (poiché le page frame sono lunghe 2 12 = 4096 byte, non è necessario specificare i 12 bit meno significativi dell indirizzo fisico);
193 180 CAPITOLO 11. GESTIONE DELLA MEMORIA PRIMARIA 12 bit contengono diversi flag di controllo, che caratterizzano lo stato della page frame ed i suoi diritti d accesso. Tipicamente, sono usati i seguenti flag: flag Present: vale 0 se la pagina non è attualmente presente in RAM; flag User/Supervisor: vale 0 se la pagina può essere indirizzata solo quando la CPU è in System mode; flag Execute: vale 1 se la CPU è autorizzata a prelevare istruzioni dalla pagina; flag Read: vale 1 se la CPU è autorizzata a leggere dati dalla pagina; flag Write: vale 1 se la CPU è autorizzata a scrivere dati nella pagina. Se il flag Present vale 0, oppure se i diritti d accesso non consentono l indirizzamento richiesto dalla CPU, il circuito di paginazione genera una eccezione di tipo Page Fault che attiva un apposito programma del Nucleo, come già descritto nel Capitolo 9. Nell accedere ad una voce di Page Directory o di Page Table, il circuito di paginazione verifica inoltre se la voce è nulla (tutti i 32 bit hanno il valore 0). È questo il caso di un indirizzo illegale, cioè non appartenente allo spazio degli indirizzi del processo; anche in questo caso, il circuito di paginazione genera una eccezione di tipo Page Fault. Altri Sistemi Operativi utilizzano, per gestire questo caso, un flag specifico, genericamente denominato flag Valid. Prima di generare una eccezione, il circuito di paginazione salva in un apposito registro (registro cr4 nel caso dei microprocessori Intel 80x86) l indirizzo logico che ha causato l eccezione. Potrebbe sembrare a prima vista che l uso della paginazione degradi sensibilmente le prestazioni del calcolatore: ogni accesso da parte della CPU ad un dato oppure ad una istruzione contenuti nella RAM richiede tre indirizzamenti consecutivi alla RAM anziché uno solo! In pratica, sono usati vari accorgimenti per ridurre il tempo di consultazione delle tabelle di traduzione. Oggi tutti i chip CPU includono memorie cache veloci di dimensioni ridotte in cui vengono conservati le coppie di valori (indirizzo logico, voce di Page Table o Page Directory) degli ultimi indirizzamenti effettuati. Grazie a tali cache, non è necessario accedere alla RAM per leggere una voce di Page Directory o di Page Table in quanto tale informazione è già presente nella cache, per cui il tempo medio di accesso alla RAM non si scosta significativamente da quello richiesto per effettuare un singolo accesso. È importante sottolineare come la paginazione consenta di proteggere efficacemente gli spazi degli indirizzi dei vari processi: in effetti, un processo non può accedere a page frame diverse da quelle assegnategli dal Sistema Operativo, se non modificando le proprie tabelle di paginazione. Poiché tali tabelle sono contenute nelle page frame riservate al Nucleo (bit di modo User/System impostato a 0), esse non possono però essere modificate da un processo in User mode e la protezione è quindi assicurata.
194 11.4. L ALGORITMO BUDDY SYSTEM Indirizzi logici riservati ai programmi del Nucleo Come già detto, ogni processo possiede una propria Page Directory ed un gruppo di Page Table. Grazie a tale approccio, gli stessi indirizzi logici possono essere usati da più processi senza che ciò causi alcuna ambiguità poiché la traduzione da indirizzo logico ad indirizzo fisico avviene utilizzando le tabelle di paginazione del processo che richiede l indirizzamento. Il Nucleo invece non è un processo e sarebbe alquanto macchinoso assegnargli apposite tabelle di paginazione. La soluzione preferita consiste invece nel riservare un parte degli indirizzi logici per i programmi del Nucleo ed includere nelle tabelle di paginazione di ogni processo un mapping tra gli indirizzi logici riservati al Nucleo e gli indirizzi fisici della RAM installata. Vediamo come ciò viene realizzato su un esempio concreto. In Linux, l insieme corrispondente a tutti i possibili indirizzi logici (di dimensione pari a 2 32 byte, ossia 4 Gbyte), viene suddiviso in due intervalli: un intervallo da 0 (incluso) a 3 Gbyte (escluso) ( ) riservato ai processi che eseguono in User mode; un intervallo da 3 Gbyte (incluso) a 4 Gbyte (escluso) ( ) riservato ai processi che eseguono in System mode, ossia ai programmi del Nucleo. Poiché ogni Page Directory include 1024 voci, ciò significa che le prime 768 voci sono riservate ai processi che eseguono in User mode, mentre le ultime 256 sono riservate ai programmi del Nucleo. Il numero di indirizzi logici a disposizione del Nucleo è più che sufficiente a mappare tutta la RAM installata, per cui buona parte delle ultime 256 voci della Page Directory sono nulle per indicare che i corrsipondenti indirizzi logici non sono validi in quanto non vi è RAM associata ad essi. È importante osservare che il mapping tra indirizzi logici usati dal Nucleo e indirizzi fisici è una semplice trasformazione lineare: indirizzo f isico = indirizzo logico c Ovviamente tale mapping vale solo per i programi del Nucleo e non per i processi in User mode. Come vedremo nel prossimo paragrafo, il Nucleo utilizza un algoritmo di gestione della memoria che assegna aree contigue di RAM, per cui non è necessario modificare il contenuto di alcuna delle ultime 256 voci della Page Directory in seguito ad una assegnazione o rilascio di memoria L algoritmo Buddy System Vediamo ora come il Nucleo gestisce la RAM facendo uso dei circuiti di paginazione. Come è ovvio, l unità minima di allocazione è la page frame, ossia 4096 byte.
195 182 CAPITOLO 11. GESTIONE DELLA MEMORIA PRIMARIA In primo luogo, il Nucleo deve riservare, durante la fase di inizializzazione, un sufficiente numero di page frame per contenere il codice e le strutture di dati statiche. Le page frame rimanenti prendono il nome di memoria dinamica e possono essere allocate e rilasciate dinamicamente per soddisfare le esigenze delle varie funzioni del Nucleo. In effetti, oltre a poche strutture statiche di dati, il Nucleo fa uso di numerose strutture dinamiche di dati che vengono create e successivamente eliminate. Diamo alcuni esempi di tali strutture: descrittore di processo: quando viene creato un nuovo processo, è necessario assegnare una nuova area di memoria per contenere il descrittore di processo; quando il processo viene eliminato, tale area di memoria può essere riutilizzata; inode: quando viene aperto un file per la prima volta, il file system copia da disco il descrittore del file in una struttura chiamata inode; l area di memoria usata da un inode può essere rilasciata quando non vi sono più processi che hanno aperto quel file; messaggi: le chiamate di sistema di tipo send() (si veda Paragrafo 8.4 pag. 140) richiedono aree di memoria per contenere i messaggi pendenti destinati ai vari processi; tali aree di memoria possono essere rilasciate quando i processi destinatari hanno consumato i messaggi. Si riconosce facilmente che tali strutture di dati corrispondono in definitiva a vari tipi di descrittori di risorsa, come già evidenziato nei Capitoli 7 e 10. Tipicamente, il Nucleo centralizza la gestione della RAM dinamica tramite due funzioni del tipo get mem() e free mem() che possono essere usate dalle varie funzioni del Nucleo per ottenere o rilasciare page frame. Per evitare problemi di frammentazione della memoria libera, è molto usato l algoritmo Buddy System che tenta di accorpare blocchi contigui di page frame libere creando così un singolo blocco libero di dimensioni maggiori. Vediamo in dettaglio come opera tale algoritmo. L algoritmo gestisce blocchi di page frame di dimensioni diverse, ad esempio blocchi da 1, 2, 4, 8, 16, 32, 64,..., 512 page frame, per cui la funzione get mem() prevede un parametro che specifica la dimensione del blocco richiesto. Per ogni tipo di blocco, il Buddy System gestisce un apposito descrittore che specifica gli indirizzi dei blocchi liberi e quelli dei blocchi occupati. Se consideriamo ad esempio i blocchi da 64 page frame, l intera RAM verrà vista come un vettore di bit dove ogni bit denota 64 4 Kbyte di RAM; il bit vale 0 se il blocco è libero, 1 se è occupato. Nel soddisfare una richiesta per un blocco di dimensione k, l algoritmo Buddy System consulta dapprima il descrittore per blocchi di dimensione k: se vi è almeno un bit uguale a 0, pone tale bit a 1 e ritorna l indirizzo iniziale del blocco appena individuato. Se non vi sono bit uguali a 0, consulta il descrittore per blocchi di dimensione 2 k e così via fino a trovare il primo descrittore che include un blocco libero.
196 11.5. ESTENSIONI DEL BUDDY SYSTEM 183 Se non vi è alcun descrittore contenente un bit uguale a 0, la richiesta di memoria non può essere soddisfatta e la funzione get mem() ritorna un codice di errore. Altrimenti, viene selezionato il primo descrittore avente un bit uguale a 0 ed avviene la suddivisione del blocco in un blocco occupato ed uno o più blocchi liberi. Facciamo un esempio specifico per chiarire meglio come ciò si verifichi. Supponiamo che la richiesta era per 64 page frame e che il blocco libero più piccolo occupa 256 page frame. In questo caso, il blocco libero da 256 page frame viene dichiarato occupato e viene suddiviso in un blocco occupato da 64 page frame, un blocco libero da 64 page frame ed un blocco libero da 128 page frame. I relativi descrittori di blocchi da 64, 128 e 256 page frame vengono opportunamente modificati. Per rilasciare un blocco, l algoritmo Buddy System verifica se il blocco gemello (in inglese buddy ) di quello che si è appena liberato è anch esso libero. Nel caso affermativo, fonde i due blocchi liberi buddy in un unico blocco libero di dimensione doppia rispetto a quella del blocco che si è reso libero. Anche in questo caso, facciamo un esempio specifico per chiarire meglio cosa si intende per coppia di blocchi buddy. Consideriamo i blocchi i blocchi da 4 page frame, ossia da 16 KB. Gli indirizzi fisici associati a tali blocchi sono multipli di 16 KB: 0, 16, 32, 48, 64, 80,... I blocchi buddy di tale gruppo sono 0 con 16, 32 con 48, 64 con 80 e così via. In effetti, fondendo il blocco 64 con quello 80 si ottiene un blocco da 32 K avente indirizzo iniziale multiplo di (32 4) K. Viceversa, due blocchi contigui da 16 K quali il blocco 48 e quello 64 non sono buddy perché l indirizzo iniziale del blocco ottenuto fondendo tale coppia di blochi non è un multiplo di (32 4) K Estensioni del Buddy System Un limite dell algoritmo Buddy System appena illustrato è che l unità minima di allocazione di memoria è un page frame, ossia 4 Kbyte. Se le richieste di memoria delle funzioni del Nucleo sono limitate (poche decine o centinaia di byte), assegnare un intera page frame quando basta una piccola frazione di essa costituisce uno spreco di memoria. In questi casi è possibile realizzare un sistema di cache software che include blocchi liberi da 32, 64, 128, 256, 512, 1024, 2048 byte e soddisfare le richieste delle funzioni del Nucleo cercando il blocco libero avente dimensione maggiore o uguale a quello del numero di byte richiesti: se, ad esempio, la funzione richiede 100 byte otterrà un blocco da 128 byte. In pratica si riapplica l algoritmo Buddy System con dimensioni dei blocchi che sono potenze di 2. Quando non vi sono più blocchi liberi di una determinata dimensione, l algoritmo invoca la funzione get mem() per ottenere una nuova page frame libera e la suddivide in blocchi della dimensione richiesta. Viceversa se una page frame usata per contenere blocchi di una dimensione prefissata, ad esempio 128 byte, si libera, essa viene mantenuta nella cache dall algoritmo e potrà quindi essere riutilizzata quando vi sarà richiesta per nuovi blocchi senza dovere invocare il Buddy System.
197 184 CAPITOLO 11. GESTIONE DELLA MEMORIA PRIMARIA 11.6 Demand Paging Il Nucleo è un componente privilegiato del Sistema Operativo, per cui le funzioni del Nucleo ottengono direttamente tutta la memoria dinamica richiesta. Del tutto diversa è l assegnazione di memoria ai processi. In questo caso vale la strategia opposta: l assegnazione di RAM ad un processo viene ritardata il più a lungo possibile, ossia fino a quando il processo esegue una istruzione che necessita la presenza di un area di memoria non ancora allocata. Tale strategia prende il nome di demand paging (paginazione a richiesta) e presuppone l esistenza nel Nucleo di appositi descrittori che identificano le regioni di memoria possedute dal processo (vedi Capitolo 9). Vediamo in che modo il Nucleo è in grado di realizzare il demand paging. Quando un processo inizia ad eseguire istruzioni, esso non ha ancora ottenuto alcuna page frame di RAM da parte del Nucleo. Ha invece ottenuto diverse regioni di memoria, per cui prima di iniziare l esecuzione del processo, il Nucleo provvede ad inizializzare opportunamente i descrittori di memoria del processo. Alcune regioni di memoria mappano (vedi Paragrafo 9.5 pag. 156) porzioni del file eseguibile associato al processo: la regione codice mappa una porzione del file eseguibile, la regione dati inizializzati mappa un altra porzione del file eseguibile e così via. L indicazione della porzione di file mappata è registrata nel descrittore di regione. Altre regioni di memoria, ad esempio la regione stack, non mappano alcuna regione di file ma costituiscono un mapping anonimo, per cui quando il processo tenterà di accedere ad un indirizzo incluso in una di tali regioni sarà sufficiente assegnare ad esso una page frame di RAM dinamica senza doverla inizializzare con dati o codice letti dal file eseguibile. Continuiamo con la descrizione di cosa avviene quando un processo ottiene per la prima volta l uso della CPU. Nell eseguire la prima istruzione avente un determinato indirizzo logico, il circuito di paginazione indirizzerà la voce opportuna della Page Directory e la troverà posta a 0. In conseguenza, genererà una eccezione di tipo errore di pagina ed un apposito programma del Nucleo chiamato gestore degli errori di pagina prenderà il controllo (vedi Paragrafo 10.3 pag. 160). Il gestore, in generale, prende in considerazione tre casi possibili: l indirizzo logico che ha causato l eccezione non appartiene allo spazio degli indirizzi del processo: in questo caso, il Nucleo elimina il processo che ha causato l eccezione; l indirizzo logico che ha causato l eccezione appartiene allo spazio degli indirizzi del processo ma il processo non è autorizzato ad accedere alla page frame nel modo richiesto (tentativo di scrittura su pagine di sola lettura, ecc.): anche in questo caso, il Nucleo elimina il processo che ha causato l eccezione; l indirizzo logico che ha causato l eccezione appartiene allo spazio degli indirizzi del processo ed il processo è autorizzato ad accedere alla page
198 11.7. SWAPPING 185 frame nel modo richiesto: in questo caso va attivato il demand paging che viene realizzato in modo diverso a seconda che il mapping della regione coinvolta sia di tipo anonimo oppure sia associato ad una porzione di file. mapping anonimo: il gestore invoca get mem() per ottenere una page frame libera ed inserisce l indirizzo fisico di tale page frame nei 20 bit più significativi della voce della Page Directory o Page Table. I flag della voce sono impostati in base ai valori associati al descrittore della regione di memoria; a questo punto l eccezione è stata trattata e l esecuzione del processo può riprendere; mapping di una porzione di file: oltre ad eseguire le azioni svolte per il mapping anonimo, il gestore provvede ad iniziare una lettura di 4 Kbyte della porzione di file mappata e pone il processo nello stato di attesa della fine lettura dati da disco ; in questo caso, l esecuzione del processo potrà riprendere solo dopo la fine della lettura dei dati richiesti da disco Swapping Un problema perenne dell informatica è quello della limitata quantità di RAM disponibile. In effetti, esiste una sorta di effetto autostrada per cui, non appena si introducono elaboratori con una maggiore quantità di RAM, i progettisti software sviluppano applicazioni che saturano tale tipo di risorsa. L introduzione di sistemi multitasking rende il problema ancora più critico poiché la RAM dell elaboratore deve essere condivisa tra più programmi indipendenti. Non esiste, viceversa, limitazione per quanto riguarda lo spazio degli indirizzi dei vari processi, ossia l insieme di indirizzi logici che il processo può utilizzare. In effetti, tutti i moderni Sistemi Operativi sfruttano appieno gli indirizzi da 32 bit garantendo ad ogni processo uno spazio degli indirizzi di 4 Gbyte. Nel caso di architetture più recenti che fanno uso di registri da 64 bit, la dimensione dello spazio degli indirizzi potrebbe raggiungere il valore astronomico di 2 elevato alla 64, ossia oltre 16 miliardi di miliardi. Per alleviare il problema causato dalla insufficiente quantità di RAM disponibile, è stata introdotta una interessante tecnica di gestione della memoria, molto utilizzata negli attuali sistemi multiprogrammati, che prende il nome di swapping 2. Tale tecnica può essere considerata come una estensione della paginazione descritta in precedenza e coinvolge la gestione simultanea di una area di disco e di una area di memoria. Sia il disco che la memoria sono gestiti tramite blocchi di lunghezza fissa, tipicamente 4 Kbyte. Ogni blocco contiene quindi una pagina di dati. Al solito, un esempio aiuta a chiarire il ruolo dell area di swap. Supponiamo di avere una RAM da 64 Mbyte e di avere definito un area di swap da 128 Mbyte. 2 Dall inglese to swap = barattare, scambiare.
199 186 CAPITOLO 11. GESTIONE DELLA MEMORIA PRIMARIA In questo caso, il sistema di swapping dà l illusione al programmatore di disporre di una RAM di = 192 Mbyte. Ovviamente, si tratta di una illusione, nel senso che la RAM aggiuntiva viene simulata tramite un area di disco, per cui il tempo di accesso a pagine contenute nell area di swap è molto elevato. Per quanto riguarda le tabelle di paginazione, una pagina appartenente al processo ma swappata su disco viene identificata impostando i 32 bit della voce di Page Table nel seguente modo: il flag di Present viene posto a 0; i rimanenti bit sono usati per specificare l indirizzo nell area di swap dove è registrata la pagina. A questo punto, è necessario parlare dei criteri con cui una pagina viene trasferita dalla RAM nell area di swap (attività di swap out) o, viceversa, viene trasferita dall area di swap nella RAM (attività di swap in). Di norma, il Nucleo evita di ricorrere all area di swap fino a quando ciò risulta possibile. Se però un processo richiede una nuova pagina e non vi è più RAM dinamica disponibile, il Nucleo effettua uno swap out su una delle pagine presenti in RAM, liberando così una page frame ed usa tale page frame per soddisfare la richiesta del processo. Come detto in precedenza, il Nucleo modifica la voce della Page Table della pagina swappata su disco impostando il flag Present a 0. Quando il processo tenta di accedere ad una pagina swappata su disco, il gestore delle eccezioni di page fault riconosce, dal fatto che la voce della Page Table non è nulla ma che il flag Present è uguale a 0, che la pagina richiesta è stata swappata su disco. In questo caso, ottiene una page frame libera ed avvia una procedura di swap in per leggere la pagina da disco. Quando la pagina contenente l indirizzo logico richiesto non è presente in RAM e non esiste una page frame libera, il Nucleo fa uso di un apposito algoritmo di sostituzione per selezionare una pagina tra quelle presenti in memoria da scrivere su disco e legge nella page frame appena liberatasi la pagina richiesta. Durante il doppio trasferimento di pagine, il processo è posto nello stato di bloccato. La Figura 11.2 illustra tale tecnica su un esempio semplificato: il processo in esecuzione richiede di indirizzare la pagina J attualmente non presente in RAM. Poiché non esistono page frame libere, l algoritmo di sostituzione seleziona la pagina Q e la trasferisce su disco; dopo di che può essere caricata nella page frame resasi libera la pagina richiesta J ed il processo riprende l esecuzione. Gli algoritmi di sostituzione utilizzati sono basati su una proprietà statistica dei programmi chiamata località: è stato osservato analizzando le sequenze di indirizzi logici emessi da vari processi durante un qualsiasi intervallo di tempo che tali indirizzi non sono uniformemente distribuiti nello spazio degli indirizzi ma che tendono invece a concentrarsi in poche pagine. La spiegazione intuitiva di tale fenomeno è che la maggior parte dei programmi sono strutturati in sottoprogrammi e vettori o matrici di dati per cui, quando un processo esegue un sottoprogramma, esso concentra gli indirizzamenti nelle pagine contenenti il sottoprogramma e le strutture di dati associate.
200 11.7. SWAPPING 187 pagine del processo contenute nell area di swap B C D J K L M N O T U V W X Y Z 2) la pagina J viene trasferita dall area di swap pagine del processo attualmente nella RAM A E F G H P Q R S 1) la pagina Q viene trasferita dalla RAM nell area di swap Figura 11.2: Swapping. Per questo motivo, è conveniente (dal punto di vista statistico) scegliere come pagina da trasferire su disco quella che non è stata indirizzata da più tempo, mentre si può supporre che quelle indirizzate più recentemente saranno invece nuovamente richieste dal processo. Sfruttando tale proprietà e introducendo hardware aggiuntivo per eseguire in modo efficiente l algoritmo di sostituzione, è possibile fare uso di aree di swapping ed ottenere tempi medi d indirizzamento di poco superiori al tempo d indirizzamento della memoria fisica. In conclusione, lo swapping, realizzabile tramite estensioni hardware e software del sistema, consente di disporre di uno spazio degli indirizzi maggiore dello spazio di memoria fisica assegnato al processo. I vantaggi per il programmatore sono che egli non deve più curare tramite apposite frasi di ingresso/uscita i trasferimenti di programmi tra memoria e disco ma può scrivere il programma come se disponesse di tutta la memoria necessaria. L efficienza di un programma eseguito con l area di swapping attivata dipende sia dalla sua località, sia dal rapporto tra il numero di pagine e il numero di page frame ottenute.
201 Capitolo 12 Gestione della Memoria Secondaria 12.1 Introduzione Si è già detto nel capitolo precedente che il Nucleo ha fra i suoi compiti principali quello di gestire convenientemente sia la memoria principale, sia quella secondaria e terziaria; e che le tecniche di gestione della memoria primaria sono diverse da quelle di gestione della memoria secondaria e terziaria, che invece non differiscono sensibilmente tra di loro. In effetti l unico aspetto degno di nota che differenzia la memoria terziaria dalla secondaria è la necessità, da parte del nucleo, di tener conto delle sostituzioni di volumi fisici nei drive. Ciò richiede che al momento di una sostituzione del tipo detto, ogni blocco di memoria primaria che funga da cache di blocchi del volume precedente debbano essere invalidati, dato che non sono più rappresentativi dei blocchi del volume, che ora è cambiato. Ciò detto, in questo capitolo non si farà più alcuna distinzione tra memoria secondaria e terziaria; ci si riferirà in generale alla prima di esse, con la tacita convenzione che quanto detto valga anche per la seconda Gestione dello spazio su disco Anche lo spazio su disco è una risorsa che deve essere gestita opportunamente. Il file system è il principale interessato nell ottenere blocchi di disco liberi per potere ampliare la dimensione di un file. In modo analogo, il file system rilascia blocchi di disco quando cancella un file o quando ne riduce la dimensione. A differenza della RAM, la gestione dello spazio su disco non richiede appositi circuiti hardware quali l unità di paginazione ma può essere interamente 188
202 12.2. GESTIONE DELLO SPAZIO SU DISCO 189 realizzata in software da appositi programmi del Nucleo. In effetti, i tempi di indirizzamento del disco sono molto elevati per cui il peso relativo dei programmi per la gestione dello spazio su disco risulta trascurabile. Un disco fisico è spesso diviso in più partizioni o dischi logici, ciascuno dei quali comprende un gruppo di cilindri contigui. Questa divisione è nota solo al device driver: il resto del Nucleo vede ogni disco logico come un disco autonomo, su cui eventualmente creare un file system o una zona di swap, o altro. I vantaggi di questo approccio sono molteplici: Diversi file system possono supportare usi differenti Migliore affidabilità Può migliorare l efficienza variando i parametri del file system a seconda degli usi: ad esempio, in Unix con mkfs(8) si può specificare il rapporto tra dimensione totale del file system e dimensione della i-list (a seconda che in un dato file system si preveda di inserire molti file piccoli, o pochi file grandi). Si può così specializzare, entro certi limiti, l uso di sottoinsiemi del disco fisico. Evita che un programma usi tutto lo spazio disponibile con unico grande file Mantenendo piccoli i singoli dischi logici, si accelerano le ricerche sui nastri di backup e i tempi di ripristino di partizioni da nastro. Ogni disco logico è gestito dal Nucleo come un array monodimensionale di blocchi logici, numerati progressivamente: 0, 1, 2,.... Un blocco logico costituisce la più piccola unità di trasferimento dati, dal punto di vista del Sistema Operativo. La dimensione di un blocco logico è normalmente un multiplo della corrispondente unità fisica, cioè il settore di un disco; ad esempio, si possono avere dischi con settori fisici da 512 byte, e gestirli con blocchi logici da 1 Kbyte. Valori comuni della dimensione di un blocco logico variano da 512 byte a 2 Kbyte, più raramente 4 Kbyte. I blocchi logici sono mappati nei settori fisici in modo sequenziale (per ciascun disco logico, dal primo settore della prima traccia del cilindro più esterno all ultimo settore dell ultima traccia del cilindro più interno). In Unix/Linux, è compito del device driver realizzare questa vista canonica del disco, che astrae dalle sue caratteristiche fisiche: il Nucleo in effetti non ha nessuna conoscenza di tracce, cilindri e così via, mentre queste sono ben note al device driver. I device driver sono dunque l unica parte del Nucleo che è al corrente delle peculiarità fisiche di ciascun particolare disco. Riassumendo, il Nucleo in generale, e più particolarmente il device driver, è responsabile di una doppia astrazione nella gestione dei dischi: la vista di un disco fisico in uno o più dischi logici; la vista di un disco logico in modo canonico (insieme di blocchi logici numerati progressivamente), facendo astrazione dalle caratteristiche fisiche della geometria del disco.
203 190 CAPITOLO 12. GESTIONE DELLA MEMORIA SECONDARIA Il file system, d altra parte, usa un altra grandezza per identificare i blocchi che compongono un file: ogni file è considerato come un vettore di blocchi di file aventi numeri d ordine 0, 1, 2,.... Ogni blocco di file ha la stessa dimensione di un blocco logico del disco; in questo contesto, un blocco logico è spesso chiamato anche blocco disco. Espandere un file significa ottenere ulteriori blocchi di file; troncare un file significa eliminare blocchi di file a partire dall ultimo. Si noti l analogia tra indirizzo di un page frame e blocco logico del disco da un lato, e tra indirizzo logico di una pagina di dati e blocco di file dall altro. Come per la gestione della RAM, ogni descrittore di file include una apposita struttura che associa ai blocchi di file i corrispondenti blocchi del disco. In questo modo, il file system può effettuare indirizzamenti all interno del file facendo riferimento a blocchi di file e può quindi ignorare quali specifici blocchi del disco siano stati assegnati al file. In conclusione, la gestione dello spazio consiste quindi in una semplice paginazione realizzata in software che consente di: sfruttare al meglio lo spazio su disco (mantenendo strutture di dati per tener traccia e identificare i blocchi disco liberi e quelli occupati); consentire di espandere o contrarre la dimensione dei file in modo arbitrario senza dovere fare uso di blocchi disco contigui Scheduling del disco Il Sistema Operativo deve assicurare un uso efficiente dell hardware; per i disk drive, ciò significa massimizzare soprattutto tempo di accesso e banda passante (velocità di trasferimento dati). Come gran parte delle risorse hardware di un sistema, l accesso al disco è sottoposto a scheduling. Il problema dello scheduling del disco (disk scheduling) consiste nell ordinare le richieste di I/O che riguardano i dischi in modo da ottenere le migliori prestazioni possibili. Si tenga presenrte che in un generico istante di tempo per un dato disco c è un certo numero N di richieste di I/O accodate (e che costituiscono la coda di richieste del disco stesso); i processi interessati ad esse sono tutti in stato Waiting, e formano la coda di dispositivo relativa a quel disco. Il tempo di accesso ha due componenti principali: Tempo di posizionamento (seek time) - tempo che impiegano le testine per posizionarsi sul cilindro che contiene il settore cercato; Latenza rotazionale (rotational latency) - tempo aggiuntivo di attesa perchè il disco ruoti fino a posizionare il settore cercato sotto le testine. La latenza rotazionale non è controllabile dal software; pertanto lo scopo primario dello scheduling del disco è minimizzare il tempo di posizionamento. Il tempo di posizionamento è circa proporzionale alla distanza di spostamento (seek distance), cioè lo spazio intercorrente tra la posizione attuale della testina
204 12.3. SCHEDULING DEL DISCO 191 e quella del cilindro che contiene il settore cercato; questa può essere misurata in numero di cilindri. Quanto alla banda passante (disk bandwidth), si riferisce alla velocità di trasferimento globale del sistema, ed è definita come il numero totale di byte trasferiti, diviso il tempo totale tra l inoltro della prima richiesta di I/O e il completamento del trasferimento relativo all ultima richiesta. Esistono diversi algoritmi di scheduling delle richieste di I/O su disco; come sempre, occorre fissare alcune metriche prestazionali per poterne misurare e confrontare gli effetti sul sistema. Le metriche prestazionali normalmente in uso sono le seguenti: distanza di spostamento totale è definita come la somma delle distanze di spostamento per tutte le richieste di I/O accodate, ovvero percorso totale effettuato dalle testine nel soddisfare tutte le richieste della coda. Tale grandezza va minimizzata. tempo d attesa della singola richiesta definito come il tempo che intercorre tra l inoltro di una richiesta e il suo completamento. Anch essa va minimizzata, ma più spesso interessa minimizzarne la varianza (per avere cioè un tempo d attesa più uniforme possibile, nell interesse del singolo utente). Il meccanismo di base è il seguente: il Nucleo sottopone al device driver una serie di richieste di I/O, espresse in termini di blocchi logici da leggere o scrivere; il device driver sa individuare il cilindro che corrisponde a un dato blocco logico. Allo scopo di valutare le prestazioni degli algoritmi, si consideri uno scenario operativo con un disco da 200 cilindri, numerati da 0 a 199, ed una coda di richeste pendenti sui seguenti cilindri: 98, 183, 37, 122, 14, 124, 65, 67 La posizione iniziale delle testine sia sul cilindro Scheduling FCFS Il primo algoritmo considerato è lo scheduling FCFS (First Come, First Served). Realizza in pratica un semplice algoritmo FIFO: ogni nuova richiesta è posta in fondo alla coda di richieste. In tal caso l ordine di esecuzione delle richieste è quello appena visto, e ad esso corrisponde una distanza di spostamento totale pari a 640 cilindri Scheduling SSTF Il secondo algoritmo è lo scheduling SSTF (Shortest Seek Time First). La prossima richiesta da servire è quella col minimo tempo di posizionamento rispetto alla posizione attuale delle testine. In tal caso l ordine di esecuzione delle richieste è il seguente:
205 192 CAPITOLO 12. GESTIONE DELLA MEMORIA SECONDARIA 65, 67, 37, 14, 98, 122, 124, 183 Ad esso corrisponde una distanza di spostamento totale pari a 236 cilindri. SSTF è una forma di scheduling analoga all algoritmo SJF per lo scheduling della CPU (si veda il Paragrafo pag. 117); pertanto può portare al blocco indefinito di alcune richieste di I/O Scheduling SCAN L algoritmo successivo è lo scheduling SCAN : il braccio delle testine parte da un estremità del disco, e si muove continuamente verso l estremità opposta, servendo richieste man mano che ne incontra; giunto all estremità del disco, il movimento del braccio è invertito e l algoritmo continua. Questo algoritmo è chiamato talvolta algoritmo dell ascensore (elevator algorithm), poiché è esattamente così che si comporta un ascensore con memoria, per cercare di minimizzare gli spostamenti fisici della cabina. L ordine di esecuzione delle richieste è allora il seguente: 37, 14, [0], 65, 67, 98, 122, 124, 183 Dopo il cilindro 14 avviene un passaggio per il cilindro 0, quindi la direzione dello spostamento delle testine viene invertita. La distanza di spostamento totale risultante è pari a 208 cilindri Scheduling C-SCAN L algoritmo SCAN presenta un problema: al giungere ad un estremità, le richieste pendenti sono concentrate soprattutto verso l estremità opposta del disco, e queste sono quelle che stanno aspettando da più tempo; rovesciando la direzione di spostamento delle testine, queste richieste attenderanno ancor più a lungo. Il risultato è che il tempo d attesa della singola richiesta può essere molto variabile. A tale problema cerca di porre rimedio lo algoritmo C-SCAN (Circular SCAN), che fornisce in effetti un tempo d attesa più uniforme (cioè con minor varianza). Il comportamento è il seguente: il braccio delle testine si sposta da un estremità all altra, servendo richieste come per SCAN. Quando raggiunge un estremità, ritorna immediatemente all inizio del disco senza servire richieste nel suo viaggio di ritorno. L ordine di esecuzione delle richieste è il seguente: 65, 67, 98, 122, 124, 183, [199], [0], 14, 37 Dopo il cilindro 183 avviene un passaggio per il cilindro 199, quindi le testine si spostano all inizio del disco senza servire richieste (passaggio per il cilindro 0), infine inizia la passata successiva a partire dal cilindro 14.
206 12.4. GESTIONE DEI DISCHI Scheduling LOOK e C-LOOK L algoritmo LOOK è una variante di SCAN. Il braccio delle testine avanza solo fino all ultima richiesta nella direzione attuale, poi inverte la direzione senza andare fino alla fine del disco. Si noti che non è sempre detto che questa strategia sia migliore della precedente: se infatti, subito dopo il raggiungimento del cilindro 183, arrivano in coda richieste per cilindri tra questo e 199, tali richieste verranno soddisfatte immediatamente nel caso dell algoritmo SCAN, mentre verranno saltate e servite solo alla prossima passata (quindi molto più tardi) nel caso dell algoritmo LOOK. L algoritmo C-LOOK è a sua volta una variante di C-SCAN, con funzionamento analogo a LOOK. Così, prendendo ad esempio quest ultimo algoritmo (C-LOOK), l ordine di esecuzione delle richieste è il seguente: 65, 67, 98, 122, 124, 183, 14, 37 Si può vedere facilmente che in questo modo, rispetto all algoritmo C-SCAN, si diminuisce la distanza di spostamento totale di 60 cilindri: da 183 a 199 e viceversa (2 16 = 32) e da 14 a 0 e viceversa (2 14 = 28) Gestione dei dischi Distinguiamo tre aspetti importanti: formattazione dei dischi ruolo del blocco di bootstrap gestione dei blocchi difettosi Formattazione dei dischi La prima operazione in assoluto che viene effettuata su un disco è la formattazione a basso livello (low-level formatting), o formattazione fisica (physical formatting); consiste nell organizzare un disco fisico (inteso come un insieme di superfici magnetizzate) in settori numerati, che il controller del disco possa leggere o scrivere. Sistemi Operativi come Unix e Windows dispongono all uopo di comandi come format, fdformat e analoghi. Per gestire dei file su un disco, il Sistema Operativo deve poi memorizzare delle apposite strutture di dati sul disco stesso; questa operazione può dividersi in due parti: la divisione del disco in uno o più dischi logici e la formattazione logica, ovvero la creazione di un file system. Unix e Windows dispongono, per il primo caso, di comandi come fdisk, divvy ed altri; e per il secondo caso del comando mkfs. Si noti che nei sistemi di tipo Windows uno stesso comando, format, provvede alla formattazione fisica e a quella logica.
207 194 CAPITOLO 12. GESTIONE DELLA MEMORIA SECONDARIA Gestione del blocco di bootstrap Il primo blocco fisico del primo disco di un sistema è detto blocco di boot (boot block), e serve a inizializzare il sistema all accensione. Questo è il motivo per cui in un file system Unix, il primo blocco non è mai utilizzato ma è lasciato libero. All accensione il sistema esegue del codice di bootstrap, memorizzato in memoria ROM, che legge il blocco di boot, lo carica in memoria RAM e gli passa il controllo. Si noti che ciò significa riuscire ad impartire istruzioni adeguate al controller del disco, e questo è un compito non triviale: a questo stadio del sistema, non esistono ancora device driver attivabili, per cui il programma deve essere autosufficiente al riguardo. Il codice così eseguito (512 byte di codice in tutto) è detto caricatore del programma d avvio (bootstrap loader), e a sua volta carica un programma dal file system, che è il vero programma d avvio (in Linux, questo programma si chiama LILO, che è l acronimo di Linux Loader). Questo programma è in grado di caricare e avviare una o più versioni disponibili del Kernel Gestione dei blocchi difettosi All uscita dalla fabbrica, o nel corso del tempo di vita di un disco, alcuni settori possono risultare difettosi (non sono in grado di memorizzare correttamente le informazioni); si generano così dei blocchi difettosi (bad block), i cui settori vanno sostituiti con altri. Oggi la maggior parte dei controller sono in grado di occuparsi di questo compito, così il Nucleo può considerare il disco come se fosse perfetto. Tuttavia è il sistema, o l utente, che deve poter segnalare al controller che un certo settore (o blocco logico intero) va sostituito. Lo scenario è il seguente: il Nucleo richiede un operazione di I/O su un blocco logico, che comprende un settore difettoso; il driver, o il controller, effettua trasparentemente l operazione sul settore di sostituzione. La tecnica utilizzata è detta accantonamento di settori (sector sparing). Normalmente viene accantonato un certo numero di settori per cilindro. È fondamentale conservare lo stesso cilindro nella sostituzione di blocchi, altrimenti tutti gli algoritmi di scheduling del disco non avrebbero più alcun senso Gestione della zona di swap Sia lo swapping che la paginazione usano spazio disco come un estensione della memoria primaria. La zona disco deputata a tale compito è la zona di swap; può essere ricavata all interno del file system normale, o risiedere in una partizione dedicata. La gestione dello spazio di swap può essere effettuata in vari modi; ad esempio in Unix 4.3BSD, lo spazio è allocato a ciascun processo mediante 2 mappe di swap (swap map), gestite dal Nucleo, una per la regione di codice e l altra per quelle di dati: ci sono così in tutto 2 swap map per processo attivo. La mappa per la regione di codice (che ha dimensione invariabile) divide lo spazio su disco in blocchi da 512 Kbyte, salvo l ultimo blocco che viene allocato in incrementi da 1 Kbyte. Mappa per le regioni di dati, le cui dimensioni sono
208 12.5. GESTIONE DELLA ZONA DI SWAP 195 variabili durante l esecuzione del processo, si compone di blocchi di dimensione variabile, ciascuno il doppio del precedente, a partire da 16 Kbyte e fino a 2 Mbyte: in generale, la dimensione del blocco i ha lunghezza 2 i 16 Kbyte. Se un processo cresce, gli viene allocato un blocco grande il doppio del precedente. I vantaggi delle swap map adottate da Unix 4.3BSD sono soprattutto i seguenti: Piccoli processi usano solo blocchi piccoli Viene minimizzata la frammentazione I blocchi dei processi grandi possono essere trovati rapidamente Le swap map rimangono piccole (cioè prendono poco spazio). Linux (così come Solaris e altri sistemi Unix) permette l uso di più zone di swap, che possono essere abilitate e/o disabilitate nel corso del funzionamento del sistema; ciò può migliorare sensibilmente le flessibilità e le prestazioni del sistema. I comandi swapon(8) e swapoff (8) sono usati rispettivamente per abilitare e disabilitare individualmente zone di swap. Ogni zona può essere una partizione o un file su disco, con spazio preallocato.
209 Capitolo 13 Gestione dei Dispositivi di I/O 13.1 Introduzione L esistenza di numerosi dispositivi di I/O, ognuno con caratteristiche particolari, complica notevolmente la struttura del Nucleo che deve includere appositi programmi di gestione per ognuno dei dispositivi supportati. Nei Nuclei attuali, oltre il 50% del codice è dedicato alla gestione dei dispositivi di I/O. Data la complessità dell argomento, la trattazione svolta in questo capitolo risulterà necessariamente sommaria. Per concretezza, faremo alcuni riferimenti al modo in cui il Sistema Operativo Unix gestisce le operazioni di I/O Architettura di I/O Prima di iniziare a descrivere il modo in cui il Nucleo gestisce i dispositivi di I/O, è opportuno dare alcuni cenni sulla architettura di I/O, ossia sul modo in cui i dipositivi di I/O sono collegati agli altri componenti dell elaboratore. Come indicato nella Figura 13.1, il dispositivo di I/O (stampante, disco, tastiera, mouse, ecc.) è collegato ad un apposito controllore hardware. Tale controllore è collegato, a sua volta, ad una interfaccia la quale include alcune porte di I/O collegate al bus. Dal punto di vista del programmatore, l accesso ai dispositivi di I/O si realizza tramite porte di I/O. Ogni porta di I/O possiede un apposito indirizzo fisico. Esistono inoltre apposite istruzioni (istruzioni in e out nell architettura Intel 80x86) che consentono di trasferire dati da una porta di I/O ad un registro della CPU o viceversa. Tali istruzioni includono appositi indirizzi di I/O da 16 bit che si possono considerare come logici e fisici allo stesso tempo: in effetti, la CPU non fa uso del circuito di paginazione nell eseguire le suddette istruzioni. Le porte di I/O consentono di realizzare le seguenti funzioni: 196
210 13.2. ARCHITETTURA DI I/O 197 CPU I/O bus porta di I/O... porta di I/O interfaccia di I/O controllore di I/O dispositivo di I/O Figura 13.1: Architettura di I/O. inviare un comando al dispositivo; leggere lo stato del dispositivo; leggere dati provenienti dal dispositivo; inviare dati al dispositivo. La maggior parte dei dispositivi di I/O prevede la possibilità di emettere appositi segnali di interruzione per segnalare la fine di una operazione di I/O. Esiste inoltre un apposito flag, programmabile tramite porta di I/O, che abilita o disabilita le interruzioni di I/O emesse dal dispositivo. Dispositivi con elevato tasso di trasferimento (ad esempio, i dischi magnetici) possono essere collegati ad un processore autonomo di I/O chiamato Direct Memory Access Controller (DMAC). Anche in questo caso, è possibile programmare tramite appositi flag di una porta di I/O l attivazione del DMAC per il dispositivo. Grazie al DMAC, il Nucleo è in grado di avviare una operazione di I/O relativamente complessa, ad esempio la lettura di più blocchi di dati da disco in un area prefissata di RAM, per poi passare a svolgere altre attività senza aspettare la terminazione dell operazione di I/O. Quando il DMAC ha terminato di trasferire dati, invia un segnale alla interfaccia di I/O interessata, la quale genera, a sua volta, un segnale di interruzione.
211 198 CAPITOLO 13. GESTIONE DEI DISPOSITIVI DI I/O 13.3 Dispositivi di I/O riconosciuti dal file system Ogni Sistema Operativo tenta di virtualizzare nel modo più efficace possibile i vari dispositivi di I/O supportati offrendo al programmatore una interfaccia omogenea. Per concretezza, diamo alcuni cenni sul come ciò viene realizzato nel Sistema Operativo Unix. In primo luogo, Unix riconosce due classi di dispositivi di I/O: dispositivi a caratteri: sono dispositivi lenti (tastiera, stampante, ecc.) che trasferiscono dati un carattere alla volta e non richiedono buffer; solitamente, l accesso ai dispositivi a caratteri è di tipo sequenziale; dispositivi a blocchi: sono dispositivi più complessi (disco rigido, lettore CD-ROM, unità a nastro, ecc.) che trasferiscono dati un blocco alla volta e richiedono pertanto un buffer per contenere tale blocco; l accesso ai dispositivi a blocchi è di tipo casuale. Ogni dispositivo è identificato da Unix tramite due identificatori chiamati major device number (MDN ) e minor device number (mnd). Ad esempio, i vari dischi gestiti dallo stesso controllore di disco hanno lo stesso MDN ma diversi mdn. Spesso si parla anche di device number, che è un unico intero che codifica, solitamente in due byte, entrambi gli identificatori di cui prima. Ogni dispositivo è gestito dal file system di Unix tramite un apposito special file, di tipo orientato a blocchi oppure orientato a caratteri (vedi Paragrafo pag. 77) Programmazione di un device file In Unix, la programmazione ad alto livello di un device file non differisce da quella di un file standard: le stesse primitive open(), close(), read(), write(), lseek(), ecc. usate sui file standard possono essere usate su un device file 1. Il programma di copiatura tra file visto nel Paragrafo pag. 84 può essere usato per mettere in evidenza la versatilità del file system Unix: il programma è infatti in grado di operare sia su special file che su file standard, e prescinde dal file system in cui sono registrati i file standard (Ext2, VFAT, ecc.). Ovviamente, vi sono limitazioni legate al tipo di dispositivo di I/O, per cui ogni dispositivo ammette in generale un sottoinsieme di tutte le operazioni su file esistenti. Se la periferica è un dispositivo di solo input, ad esempio un mouse, la chiamata di sistema write() non è applicabile a tale dispositivo. In modo analogo, se il 1 Esiste almeno una chiamata di sistema specifica dei device, la ioctl(); tale chiamata è utilizzabile soltanto in connessione a special file di tipo carattere, e non può essere utilizzata con file regolari.
212 13.4. SUPPORTO DEL NUCLEO 199 dispositivo è di tipo orientato a caratteri, la chiamata lseek() non è applicabile a tale dispositivo (o comunque non genera alcun effetto su di esso). Solitamente, tutti i device file sono inclusi nella directory /dev. Per potere includere un nuovo dispositivo di I/O in tale directory, è necessario averlo prima registrato. Con tale termine si intende l attività mediante la quale il Sistema Operativo acquisisce informazioni circa un nuovo tipo di dispositivo. I principali dati passati nella fase di registrazione riguardano: MDN e mdn del dispositivo; funzioni specializzate che realizzano il gruppo di operazioni applicabili al dispositivo (se una funzione non è applicabile, viene passato un puntatore nullo) Supporto del Nucleo Vediamo ora in che modo il Nucleo supporta la gestione dei dispositivi di I/O. A seconda del tipo di dispositivo, il Nucleo può offrire il seguente livello di supporto: nessun supporto; supporto limitato alla porta di I/O; supporto totale. L esempio più ovvio in Unix del primo caso riguarda l interfaccia grafica del monitor: il Nucleo di Unix non offre alcun supporto per l interfaccia grafica, per cui viene usata un apposita applicazione (solitamente, X Window) per gestire la grafica dello schermo nonché il mouse per disegnare il cursore grafico. Tale applicazione programma le porte di I/O mediante apposite istruzioni di tipo in e out dopo avere ottenuto dal Sistema Operativo i necessari permessi per accedere alle porte di I/O 2 ). Un esempio relativo al secondo caso riguarda le porte seriali presenti nel calcolatore: il Nucleo supporta le porte seriali presenti (/dev/ttys0, /dev/ttys1, ecc.) consentendo al programmatore di usare le primitive read() e write() su tali porte considerate come degli special file. Il Nucleo non supporta invece alcuno dei vari dispositivi collegabili alla porta seriale (modem, mouse, stampante, ecc.). La gestione di tali dispositivi deve essere effettuata da appositi programmi applicativi esterni al Nucleo. Il terzo caso è il più impegnativo in quanto richiede la realizzazione di un apposito programma chiamato gestore del dispositivo di I/O (in inglese I/O driver o device driver) per ognuno dei dispositivi riconosciuti dal Sistema Operativo. 2 Linux include una chiamata di sistema, ioperm(), che permette di concedere a un processo utente l accesso alle porte di I/O comprese in un certo intervallo di indirizzi; tale chiamata è ovviamente riservata al super-user.
213 200 CAPITOLO 13. GESTIONE DEI DISPOSITIVI DI I/O Tutti i dispositivi collegati alla porta parallela, ad uno dei vari tipi di bus presenti nel sistema (EIDE, PCI, SCSI, USB), oppure ad una interfaccia PCMCIA richiedono un I/O driver. I compiti dell I/O driver sono essenzialmente quattro: avviare l operazione di I/O offrendo una interfaccia semplificata che nasconda il più possibile le caratteristiche hardware interne del dispositivo; forzare la terminazione dell operazione di I/O se essa non è terminata entro un tempo prefissato; ciò viene realizzato tramite un meccanismo di time-out che invia al Nucleo un apposito segnale di interruzione quando l intervallo di tempo è scaduto; gestire i segnali di interruzione emessi dal dispositivo risvegliando il processo bloccato in attesa della terminazione dell operazione di I/O; analizzare l esito dell operazione di I/O inviando eventuali messaggi di errore nel caso in cui essa non sia andata a buon fine. Poiché ogni dispositivo di I/O ha caratteristiche peculiari e condizioni di errore proprie (ad esempio, un gestore di stampante deve essere in grado di gestire segnali di fine carta nel contenitore della stampante), i moderni Nuclei includono centinaia di gestori distinti. Allo stesso tempo, l incessante innovazione tecnologica fa sì che appaiano ogni giorno sul mercato nuovi dispositivi di I/O che devono essere integrati nei Nuclei esistenti (un esempio è costituito dai lettori DVD apparsi da poco tempo sul mercato dei Personal Computer). Per quanto detto prima, un gestore di dispositivo di I/O non può essere realizzato come un processo di sistema poiché deve essere in grado di gestire un apposito segnale di interruzione; esso deve essere invece integrato nel Nucleo. In conseguenza, il Nucleo deve essere predisposto ad accogliere nuovi gestori di dispositivi Sincronizzazione tra CPU e dispositivo di I/O Non è possibile in questo breve testo dare ulteriori dettagli sulla struttura interna dei driver di I/O. Vale la pena tuttavia accennare al fatto che la sincronizzazione tra CPU e dispositivo di I/O può essere svolta in due modi diversi: Polling: la CPU interroga periodicamente l apposita porta di I/O per leggere lo stato del dispositivo e verificare se l operazione di I/O è terminata. Il ciclo di polling prevede solitamente il rilascio della CPU (una funzione del Nucleo simile a quella usata per realizzare la chiamata di sistema sched yield()) in modo da consentire ad altri processi di usare la CPU.
214 13.6. USO DI CACHE NEI DRIVER PER DISCHI 201 Interruzioni: il driver di I/O avvia l operazione e pone il processo nello stato di bloccato sull evento interruzione proveniente dal dispositivo. Il processo verrà posto successivamente nello stato di pronto non appena si sarà verificata l interruzione richiesta. Quando ciò avviene, il driver di I/O riprende l esecuzione ed analizza il codice di terminazione dell operazione di I/O prima di ridare il controllo al processo interessato. Il primo approccio è preferibile al secondo quando il tempo di risposta del dispositivo di I/O è breve rispetto al tempo richiesto per effettuare la commutazione di processi: le attuali stampanti dotate di buffer di notevoli dimensioni sono solitamente gestite con la tecnica del polling. Il secondo approccio è usato quando vi sono molti byte da trasferire: i driver di disco fanno uso del DMAC e di interruzioni per segnalare alla CPU la fine dell operazione di I/O Uso di cache nei driver per dischi La caratteristica più importante di un Sistema Operativo è l efficienza, in particolare l efficienza del Nucleo. Diverse categorie di utenti sottomettono vari Sistemi Operativi commerciali ad una serie di rigidi test basati sull uso di programmi di prova (i cosiddetti programmi benchmark) e stabiliscono classifiche, peraltro non sempre imparziali. Dato che le interfacce verso l utente risultano oggi abbastanza simili tra loro e che le tecniche di compilazione hanno raggiunto un livello difficilmente migliorabile, l area in cui si può sperare di raggiungere migliori prestazioni è quella del Nucleo. In effetti, i moderni calcolatori spendono oltre il 50% del tempo nello stato Kernel, ossia eseguendo programmi del Nucleo, per cui risulta cruciale tentare di migliorare le prestazioni di tale componente del Sistema Operativo. Ciò premesso, è ragionevole affermare che il collo di bottiglia degli attuali calcolatori è costituito dalla lentezza dei dispositivi di I/O, ed in particolare dei dischi rispetto alle capacità di elaborazione della CPU. Mentre un disco è in grado di trasferire poche centinaia di byte in alcuni millisecondi, la CPU è in grado di eseguire decine di migliaia di istruzioni nello stesso intervallo di tempo. La risposta dei progettisti di Nuclei è stata di ricorrere ad un uso intensivo di memorie di tipo cache. Una cache è una zona di memoria RAM destinata a contenere una piccola parte dei dati contenuti nel disco. Tipicamente, per un disco avente una capacità di qualche Gigabyte si fa uso di una cache di qualche Megabyte, ossia di una zona di memoria avente una dimensione di tre ordini di grandezza inferiore a quella del disco. Quando viene richiesta una lettura o scrittura sul disco, il Nucleo verifica se l informazione richiesta non sia già disponibile nella cache. Nel caso affermativo, la preleva dalla cache evitando di effettuare un indirizzamento del disco, e quindi con un notevole risparmio di tempo. Se gli indirizzamenti ai settori del disco fossero distribuiti in modo uniforme, le cache non avrebbero ragione di esistere poiché la probabilità di trovare il dato richiesto nella cache sarebbe dell ordine di uno su mille.
215 202 CAPITOLO 13. GESTIONE DEI DISPOSITIVI DI I/O Fortunatamente, la maggior parte dei programmi utenti effettua indirizzamenti al disco in modo locale, per cui è molto probabile che il programma utente continui ad operare per qualche tempo sugli stessi blocchi di dati prima di richiederne altri (vedi Paragrafo 11.7 pag. 185). La località in termini di programmi significa che il processo non utilizza simultaneamente tutti i sottoprogrammi ma solo alcuni di essi. La località in termini di dati significa che il processo non utilizza simultaneamente tutte le strutture di dati ma solo su alcune di esse. L idea vincente che ha portato all introduzione della cache è quella di conservare nella RAM blocchi di dati anche quando nessun processo sta attualmente operando su tali dati. Supponiamo che un processo abbia aperto un file ed abbia quindi effettuato scritture sul file. In questo caso, i blocchi di dati modificati verranno conservati nella cache anche dopo che il processo ha chiuso il file. In effetti, il conservare nella cache i blocchi di dati indirizzati più recentemente, aumenta notevolmente la probabilità di non dovere effettuare indirizzamenti al disco, e quindi diminuisce il tempo medio di accesso al disco e migliorano le prestazioni dell intero sistema. L utente può verificare con mano la presenza di una cache del disco rieseguendo due volte lo stesso comando e misurandone la durata. Eseguendo ad esempio: time ls -R /usr/src/linux due volte di seguito, si noterà come la seconda esecuzione sia molto più rapida della prima per via dei dati ormai presenti nella cache. Tra i vari compiti del Nucleo si aggiunge quindi quello di gestire le cache dei dischi e di stabilire una strategia di svuotamento automatico della cache. In effetti, ogni volta che un settore di disco non si trova nella cache, esso viene letto da disco ed inserito nella cache. Per evitare il riempimento totale della cache, il Nucleo deve provvedere a riutilizzare alcune parti della cache; a tale scopo, rilascia blocchi di dati nel seguente ordine: 1. quelli letti da disco ma non modificati; tra quelli sceglie quelli non indirizzati da più tempo; 2. se non è stata rilasciata sufficiente memoria dalla cache, quelli letti e modificati; in questo caso, è necessario scrivere su disco la versione aggiornata prima di riutilizzare l area di memoria. È interessante osservare che l uso di cache migliora a tale punto le prestazioni del disco che i dischi più moderni includono spesso una propria cache inclusa nell unità di controllo del disco che viene gestita interamente in hardware. Le notevoli prestazioni di tali dischi vanno lette tenendo presente che i tempi medi d accesso dichiarati non si riferiscono solo alla parte meccanica ma tengono presente anche i benefici indotti dalla cache locale.
216 Bibliografia [1] V. Asta, UNIX Utilisation, Axis Digital, Editions LASER 1995 [2] V. Asta, Administration UNIX, Axis Digital, Boulogne 1992 [3] V. Asta, Introduzione ai Sistemi Operativi Unix e GNU/Linux, Tomo 1 (Lezioni), vito/solt/books/unixlinux.pdf, vers. 1.4, 2003 [4] V. Asta, Esercitazioni di programmazione in linguaggio shell, vito/solt/books/esercitazionishell.pdf, vers. 1.4, 2003 [5] M. Bach, The Design of the Unix Operating System, Prentice-Hall 1986 [6] D.P. Bovet - M. Cesati, Understanding the Linux Kernel, O Reilly 2001 [7] Intel, Intel Architecture Software Developer s Manual, vol. 3: System Programming, [8] WG15 standardization Working Group, Official Home of JTC1/SC22/WG15 - POSIX, [9] B.W. Kernighan - R. Pike, The UNIX Programming Environment, Prentice-Hall 1984 [10] B. Kernighan - R. Pike, The Practice of Programming, Addison-Wesely 1999 [11] M. Mitchell - J. Oldham - A. Samuel, Advanced Linux Programming, New Riders 2001 [12] G. Nutt, Operating Systems - A Modern Perspective, 2. Edition, Addison-Wesley 2000 [13] A. Silberschatz - P.B. Galvin - G. Gagne, Operating System Concepts, 6. Edition, Addison Wesley 2003 [14] W.R. Stevens, UNIX Network Programming Vol 1: Networking APIs - Sockets and XTI, 2. Edition, Prentice-Hall
217 204 BIBLIOGRAFIA [15] A.S. Tanenbaum, Structured Computer Organization, 3. Edition, Prentice-Hall 1990 [16] A.S. Tanenbaum, Computer Networks, 3. Edition, Prentice Hall 1996 [17] M. Welsh - M.K. Dalheimer - T. Dawson - L. Kaufman, Running Linux, 4. Edition, O Reilly 2002
Il Sistema Operativo (1)
E il software fondamentale del computer, gestisce tutto il suo funzionamento e crea un interfaccia con l utente. Le sue funzioni principali sono: Il Sistema Operativo (1) La gestione dell unità centrale
Il Sistema Operativo. C. Marrocco. Università degli Studi di Cassino
Il Sistema Operativo Il Sistema Operativo è uno strato software che: opera direttamente sull hardware; isola dai dettagli dell architettura hardware; fornisce un insieme di funzionalità di alto livello.
Dispensa di Informatica I.1
IL COMPUTER: CONCETTI GENERALI Il Computer (o elaboratore) è un insieme di dispositivi di diversa natura in grado di acquisire dall'esterno dati e algoritmi e produrre in uscita i risultati dell'elaborazione.
Con il termine Sistema operativo si fa riferimento all insieme dei moduli software di un sistema di elaborazione dati dedicati alla sua gestione.
Con il termine Sistema operativo si fa riferimento all insieme dei moduli software di un sistema di elaborazione dati dedicati alla sua gestione. Compito fondamentale di un S.O. è infatti la gestione dell
Il software impiegato su un computer si distingue in: Sistema Operativo Compilatori per produrre programmi
Il Software Il software impiegato su un computer si distingue in: Software di sistema Sistema Operativo Compilatori per produrre programmi Software applicativo Elaborazione testi Fogli elettronici Basi
MODELLO CLIENT/SERVER. Gianluca Daino Dipartimento di Ingegneria dell Informazione Università degli Studi di Siena [email protected]
MODELLO CLIENT/SERVER Gianluca Daino Dipartimento di Ingegneria dell Informazione Università degli Studi di Siena [email protected] POSSIBILI STRUTTURE DEL SISTEMA INFORMATIVO La struttura di un sistema informativo
Un sistema operativo è un insieme di programmi che consentono ad un utente di
INTRODUZIONE AI SISTEMI OPERATIVI 1 Alcune definizioni 1 Sistema dedicato: 1 Sistema batch o a lotti: 2 Sistemi time sharing: 2 Sistema multiprogrammato: 3 Processo e programma 3 Risorse: 3 Spazio degli
Software di sistema e software applicativo. I programmi che fanno funzionare il computer e quelli che gli permettono di svolgere attività specifiche
Software di sistema e software applicativo I programmi che fanno funzionare il computer e quelli che gli permettono di svolgere attività specifiche Software soft ware soffice componente è la parte logica
Protocolli di Sessione TCP/IP: una panoramica
Protocolli di Sessione TCP/IP: una panoramica Carlo Perassi [email protected] Un breve documento, utile per la presentazione dei principali protocolli di livello Sessione dello stack TCP/IP e dei principali
FONDAMENTI di INFORMATICA L. Mezzalira
FONDAMENTI di INFORMATICA L. Mezzalira Possibili domande 1 --- Caratteristiche delle macchine tipiche dell informatica Componenti hardware del modello funzionale di sistema informatico Componenti software
Introduzione alle tecnologie informatiche. Strumenti mentali per il futuro
Introduzione alle tecnologie informatiche Strumenti mentali per il futuro Panoramica Affronteremo i seguenti argomenti. I vari tipi di computer e il loro uso Il funzionamento dei computer Il futuro delle
Software relazione. Software di base Software applicativo. Hardware. Bios. Sistema operativo. Programmi applicativi
Software relazione Hardware Software di base Software applicativo Bios Sistema operativo Programmi applicativi Software di base Sistema operativo Bios Utility di sistema software Software applicativo Programmi
INFORMATICA. Il Sistema Operativo. di Roberta Molinari
INFORMATICA Il Sistema Operativo di Roberta Molinari Il Sistema Operativo un po di definizioni Elaborazione: trattamento di di informazioni acquisite dall esterno per per restituire un un risultato Processore:
Sistema Operativo. Fondamenti di Informatica 1. Il Sistema Operativo
Sistema Operativo Fondamenti di Informatica 1 Il Sistema Operativo Il Sistema Operativo (S.O.) è un insieme di programmi interagenti che consente agli utenti e ai programmi applicativi di utilizzare al
Sistemi operativi e reti A.A. 2013-14. Lezione 2
Università di Roma Tor Vergata Corso di Laurea triennale in Informatica Sistemi operativi e reti A.A. 2013-14 Pietro Frasca Lezione 2 Giovedì 10-10-2013 1 Sistemi a partizione di tempo (time-sharing) I
Il Sistema Operativo
Il Sistema Operativo Il Sistema Operativo Il Sistema Operativo (S.O.) è un insieme di programmi interagenti che consente agli utenti e ai programmi applicativi di utilizzare al meglio le risorse del Sistema
La gestione di un calcolatore. Sistemi Operativi primo modulo Introduzione. Sistema operativo (2) Sistema operativo (1)
La gestione di un calcolatore Sistemi Operativi primo modulo Introduzione Augusto Celentano Università Ca Foscari Venezia Corso di Laurea in Informatica Un calcolatore (sistema di elaborazione) è un sistema
Il sistema di I/O. Hardware di I/O Interfacce di I/O Software di I/O. Introduzione
Il sistema di I/O Hardware di I/O Interfacce di I/O Software di I/O Introduzione 1 Sotto-sistema di I/O Insieme di metodi per controllare i dispositivi di I/O Obiettivo: Fornire ai processi utente un interfaccia
Il software di base comprende l insieme dei programmi predisposti per un uso efficace ed efficiente del computer.
I Sistemi Operativi Il Software di Base Il software di base comprende l insieme dei programmi predisposti per un uso efficace ed efficiente del computer. Il sistema operativo è il gestore di tutte le risorse
Corso di Informatica
Corso di Informatica Modulo T1 4-Panoramica delle generazioni 1 Prerequisiti Monoprogrammazione e multiprogrammazione Multielaborazione Linguaggio macchina Linguaggi di programmazione e compilatori Struttura
Il SOFTWARE DI BASE (o SOFTWARE DI SISTEMA)
Il software Software Il software Il software è la sequenza di istruzioni che permettono ai computer di svolgere i loro compiti ed è quindi necessario per il funzionamento del calcolatore. Il software può
STRUTTURE DEI SISTEMI DI CALCOLO
STRUTTURE DEI SISTEMI DI CALCOLO 2.1 Strutture dei sistemi di calcolo Funzionamento Struttura dell I/O Struttura della memoria Gerarchia delle memorie Protezione Hardware Architettura di un generico sistema
Lezione 4 La Struttura dei Sistemi Operativi. Introduzione
Lezione 4 La Struttura dei Sistemi Operativi Introduzione Funzionamento di un SO La Struttura di un SO Sistemi Operativi con Struttura Monolitica Progettazione a Livelli di un SO 4.2 1 Introduzione (cont.)
Definizione Parte del software che gestisce I programmi applicativi L interfaccia tra il calcolatore e i programmi applicativi Le funzionalità di base
Sistema operativo Definizione Parte del software che gestisce I programmi applicativi L interfaccia tra il calcolatore e i programmi applicativi Le funzionalità di base Architettura a strati di un calcolatore
Approccio stratificato
Approccio stratificato Il sistema operativo è suddiviso in strati (livelli), ciascuno costruito sopra quelli inferiori. Il livello più basso (strato 0) è l hardware, il più alto (strato N) è l interfaccia
I Thread. I Thread. I due processi dovrebbero lavorare sullo stesso testo
I Thread 1 Consideriamo due processi che devono lavorare sugli stessi dati. Come possono fare, se ogni processo ha la propria area dati (ossia, gli spazi di indirizzamento dei due processi sono separati)?
Rack Station RS407, RS408, RS408-RP
Rack Station RS407, RS408, RS408-RP Guida di Installazione Rapida ISTRUZIONI PER LA SICUREZZA Leggere accuratamente le presenti avvertenze ed istruzioni prima dell uso e conservarle per riferimenti futuri.
Corso di Informatica
Corso di Informatica Modulo T2 1 Sistema software 1 Prerequisiti Utilizzo elementare di un computer Significato elementare di programma e dati Sistema operativo 2 1 Introduzione In questa Unità studiamo
Gestione della memoria centrale
Gestione della memoria centrale Un programma per essere eseguito deve risiedere in memoria principale e lo stesso vale per i dati su cui esso opera In un sistema multitasking molti processi vengono eseguiti
ASPETTI GENERALI DI LINUX. Parte 2 Struttura interna del sistema LINUX
Parte 2 Struttura interna del sistema LINUX 76 4. ASPETTI GENERALI DEL SISTEMA OPERATIVO LINUX La funzione generale svolta da un Sistema Operativo può essere definita come la gestione dell Hardware orientata
Corso di Informatica
Corso di Informatica Modulo T2 3-Compilatori e interpreti 1 Prerequisiti Principi di programmazione Utilizzo di un compilatore 2 1 Introduzione Una volta progettato un algoritmo codificato in un linguaggio
GESTIONE DEI PROCESSI
Sistemi Operativi GESTIONE DEI PROCESSI Processi Concetto di Processo Scheduling di Processi Operazioni su Processi Processi Cooperanti Concetto di Thread Modelli Multithread I thread in Java Concetto
SISTEMI OPERATIVI. Prof. Enrico Terrone A. S: 2008/09
SISTEMI OPERATIVI Prof. Enrico Terrone A. S: 2008/09 Che cos è il sistema operativo Il sistema operativo (SO) è il software che gestisce e rende accessibili (sia ai programmatori e ai programmi, sia agli
Architettura di un sistema operativo
Architettura di un sistema operativo Dipartimento di Informatica Università di Verona, Italy Struttura di un S.O. Sistemi monolitici Sistemi a struttura semplice Sistemi a livelli Virtual Machine Sistemi
Le Infrastrutture Software ed il Sistema Operativo
Le Infrastrutture Software ed il Sistema Operativo Corso di Informatica CdL: Chimica Claudia d'amato [email protected] Il Sistema Operativo (S0) (Inf.) E' l'insieme dei programmi che consentono
Introduzione. Classificazione di Flynn... 2 Macchine a pipeline... 3 Macchine vettoriali e Array Processor... 4 Macchine MIMD... 6
Appunti di Calcolatori Elettronici Esecuzione di istruzioni in parallelo Introduzione... 1 Classificazione di Flynn... 2 Macchine a pipeline... 3 Macchine vettoriali e Array Processor... 4 Macchine MIMD...
Sistemi operativi. Esempi di sistemi operativi
Sistemi operativi Un sistema operativo è un programma che facilita la gestione di un computer Si occupa della gestione di tutto il sistema permettendo l interazione con l utente In particolare un sistema
SDD System design document
UNIVERSITA DEGLI STUDI DI PALERMO FACOLTA DI INGEGNERIA CORSO DI LAUREA IN INGEGNERIA INFORMATICA TESINA DI INGEGNERIA DEL SOFTWARE Progetto DocS (Documents Sharing) http://www.magsoft.it/progettodocs
Corso di Informatica
Corso di Informatica Modulo T3 3-Schedulazione 1 Prerequisiti Concetto di media Concetto di varianza 2 1 Introduzione Come sappiamo, l assegnazione della CPU ai processi viene gestita dal nucleo, attraverso
Architetture Applicative
Alessandro Martinelli [email protected] 6 Marzo 2012 Architetture Architetture Applicative Introduzione Alcuni esempi di Architetture Applicative Architetture con più Applicazioni Architetture
Secondo biennio Articolazione Informatica TPSIT Prova Quarta
Sistema operativo: gestione memoria centrale La Memoria Virtuale consente di superare i limiti della Memoria Centrale : A. no B. a volte C. si, ma non sempre e' adeguata D. si, attraverso tecniche di gestione
L informatica INTRODUZIONE. L informatica. Tassonomia: criteri. È la disciplina scientifica che studia
L informatica È la disciplina scientifica che studia INTRODUZIONE I calcolatori, nati in risposta all esigenza di eseguire meccanicamente operazioni ripetitive Gli algoritmi, nati in risposta all esigenza
Sistemi Operativi STRUTTURA DEI SISTEMI OPERATIVI 3.1. Sistemi Operativi. D. Talia - UNICAL
STRUTTURA DEI SISTEMI OPERATIVI 3.1 Struttura dei Componenti Servizi di un sistema operativo System Call Programmi di sistema Struttura del sistema operativo Macchine virtuali Progettazione e Realizzazione
Architettura di un calcolatore
2009-2010 Ingegneria Aerospaziale Prof. A. Palomba - Elementi di Informatica (E-Z) 7 Architettura di un calcolatore Lez. 7 1 Modello di Von Neumann Il termine modello di Von Neumann (o macchina di Von
In un modello a strati il SO si pone come un guscio (shell) tra la macchina reale (HW) e le applicazioni 1 :
Un Sistema Operativo è un insieme complesso di programmi che, interagendo tra loro, devono svolgere una serie di funzioni per gestire il comportamento del computer e per agire come intermediario consentendo
Il Software. Il software del PC. Il BIOS
Il Software Il software del PC Il computer ha grandi potenzialità ma non può funzionare senza il software. Il software essenziale per fare funzionare il PC può essere diviso nelle seguenti componenti:
Architettura di un sistema di calcolo
Richiami sulla struttura dei sistemi di calcolo Gestione delle Interruzioni Gestione della comunicazione fra processore e dispositivi periferici Gerarchia di memoria Protezione. 2.1 Architettura di un
Il memory manager. Gestione della memoria centrale
Il memory manager Gestione della memoria centrale La memoria La memoria RAM è un vettore molto grande di WORD cioè celle elementari a 16bit, 32bit, 64bit (2Byte, 4Byte, 8Byte) o altre misure a seconda
Funzioni del Sistema Operativo
Il Software I componenti fisici del calcolatore (unità centrale e periferiche) costituiscono il cosiddetto Hardware (ferramenta). La struttura del calcolatore può essere schematizzata come una serie di
Creare una Rete Locale Lezione n. 1
Le Reti Locali Introduzione Le Reti Locali indicate anche come LAN (Local Area Network), sono il punto d appoggio su cui si fonda la collaborazione nel lavoro in qualunque realtà, sia essa un azienda,
La Gestione delle risorse Renato Agati
Renato Agati delle risorse La Gestione Schedulazione dei processi Gestione delle periferiche File system Schedulazione dei processi Mono programmazione Multi programmazione Gestione delle periferiche File
IL SOFTWARE TIPI DI SOFTWARE. MACCHINE VIRTUALI Vengono definite così perché sono SIMULATE DAL SOFTWARE, UNIFORMANO L ACCESSO SISTEMA OPERATIVO
IL SOFTWARE L HARDWARE da solo non è sufficiente a far funzionare un computer Servono dei PROGRAMMI (SOFTWARE) per: o Far interagire, mettere in comunicazione, le varie componenti hardware tra loro o Sfruttare
Scheduling della CPU. Sistemi multiprocessori e real time Metodi di valutazione Esempi: Solaris 2 Windows 2000 Linux
Scheduling della CPU Sistemi multiprocessori e real time Metodi di valutazione Esempi: Solaris 2 Windows 2000 Linux Sistemi multiprocessori Fin qui si sono trattati i problemi di scheduling su singola
Laboratorio di Informatica
per chimica industriale e chimica applicata e ambientale LEZIONE 4 - parte II La memoria 1 La memoriaparametri di caratterizzazione Un dato dispositivo di memoria è caratterizzato da : velocità di accesso,
C. P. U. MEMORIA CENTRALE
C. P. U. INGRESSO MEMORIA CENTRALE USCITA UNITA DI MEMORIA DI MASSA La macchina di Von Neumann Negli anni 40 lo scienziato ungherese Von Neumann realizzò il primo calcolatore digitale con programma memorizzato
Corso di Sistemi di Elaborazione delle informazioni
Corso di Sistemi di Elaborazione delle informazioni Sistemi Operativi Francesco Fontanella Complessità del Software Software applicativo Software di sistema Sistema Operativo Hardware 2 La struttura del
Gestione del processore e dei processi
Il processore è la componente più importante di un sistema di elaborazione e pertanto la sua corretta ed efficiente gestione è uno dei compiti principali di un sistema operativo Il ruolo del processore
Sistemi Operativi (modulo di Informatica II) I processi
Sistemi Operativi (modulo di Informatica II) I processi Patrizia Scandurra Università degli Studi di Bergamo a.a. 2009-10 Sommario Il concetto di processo Schedulazione dei processi e cambio di contesto
Introduzione alla Virtualizzazione
Introduzione alla Virtualizzazione Dott. Luca Tasquier E-mail: [email protected] Virtualizzazione - 1 La virtualizzazione è una tecnologia software che sta cambiando il metodo d utilizzo delle risorse
Pronto Esecuzione Attesa Terminazione
Definizione Con il termine processo si indica una sequenza di azioni che il processore esegue Il programma invece, è una sequenza di azioni che il processore dovrà eseguire Il processo è quindi un programma
TEORIA DEI SISTEMI OPERATIVI
TEORIA DEI SISTEMI OPERATIVI Classificazione dei sistemi operativi (Sistemi dedicati, Sistemi batch, Sistemi interattivi multiutente) CLASSIFICAZIONE DEI SISTEMI OPERATIVI Le tre principali configurazioni
Il database management system Access
Il database management system Access Corso di autoistruzione http://www.manualipc.it/manuali/ corso/manuali.php? idcap=00&idman=17&size=12&sid= INTRODUZIONE Il concetto di base di dati, database o archivio
Informatica - A.A. 2010/11
Ripasso lezione precedente Facoltà di Medicina Veterinaria Corso di laurea in Tutela e benessere animale Corso Integrato: Matematica, Statistica e Informatica Modulo: Informatica Esercizio: Convertire
Il sistema operativo. Sistema operativo. Multiprogrammazione. Il sistema operativo. Gestione della CPU
Il sistema operativo Sistema operativo Gestione della CPU Primi elaboratori: Monoprogrammati: un solo programma in memoria centrale Privi di sistema operativo Gestione dell hardware da parte degli utenti
Il Sistema Operativo. Introduzione di programmi di utilità. Elementi di Informatica Docente: Giorgio Fumera
CPU Memoria principale Il Sistema Operativo Elementi di Informatica Docente: Giorgio Fumera Corso di Laurea in Edilizia Facoltà di Architettura A.A. 2009/2010 ALU Unità di controllo Registri A indirizzi
MANUALE MOODLE STUDENTI. Accesso al Materiale Didattico
MANUALE MOODLE STUDENTI Accesso al Materiale Didattico 1 INDICE 1. INTRODUZIONE ALLA PIATTAFORMA MOODLE... 3 1.1. Corso Moodle... 4 2. ACCESSO ALLA PIATTAFORMA... 7 2.1. Accesso diretto alla piattaforma...
Laboratorio di Informatica
Laboratorio di Informatica SOFTWARE Francesco Tura [email protected] 1 Le componenti del calcolatore: HARDWARE E SOFTWARE HARDWARE parti che compongono fisicamente il calcolatore componente multifunzionale
Software di base. Corso di Fondamenti di Informatica
Dipartimento di Informatica e Sistemistica Antonio Ruberti Sapienza Università di Roma Software di base Corso di Fondamenti di Informatica Laurea in Ingegneria Informatica (Canale di Ingegneria delle Reti
Airone Gestione Rifiuti Funzioni di Esportazione e Importazione
Airone Gestione Rifiuti Funzioni di Esportazione e Importazione Airone Funzioni di Esportazione Importazione 1 Indice AIRONE GESTIONE RIFIUTI... 1 FUNZIONI DI ESPORTAZIONE E IMPORTAZIONE... 1 INDICE...
01/05/2014. Dalla precedente lezione. Ruolo dei sistemi operativi. Esecuzione dei programmi
Marco Lapegna Laboratorio di Programmazione Dalla precedente lezione 6. I sistemi operativi LABORATORIO DI PROGRAMMAZIONE Corso di laurea in matematica I Sistemi Operativi Il linguaggi di programmazione
Database. Si ringrazia Marco Bertini per le slides
Database Si ringrazia Marco Bertini per le slides Obiettivo Concetti base dati e informazioni cos è un database terminologia Modelli organizzativi flat file database relazionali Principi e linee guida
Introduzione ai Sistemi Operativi
Introduzione ai Sistemi Operativi Sistema Operativo Software! Applicazioni! Sistema Operativo! È il livello di SW con cui! interagisce l utente! e comprende! programmi quali :! Compilatori! Editori di
Come funziona un sistema di elaborazione
Introduzione Cosa è un Sistema Sste aoperativo? Come funziona un sistema di elaborazione Proprietà dei Sistemi Operativi Storia dei Sistemi di Elaborazione Sistemi Mainframe Sistemi Desktop Sistemi i Multiprocessori
Registratori di Cassa
modulo Registratori di Cassa Interfacciamento con Registratore di Cassa RCH Nucleo@light GDO BREVE GUIDA ( su logiche di funzionamento e modalità d uso ) www.impresa24.ilsole24ore.com 1 Sommario Introduzione...
Sistemi Operativi. Processi GESTIONE DEI PROCESSI. Concetto di Processo. Scheduling di Processi. Operazioni su Processi. Processi Cooperanti
GESTIONE DEI PROCESSI 4.1 Processi Concetto di Processo Scheduling di Processi Operazioni su Processi Processi Cooperanti Concetto di Thread Modelli Multithread I thread in diversi S.O. 4.2 Concetto di
Scopo della lezione. Informatica. Informatica - def. 1. Informatica
Scopo della lezione Informatica per le lauree triennali LEZIONE 1 - Che cos è l informatica Introdurre i concetti base della materia Definire le differenze tra hardware e software Individuare le applicazioni
Organizzazione Monolitica
Principali componenti di un sistema Applicazioni utente Interprete di comandi (shell) Interfaccia grafica (desktop) Gestore del processore / Scheduler(s) Gestore della memoria Gestore delle periferiche/
1) GESTIONE DELLE POSTAZIONI REMOTE
IMPORTAZIONE ESPORTAZIONE DATI VIA FTP Per FTP ( FILE TRANSFER PROTOCOL) si intende il protocollo di internet che permette di trasferire documenti di qualsiasi tipo tra siti differenti. Per l utilizzo
Contenuti. Visione macroscopica Hardware Software. 1 Introduzione. 2 Rappresentazione dell informazione. 3 Architettura del calcolatore
Contenuti Introduzione 1 Introduzione 2 3 4 5 71/104 Il Calcolatore Introduzione Un computer...... è una macchina in grado di 1 acquisire informazioni (input) dall esterno 2 manipolare tali informazioni
Sistemi Operativi GESTIONE DELLA MEMORIA CENTRALE. D. Talia - UNICAL. Sistemi Operativi 6.1
GESTIONE DELLA MEMORIA CENTRALE 6.1 Gestione della Memoria Background Spazio di indirizzi Swapping Allocazione Contigua Paginazione 6.2 Background Per essere eseguito un programma deve trovarsi (almeno
ISTVAS Ancona Introduzione ai sistemi operativi Tecnologie Informatiche
ISTVAS Ancona Introduzione ai sistemi operativi Tecnologie Informatiche Sommario Definizione di S. O. Attività del S. O. Struttura del S. O. Il gestore dei processi: lo scheduler Sistemi Mono-Tasking e
Università di Roma Tor Vergata Corso di Laurea triennale in Informatica Sistemi operativi e reti A.A. 2013-14. Pietro Frasca.
Università di Roma Tor Vergata Corso di Laurea triennale in Informatica Sistemi operativi e reti A.A. 2013-14 Pietro Frasca Lezione 11 Martedì 12-11-2013 1 Tecniche di allocazione mediante free list Generalmente,
Sistema operativo: Gestione della memoria
Dipartimento di Elettronica ed Informazione Politecnico di Milano Informatica e CAD (c.i.) - ICA Prof. Pierluigi Plebani A.A. 2008/2009 Sistema operativo: Gestione della memoria La presente dispensa e
Sistemi Operativi Kernel
Approfondimento Sistemi Operativi Kernel Kernel del Sistema Operativo Kernel (nocciolo, nucleo) Contiene i programmi per la gestione delle funzioni base del calcolatore Kernel suddiviso in moduli. Ogni
HARDWARE. Relazione di Informatica
Michele Venditti 2 D 05/12/11 Relazione di Informatica HARDWARE Con Hardware s intende l insieme delle parti solide o ( materiali ) del computer, per esempio : monitor, tastiera, mouse, scheda madre. -
SISTEMI DI ELABORAZIONE DELLE INFORMAZIONI
SISTEMI DI ELABORAZIONE DELLE INFORMAZIONI Prof. Andrea Borghesan venus.unive.it/borg [email protected] Ricevimento: martedì, 12.00-13.00. Dip. Di Matematica Modalità esame: scritto + tesina facoltativa 1
Introduzione alla programmazione in C
Introduzione alla programmazione in C Testi Consigliati: A. Kelley & I. Pohl C didattica e programmazione B.W. Kernighan & D. M. Ritchie Linguaggio C P. Tosoratti Introduzione all informatica Materiale
Sistemi Operativi. Scheduling della CPU SCHEDULING DELLA CPU. Concetti di Base Criteri di Scheduling Algoritmi di Scheduling
SCHEDULING DELLA CPU 5.1 Scheduling della CPU Concetti di Base Criteri di Scheduling Algoritmi di Scheduling FCFS, SJF, Round-Robin, A code multiple Scheduling in Multi-Processori Scheduling Real-Time
Sistemi Operativi SCHEDULING DELLA CPU. Sistemi Operativi. D. Talia - UNICAL 5.1
SCHEDULING DELLA CPU 5.1 Scheduling della CPU Concetti di Base Criteri di Scheduling Algoritmi di Scheduling FCFS, SJF, Round-Robin, A code multiple Scheduling in Multi-Processori Scheduling Real-Time
MANUALE DI UTILIZZO: INTRANET PROVINCIA DI POTENZA
MANUALE DI UTILIZZO: INTRANET PROVINCIA DI POTENZA Fornitore: Publisys Prodotto: Intranet Provincia di Potenza http://www.provincia.potenza.it/intranet Indice 1. Introduzione... 3 2. I servizi dell Intranet...
Università di Roma Tor Vergata Corso di Laurea triennale in Informatica Sistemi operativi e reti A.A. 2013-14. Pietro Frasca.
Università di Roma Tor Vergata Corso di Laurea triennale in Informatica Sistemi operativi e reti A.A. 2013-14 Pietro Frasca Lezione 3 Martedì 15-10-2013 1 Struttura ed organizzazione software dei sistemi
