Anda di halaman 1dari 142

INTRODUZIONE ! Un Sistema Operativo un insieme di programmi con lo scopo di: ! !

! gestire in maniera ottimale le risorse (CPU, RAM, device, schede, ...) ! ! facilitare lutilizzo del PC a utenti e programmatori Application Programs System Programs Hardware ! ! ! ! ! ! AP: banche, linee aeree, applicazioni web. SP: compilatori, editor, interpreti. Programmatori SP: linguaggio C, Assembler, Programmatori AP: strumenti di sviluppo di alto livello (da java in su). Il livello SP richiede una buona conoscenza dellHW mentre per il livello AP tale conoscenza spesso non necessaria.

KERNEL MODE ! Il SO deve gestire tutte le operazioni che vengono eseguite sul computer e deve quindi ! essere il Dio stesso della macchina. Per fare questo il SO eseguito in Kernel Mode. ! Il KM un particolare tipo di esecuzione del processore che consente lesecuzione di ! tutte le istruzioni della CPU, comprese le istruzioni privilegiate. ! Un programma che non opera in KM non pu eseguire istruzioni privilegiate ma pu ! rivolgersi al SO richiedendone lesecuzione. ! Il SO lunico programma che si esegue in KM, tutti gli altri programmi operano in User ! Space; per questo motivo tutte le operazioni privilegiate vengono eseguite solo ed ! esclusivamente dal SO. ! SW di sistema = SW di base (Ne fanno parte il SO, i driver, compilatori, interpreti, ...) ! Diversamente dagli AP il SO fortemente dipendente dallHW.

Banche Compilatori

Linee aeree Editors Sistema Operativo

Web applications Interpreti

Application Programs System Programs

Linguaggio Macchina Micro Architettura Devices Fisici ! ! ! ! ! ! Hardware

Il SW di base deve eseguire i seguenti compiti: ! permettere allutente lutilizzo del computer e delle sue componenti. ! gestire le risorse del sistema. ! facilitare luso della macchina ad utenti e programmatori. Il SW applicativo deve soddisfare le esigenze degli utenti nali.
1 di 142

INDICI PRESTAZIONALI ! Tempo di Turnaround: ! tempo trascorso dalla sottoposizione di un processo al sistema ! ! ! (Job) no al termine della sua esecuzione. ! Execution Time: ! quantit di tempo impiegata dalla CPU per eseguire un processo. ! ! ! Questa misura esclude i tempi dovuti ai device e dipendenti ! ! ! dallinterazione uomo-macchina. ! Tempo di risposta:! ! ! ! tempo intercorso tra lesecuzione di un processo e la generazione delloutput.

! Utilizzo CPU: ! ( Execution Time ) / T ! ! ! Dove T un intervallo di tempo. ! Troughput nellintervallo T:! ( #Processi eseguiti ) / T

EVOLUZIONE DEI CALCOLATORI I Generazione ! La programmazione avveniva direttamente in linguaggio macchina utilizzando ! inizialmente dei li (Mark-1) e successivamente degli switch. Non vi era presenza di ! SO. Questa generazione poco efciente: utilizzo CPU = 1/20 = 5% (un utente ogni 20 ! minuti). La memoria era completamente a disposizione dellutente nale. Memoria Vuota Programma

II Generazione ! Si cerc di velocizzare il processo di caricamento del programma nel calcolatore (input) ! utilizzando delle schede forate. Tali schede potevano essere generate ofine riducendo ! cos il tempo di input ottenendo un utilizzo di CPU = 33/60 = 55%. ! Per poter utilizzare le schede era per necessario un programma in grado di: ! ! leggere le schede ! ! interpretare i comandi ed eseguirli ! ! copiare il contenuto delle schede in memoria ! ! lanciare lesecuzione del programma ! Questo programma, che risiedeva permanentemente in memoria, stato il primo ! esempio di SO. Memoria Vuota Programma SO
2 di 142

! ! ! ! ! ! ! !

Sistemi batch: sono basati su nastri magnetici che permettono di velocizzare lI/O. Sia il programma che loutput venivano messi su nastri magnetici e questi nastri venivano letti per caricare il programma e per stampare loutput. Uso della CPU = 55/60 = 91% Lo svantaggio di questi sistemi era che il tempo di risposta era ancora troppo elevato ed in quegli anni diventava sempre pi frequente lutilizzo di I/O, ovvero il processore passava la maggior parte del tempo a gestire le periferiche. (passaggio da CPUbound ad I/O-bound)

III Generazione: Multiprogrammazione. ! La CPU non attende pi che sia disponibile il dato del processo in esecuzione ma ! procede con lesecuzione di altri processi. Quindi la CPU deve essere in grado di ! eseguire diversi programmi contemporaneamente. ! Questi sistemi prevedono la coesistenza di due o pi programmi in memoria: questo ! perch se si dovesse prelevare il secondo programma da disco, mentre la CPU attende ! un dato dal disco, si dovrebbe comunque aspettare la risposta della periferica riferita al ! primo programma eseguito nellordine temporale. Memoria Vuota Job 4 Job 3 Job 2 Job 1 SO ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! Il SO si deve occupare della gestione dellesecuzione di due o pi processi simultaneamente. Nei sistemi multiprogrammati la CPU si occupa di un solo programma alla volta anche se si ha limpressione di avere un sistema totalmente dedicato per ogni operazione che si esegue. La CPU si svincola dallesecuzione di un processo demandando alla periferica specica lesecuzione delloperazione richiesta e sospendendo temporaneamente il processo. A questo punto la CPU pu dedicarsi ad un altro processo. Quando la periferica ha terminato lesecuzione del comando invia un segnale alla CPU: tale segnale viene detto interrupt. Un interrupt un segnale elettrico inviato da un dispositivo esterno al controllore della CPU. Per fare si che questo sistema funzioni sono necessari: ! routine di gestione degli interrupt ! moduli per la gestione dei programmi sospesi e di quelli da eseguire ! moduli per la gestione delle periferiche ! moduli per la gestione della memoria Queste funzionalit devono essere accorpate nel SO inoltre per accorgersi della presenza di un interrupt necessario modicare il ciclo della CPU: IF interrupt THEN {...} ELSE FETCH DECODE EXECUTE

3 di 142

! ! ! ! ! ! ! ! ! ! ! !

Sistemi Time-Sharing: il processore dedica lo stesso lasso di tempo ad ogni processo presente nel sistema. Il tempo dedicato detto quanto di tempo. Lutente ha limpressione di avere un sistema dedicato. Anche i sistemi monoprogrammati possono essere basati su Time-Sharing per ogni volta lo stato del processo deve essere salvato su disco per evitare la perdita dei dati quando si carica il secondo processo. Sistemi Real-Time: sono sistemi che devono garantire lesecuzione di determinate istruzioni entro uno specico lasso di tempo. Esistono delle tecniche che permettono di scrivere codice molto veloce. I sistemi Real-Time devono poi essere testati in termini di tempo. ! Hard Real-Time: i vincoli di tempo devono essere assolutamente rispettati. ! Soft Real-Time: esistono vincoli di tempo ma ammesso violarli entro ! determinati limiti.

IV Generazione ! Sono i sistemi pi moderni e non introducono cambiamenti sostanziali ma nascono ! principalmente per rendere il computer maggiormente user-friendly. Sono sistemi ! generalmente di tipo Time-Sharing, di facile utilizzo e installabili da utenti poco esperti. ! Viene introdotta linterfaccia utente a nestre, nasce la rete e con essa le varie ! componenti HW e SW necessarie. FUNZIONALIT DEL SO Inizializzazione del sistema. Gestione dei processi. ! Un processo una qualunque attivit svolta dal sistema operativo ed sempre ! riconducibile allesecuzione di un programma. Un processo caratterizzato da tre ! componenti: data, testo e stack. Memoria Stack

Data Text

! Riguardo ai processi il SO deve permetterne la comunicazione e la sincronizzazione. Gestione della memoria. ! La memoria centrale un enorme array di byte utilizzato per la memorizzazione ! temporanea dei dati e dei programmi in esecuzione. E direttamente accessibile dalla ! CPU. Le attivit di memory management svolte dal SO sono: ! ! tenere traccia delle porzioni di memoria usate e di chi le sta utilizzando. ! ! decidere quali attivit svolgere quando si libera memoria. ! ! allocazione/deallocazione della memoria. ! Secondary Storage Management: memoria secondaria utile a preservare le ! informazioni da elaborare (tipicamente i dischi).
4 di 142

! ! ! !

Riguardo al disk management il SO deve: ! allocare spazio quando richiesto. ! gestire gli spazi liberi. ! gestire le richieste di accesso da parte dei processi

Gestione delle periferiche di I/O ! Il SO deve predisporre le primitive per il trasferimento dei dati, deve ottimizzare ! lacquisizione ed il trasferimento dei dati e deve provvedere alla gestione delle ! periferiche connesse. Gestione dei le. ! Un le una collezione di dati opportunamente organizzati. Sono di competenza del ! File System: ! ! la creazione/cancellazione di le/directory. ! ! la gestione delle primitive per la manipolazione di le/directory. ! ! la protezione di le/directory. ! ! il trasferimento dei le da e verso i dispositivi di memoria. Protezione ! Per protezione si intende linsieme di meccanismi per controllare quale utente/processo ! accede ad una risorsa e con che modalit (lettura, scrittura, ). ! I meccanismi di protezione devono: ! ! distinguere tra uso autorizzato e non autorizzato delle risorse. ! ! specicare i controlli di accesso alle risorse. Interprete dei comandi COMUNICARE CON IL SO ! Lutente comunica con il SO in due modi: ! ! modalit interattiva: tramite shell. Questo metodo usato dai sistemisti che sono ! ! coloro che conoscono il SO e si occupano della sua gestione. ! ! modalit programmata: tramite system call. Metodo utilizzato dai programmatori. ! ! Astrazioni: descrizione di un processo o oggetto il cui obbiettivo rendere pi facile ! luso delloggetto o processo. Consente un pi facile utilizzo di un complesso sistema. Il ! PC non ha un ottimo livello di astrazione. Le system call sono un livello di astrazione. ! Interfaccia delle system call: sono tutte le system call messe a disposizione dal SO ! (linux ~ 300, win ~ 1000). SYSTEM CALL ! Per eseguire una system call in ambiente intel x86 esistono le istruzioni Assembly ! int/iret. Le syscall sono ci che il programmatore vede della struttura sottostante ed il ! questo senso costituiscono un interfaccia. ! Lesistenza delle system call garantisce un buon livello di sicurezza nel senso che ! tramite esse il SO pu controllare tutte le operazioni che vengono svolte nel sistema. Le ! syscall sono fondamentali per i processi che girano in user space in quanto permettono ! ad essi di accedere allHW gestito dal SO e di comunicare con il kernel.

5 di 142

User Space System calls Kernel Space HW commands HW Interrupts Up calls

! Per facilitare lutilizzo di questo sistema a syscall si dispone di un nuovo livello di ! astrazione: le librerie.

System calls Librerie Applicazioni

Kernel Space User Space

! Le librerie rendono invisibile lo strato delle syscall al programmatore in quanto si ! interfacciano di rettamente ad esse. Inoltre alcune funzioni di libreria non utilizzano ! system calls. Applicazione printf (...); printf (char *input) {! ! [...] write (...); ! [...] Libreria

Codice Kernel sys_write (...) { ! [...] }

! }

Kernel Space ! ! ! ! ! ! ! ! Le funzioni di libreria predispongono lambiente per la chiamata alla syscall e lavorano in User space (wrapper alla system call). Ad esempio la funzione printf () esegue la formattazione dei dati prima di chiamare la write (). Altrimenti possibile chiamare la syscall senza ulteriori elaborazioni sui dati o ancora non chiamarla affatto (come ad esempio la funzione atoi ()). Lesistenza delle librerie permette un efciente mantenimento dellintrecciamento tra le applicazioni e le syscalls: in caso modica di una system call sar necessario modicare solo le funzioni di libreria che ne fanno uso e non le applicazioni.

6 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

API: Application Programming interface, linsieme delle interfacce di facile uso utilizzate per la stesura di programmi applicativi. Le applicazioni fanno riferimento a queste interfacce e non alle syscall direttamente. La stessa API pu esistere su sistemi diversi e fornisce in questo senso una buona portabilit del codice. Ad esempio, su due sistemi diversi ma con la stessa API il codice sar compilabile su entrambe le macchine, inoltre se larchitettura identica sar portabile anche leseguibile. POSIX: lAPI pi diffusa nel mondo unix. LibC: interfaccia delle syscalls pi diffusa in ambiente linux. Per denire una syscall occorre specicare: ! uno o pi argomenti di input. ! un valore di ritorno, indice del corretto esito dellelaborazione della syscall. ! un comportamento ben denito Ad ogni system call assegnato un valore numerico ed il SO riconosce le syscalls grazie a tale valore e non per nome. Il numero di una system call immutabile e se viene rimossa il suo ID numerico non potr essere riutilizzato. System call handler: procedura a livello SO che smista le chiamate ed esegue diversi controlli sulle system calls (parametri, correttezza chiamata, sicurezza ...).

Call Read()

Read() wrapper

System_call()

Sys_read()

Applicazione

Libreria C

Syscall Handler

User Space Kernel Space ! ! Parameter passing: i parametri di una syscall vengono scritti nei registri della CPU ! prima di effettuare la chiamata e se sono richiesti pi argomenti del numero di registri ! disponibile si utilizza lo stack. Il valore di ritorno viene restituito tramite il registro eax. ! ! ! ! ! ! Una syscall di fondamentale importanza la fork(): la fork genera un nuovo processo partendo dal processo padre che lha invocata. Per fare questo duplica in memoria la struttura dati del processo padre (Codice, data e stack) generando cos un nuovo processo. La computazione dei due processi vincolata dal valore di ritorno della fork(), la quale restituisce un valore diverso da zero nel processo padre (il PID del glio), mentre restituisce zero al processo glio.

7 di 142

STRUTTURA DEI SO Monolitici ! Sono SO costituiti da un insieme di moduli distinti senza alcune relazione gerarchica. ! Ogni modulo pu chiamare ogni altro modulo e tutti i moduli possono accedere ai dati i ! quali sono presenti nellarea kernel e sono condivisi. E un sistema semplice ed ! efciente in termini di esecuzione in quanto le routine appartengono tutte allo stesso ! programma e condividono la memoria. Layered/Straticati ! Esiste una gerarchia tra i moduli: sono divisi in livelli identicato da un valore numerico ! e ogni modulo di livello N potr accedere solo a funzionalit e dati del livello ! sottostante. Le funzioni correlate fra loro secondo il loro compito vengono raggruppate ! nel medesimo livello. Il livello zero lHW control. ! In questo modo il sistema pi facilmente manutenibile e di maggiore portabilit, perde ! per in efcienza rispetto al monolitico. Microkernel ! Un microkernel un kernel che contiene solo i servizi indispensabili per il ! funzionamento del sistema: ! ! gestore dei processi. ! ! comunicazione tra processi. ! ! gestore dei messaggi. ! Tutto il resto (gestione memoria, le system, gestore nestre, ...) viene considerato ! come processo utente e questi processi vengono eseguiti in maniera indipendente luno ! dallaltro. Solo il microkernel gira in kernel mode. ! Questo sistema molto leggero, facilmente manutenibile e personalizzabile ma ! svantaggioso in termini prestazionali. Ad esempio immaginiamo di dover trasferire un ! dato dalla memoria centrale al disco: ! ! Write() syscall File System Memory Management FS scrittura su disco ! ! ! ! ! ! ! ! ! Infatti in sistemi microkernelizzati come Minix, il FS ed il MM sono programmi totalmente distinti che per comunicare necessitano di eseguire richieste al microkernel passando da UM a KM (il che pu essere unoperazione molto costosa). I sistemi microkernelizzati vengono anche detti client-server OS per via del fatto che i servizi non fondamentali del SO sono indipendenti ma possono operare fra loro, o con le applicazioni dellutente, realizzando una sorta di sistema client server. In questo modello o servizi del SO sono i server mentre le applicazioni utente sono i client. I sistemi microkernelizzati vengono utilizzati per sistemi dedicati in cui le prestazioni non sono fondamentali.

Virtualizzazione ! Un VMM uno strato di SW che si pone immediatamente sopra lHW ed il cui compito ! quello di simulare, contemporaneamente, N copie dellunica macchina sica. In questo ! modo possibile disporre di diverse architetture pur possedendo una sola macchina ! sica. Le applicazioni della VMM sono dette macchine virtuali. ! E possibile eseguire pi di un SO virtuale sulla stessa macchina sica ed anche ! possibile utilizzare diversi sistemi operativi contemporaneamente sulla stessa macchina ! virtuale. Il sistema reale detto host mentre il sistema virtualizzato detto Guest.

8 di 142

Processes Kernel VM1

Processes Kernel VM2 Virtual Machine Implementation HW

Processes Kernel VM3

La virtualizzazione vantaggiosa per diversi motivi: ! ! permette un miglior sfruttamento dellHW ! ! favorisce lo sviluppo del SW (portabilit e compatibilit) ! ! sicurezza (isolamento)

PROCESSI
9 di 142

! ! ! ! !

Un sistema monoprocessore pu eseguire una sola operazione in un dato intervallo di tempo. Con lavvento della multiprogrammazione tale sistema si trova a dover gestire lesecuzione parallela di diversi processi. Nel caso di sistemi monoprocessore si parla di pseudo parallelismo mentre i sistemi multiprocessore sono in grado di eseguire processi realmente in parallelo.

C B A ! !

C B A Utilizzo CPU - sistema multiprocessore

Utilizzo CPU - sistema monoprocessore

Solitamente nei sistemi multiprocessore si dedica un processore alla gestione delle ! operazioni di I/O. ! Processo: unentit attiva che viene creata su un sistema, evolve ed inne termina. ! Quindi il ciclo di vita di un processo composto essenzialmente dalle seguenti tre fasi: ! creazione, evoluzione e terminazione. ! La creazione di un processo pu avvenire in seguito a: ! ! richiesta esplicita da parte di un utente; questo avviene ogni volta che avviamo ! ! unapplicazione. ! ! generazione a partire da un processo preesistente (funzione fork). ! ! inizializzazione allavvio del sistema. ! La terminazione di un processo pu essere dovuta a diverse ragioni: ! ! terminazione normale (exit). ! ! scadenza del tempo di permanenza nel sistema. ! ! memoria non disponibile. ! ! violazione delle protezioni. ! ! errori durante lesecuzione. ! La fase di evoluzione di un processo pu essere rappresentata dal seguente schema:

Running

Ready

Blocked
10 di 142

! ! ! ! ! ! !

Blocked: il processo sospeso (generalmente a seguito di richieste di I/O). Ready: il processo pu essere eseguito ma la CPU il quellistante dedicata ad altro. Un processo passa da Ready a Running quando la CPU disponibile e lo scheduler ha selezionato tale processo dalla coda dei processi che aspettano di essere seguiti. Un processo passa da Running a Ready se, per svariate ragioni, la CPU deve essere dedicata ad altro ed il processo non ha ancora terminato. Un processo diventa Blocked quando esegue operazioni di I/O.

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

Dispatcher: un modulo del SO che si occupa di gestire i processi in esecuzione, cio i processi che stanno richiedendo lattenzione della CPU per varie ragioni: esecuzione, I/O, terminazione, timeout, Lalgoritmo che implementa loperazione Ready Running in Linux e Windows basato su time sharing. Quando la CPU deve essere dedicata ad un altro processo il SO deve tenere traccia dello stato del sistema e memorizzare tutto ci che serve per riprendere lesecuzione del processo in un secondo momento senza che si verichino errori o perdite di dati. Il procedimento che permette questa operazione viene detto Context Switch e comprende: ! la sospensione dellattivit in corso. ! la memorizzazione dello stato del processo in predisposte strutture dati. ! lesecuzione di un altro processo. ! al termine del processo, ripristino dello stato precedentemente memorizzato. ! ripresa dellesecuzione del precedente processo dal punto in cui si era interrotto. Loperazione di context switch interrupt driven e viene eseguita nei seguenti casi: ! clock interrupt. ! I/O interrupt. ! memorY fault. ! system calls. ! eccezioni. Quindi il context switch non altro che la sospensione dellattivit in corso a favore di unaltra attivit (processo utente o SO). E unoperazione fondamentale quando esistono pi attivit ed ununica risorsa disponibile. Si dice che il context switch interrupt driven perch linterrupt lunico modo per interrompere il ciclo di esecuzione della CPU. Le syscalls e le eccezioni sono gli unici interrupt generati al livello SW. E interessante notare che per passare lesecuzione da un processo ad un altro necessario lintervento del SO e quindi i context switch che si vericano sono due: il primo per passare dal processo che deve abbandonare la CPU al SO, mentre il
11 di 142

secondo per passare il controllo dal SO al nuovo processo che deve riprendere/iniziare la sua elaborazione. Processo K

P Memoria Centrale T0+1

P Memoria Centrale

T0

T1 T1+1

P Memoria Centrale T2+1

P Memoria Centrale

T3

T2

Processo P ! ! ! ! ! ! ! ! !

SO

Questo procedimento prende il nome di Process Switch. Dal punto di vista dei processi il SO deve garantire: ! la predisposizione delle strutture dati e delle procedure necessarie per ! interrompere/riprendere lesecuzione dei processi. ! la corretta evoluzione del sistema. I processi vengono identicati nel sistema attraverso un valore numerico, detto PID, assegnato automaticamente dal SO durante la creazione del processo stesso. Nei sistemi Unix ad ogni processo assegnato anche il PPID, ovvero il PID del padre di tale processo.

CREAZIONE ! Per la creazione di un nuovo processo esiste la syscall fork() la quale segue una copia ! esatta della memoria del processo padre: codice, stack, le descriptor, variabili globali e ! program counter. Inoltre il glio riceve un nuovo PID, un tempo azzerato, no signals, le ! locks,

12 di 142

! Grazie a questa procedura, nei sistemi unix, possibile generare una gerarchia di ! processi di tipo padre-glio. In Windows questo non possibile in quanto non esiste ! una relazione di parentela tra processi. TERMINAZIONE ! Esistono diversi tipi di terminazione per un processo: ! ! normale/volontaria: al termine del main() o in seguito ad una exit(). ! ! per errore: exit(2) oppure abort(). ! ! per errore imprevisto: divisione per zero, segmentation fault, ! ! killed: signal kill(PID). ! Quando un processo termina fondamentale eseguire se seguenti operazioni: ! ! chiudere i le aperti. ! ! eliminare i le temporanei. ! ! deallocare le risorse del processo e quelle condivise con i processi gli (questo ! ! perch si suppone che ormai non servano pi ai processi gli). ! ! il processo padre deve essere noticato via signal. ! ! lo stato di terminazione (exit status) disponibile al padre attraverso la syscall ! ! wait(). ! Wait(): blocca il processo padre no alla terminazione di qualche glio. Restituisce il ! PID del glio che ha terminato e lo exit status. ! Waitpid(): attende la terminazione di uno specicato glio. STRUTTURE DATI ! Per gestire i processi il SO utilizza una struttura dati chiamata Process Control Block, ! (PCB). Tale struttura allocata allavvio di un processo e viene deallocata alla sua ! terminazione, inoltre unica e distinta per ogni processo in esecuzione. ! Durante un context switch i dati del processo in esecuzione vengono memorizzati nei ! campi del PCB corrispondente a quel processo in modo da preservarne lo stato. Il PCB ! una sorta di istantanea del processo che permette di riprendere lesecuzione senza ! errori o dati corrotti. ! I principali campi del PCB sono i seguenti: ! ! ID processo (PID). ! ! PPID. ! ! ID utente proprietario del processo. ! ! stato del processore: ! ! ! registri del processore. ! ! ! program counter. ! ! ! condition code (ulteriori registri). ! ! ! variabili di stato. ! ! ! stack pointer associato al processo. ! ! ! Program Status Word (PSW): registro che contiene le modalit esecuzione ! ! ! di alcune istruzioni. ! ! Process Control Information: ! ! ! process state (Ready, Running, Waiting, Halted). ! ! ! priorit di scheduling. ! ! ! info per lalgoritmo di scheduling.! ! ! ! ID evento atteso dal processo. ! ! ! campi di strutture dati: puntatori utili quando il PCB in lista di attesa. ! ! ! variabili per la comunicazione tra processi. ! ! ! privilegi del processo (memoria, risorse, ...). ! ! ! gestione della memoria (pagine/segmenti). ! ! ! risorse utilizzate (le aperti/creati).
13 di 142

PCB1

PCB2

PCB3

PCB4

Pointer

Pointer

Pointer

Pointer

Ready Queue I/O disk Quelle I/O keyboard Queue

! ! ! ! ! ! ! ! ! !

La gestione dei PCB durante un Context Switch avviene nel seguente modo: ! salva lo stato del processore. ! aggiorna il PCB del processo in esecuzione. ! sposta il PCB del processo in esecuzione nella coda adeguata. ! il SO predispone lambiente per proseguire lesecuzione: si seleziona un altro ! processo da eseguire. ! se si sta eseguendo un Process Switch: ! ! aggiorna il PCB del processo selezionato. ! ! aggiorna le strutture di gestione della memoria. ! ! ripristina il processore con i dati del processo selezionato.

14 di 142

THREADS ! I threads sono delle sottoattivit di un processo che godono della propriet di poter ! essere eseguite in parallelo. In questo caso il termine parallelo indica che i threads ! possono essere eseguiti in maniera indipendente luno dallaltro in quanto non esistono ! vincoli tra di essi. Quindi i threads sono dei sottoprocessi ottenuti dalla scomposizione ! di altri processi. Processo K Threads 0 Threads 1 Threads 2 Threads 3 Threads 4 ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! In un sistema multithread le queue di sistema conterranno sia processi (PCB) che threads. Lintroduzione dei threads permette di velocizzare lesecuzione di un processo: ad esempio, senza i threads un processo che ha eseguito una richiesta di I/O rimarrebbe bloccato sulla coda di uno specico device anche se potenzialmente potrebbe continuare lelaborazione di dati che non dipendono da quella richiesta. Utilizzando i threads invece possibile permettere alla sezione di elaborazione di proseguire nella coda Ready mentre unaltra sezione di codice (un altro thread) attende la risposta del device di I/O. In questo modo un processo minimizza lo spreco di tempo durante i suoi tempi morti. I threads di uno stesso processo condividono: ! PID. ! spazio degli indirizzi (codice, dati non locali). ! variabili globali. ! les aperti. ! aree dati (lettura/scrittura). ! signals e signal handler. ! current working directory. ! user ID. ! group ID. Non condividono: ! thread ID. ! linsieme dei registri del processore, compresi Program Coulter e Stack Pointer. ! stack per le variabili locali e record di attivazione.! ! ! maschera signals: metodo tramite cui il SO comunica con il processo. Possono ! essere importante dal programmatore. Solitamente il Context Switch fra threads molto pi veloce di quello fra processi; questo perch il Context Switch di un threads fatto quasi interamente a livello HW ( necessario salvare i registri del processore e poche altre cose), inoltre lambiente di esecuzione di un threads molto ridotto e quindi le cose da salvare sono in quantit minore rispetto ad un normale processo.

15 di 142

IMPLEMENTAZIONI ! Modello 1 - Threads in User Space. ! In questo modello il SO ignora la presenza dei threads e lo considera come dei normali ! processi. Il kernel schedula tutto come se stesse trattando esclusivamente dei processi ! e solo successivamente i threads vengono schedati come threads a runtime. In questo ! metodo il threads viene implementato al livello utente mediante delle apposite librerie ! ed possibile specicare la politica di scheduling pi adeguata per quel determinato ! processo. Inoltre ogni processo che implementa i threads in user space deve disporre ! di una tabella dei threads, ovvero una struttura dati che possa contenere le informazioni ! riguardo a tutti i threads che compongono il processo. La gestione della tabella ! analoga alla gestione della tabella dei processi svolta dal kernel ma per i threads il ! procedimento decisamente pi veloce. ! Questa struttura ad alto livello permette una buona portabilit in quanto non richiesto ! alcun particolare intervento del SO ed inoltre non si fa uso di syscall e questo rende il ! procedimento molto veloce. I principali svantaggi di questo metodo consistono ! nellimpossibilit di sfruttare adeguatamente lHW e nellimpossibilit di proseguire ! lelaborazione di un processo quando uno dei threads si blocca. Entrambi questi ! problemi sono dovuti al fatto che il SO non in grado di riconoscere i threads.

! ! ! ! ! ! ! ! ! ! ! ! ! !

Modello 2 - Threads in Kernel Space. Il SO associa ad ogni thread in User Space un thread a livello kernel. Ogni kernel thread schedulato indipendentemente ed il SO in grado di riconoscere i threads. Tutte le operazioni sui threads vengono eseguite dal SO operations. Non pi necessario disporre di una tabella dei threads in user space, associata a ogni processo, ma sufciente che tale tabella sia situata nello spazio del kernel e venga gestita direttamente dal kernel. Il kernel dispone anche della tabella dei processi, che solitamente contiene maggiori informazioni rispetto alla tabella dei threads (questo perch per quanto riguarda i threads sufciente memorizzare solo lo stato del thread). un sistema vantaggioso per quanto riguarda lo sfruttamento dellHW ed inoltre un processo pu proseguire la sua esecuzione anche se uno dei threads viene bloccato. I principali svantaggi sono dovuti al fatto che le operazioni sui threads sono costose (overhead) perch sono svolte mediante chiamate di sistema ed inoltre il SO deve essere in grado di gestire molti threads.

16 di 142

! ! ! ! ! ! ! !

Modello 3 - Implementazione ibrida. Questo modello estrapola i vantaggi dei due modelli precedenti ma rende il sistema un po pi complicato. Il procedimento di gestione il seguente: lapplicazione genera M user threads mentre il SO fornisce N kernel threads, ad ogni kernel threads vengono associati uno o pi user threads. Il kernel si occuper solo di amministrare i kernel threads, ignorando i threads a livello utente. Il principale svantaggio di questa implementazione la determinazione del numero di kernel threads da generare (viene denito dal programmatore o lo gestisce dinamicamente il SO).

INTERPROCESS COMUNICATION (IPC) ! Per poter cooperare in maniera efciente i processi ed i threads devono essere in grado ! di comunicare fra loro. Questa funzionalit pu essere svolta utilizzando i seguenti tre ! metodi: ! ! Shared Memory. ! ! Message Passing. ! ! Signals. ! Shared Memory: dato che ogni processo ha un proprio spazio di indirizzamento ! indipendente da quello di ogni altro processo, necessario che il SO fornisca una ! porzione di memoria condivisa tra tutti i processo che devono comunicare. Per quanto
17 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

riguarda i threads questo metodo risulta molto semplice da implementare, infatti i threads condividono gi le sezioni di memoria del processo cui appartengono. Il protocollo di comunicazione molto semplice: lentit, processo o thread, che deve comunicare, scrive il suo messaggio nella memoria condivisa e tutti coloro che condividono la stessa sezione di memoria possono leggere. Questa tecnica vantaggiosa in quanto fornire una memoria condivisa unoperazione abbastanza semplice per i processi e implicitamente contenuta nella denizione di thread, ma comporta due svantaggi principali: la concorrenza sulla sezione di memoria condivisa, e la necessit di sincronizzare precisamente i processi e i threads. Message Passing: i dati vengono trasmessi dal processo sender al processo receiver in maniera esplicita (ES: pipe di Unix). Questa tecnica evidenzia i legami trai vari moduli in quanto la condivisione dei dati esplicita per crea diversi problemi: ! gestione di diverse tipologia di sender/receiver (processo, insieme di processi ..). ! overhead dovuto alla copia dei messaggi. ! bloccare il sender in attesa del receiver rallenta il mittente. ! non bloccare il sender in attesa del receiver impone lutilizzo di un buffer. Signals: la signal sono interrupt software che servono a noticare ad un processo il vericarsi di un determinato evento. Il processo che riceve una signal pu: ! catch: specicare una routine (signal handler) per la gestione della signal. ! ignore: eseguire unoperazione standard prevista dal SO. ! mask: bloccare il segnale afnch non venga consegnato. Questo metodo non permette di comunicare dati ed di difcile gestione con i threads: il SO deve sapere a quale threads del del processo consegnare la signal. Un metodo per gestire questo problema pu essere il seguente: il sender specica il TID del destinatario ma non detto che il sender conosca gli ID degli altri threads del processo; in secondo metodo consiste nel consegnare la signal a tutti i threads in grado di riceverla. possibile denire se un threads in grado o meno di ricevere determinati tipi di signal modicando la signal mask, ovvero una maschera di bit propria di ogni thread.

18 di 142

SCHEDULING ! Dal punto di vista dellesecuzione ogni processo si divide in burst di CPU e tempi di I/O. ! ! Processi CPU-bound: lunghe e frequenti computazioni alternate a poco frequenti ! ! operazioni di I/O. ! ! Processi I/O-bound: alternano brevi computazioni a frequenti attivit di I/O. I ! ! tempi di burst si riducono sempre pi con laumentare delle prestazioni dei ! ! processori. ! Per evitare spreco di tempo di CPU durante lI/O di un processo necessario mettere in ! wait (o blocked) tale processo. Un nuovo processo viene selezionato dalla Ready ! queue e potr essere eseguito. Queste attivit sono svolte dallo scheduler. ! Lo scheduler un modulo del SO che sceglie quale processo eseguire tra quelli ! presenti nelle varie code di sistema. Otre a questo lo scheduler deve garantire un ! efciente uso della CPU perch il context switch comunque unoperazione costosa. ! Lo scheduler viene invocato se: ! ! stato creato un nuovo processo e si deve decidere se eseguire il processo ! ! padre o il processo glio. ! ! il processo in esecuzione ha terminato. ! ! il processo in esecuzione ha eseguito una richiesta di I/O ed stato messo in ! ! wait, rilasciando la CPU e rendendola disponibile per un altro processo. ! ! quando un processo viene bloccato per una qualsiasi ragione (ES: semafori). ! ! a seguito di uninterruzione di I/O, ovvero quando i device di I/O comunicano al ! ! processore di aver terminato il loro lavoro e di essere pronti per servire uno dei ! ! processi sulla loro coda. ! A seconda del motivo dinvocazione e delle funzioni svolte, esistono diversi tipi di ! scheduling: ! ! Long-term scheduling: decisione di aggiungere un processo alla Ready queue. ! ! Medium-term scheduling: consente il caricamento dellapplicazione in memoria. ! ! Short-term scheduling: decisione di assegnare alla CPU un processo tra quelli ! ! disponibili. ! ! I/O scheduling: consente ad uno dei processi in coda di essere servito dal device ! ! di I/O per il quale aveva fatto richiesta. ! ! SWAP area: area del disco rigido utilizzata dallo scheduler del SO per depositare i ! processi in coda (sospesi), liberando in questo modo la memoria centrale. SHORT-TERM SCHEDULING ! lo scheduler invocato pi di frequente e viene anche detto Dispatcher o CPU ! scheduler. Viene invocato quando si verica uno dei seguenti eventi: ! ! clock interrupt. ! ! I/O interrupt. ! ! OS syscall. ! ! signal. ! In pratica lo short-term scheduler viene invocato ogni volta che un processo cambia il ! suo stato da Ready a Running e viceversa.

Ready

Running

19 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

Il dispatcher decide: ! quando eseguire un processo: ! ! creazione di un nuovo processo. ! ! terminazione di un processo. ! ! sospensione di un processo attivo. ! ! interruzione di un processo attivo. ! ! esaurimento del quanto di tempo assegnato. ! quale processo eseguire. ! per quanto tempo lasciarlo in esecuzione. Lo scheduler pu essere progettato secondo due approcci: ! User Oriented:! privilegia il tempo di risposta (tempo tra la sottomissione del job ! ! ! ! al sistema ed il primo output). ! System Oriented: privilegia luso efciente delle risorse del sistema. Gli obbiettivi generali nella progettazione di uno scheduler sono: ! fairnes/imparzialit: lo scheduler deve garantire a tutti i processi di poter ! ! ! ! accedere alla CPU prima o poi. ! policy enforcement. ! bilanciamento: evitare di intasare un device lasciando in attesa gli altri. Bilancia ! ! ! luso delle componenti di un sistema. Gli obbiettivi particolari dei sistemi batch sono: ! troughput: massimizza il numero di lavori per ora. ! turnaround: minimizza il tempo tra la sottomissione e la terminazione. ! CPU usage: mantiene la CPU occupata il maggior tempo possibile. Gli obbiettivi particolari dei sistemi interattivi sono: ! tempo di risposta: rispondere alla chiamate velocemente. ! proportionality: incontro alle aspettative dellutente. Gli obbiettivi particolari dei sistemi real-time sono: ! scadenze: impedire la perdita dei dati. ! prevedibilit: impedire la degradabilit.

ALGORITMI DI SCHEDULING ! Gli algoritmi di scheduling si divino in due categorie: ! ! Preemptive (con diritto di prelazione): prevedono la possibilit di interrompere ! ! momentaneamente (sospendere) il processo in esecuzione per eseguire altri ! ! processi. ! ! Non preemptive: il processo in esecuzione mantiene lutilizzo della CPU no alla ! ! sua terminazione o no ad una richiesta di I/O. FCFS: First Come First Served. ! un semplice algoritmo non preemptive. caratterizzato da un wait time difcilmente ! ottimale e molto variabile in quanto dipende dai tempi di esecuzione dei processi in ! coda. Non adatto nei sistemi interattivi. ! ES1: ! ! P1 arriva a t=12 e ha burst=24. ! ! P2 arriva a t=0 e ha burst=3. ! ! P3 arriva a t=10 e ha burst=3. ! ! Indichiamo di rosso il tempo di attesa del processo.
20 di 142

! ! ! ! !
PROCESSI

0===3
P2

! !

! !

10===13
P3

12=13========================37
P1

TEMPO

! ! ! ! ! ! ! ! ES2: ! ! ! ! ! ! ! ! ! ! ! ! !

P2 attende t=0 P3 attende t=0! P1 attende t=1

==> Avarage Wait Time (AWG) = (0+0+1)/3 = 0,33

Avarage turnaround = (3+3+25)/3 = 10,33

P1 arriva a t=0 e ha burst=24. P2 arriva a t=10 e ha burst=3. P3 arriva a t=12 e ha burst=3. Indichiamo di rosso il tempo di attesa del processo. 0========================24
P1

PROCESSI

! !

! !

10=========24===27
P2

12============27===30
P3

TEMPO

! ! ! ! ! ! ! ! ! ! ! ! !

P1 attende t=0 P2 attende t= (24-10) = 14 ! P3 attende t= (27-12) = 15

==> AWG = (0+14+15)/3 = 9,66

Avarage turnaround = (24+17+18)/3 = 19,66

Il grande svantaggio di questo algoritmo che durante lesecuzione di un processo prevalentemente CPU-bound molti altri processi, magari I/O-bound, non possono essere eseguiti. Dare la precedenza ai processi I/O-bound pu alle volte essere vantaggioso in quanto possibile sfruttare i tempi di I/O di questi processi per eseguirne altri che si trovano in Ready queue.

SJF: Shortest Job First. ! Questo algoritmo presuppone la conoscenza del tempo di esecuzione di ciascun ! processo. Tutti i processi hanno uguale importanza. La CPU viene assegnata al ! processo con tempo di esecuzione minore. Questo algoritmo garantisce il minor tempo ! di turnaround.
21 di 142

! ! ! ! ! ! ! ! ! ! !

Versione preemptive: shortest remaining time next; allarrivo di un nuovo job si rivaluta lordine di esecuzione e la scelta corrente, mentre per il processo che in esecuzione, momentaneamente sospeso, si valuta solo il tempo rimanente per terminare. A questo punto si calcola il job con minore tempo di esecuzione e si esegue il processo corrispondente. Se tale processo A in coda si mette in Ready queue il processo al quale era assegnata la CPU che, una volta liberata, potr eseguire il processo A. ES: ! ! ! ! P1 arriva a t=0 e ha burst=7. P2 arriva a t=2 e ha burst=4. P3 arriva a t=4 e ha burst=1. P4 arriva a t=5 e ha burst=4. Versione con preemptive: 0==2!!
P1

! ! ! ! ! ! ! ! ! ! ! ! ! ! !
PROCESSI

! !

! ! !

! ! ! !

! ! ! ! !

Burst P1 = (7-2) = 5 Burst P2 = (4-2) = 2 P3 ha terminato P2 ha terminato P4 ha terminato P1 ha terminato

2==4!
P2

! ! !

4=5!!
P3 P2

4=5==7! !
P4

5==7====11! !
P1

2============11=====17!
TEMPO

! ! ! ! ! ! ! ! ! ! ! ! ! ! !
PROCESSI

AWT = (9 + 1 + 0 + 2)/4 = 3 AT = (16+ 5 + 1 + 6)/4 = 7 Versione senza preemptive: 0=======7


P1

4===7=8
P3

2=======8====12
P2

! !

5=========12====16
P4

TEMPO

! ! ! !

AWT = (0 + 6 + 3 + 7)/4 = 4 AT = (7 + 10 + 4 + 11)/4 = 8

22 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

NB:! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

Il burst di un processo non calcolabile. Una algoritmo di questo tipo non implementabile su un calcolatore ma utile per calcolare il grado di bont di altri algoritmi di scheduling che non richiedono la conoscenza del burst dei processi. In poche parole forniscono un metro di paragone per determinare lefcienza di un algoritmo implementabile. In alternativa esistono tecniche di predizione del burst di un processo che consentono di stimare il tempo di esecuzione in base allesperienza passata: per ogni processo, in coda e in esecuzione, si stima il probabile tempo di burst facendo la media pesata tra gli ultimi due i tempi di burst precedentemente calcolati. A seconda del valore di peso della media possibile determinare se lalgoritmo di dimentica velocemente dei vecchi tempi di burst oppure no. ES: ! Supponiamo che lesecuzione di un comando richieda un tempo T0. Inoltre ! supponiamo di aver calcolato il burst successivo T1. Possiamo aggiornare la ! nostra stima con la media pesata: ! ! ! ! ! ! T0 + (1-a)T1 ! Consideriamo a=1/2, la successone sar: ! ! ! ! T0, ! ! ! ! T0/2 + T1/2, ! ! ! ! T0/4 + T1/4 + T2/2, ! ! ! ! T0/8 + T1/8 + T2/4 + T3/2 Questa tecnica viene chiamata invecchiamento (aging) ed molto utilizzata. In particolare si preferisce assumere a=1/2 in quanto comporta lesecuzione di divisioni per 2 che nei calcolatori sono facilmente implementabili con gli shift dei registri.

RR: Round Robin. ! Lalgoritmo RR un algoritmo preemptive dove ad ogni processo viene assegnato un ! quanto di tempo. Esaurito il quanto, se il processo non ha ancora terminato viene ! rimesso in coda. ! La scelta della durata del quanto critica: ! ! un quanto troppo breve genera molto tempo sprecato dovuto ai frequenti context ! ! switch. ! ! un quanto di tempo troppo lungo porta il RR a degenerare in un FCFS con ! ! conseguente peggioramento delle prestazioni. ! La miglior durata del quanto corrisponde ad un tempo poco pi lungo del burst di CPU: ! la prelazione si riduce e migliorano le prestazioni. ! Lalgoritmo Round Robin minimizza il tempo di risposta, ovvero il tempo intercorso tra ! lentrata del processo nel sistema e la prima volta che tale processo viene servito. Se ! un processo termina prima dellesaurimento del suo quanto di tempo viene mandato in ! esecuzione il successivo processo, senza attendere lo scadere del tempo. ! ES: ! ! Consideriamo 4 processi che arrivano tutti allo stesso istante t=0. ! ! Il quanto di tempo per ogni processo pari a 20. ! ! ! ! ! ! ! ! P1 ha burst=53 P2 ha burst=17 P3 ha burst=68 P4 ha burst=24

23 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !
PROCESSI

0==20!

! !

! ! ! !

! ! ! ! !

! ! ! ! ! !

! ! ! ! ! ! !

! ! ! ! ! ! ! !

! ! ! ! ! ! ! ! !

Burst P1 = 33 P2 ha terminato Burst P3 = 48 Burst P4 = 4 Burst P1 = 13 Burst P3 = 28 P4 ha terminato P1 ha terminato Burst P3 = 8 P3 ha terminato

0==20==37!!

0======37==57! ! 0==========57==77! ! ! ! ! ! !

20=========77==97! ! ! ! ! ! ! ! ! ! ! ! ! !

57======97==117!

77======117==121!

97=======121==134!

117=======134==154! !
TEMPO

154===162!

! ! ! !

! ! ! !

Waiting Time(P1) = (77-20) + (121-97) = 57 + 24 = 81 Waiting Time(P2) = 20 Waiting Time(P3) = 37 + (97-57) + (134-117) = 77 + 17 = 94 Waiting Time(P4) = 57 + (117-77) = 97 AWT = (81 + 20 + 94 + 97)/4 = 73

! !

SCHEDULING CON PRIORITA. ! Un esempio di scheduling con priorit lalgoritmo SJF: in questo caso lindice di priorit ! implicito ed dato dallinverso della lunghezza del burst del processo (1/CPU Burst). ! ! ! ! Code Priorit 4 ! Priorit 3 Priorit 2 Priorit 1

Processi eseguibili

24 di 142

! ! ! ! ! ! ! ! ! ! ! ! !

Questo tipo di scheduling presuppone che ad ogni processo venga assegnata una priorit in modo che lo scheduler possa selezionare quello con priorit massima e assegnargli la CPU. I processi multipli con lo stesso livello di priorit vengono trattati mediante altri algoritmi di scheduling (FCFS, RR, ...). Lo scheduler a priorit pu essere non preemptive o preemptive: in questultimo caso, quando arriva un nuovo processo in coda si valuta la priorit di tale processo e se maggiore della priorit del processo corrente si interrompe lesecuzione in favore del processo appena giunto. Un problema nellutilizzo della priorit e la starvation dei processi a priorit bassa: pu capitare che alcuni processi con priorit minima rimangano indenitamente in attesa della terminazione dei processi a priorit pi alta. Una possibile soluzione data dalla funzione di aging, grazie alla quale la priorit assegnata ad un processo varia in funzione del tempo di permanenza nel sistema e del tempo di CPU usato dal processo.

PROPRIET DI UN SO REAL-TIME. ! Determinismo: !tutte le operazioni devono essere svolte in intervalli di tempo ssati o ! ! ! ! entro limiti di tempo predeterminati. Il SO deve essere in grado di ! ! ! ! rispondere ad un interrupt entro e non oltre un certo limite di tempo. ! Context switch molto veloce. ! Capacit di rispondere molto velocemente ad interrupt esterni. ! Multitasking. ! Files in grado di memorizzare grosse quantit di dati ad alta velocit. SCHEDULING DI THREADS. ! Lo scheduling dei threads possibile sia per user threads, sia per kernel threads. ! Ad esempio dei metodi di scheduling possibili prevedono un quanto per processo di ! 50ms ma mentre i threads a livello utente lasciano solo spontaneamente la CPU (il ! sistema runtime non pu interromperli), i kernel threads lavorano a burst di CPU di 5ms.

25 di 142

LA CONCORRENZA. ! Lesecuzione contemporanea dei processi richiede una certa attenzione da parte del ! programmatore: quando esiste una dipendenza tra processi ci sono vari aspetti che non ! possono/devono essere trascurati. Inoltre fondamentale garantire un buon livello di ! comunicazione anche tra processi indipendenti. i processi devono essere in grado di ! passarsi informazioni, non devono disturbarsi fra loro quando svolgono attivit critiche ! ed inoltre necessario ordinare la loro esecuzione quando si in presenza di ! dipendenze. ! Per quanto riguarda i threads la situazione analoga; lunico aspetto differente che ! per comunicare dispongono gi di una memoria condivisa. ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! Due processi si dicono concorrenti se la loro esecuzione sovrapposta nel tempo. Lesecuzione concorrente di pi processi in un sistema monoprocessore avviene mediante interleaving delle istruzioni dei processi. Interleaving: il mescolamento delle istruzioni dei processi attivi allinterno del sistema ed molto mutevole in quanto lordine di esecuzione condizionato da molte situazioni. Non prevedibile ed praticamente impossibile riprodurre un determinato interleaving. Data limprevedibilit dellinterleaving, un sistema che supporta lesecuzione concorrente dei processi deve garantire la correttezza della loro computazione indipendentemente dallinterleaving eseguito. In altre parole, indipendentemente dallinterleaving delle sue istruzioni, il sistema deve garantire la correttezza di esecuzione di un processo. Si denisce race condition la situazione in cui due o pi processi leggono/scrivono dati condivisi ed i risultati nali dipendono dallordine di esecuzione delle istruzioni dei processi (non determinismo), ovvero dallinterleaving. ES: accredito e addebito. I processi P1 (accredito) e P2 (addebito) contengono al loro interno una struttura dati (conto) che mantiene i dati del cliente e della banca. Implementano rispettivamente le procedure accredito e addebito le quali aprono un le contenente i c.c. dei clienti, prelevano un record e mappano i dati di questo record nella struttura conto. Successivamente queste procedure eseguono laccredito/addebito modicando il campo saldo della struttura conto ed inne riscrivono i dati sul le. Supponiamo che P1 e P2 vengano eseguiti in modo concorrente e che operino sullo stesso record accreditando 500 e addebitando 300. ! P1! ! + 500 ! P2 - 300 ! ! P P1 P2

Partendo da un saldo di 1000, il saldo nale dovrebbe essere di 1200. Consideriamo la seguente esecuzione: ! P1 apre il le dei c.c. e salva i dati nella struttura conto ! P1 incrementa il campo saldo (1500) ! ne quanto di tempo per P1, parte P2 ! P2 apre il le dei c.c. E legge un saldo pari a 1000, salva i dati in conto ! P2 decrementa il campo saldo (700) ! ne quanto di tempo per P2, riparte P1 ! P1 scrive sul le i nuovi dati e termina. Il saldo di 1500 ! P1 terminato: context switch, riparte P2. ! P2 scrive sul le i suoi dati e termina. Il saldo pari a 700
26 di 142

! ! ! ! ! ! ! ! !

Il saldo nale scorretto e questo perch i processi concorrenti dipendono dallinterleaving alcuni interleaving possono generare errori durante lesecuzione. Quando due o pi processi condividono porzioni di memoria o altre risorse (les, ...) ci si pu trovare in situazioni di accesso concorrente. La porzione di codice in cui il processo tenta di accedere a risorse condivise con altri processi viene detta sezione critica. Per risolvere i problemi della concorrenza necessario che il processo che accede ad una risorsa possa bloccare tutti gli altri processi con i quali condivide la risorsa: questo meccanismo viene detto mutua esclusione.
A entra in sezione critica

! !

A
A lascia la sezione critica B entra in sezione critica

! !

B!
B cerca di entrare in sezione critica ma viene bloccato B lascia la sezione critica

! ! ! ! ! ! ! ! ! ! !

NB:!la risorsa condivisa disponibile ad un secondo processo solo quando il primo ! processo la rilascia. Due o pi processi hanno un accesso mutuamente esclusivo solo se: ! nessuna coppia di processi si trova simultaneamente in sezione critica. ! laccesso alla regione critica non regolato da alcuna assunzione temporale o sul ! numero di CPU. ! i processi che non si trovano in sezione critica non possono bloccare un processo ! che vuole entrarci. ! nessun processo deve attendere indenitamente per entrare in sezione critica. In generale la struttura di un processo che contiene una sezione critica la seguente: [ sezione non critica ] Entra nella regione [ sezione critica ] Lascia la regione [ sezione non critica ]

MUTUA ESCLUSIONE CON BUSY WAITING. ! Il metodo di busy waiting consiste nel far aspettare i processi che non possono entrare ! in sezione critica forzandoli a valutare continuamente una variabile. ! Il metodo standard per realizzare questo comportamento sfrutta un ciclo che blocca il ! processo su una o pi variabili accessibili da tutti i processi concorrenti: while (check variables) { } ! Questa istruzione prende il nome di Spin Lock.
27 di 142

STRETTA ALTERNANZA. ! Mediante una variabile condivisa, turn, si autorizza un processo, piuttosto che un altro, ! ad utilizzare la risorsa condivisa e quindi ad entrare in sezione critica. I processi ! bloccati vengono forzati ad eseguire uno spin lock per tutta la durata del loro quanto di ! tempo (se non vengono interrotti da eventi esterni). Questa struttura impone una forte ! sincronizzazione dei processi e viola il terzo punto della mutua esclusione, i processi che non si trovano in sezione critica non possono bloccare un processo che vuole entrarci ! In quanto, se i processi hanno velocit di esecuzione diverse (diverso turnaround), pu ! capitare che un processo che non si trova in sezione critica impedisca ad un altro ! processo di accedervi. Diventa fondamentale allora la velocit ed il numero di istruzioni ! che i processi eseguono in sezione critica. ! ! ! ! ! ! ! ! Processo 0! ! ! ! while (TRUE) {! ! ! ! ! while (turn != 0) {/*SpinLock*/}! ! ! sezione_critica();! ! ! ! turn = 1;! ! ! ! ! sezione_non_critica();! ! }! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! Processo 1 while (TRUE){ ! while (turn != 1) {/*SpinLock*/} ! sezione_critica(); ! turn = 0; ! sezione_non_critica(); }

! Una soluzione ai problemi della stretta alternanza consiste nellintrodurre un array, ! indicizzato dai processi che condividono la risorsa, che contiene valori booleani che ! indicano la volont dei processi di accedere alla sezione critica. ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! INTERESTED[N] ! ! consideriamo il caso con due soli processi P0 e P1: ! ! ! ! larray sar composto da due celle che potranno ! ! ! ! contenere 0 oppure 1, e saranno indicizzate dal valore ! ! ! ! identicativo dei processi. ! Entrambi i processi implementano la seguente struttura di accesso alla sezione critica: ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! void enter_region(int process){! ! ! ! ! int other; /* numero dellaltro processo */ ! other = 1 process; ! ! ! ! ! interested[process] = TRUE; ! ! ! ! while (interested[other] == TRUE);! }! SEZIONE CRITICA

! ! ! ! void leave_region (int process){ ! ! ! ! ! interested[process] = FALSE; ! ! ! ! ! } ! Ogni processo dichiara la propria intenzione di accedere alla risorsa condivisa e, nello stesso tempo, controlla se laltro processo ha dichiarato di voler entrare in sezione critica. Se anche laltro processo vuole utilizzare la risorsa il processo corrente si blocca un uno spin lock, altrimenti entra in sezione critica.
28 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

Questo algoritmo presenta per un grosso problema, consideriamo infatti la seguente esecuzione (interleaving) tra due processi P0 e P1: ! 0 1 ! larray inizialmente ! parte P0 e mette interested[0] = 1 ! ! larray diventa! 1 ! 1 e P0 entra nello spin lock ! scade il quanto di tempo per P0 e parte P1 ! parte P1 che mette interested[1] = 1 ed entra nello spin lock ! scade il suo quanto di tempo: da questo momento in poi nessuno dei due processi ! in grado di uscire dagli spin lock Un esecuzione di questo tipo genera un Dead Lock. importante sottolineare che il dead lock si verica solo sotto alcuni interleaving ed quindi praticamente impossibile da riprodurre volontariamente. Riguardo a questo algoritmo di stretta alternanza, il dead lock si verica quando entrambi i processi hanno dichiarato la propria intenzione di accedere alla sezione critica ma entrambi continuano a controllare cosa vuole fare laltro senza accedere mai. Una possibile soluzione al problema del dead lock consiste nellintrodurre delle semplici istruzioni allinterno degli spin lock che, a seconda dellinterleaving, permettono di evitare o sbloccare un dead lock. Gli spin lock diventano quindi: ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! while (interested[other] == TRUE){ ! interested[process] = FALSE; ! interested[process] = TRUE; }

evidente come questa struttura permetta di sbloccare un dead lock: la prima istruzione allinterno del ciclo modica la dichiarazione del processo correntemente in esecuzione mentre la successiva istruzione riporta la dichiarazione alla situazione originale. Questo, sotto determinati interleaving, permette al processo other di uscire dallo spin lock perch vede FALSE la cella indicizzata da process. Perch questo avvenga linterleaving deve essere tale da interrompere lesecuzione di uno dei due processi tra le due istruzioni interne al ciclo while. Questa soluzione risolve il dead lock ma introduce un nuovo problema: la starvation. Consideriamo il seguente interleaving, sempre tra due processi P0 e P1: ! P0 mette interested[0] = TRUE ! P1 mette interested[1] = TRUE ! P1 entra nello spin lock e mette interested[1] = FALSE ! P0 accede alla sezione critica ! P0 richiede di accedere di nuovo alla sezione critica ! P1 pone interested[1] = TRUE ! ritorna al terzo punto Questa sequenza di istruzioni evidenzia un caso di starvation: il processo P1 continua a fornire laccesso alla sezione critica al processo P0 senza mai ottenerlo a sua volta. Lalgoritmo viola quindi la quarta condizione della mutua esclusione: nessun processo deve attendere indenitamente per entrare in sezione critica.

29 di 142

ALGORITMO DI PEATERSON. ! Questo algoritmo analogo ai precedenti, infatti si introduce solamente una nuova ! variabile, turn, che indica qual stato lultimo processo ad avere avuto la possibilit di ! accedere alla sezione critica. turn = process; ! ! ! ! ! ! Inoltre viene modicata la condizione di spin lock in modo che un processo potr entrare in sezione critica se e solo se laltro processo non interessato ad accedervi oppure se il valore della variabile turn identica un altro processo. Questo signica che un processo che si trova in sezione critica non pu essere lultimo ad aver avuto la possibilit di entrarci. Lalgoritmo completo il seguente: #dene FALSE 0 #dene TRUE 1 #dene N 2! ! int turn; int interested[N];!

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

! !

! !

! !

/* numero di processi */ /* inizializzato interamente a 0 */

void enter_region(int process){ ! int other;! ! ! ! /* identicativo dellaltro processo*/ ! other = 1 - process;!! ! /* lopposto del processo corrente */ ! interested[process] = TRUE;! /* dichiara linteresse alla sezione critica */ ! turn = process;! ! ! /* setta la ag del turno */ ! while (turn == process && interested[other] == TRUE) {/* spin lock */} } void leave_region(int process){ ! interested[process] = FALSE; /*dichiara il non interesse alla sezione critica*/ }

! ! ! ! ! !

30 di 142

PRIMITIVE PER LA CONCORRENZA. ! Il problema della concorrenza su zone critiche fortemente legato allinterruzione ! dellesecuzione dei processi tramite interrupt. Infatti due processi si possono trovare ! contemporaneamente in sezione critica solo se il primo ad aver avuto laccesso stato ! interrotto e la CPU stata passata ad un altro processo: questo pu avvenire solo in ! seguito ad un interrupt. Se si potessero disabilitare gli interrupt durante lesecuzione di ! una sezione critica si potrebbe impedire che la CPU venga passata ad altri processi. ! In questo modo il processo potrebbe eseguire interamente il suo lavoro in sezione ! critica senza interruzioni. ! Disabilitare gli interrupt per decisamente sconsigliato: ! ! rischioso lasciare allutente (programmatore) il compito di disabilitare gli interrupt ! ! in quanto si parla di unistruzione privilegiata (inoltre ci si potrebbe scordare di ! ! riabilitarli). ! ! se non vengono riabilitati gli interrupt non pi possibili accedere alla CPU. ! ! la capacit di risposta ad eventi esterni risulterebbe molto rallentata. ! ! non di alcuna utilit nei sistemi multiprocessore. ! ! ! ! Una soluzione accettabile si ottiene mediante il linguaggio machina: a partire dallo OS/360 si introdusse listruzione TEST and SET LOCK. unistruzione atomica, ovvero la sua intera computazione viene eseguita come se fosse una singola istruzione (non pu essere interrotta). tsl register, ag register = ag ag = 1

! Sintassi:! ! ! Semantica:! ! ! ! !

! unistruzione semplice e veloce che consiste in una load e in una store. Su un ! sistema RISC non possibile implementare queste due operazioni in ununica ! istruzione, al contrario, facilmente realizzabile su di una architettura CISC. ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! PO! ! ! enter_region:! ! ! ! ! ! tsl $1, lock! ! ! ! cmp $1, #0! ! ! ! jne enter_region! ! ! ret ! ! ! leave_region:! ! ! ! ! move lock, #0! ! ! Ret! ! ! ! ! ! ! ! ! ! ! ! ! ! P1 enter_region: ! ! tsl $1, lock ! ! cmp $1, #0 ! ! jne enter_region ! ! ret leave_region: ! ! move lock, #0 ! ! ret

fondamentale che il lock sia 0 allinizio della computazione. Vediamo un esempio di esecuzione:! ! [lock, $1] ! lock=0, parte P0.! ! ! ! ! [0,0] ! P0 esegue tsl $1, lock.! ! ! ! [1,0] ! P0 esegue il confronto cmp $1, #0! ! [1,0] ! ed ottiene lautorizzazione di accesso.!! ! parte P1 ed esegue tsl $1, lock.!! ! [1,1] ! P1 esegue il confronto cmp $1, #0 ma ! rimane ad eseguire il loop.! ! ! [1,1] ! NB: P1 esce dal loop solo quando P0 ! rilascia la risorsa eseguendo leave_region. ! parte P0 che termina lasciano la sezione ! [0,1] ! critica: move lock, #0.
31 di 142

! ! ! ! ! ! ! !

parte P1 ed esegue tsl $1, lock.!! P1 entra in sezione critica! !

! !

[1,0] [1,0]

NB:!quando il lock a 1 laccesso non autorizzato. fondamentale che inizialmente il ! lock sia settato a 0 altrimenti nessun processo pu accedere alla sezione critica e ! siccome alcun processo vi ha acceduto, non c nessuno che pu lasciare la ! regione e impostare il lock a 0.

In generale le soluzione basate su busy waiting risultano svantaggiose: ! ! possibile starvation durante lo scheduling. ! ! dead lock per inversione di priorit: questo tipo di dead lock si verica con ! ! ! lintroduzione delle priorit e consiste nellimpossibilit di eseguire un ! ! ! processo a priorit maggiore perch un processo a priorit minore ha ! ! ! bloccato laccesso alla sezione critica. Lo scheduler selezioner sempre il ! ! ! processo con priorit pi alta senza dare la possibilit al processo di basso ! ! ! livello di terminare la sua sezione critica e liberare laccesso. I SEMAFORI. ! I semafori sono costituiti da variabili condivise da pi processi. Queste variabili possono ! assumere valore 0 oppure 1 e di questo caso si parla di semafori binari. Quando i ! semafori possono assumere un valore intero maggiore o uguale a 0 i parla di semafori ! generalizzati. ! Le operazioni (syscall) eseguibili sui semafori sono due e sono atomiche: ! ! P(sem) / Down(sem) / Wait(sem): verica se il semaforo zero e se lo mette il ! ! processo che deve entrare in sezione critica in coda al semaforo. ! ! V(sem) / Up(sem) / Signal(sem): se non ci sono processi in coda al semaforo ! ! assegna a sem il valore 1, altrimenti prende il primo processo e lo esegue ! ! dandogli lautorizzazione di accedere alla sezione critica. ! ! ! ! ! ! ! ! ! ! ! ! ! Down(sem): ! If (sem == 0)!then wait on sem; ! ! ! else sem=0; Up(sem): ! If ( some process si waiting on sem ) ! then awake it; ! ! ! ! ! ! ! else sem=1;

SEMAFORI BINARI. ! ! I processi non attendono pi in busy waiting. ! ! A ciascun semaforo associata una coda dove risiedono i processi bloccati. ! ! La gestione della coda non svolta dai semafori secondo uno specico schema ! ! (generalmente FIFO). ! ! Un processo viene risvegliato dalla coda di un semaforo quando un altro processo ! ! esegue una up sul medesimo semaforo (cio sul semaforo al quale il processo ! ! risvegliato era associato). ! ! Mediante i semafori possibile risolvere il problema della concorrenza anche tra N ! ! processi: ! ! ! ! ! ! Down(sem)! ! /*enter region*/ ! ! ! Sezione critica ! ! ! Up(sem)! ! /*leave region*/
32 di 142

! I semafori possono anche essere utilizzati per sincronizzare lesecuzione dei processi, ! ovvero per determinarne un ordine di esecuzione. ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ES: ! ! P1! ! P2! ! P3! ! ! sem ! down(S1)! down(S2) down(S3)! ! S1 = sem. 1 = 1 ! [...]! ! [...]! ! [...]! ! S2 = sem. 2 = 0 ! up(S2)! up(S3) up(S1)! ! S3 = sem. 3 = 0 ! ! Supponiamo che lo scheduler selezioni il seguente ordine di esecuzione: <P1, P2, P3> ! Avremo quindi: ! ! P1 esegue la down di S1 che avendo valore 1 permette al processo di ! ! proseguire la sua elaborazione. S1 assume valore 0. ! ! P1 esegue la up di S2 ed il semaforo passa da 0 a 1. ! ! parte P2, esegue la down di S2. Il semaforo vale 1 quindi il processo ! ! prosegue la sua computazione ed al termine esegue la up di S3. ! ! S3 passa da 0 a 1. ! ! parte P3 che esegue la down di S3. P3 trova il semaforo a 1 quindi pu ! ! proseguire no ad eseguire la up di S1: il semaforo S1 passa da 0 a 1. ! ! al termine i semafori S1, S2 ed S3 avranno tutti valore 1. ! ! ! ! ! ! ! NB: ! ! ! ! ! ! ! quello appena descritto lunico ordine di esecuzione che porta alla esecuzione completa di tutte le istruzioni dei processi considerati. Infatti se lo scheduler avesse selezionato un ordine di esecuzione differente, dove P1 non il primo processo, i semafori avrebbero impedito lesecuzione dei tre processi. Quindi utilizzando i semafori in questo modo possibile forzare lordine di esecuzione dei processi: equivale a dire che i processi sono sincronizzati.

SEMAFORI GENERALIZZATI. ! I semafori generalizzati vengono utilizzati per permettere a pi processi di entrare in ! sezione critica contemporaneamente (questo pu essere necessario in alcune ! situazioni). i semafori generalizzati sono implementati mediante un contatore che tiene ! traccia del numero di processi che possono entrare in sezione critica. ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! down (sem): ! If (sem == 0)!then wait on sem; ! ! ! else sem = sem -1 ; up (sem): ! If ( some process is waiting on sem )! ! ! ! ! ! ! ! then awake it; Else sem = sem + 1;

Come per i semafori binari il sistema garantisce latomicit di queste operazioni. Vediamo un esempio: ! ! ! P1! ! P2! ! P3! ! ! sem ! down(S1) down(S2) down(S3)! S1 = 0 ! write(ab) write(bc) write(cd)! S2 = 2 ! up(S3) up(S1) up(S2)! ! S3 = 1
33 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! !

! ! ! ! ! ! ! ! ! ! ! ! ! !

bcababbccdab:! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! bcabcdcdbcbc:! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

NON una stringa possibile perch non si pu vericare un interleaving che ammette la sottostringa bcabab: bc scritto da P2 che esegue la down di S2 e ottiene laccesso alla sezione critica, poi esegue la up di S1 che assume valore 1; a questo punto P1 scrive ab perch fa la down di S1 che passa da 1 a 0. Il secondo ab dovrebbe essere scritto da P1 ma non lo pu fare perch quando esegue la down di S1 trova S1=0. bc: ! ! ! S2=1, bcab: ! ! S1=0, bcabcd: ! ! S1=0, bcabcdcd: ! ! S1=0, bcabcdcdbc: ! S1=1, bcabcdcdbcbc: ! S1=2, una stringa possibile. S1=1, S3=2, S3=1, S3=0, S3=0, S3=0, S3=1 S2=1 S2=2 S2=3 S2=2 S2=1

! Lesempio mostra che il sistema a semafori non deterministico, cio non possibile ! prevedere lordine di esecuzione. IMPLEMENTAZIONE DEI SEMAFORI. ! I semafori sono implementati a livello di SO ovvero la loro struttura dati allocata ! nellarea kernel. Ne segue che laccesso ai campi della struttura dati dei semafori ! consentito solo al SO. typedef struct { int valore; pcb *next; } semaphore; ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! Inoltre per vericare se le code sono vuote si evita di controllare se il puntatore al prossimo PCB nil perch unoperazione dispendiosa in termini di tempo: lefcienza di esecuzione fondamentale dato che durante le operazioni up e down il sistema cieco (interrupt disabilitati). Loperazione If ( some process si waiting on sem ) si riesce ad eseguire in maniera efciente eseguendo opportuni controlli sul valore intero dei semafori e modicando leggermente la struttura delle procedure up e down. ! ! ! ! ! ! ! ! ! ! ! ! down(sem): ! sem.valore = sem.valore 1; ! if(sem.valore < 0)! then {! ! ! ! ! ! aggiungi un processo a sem.next ; ! ! ! ! ! metti il processo in waiting; ! ! ! ! } up(sem): ! sem.valore = sem.valore + 1; ! if (sem.valore <=0)! then { ! ! ! ! ! rimuovi un processo da sem.next; ! ! ! ! ! metti il processo in ready; ! ! ! ! }

34 di 142

! Abbiamo detto che i sistemi multiprocessore non possono sfruttare la disabilitazione ! degli interrupt per garantire la coerenza dei dati in memoria condivisa durante ! lesecuzione di processi concorrenti; vediamo perch: ! P1 P1 P2 P2

S E M ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

Shared Memory

P1 e P2 sono due processi eseguiti rispettivamente dal primo processore e dal secondo; entrambi lavorano sullo stesso semaforo ed eseguono una down come prima istruzione. Supponiamo che il semaforo sia inizializzato a 1 quando i due processi iniziano la computazione. ! parte P2 ed segue la down di sem; P2 decrementa il valore sem.valore e questo ! corrisponde alle seguenti istruzioni assembler: ! ! ! ! ! lw ! $1, sem.valore ! ! ! ! ! sub ! $1, 1 ! ! ! ! ! sw ! $1, sem.valore ! possibile per che il processo P1, che eseguito dallaltro processore, acceda a ! sem.valore prima che P2 abbia avuto il tempo di aggiornarlo mediante la store ! word e quindi P1 preleva il valore errato. Sarebbe necessario disabilitare gli interrupt su tutti i processori in modo che solo il processore che ha lanciato la disable interrupt possa proseguire lelaborazione. Questa operazione non per possibile. Una soluzione adottabile dai sitemi multiprocessore consiste nellintrodurre un sistema di bloccaggio dei semafori che viene chiamato lock. Quando un processo eseguito dal processore A mette in lock un semaforo, laccesso alla sezione di memoria del semaforo viene impedito a tutti i processi che girano sugli altri processori: solo il processore A in grado di far girare codice che ha laccesso al semaforo. Il semaforo viene messo in lock durante loperazione di down mentre viene rilasciato con una up.

PROGRAMMAZIONE CONCORRENTE. ! Vediamo un esempio che impiega le tecniche appena illustrate per ovviare ad un ! problema di concorrenza tra processi. ! Consideriamo due processi, produttore e consumatore, che lavorano su un buffer ! condiviso che contiene i dati prodotti da un processo e che verranno consumati ! dallaltro. Il buffer utile perch svincola lesecuzione dei due processi: un produttore ! non deve necessariamente attendere che il dato appena prodotto venga utilizzato prima ! di poterne girare un altro e, analogamente, il consumatore pu consumare pi dati ! consecutivi senza attendere ogni volta il produttore. Ovviamente bisogna trattare i casi ! di buffer pieno e buffer vuoto. Di fondamentale importanza sono le due seguenti ! condizioni: ! ! il produttore deve usare unicamente celle libere. ! ! il consumatore non deve consumare dati non pronti.
35 di 142

! Buffer innito: ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! Semaphore sem; int buffer[innito]; void prod (void) { ! ! ! ! int i=0;!! ! ! ! int item; ! ! ! ! while (TRUE) {! ! ! ! produci(&item);! ! ! buffer[i]=item; ! ! ! i = i+1; ! ! ! ! up(sem); ! ! ! }! ! ! ! }! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! void cons (void) { ! int i=0;! ! int item; ! while (TRUE) { ! ! down(sem); ! ! item=buffer[i]; ! ! i = i+1; ! ! consuma(&item); ! } }

Il produttore genera lelemento (item) e fa una up del semaforo generalizzato condiviso dai due processi. Il processo consumatore invece esegue la down del semaforo e se ottiene laccesso pu consumare i dati presenti nel buffer. In questo algoritmo il semaforo generalizzato ha il compito di mantenere in memoria il numero di dati prodotti durante lesecuzione: il produttore esegue una up incrementando il valore del semaforo ogni volta che scrive nel buffer un elemento, mentre il consumatore esegue una down che decrementa il valore del semaforo e serve anche ad evitare che vengano consumati dati che non sono stati prodotti (il semaforo bloccante quando zero). I contatori tengono traccia delle celle lette/scritte e sono variabili locali. Supponendo di avere N consumatori e N produttori i contatori devono essere globali: un contatore per i produttori e uno per i consumatori. Nasce per un problema di concorrenza sullaccesso ai contatori: a seconda dellinterleaving delle istruzioni dei vari processi pu capitare che alcuni elementi vengano sovrascritti o consumati pi volte. Questo problema si risolve utilizzando un semaforo binario di mutua esclusione (mutex) che consente laccesso al buffer e ai contatori ad un solo processo alla volta. ! ! ! ! ! ! ! ! ! ! ! ! ! ! Semaphore sem, mutex; int buffer[innito]; void prod (void) { ! ! ! ! int i=0;!! ! ! ! int item; ! ! ! ! while (TRUE) {! ! ! ! produci(&item);! ! ! down(mutex);! ! ! buffer[i]=item; ! ! ! i = i+1; ! ! ! ! up(mutex);! ! ! ! up(sem); ! ! ! }! ! ! ! }! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! void cons (void) { ! int i=0;! ! int item; ! while (TRUE) { ! ! down(sem); ! ! down(mutex); ! ! item=buffer[i]; ! ! i = i+1; ! ! up(mutex); ! ! consuma(&item); ! } }

Il semaforo binario mutex deve essere inizializzato a 1 per permettere al primo processo schedato di ottenere laccesso. Il semaforo sem deve invece essere inizializzato a 0 perch svolge la funzione di contatore.
36 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

Buffer nito: Si introduce un buffer circolare per permettere lutilizzo di un buffer nito. Per buffer circolare si intende un buffer di dimensione ssata N le cui posizioni vengono utilizzate in maniera sequenziale dalla cella 0 no alla cella N-1 e allaccesso successivo verr utilizzata la cella 0. Questo tipo di buffer viene detto circolare perch idealmente come se la testa e la ne del buffer fossero adiacenti e le celle del buffer formassero una sorta di percorso circolare. Con questo buffer si introduce la necessit di impedire ai produttori di sovrascrivere celle non ancora consumate, oltre al problema dei consumatori visto nel precedente caso. Una soluzione consiste nellintrodurre un nuovo semaforo generalizzato per la gestione dei produttori: ! ! #dene N 100 ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! Semaphore full, empty, mutex; int buffer[N]; void prod (void) { ! ! ! ! int i=0;!! ! ! ! int item; ! ! ! ! while (TRUE) {! ! ! ! produci(&item);! ! ! down(empty);! ! ! down(mutex);! ! ! buffer[i]=item; ! ! ! i = (i+1)modN; ! ! ! up(mutex);! ! ! ! up(full); ! ! ! }! ! ! ! }! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! void cons (void) { ! int i=0;! ! int item; ! while (TRUE) { ! ! down(full); ! ! down(mutex); ! ! item=buffer[i]; ! ! i= (i+1)modN; ! ! up(mutex); ! ! up(empty); ! ! consuma(&item); ! } }

Il semaforo generalizzato empty svolge la funzione di contatore del numero di celle disponibili per i produttori: viene decremento ogni volta che un produttore esegue la down di empty ma solo un produttore alla volta pu entrare in sezione critica (per risolvere la race condition sul contatore i). Il semaforo empty ferma in coda tutti i produttori che vogliono accedere alla sezione critica ma per i quali non vi sono al momento celle disponibili. Questi processi saranno risvegliati dalle operazioni up su empty eseguite dai consumatori: tali operazioni comunicano ai produttori che alcune celle di memoria si sono liberate in quanto sono state consumate e quindi possibile scriverle. La situazione analoga per quanto riguarda i consumatori: anchessi utilizzano un semaforo generalizzato per mantenere traccia delle celle da leggere. Questo semaforo si chiama full ed il corrispondente di sem nellesempio a buffer innito. Anche qui i produttori possono svegliare i consumatori che sono in coda al semaforo full facendo delle up di full.

MONITOR. ! I semafori sono unottima soluzione per i problemi di concorrenza ma programmare ! mediante essi risulta molto difcile, il codice di difcile comprensione ed molto alta ! la possibilit di fare errori e di incorrere in dead lock e violazioni delle condizioni di ! mutua esclusione. Si cercato quindi di trovare una strategia pi ad alto livello e pi ! facilmente gestibile che permetta di risolvere efcientemente i problemi di concorrenza. ! I monitor sono dei costrutti di un linguaggio ad alto livello che consentono di controllare
37 di 142

! ! ! ! ! ! ! ! ! ! ! !

laccesso ad una risorsa condivisa. Le primitive di mutua esclusione non vengono pi utilizzate direttamente dal programmatore ma sono gestite dal compilatore e dal runtime system. i processi bloccati vengono messi in coda al monitor specicato dal programmatore e la gestione della coda ancora compito del compilatore e del runtime system. Un monitor una classe contenente: ! dati condivisi tra processi. ! le procedure che operano su questi dati. ! primitive per la sincronizzazione dei processi che utilizzano questi dati. Laccesso a queste classi regolato da un semaforo, evitando quindi un utilizzo pericoloso dei dati, ed inoltre laccesso ai dati pu avvenire solo mediante procedure ben denite. P(mutex) Dati Met1 Met2 [] MetK V(mutex)

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

Un monitor garantisce la mutua esclusione: ! ! un solo thread alla volta pu eseguire una procedura denita nel monitor (si ! ! dice che il thread nel monitor). ! ! quando un thread richiama una procedura di un monitor nel quale gia ! ! presente un altro thread, viene bloccato su una coda associata al monitor. ! ! quando un thread allinterno di una monitor si blocca un altro thread deve avere ! ! la possibilit di entrare nel monitor. Variabili condition: sono dei meccanismi volti a sospendere i threads che sono in attesa della verica di particolari condizioni. Sono variabili dichiarate nei monitor e alle quali corrispondono diverse code. Le operazioni possibili sono: ! ! wait(variable): il processo che la effettua rilascia il lock sul monitor e si mette in ! ! attesa di essere risvegliato da una operazione signal sulla stessa variabile. ! ! Quindi esiste una coda per ogni condition variable. ! ! signal(variable): risveglia un thread in coda alla variabile. ! ! broadcast: risveglia tutti i processi in attesa. ! ! NB: le variabili condition non sono oggetti booleani. Solitamente le variabili condition sono associate a degli eventi signicativi nellambito del programma a cui appartengono: nellesempio produttori-consumatori si possono usare due variabili condition, una associata allevento buffer pieno e laltra associata allevento buffer vuoto. Possono anche essere usate per sincronizzare i processi tra loro: proprio questo sarebbe il loro scopo nellesempio produttori-consumatori, ovvero fermare lesecuzione a seconda che il buffer sia pieno o vuoto.

Classe MONITOR

38 di 142

ES: produttori-consumatori.

! Variabili condition vs. Semafori: ! ! ! i semafori possono essere utilizzati in qualunque punto del codice mentre le ! ! ! variabili condition solo allinterno dei monitor. ! ! ! la wait() sempre bloccante mentre la down() pu non esserlo. ! ! ! la signal() sblocca un processo se presente un processo bloccato, altrimenti ! ! ! va a vuoto. La up() incrementa il valore del semaforo se non ci sono processi ! ! ! da risvegliare. ! Loperazione signal pu, a volte, generare alcuni problemi: quando un thread A, ! allinterno del monitor, effettua una signal, pu risvegliare un thread B che si trover a ! sua volta allinterno del monitor in quanto proprio nel monitor era stato bloccato. Quindi ! viene meno il principio della mutua esclusione allinterno del monitor. ! Esistono due diverse soluzioni a questo problema: ! ! ! monitor di Hoare: il thread A viene sospeso ed il controllo passa a B, come una ! ! ! sorta di priorit per anzianit. Lesecuzione di A verr ripresa in un momento ! ! ! successivo. ! ! ! monitor Mesa: il processo A continua la sua esecuzione mentre il processo B ! ! ! verr ripreso quando il monitor si libera. NB: pu capitare che levento che ha ! ! ! permesso si risvegliare B non sia pi vero quando il monitor risulta libero e ! ! ! pronto per proseguire lesecuzione di B (le code dei monitor hanno schedulatori ! ! ! non deterministici e quindi pu capitare che venga eseguito un processo C ! ! ! prima di B). ! Esistono situazioni in cui possibile, e conveniente, utilizzare le variabili condition al di ! fuori dei monitor. Tipicamente vengono utilizzate per gestire la sincronizzazione tra ! processi sulla base del vericarsi o meno di certe condizioni. Per queste ragioni ! possibile trovare le variabili condition anche in libreria di supporto alla programmazione ! concorrente.

39 di 142

I MESSAGGI. ! I semafori e i monitor sono i principali strumenti di supporto alla programmazione ! concorrente ma richiedono uno spazio di memoria condiviso ed un sistema di gestione ! delle code. Parallelamente a queste tecniche si sono sviluppati metodi che non sono ! basati sulla condivisione ma sulla comunicazione. In questi metodi la sincronizzazione ! tra i processi avviene permettendo ad essi di comunicare fra loro. ! Non essendo presente una shared memory non si pone il problema della mutua ! esclusione. Inoltre diversi processi/threads possono essere localizzati su diverse ! macchine, collegati tra loro da una rete di comunicazione. ! Nel message passing esistono due principali primitive di comunicazione: send e ! receive. Per stabilire una comunicazione fondamentale che i processi deniscano a ! chi vogliono inviare e da chi vogliono ricevere messaggi. Esistono due tipi di approcci: ! ! ! comunicazione diretta: si comunica direttamente con linterlocutore. ! ! ! comunicazione indiretta: esiste un mediatore della comunicazione al quale ! ! ! sender e receiver fanno riferimento. ! Tra due processi comunicanti il sistema crea almeno un canale di comunicazione che ! generalmente costituito da una coda FIFO di messaggi che pu essere ! monodirezionale o bidirezionale. ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! Comunicazione diretta: i processi devono conoscere lidentit dellinterlocutore e devono esprimerla esplicitamente: ! ! send(p, message). ! ! receive(p, message). Dove p lidenticativo di un processo. In questo caso il SO provvedere a creare un canale di comunicazione tra i processi che vogliono comunicare e tale collegamento solitamente a due vie. Questo metodo non gode di una buona portabilit del codice. Comunicazione indiretta: i messaggi sono inviati/ricevuti su opportune strutture dati chiamate mailbox o porte. Ogni mailbox unicamente identicata allinterno del sistema ed i processi possono comunicare tra loro solo se condividono mailbox. In questo caso le primitive di comunicazione sono: ! ! send(mailbox, message). ! ! receive(mailbox, message). Le mailbox possono essere create e distrutte mediante speciche procedure. possibile che pi di due processi condividano lo stesso canale di comunicazione (mailbox) e anche che una coppia di processi condivida pi mailboxes. Quando una mailbox condivisa da pi di due processi si pone il problema di denire chi il ricevente di un messaggio. Per ovviare a questo quesito esistono due soluzioni: ! ! stabilire una politica di ricezione che consente ad un solo processo alla volta di ! ! riceve. ! ! permettere al sistema di scegliere il ricevente che verr poi comunicato al ! ! mittente.

SINCRONIZZAZIONE NEL MESSAGE PASSING. ! Lo scambio di messaggi pu essere sia bloccante che non: ! ! ! bloccante (sincrono): il sender si blocca nche il receiver non ha ricevuto il ! ! ! messaggio ed il receiver si blocca nche il messaggio non disponibile. ! ! ! asincrono: il sender invia un messaggio e prosegue la sua elaborazione. Il ! ! ! ricevente accede al canale e recupera un messaggio o nil. ! Il canale di comunicazione pu essere realizzato mediante un buffer che corrisponde ad ! uno dei seguenti schemi: ! ! ! capacit nulla: il sender deve attendere il receiver (rendez-vous).
40 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

! ! !

! capacit nita: in uno schema asincrono il mittente dovr fermarsi quando il ! canale pieno. ! capacit innita: in uno schema asincrono il sender non aspetta mai.

Rendez-vous: questo meccanismo comporta leliminazione del buffer di comunicazione ed utilizzato nello scambio sincrono: se la chiamata alla procedura send viene effettuata prima della chiamata a receive, il sender si blocca in attesa dei ricevente; analogamente, se la receive viene effettuata prima della chiamata a send il ricevente si blocca in attesa del mittente. Il metodo sincrono presenta i seguenti vantaggi: ! ! consumo di memoria ridotto per via dellutilizzo di buffer di dimensioni limitate. ! ! maggiore controllo sul canale di comunicazione in quanto ogni thread pu ! ! avere al pi un messaggio non processato su ogni canale. Gli svantaggi del metodo sincrono sono: ! ! minore efcienza: quando due threads devono comunicare almeno uno dei due ! ! si blocca. ! ! possibilit di dead lock.

41 di 142

IL DEAD LOCK. ! Starvation: situazione in cui lesecuzione di uno o pi threads viene posticipata ! ! ! indenitamente. ! Livelock: situazione in cui i processi eseguono continuamente operazioni marginali ma ! ! ! non progrediscono nella computazione principale. ! Deadlock: situazione in cui tutti i threads di applicazioni cooperanti sono bloccati. ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! Il deadlock una forma di starvation ma la fondamentale differenza che la starvation pu terminare mentre il deadlock, per denizione, pu essere risolto solo da un evento esterno. NB: la starvation pu terminare ma non garantito che ci accada in quanto, come il deadlock, la starvation fortemente dipendente dallinterleaving. Un insieme di processi in deadlock quando ciascuno di essi in attesa di riceve una risorsa trattenuta da un altro processo del medesimo insieme. stato dimostrato che per generare una situazione di deadlock si devono vericare le seguenti condizioni: ! ! accesso alle risorse in mutua esclusione (shared mamory, stampanti, ). ! ! Hold and Wait: i processi che possiedono risorse possono continuare a ! ! chiederne di nuove senza rilasciare le risorse gi acquisite, anche se ! ! rimangono bloccati in attesa. ! ! no preemption: esistono risorse che non sono perfezionabili, cio sono risorse ! ! che una volta assegnate ad un processo non possono essere cedute (es: ! ! stampante). ! ! attesa circolare: due o pi processi sono in attesa di una risorsa trattenuta da ! ! un altro processo dello stesso gruppo. NB: queste condizioni sono necessarie ma non sufcienti, ovvero il vericarsi del ! deadlock non certo.

RAG: Resource Allocation Graph. ! Il deadlock pu essere descritto utilizzando un RAG. Un RAG un grafo dove i nodi ! sono sia processi che risorse e gli archi sono le richieste di risorse da parte di un ! processo. Deniamo P linsieme dei nodi che rappresentano i processi del sistema, ed ! R linsieme dei nodi che rappresentano le risorse. Un arco da un elemento di P ad un ! elemento di R denota la volont d un processo di ottenere accesso ad una risorsa; un ! arco da n elemento di R ad un elemento di P indica lassegnazione di una risorsa ad un ! processo. Ovviamente archi da R in R oppure archi da P in P non hanno alcun senso.

Assegnamento

Richiesta

42 di 142

! ! ! ! ! ! ! ! !

Diamo alcune propriet: ! ! ogni risorsa pu avere un numero di copie maggiore di uno. ! ! se il grafo non presenta cicli, non vi pu essere presenza di deadlock nel ! ! sistema. ! ! se il grafo presenta cicli, allora possibile che si verichi un deadlock: ! ! se il grafo rappresenta un sistema con una sola istanza per ogni risorsa allora ! ! c sicuramente deadlock. ! ! se il grafo rappresenta un sistema con istanze multiple allora il deadlock ! ! possibile.

! Esempio: ! R1 R3

P1

P2

P3

R2

! ! ! !

Il graco RAG non mostra cicli al suo interno quindi non si potranno vericare deadlock.

R1

R3

P1

P2

P3

R2

43 di 142

! ! ! ! ! ! ! ! ! !

! ! ! ! ! ! ! ! ! !

Il secondo grafo RAG presenta cicli ed essendo a istanze multiple pu essere presente un deadlock: ! P1 detiene una delle due istanze della risorsa R2. ! P1 richiede R1. ! P2 detiene lunica istanza di R1 e quindi blocca P1. ! P2 richiede R3. ! P2 detiene unistanza di R2. ! P3 detiene lunica istanza di R3 e quindi blocca P2. ! P3 richiede R2 ma le due istanze di R2 sono gai assegnate: P3 bloccato.

GESTIONE DEI DEAD LOCK. ! In generale per gestire i deadlock si possono applicare le seguenti strategie: ! ! ! ignorarli: quando possibile si cerca di ignorare i dead lock in quanto non ! ! ! sempre gli sforzi per la loro gestione sono sostenibili. ! ! ! prevenirli: si prendono tutte le precauzioni necessarie afnch una delle 4 ! ! ! condizioni non si possa vericare mai. Vediamo come: ! ! ! Mutua esclusione: si garantisce che ogni processo accede immediatamente alla ! ! ! risorsa di cui ha bisogno (difcilmente realizzabile). ! ! ! Hold and Wait: un processo deve rilasciare tutte le risorse precedentemente ! ! ! acquisite prima di poterne acquisire una nuova. Questa soluzione pu generare ! ! ! starvation per alcuni processi. ! ! ! No preemption: si consente al sistema di prelazionare tutte le risorse. Anche ! ! ! questa tecnica poco realistica: si pensi alla possibilit di prelazionare un ! ! ! masterizzatore durante la scrittura di un CD. ! ! ! Attesa circolare: si impone un ordinamento numerico delle risorse ed i processi ! ! ! sono forzati a richiedere le risorse rispettando tale ordine. ! ! ! detection and recovery: si verica costantemente il grafo RAG, appena si ! ! ! individua un ciclo si killa uno dei processi coinvolti e successivamente si ! ! ! ripristina (recovery) il processo killato. ! ! ! avoidance: si allocano in maniera accurata le risorse del sistema. Tutti i ! ! ! processi dichiarano, in fase di caricamento, quali sono le risorse di cui hanno ! ! ! bisogno in modo che il sistema possa determinare una sequenza sicura di ! ! ! esecuzione di questi processi. AVOIDANCE. ! Ogni processo deve comunicare preventivamente quali risorse avr bisogno per la sua ! esecuzione in modo che il deadlock non si verichi. Il sistema acconsente a cedere una ! risorsa al processo solo se certo del fatto che in futuro potr soddisfare tutte le ! richieste del processo. I principali problemi di questa tecnica sono dovuto al fatto che ! non semplice determinare quali e quante risorse avr bisogno un processo ed inoltre ! la computazione generale per determinare lordine di esecuzione non leggera. ! Il sistema deve riuscire a garantire una sequenza di esecuzione sicura: questa ! sequenza viene detta safe sequence e generalmente viene determinata assumendo ! che i processi coinvolti utilizzino il massimo delle risorse (caso peggiore). Un safe state, ! o stato sicuro, uno stato in cui possibile determinare una sequenza sicura di ! esecuzione, ovvero dove possibile soddisfare tutte le richieste dei processi e ! garantirne la terminazione. Gli algoritmi di deadlock avoidance garantiscono che il ! sistema si trovi sempre in uno stato sicuro. ! NB: gli stati non sicuri, o unsafe, potrebbero generare una situazione di deadlock ma ! non vi la certezza assoluta che questo accada. Comunque il sistema evita questi stati ! a prescindere e considera unicamente gli stati safe. !
44 di 142

has A B C D 0 0 0 0

max 6 5 4 7 A B C D

has 1 1 2 4

max 6 5 4 7 A B C D

has 1 2 2 4

max 6 5 4 7

[a] libere: 10

[b] libere: 2

[c] libere: 1

Consideriamo il seguente esempio dove non si considera il tipo delle risorse: ! Lo stato [a] sicuro in quanto dispongo di 10 risorse e posso garantire la terminazione ! di uno o pi processi. In particolare possono terminare i seguenti processi: (A) v (B) v (C) v (D) v (A^C) v (B^C) ! Lo stato [b] sicuro perch, disponendo di due risorse, il processo C in grado di ! terminare. ! Lo stato [c] non sicuro, infatti, con una sola risorsa disponibile, il sistema non in ! grado di garantire la terminazione di nessuno dei processi. LALGORITMO DEL BANCHIERE. ! un algoritmo di avoidance che permette di gestire lassegnazione di risorse multiple. ! Anche in questo caso i processi devono preventivamente comunicare il quantitativo e la ! tipologia di risorse di cui necessitano per completare la loro computazione. ! Il SO gestisce un array Available in cui indicato, per ogni tipo di risorsa del sistema, il ! numero di istanze disponibili in un dato momento. Ogni processo dichiara, in fase di ! avvio, un array Max che denisce il numero massimo distanze, per ogni tipo di risorsa, ! che intende utilizzare durante la sua esecuzione. ! Il SO mantiene anche un array bidimensionale Allocation dove, per ogni per ogni risorsa ! j e per ogni processo i, viene specicato il numero di istanze di j assegnate ad i. ! Lalgoritmo calcola il numero di istanze necessarie ad ogni processo per garantirne la ! terminazione e determina se il sistema si trova o meno in uno stato sicuro: Needi [ j ] = Maxi [ j ] - Allocation[ i ][ j ] ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! La computazione avviene assumendo il caso peggiore ovvero che ogni processo utilizzer tutte le risorse dichiarate per giungere al termine. Quando un processo nisce la sua elaborazione, rilascia tutte le risorse che gli erano state allocate. Idea dellalgoritmo: ! ! considera lelenco dei processi che devono ancora terminare. ! ! do { ! ! ! Individua un processo i tale che Needi [ j ] Available[ j ] per tutte le ! ! ! risorse j. ! ! ! Elimina questo processo dallelenco e dealloca le sue risorse. ! ! ! Available[ j ] = Available[ j ] + Maxi [ j ] per tutte le risorse j. ! ! } while! ( ! ! ! non ci sono pi processi, oppure non stato possibile identicare ! ! ! almeno un processo i ! ! ! ) ! ! se lelenco vuoto signica che il sistema si trova in uno stato sicuro.
45 di 142

! Esempio: !

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

E = risorse esistenti. P = risorse assegnate. A = risorse disponibili. ! lunico processo che pu terminare, con le risorse disponibili, il processo D. ! assegno a D una risorsa di tipo 3. ! A = (1010). ! risorse del processo D = (1111). ! D termina e rilascia le sue risorse. ! A = (2121). ! ora possono terminare A v B v E. ! assegno ad A una risorsa di tipo 1 e una di tipo 2. ! A = (1021). ! risorse del processo A = (4111). ! A termina e rilascia le sue risorse. ! A = (5132). ! un qualsiasi processo in grado di terminare con le risorse disponibili. ! assegno a C tre risorse di tipo 1 ad una risorsa di tipo 2. ! A = (2032). ! risorse del processo C = (4210). ! il processo C termina e rilascia le sue risorse. ! A = (6242). ! assegno al processo B una risorsa di tipo 2, una di tipo 3 e due di tipo 4. ! A = (6130). ! risorse del processo B = (0212). ! il processo B termina e rilascia le sue risorse. ! A = (6342). ! assegno ad E una risorsa di tipo 2, una di tipo 3 e due di tipo 1. ! A = (4232). ! risorse del processo E = (2110). ! il processo E termina e rilascia le sue risorse. ! A = (6342). Tutti i processi hanno terminato. Una sequenza di esecuzione sicura : DACBE.

! ! ! !

46 di 142

! Esercizio:

! ! ! ! ! ! ! ! ! ! ! ! ! ! !

P1 richiede una nuova istanza di A e due di C, possiamo soddisfare la richiesta? ! ! assegnamo a P1 unistanza di A e due di C. ! ! Available = (230). ! ! risorse allocate per P1 = (302). ! ! P1 necessita ancora di due risorse di tipo B per terminare. ! ! cerchiamo un processo in grado di terminare con le risorse (230). ! ! P1 lunico processo in grado di terminare. Assegnamo a P1 due risorse di tipo ! ! B. ! ! Available = (210). ! ! risorse allocate per P1 = (322). ! ! P1 termina e rilascia le sue risorse. ! ! Available = (532). ! ! cerchiamo un processo che sia in grado di terminare con le risorse (532). ! ! nessun processo pu terminare. Non possiamo soddisfare la richiesta di P1. Ci porterebbe ad uno stato unsafe.

DETECTION AND RECOVERY. ! Questa tecnica assume la possibilit che il deadlock si verichi e prevede tecniche per ! individuare le situazioni di blocco e per risolvere il deadlock. Servono quindi due ! algoritmi: uno per individuare loccorrere di un deadlock e un altro per il ripristino di una ! situazione deadlock free. ! ! Deadlock detection: ! ! ! nel caso di sistemi con risorse multiple viene eseguito periodicamente ! ! ! lalgoritmo di verica dello stato corrente. Se il sistema si trova un uno stato non ! ! ! sicuro i processi critici sono quelli per i quali non possibile soddisfare le ! ! ! richieste. ! ! ! nel caso di risorse a singola istanza si visita il RAG alla ricerca di cicli. Se si ! ! ! verica la presenza di un ciclo, il sistema forza la prelazione di una risorsa ! ! ! coinvolta nel deadlock forzando un processo a rilasciarla. ! Queste tecniche per sono costose in termini di risorse e non garantiscono la ! risoluzione del problema. Diventa perci critica la scelta della frequenza con la quale ! eseguire loperazione di deadlock detection: ! ! ! ad ogni richiesta di una risorsa (molto costoso). ! ! ! ogni k minuti. ! ! ! quando lutilizzo della CPU basso. Questa strategia sia basa sul fatto che, se ! ! ! molto processi sono bloccati per via di un deadlock, nessuno di essi potr avere ! ! ! accesso alla CPU che risulter poco utilizzata.

47 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

Una volta che il deadlock stato individuato possibile agire nei seguenti modi. Abort dei processi: ! ! si forza lAbort di tutti i processi coinvolti nel deadlock. Questi processi ! ! dovranno poi ripartire secondo una politica di recovery. ! ! si forza lAbort di un processo alla volta nch non si elimina il ciclo nel RAG. In ! ! questo caso necessario rieseguire lalgoritmo di deadlock detection dopo ogni ! ! Abort. Prelazione delle risorse: ! ! si individuano processo e risorse da prelazionare. ! ! si esegue il rollback del processo selezionato. ! ! se il processo richiede la risorsa non la potr ottenere subito esche sar stata ! ! assegnata ad unaltro processo coinvolto nel deadlock. ! ! NB: si pu generare starvation. Rollback: periodicamente vengono eseguiti dei punti di controllo sui processi che servono a salvare in un le lo stato del processo generando una sorta di storia o percorso. Quando si scopre un deadlock facile individuare quali sono le risorse coinvolte ed altrettanto facile, una volta selezionata la risorsa da prelazionare, riportare il processo ad uno stato in cui non aveva ancora acquisito la risorsa. Loperazione che riporta un processo ad un suo stato precedente, facendolo ripartire da uno dei punti di controllo, viene detta rollback. A volte pu tornare utile forzare lAbort di un processo che non coinvolto nel !deadlock ma che potrebbe, liberando le sue risorse, risolvere il blocco. Queste sono decisioni che dipendono fortemente dai processi in esecuzione nel sistema nel momento di stallo: sempre preferibile fermare un processo che pu essere rieseguito senza arrecare danni.

48 di 142

I MICROPROCESSORI INTEL. ! Nel 1979 Intel introduceva famiglia dei processori 8086 con le seguenti caratteristiche: ! ! ! processori a 16 bit con registri a 16 bit. ! ! ! bus dati a 16 bit e bus indirizzi a 20 bit. ! ! ! coprocessore oating-point. ! ! ! due modalit di accesso alla memoria: segmentazione e real-address mode. ! ! ! Ogni segmento pu indirizzare 216 bytes = 64 KB. ! A partire dal 80286 si introducono: ! ! ! un bus indirizzi a 24 bit, 224 bytes = 16MB di indirizzamento. ! ! ! la modalit di gestione della memoria nota come protected mode. ! Dal 80386 le caratteristiche diventano: ! ! ! processore a 32 bit con registri general-pupose a 32 bit. ! ! ! 32 bit per il data bus e 32 bit per laddusse bus. ! ! ! 232 bytes, ovvero 4GB di indirizzamento. ! ! ! nuove modalit di gestione della memoria: paging, virtual memory, at memory. ! ! ! La segmentazione pu essere disabilitata. ! Nel 1989 si introduce l80486: ! ! ! versione migliorativa del 80386. ! ! ! unit oating-point. ! ! ! cache unicata per istruzioni e data (8 KB). ! ! ! utilizzo della pipeline (completa unistruzione per ciclo di clock). ! Il processore Pentium introduce: ! ! ! bus dati a 64 bit, mentre resta a 32 bit il bus indirizzi. ! ! ! due linee di pipeline: U-pipe e V-pipe. ! ! ! il processore diventa superscalari, ovvero riesce a completare due istruzioni per ! ! ! ciclo di clock. ! ! ! cache dati e cache istruzioni separate da 8 KB ciascuna. ! ! ! istruzioni MMX per applicazioni multimediali. ! ! ! ! ! ! ! ! ! ! I processori seguenti, no allo Xeon, sono un continuo potenziamento delle caratteristiche gi presenti nei processori ma non introducono sostanziali cambiamenti. Si spinge sempre pi sulla superscalarit, si introduce un bus indirizzi a 36 bit, ottenendo un indirizzamento di 64 GB, viene aggiunta una cache di secondo livello da 256 KB, vengono aggiunte alcune istruzioni MMX e le istruzioni SSE (streaming SIDM extension). Con il Pentium 4 so nota un uso esasperato di instruction pipeline, e si passa da SSE a SSE2. Nel 2002 Intel introduce lHyper-Threading che consente lesecuzione parallela di due programma, permettendo la condivisione di alcune risorse. Nel 2006 si giunge ai processori multicore.

I REGISTRI. ! otto registri a 32 bit general-purpose. ! ! EAX, EBX, ECX, EDX, EBP, ESP, ESI, EDI. ! sei segment register a 16 bit. ! ! CS, SS, DS, ES, FS, GS. ! Processor Status Flags (EFLAGS) e Instruction Pointer. ! ! EFLAGS, EIP. ! ! ! ! ! Anche se registri EAX, EBX, ECX, EDX sono a 32 bit possibile accedere solo ad alcune parti di essi: i 16 bit meno signicativi del registro EAX sono identicati da AX che a sua volta diviso in due parti, gli 8 bit meno signicativi (AL) e gli 8 bit pi signicativi (AH). Questa possibilit una sorta di eredit dei vecchi processori: per garantire una buona portabilit del codice e permettere la retrocompatibilit si scelto
49 di 142

! di mantenere le vecchie notazioni (come AX, AH, AL) e vecchie istruzioni anche nei ! nuovi processori. Nei registri ESI, EDI, EBP, ESP si pu accedere ai 16 bit meno ! signicativi utilizzando i nomi dei registri privati della E iniziale (Extended). MODALIT OPERATIVE. ! Larchitettura IA-32 fornisce una serie di funzionalit per la realizzazione di SW di ! sistema che sono strettamente legate alla modalit in cui pu operare il processore: ! ! ! Real Mode. ! ! ! Protected Mode. ! ! ! Virtual 8086 Mode. ! ! ! System Management Mode. ! ! ! IA-32e Mode. ! ! ! ! Real-Address Mode: la modalit con cui il processore si avvia allaccensione del sistema e fornisce le stesse funzionalit presenti nel 8086 con laggiunta di alcune estensioni, come la possibilit di cambiare modalit in Protected o System Management Mode.

! Protected Mode: fornisce un insieme di funzionalit per la realizzazione di sistemi ! multiprogrammati garantendo la retrocompatibilit e sistemi di protezione dellambiente ! di lavoro. possibile passare da Protected a Virtual 8086 Mode. ! Virtual 8086 Mode: consente al processore di eseguire SW predisposto per larchitettura ! 8086 in un ambiente multiprogrammato e protetto. quindi un misto delle due modalit ! precedenti. ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! System Management Mode (SMM): questa modalit permette al processore di eseguire porzioni di codice contenute in zone di memoria riservate (SMRAM, System Management RAM). Queste zone sono accessibili solo al processore e a nessuna componente SW ed il codice viene eseguito con un livello di priorit estremamente alto. Generalmente queste operazioni sono volte alla gestione e alla diagnostica dellHW e ad operazioni sensibili come laccensione e lo spegnimento del sistema. IA-32 Mode: consente il supporto di applicazioni a 32 e a 64 bit. Pi precisamente il SW pu essere eseguito in due modalit: ! ! 64-bit Mode: il processore supporta applicativi e SO a 64 bit ! ! Compatibility Mode: consente ad un sistema a 64 bit di gestire applicativi a ! ! 32/64 bit. Allaccensione il sistema parte il Real-Address Mode ed il valore della ag PE nel registro CR0 determina se il processore dovr operare in Real o Protected Mode. Quando il processore sente un interrupt SMI entra in SMM indipendentemente dalla modalit operativa precedente. Dalla SMM possibile ritornare alla modalit precedente solo a seguito dellesecuzione di unistruzione RSM.

50 di 142

PROTECTED MODE. ! Questa modalit stata introdotta per facilitare la soluzione di problemi di sicurezza ! introdotti dalla multiprogrammazione. Gli obbiettivi della Protected Mode sono i ! seguenti: ! ! ! impedire ad un processo di modicare larea dati di un altro processo (visibilit ! ! ! della memoria). ! ! ! impedire ad un processo di accedere direttamente alle risorse del SO. ! ! ! in caso di errore in un task, assicurare la sopravvivenza del SO e non ! ! ! compromettere la funzionalit degli altri task. ! ! Per garantire i suddetti punti lHW implementa dei meccanismi basati su due principi: ! lisolamento e la protezione. Per isolamento si intende la separazione sica dellarea di ! lavoro dei singoli processi, ovvero ad ogni processo assegnato uno spazio di ! indirizzamento diverso in modo tale che non possano interferire tra loro se non ! attraverso meccanismi controllati di IPC. ! Il principio della protezione invece instaura una sorta di gerarchia allinterno del ! sistema: ad ogni oggetto viene assegnato un livello di protezione, identicabile con un ! valore numerico, ed oggetti con un livello di protezione K possono accedere solo ad ! oggetti con livello maggiore o uguale a K. I SEGMENTI. ! Iisolamento viene implementato tramite il processo di segmentazione: i segmenti sono ! porzioni di memoria disgiunte in cui sono caricati i processi, i dati e le opportune ! strutture dati. Nella modalit di esecuzione Protected i processi utente vengono ! spezzati in due o pi segmenti che possono essere di due tipi: codice o dati/stack. ! Supponiamo che un processo P1 venga diviso in due segmenti di cui S1 di tipo ! codice mentre S3 di tipo dati/stack: gli indirizzi generati da P1 potranno essere riferiti ! solo alla parte di memoria di S1 o S3 altrimenti verr restituito un errore di ! segmentazione (segmentation fault). ! Siccome loperazione di controllo della corrispondenza tra gli indirizzi generati dai ! processi ed i segmenti a loro assegnati ha unaltissima frequenza, necessario che ! venga implementato un meccanismo efciente a livello HW in grado di ottimizzare il
51 di 142

! ! ! ! ! ! !

procedimento. Questo meccanismo utilizza una tabella contente le informazioni basilari dei segmenti allocati dal sistema: i descrittori. Inoltre esistono registri specici il cui compito quello di puntare ai descrittori dei segmenti associati al processo corrente in modo da creare il ponte tra HW e SW che permette allHW di eseguire tutti i controlli necessari. Questa tecnica permette di scaricare gran parte del lavoro sullHW che a questo punto non deve pi interagire con il SW e pu lavorare molto pi velocemente.

S3 S2 S1 S0 ! ! ! ! ! !

0x4001 0x4000 0x3001 0x3000 0x2001 0x2000 0x1001 0x1000 0x0000

Sono presenti altri due importanti segmenti: TSS e LDT. TSS (Task State Segment) il segmento che il processore utilizza per la gestione dei processi mentre LDT (Local Descriptor Table) un segmento costituito da tabelle che specicano gli oggetti accessibili da un determinato processo. Quindi, riassumendo, le tipologie di segmenti che incontreremo sono: codice, dati/stack, TSS e LDT.

I DESCRITTORI. ! Ogni segmento, indipendentemente dal tipo, deve esse provvisto di una struttura dati ! che ne specica le principali propriet, ovvero il descrittore (Segment Descriptor). Un ! descrittore di segmento fornisce al processore le seguenti informazioni: ! ! ! dimensione ed indirizzo di partenza del segmento. ! ! ! informazioni per il controllo degli accessi ! ! ! informazioni sullo stato del segmento ! NB: i descrittori sono gestiti dal SO o da programmi di sistema. ! Un segment descriptor contiene: ! ! ! Base Address: un valore a 32 bit che denisce lindirizzo di partenza di un ! ! ! segmento. Questo valore splittato in tre parti lungo il descrittore per problemi ! ! ! di compatibilit con i processori a 16 bit. ! ! ! ( 32-bit Base Address + 32-bit Offset ) = 32-bit Linear Address

52 di 142

! ! ! ! ! !

! ! ! ! ! !

! ! ! ! ! !

Segment Limit: un numero di 20 bit che specica la dimensione del segmento. Anche questo valore splittato lungo il descrittore in due parti e a seconda della granularit (campo G del descrittore) viene interpretato come unit in byte o pagine (4KB). Diritti di accesso: specicano se il segmento contiene codice o dati, se i dati sono read-only o anche scrivibili, ed inne il livello di privilegio del segmento.

! Larchitettura prevede anche un insieme di descrittori speciali, non legati ai segmenti, ! che vengono chiamati GATE e che permettono allHW di controllare lesecuzione di ! codice privilegiato da parte di applicazioni che operano in user space. ! ! ! ! ! I descrittori sono, a loro volta, radunati allinterno di alcune tabelle denominate Tabelle di Descrittori. Queste tabelle sono organizzate in forma di vettori e sono referenziate attraverso dei registri di sistema. Per accedere ai descrittori, contenuti allinterno delle tabelle, si utilizza un indice detto selettore. Un selettore ha la seguente struttura:

! ! ! ! ! !

Le tabelle principali sono: ! ! GDT (Global Descriptor Table): una tabella contenente descrittori di segmenti ! ! accessibili a tutti i processi. Non un segmento, il suo accesso non avviene ! ! attraverso selettori . ! ! LDT: una tabella di descrittori di segmenti visibili solo dal processo (Task) a ! ! cui la LDT associata.
53 di 142

! ! ! !

! ! ! !

! ! ! !

IDT (Interrupt Descrition Table): una tabella contenente i descrittori dei Gates di accesso alle procedure associati agli interrupt. Corrisponde alla Interrupt Table dell8086 e pu contenere solo descrittori di Gates (Interrupt, Trap e Task Gate).

REGISTRI DI SISTEMA. ! Per operare in maniera efciente sulle tabelle la CPU dispone dei seguenti registri: ! ! ! GTDR: punta alla GTD della piattaforma e viene inizializzato nella fase di boot. ! ! ! LDTR: punta alla LDT del task corrente (in running) e deve essere aggiornata ! ! ! ad ogni task switch. ! ! ! IDTR: punta alla IDT della piattaforma e viene inizializzato nella fase di boot. ! ! ! TR o Task Register: punta al TSS del task corrente. ! Vi sono poi altri registri fondamentali per la gestione del sistema e per la maggior parte ! sono accessibili dal SO mediante istruzioni privilegiate. Questi registri sono: ! ! ! EIP. ! ! ! EFLAGS. ! ! ! Control Registers: CR0, CR1, CR2, CR3, CR4. ! ! ! Segment Registers. !

54 di 142

EFLAGS

SEGMENT REGISTERS. ! I segment registers sono sei registri a 16-bit che servono da supporto per la memoria ! segmentata. Un processo pu accedere ad un segmento se il selettore del segmento ! stato preventivamente caricato nel corrispondente registro. Vengono utilizzati come ! cache per i selettori di segmento. I segmenti a cui fanno riferimento i selettori possono ! essere di tipo codice, dati o stack. ! In questi registri contenuta lunico riferimento al segmento visibile al programmatore: il ! descrittore del segmento viene caricato dallHW non pu essere visualizzato dal ! programmatore.

55 di 142

TASK STATE SEGMENT. ! Un task ununit di lavoro che il processore pu attivare, eseguire e sospendere. Pu ! essere utilizzato per svariate ragioni: eseguire un programma, una routine del SO, un ! gestore di interrupt, ! Ogni task composto da due componenti: uno spazio di esecuzione e un TSS. Lo ! spazio di esecuzione costituito da un segmento codice, un segmento stack e uno o ! pi segmenti dati. Il TSS un segmento che contiene una struttura dati nella quale ! sono memorizzate le seguenti informazioni:! ! ! ! ! lo stato dellambiente desecuzione di un task. ! ! ! i selettori e gli stack pointers che puntano a tre segmenti stack, uno stack per ! ! ! ogni livello di privilegio. ! ! ! il selettore di segmento per la LDT associata al task. ! ! ! le informazioni necessarie per gestire la paginazione. ! ! ! ! ! ! ! ! ! ! Quindi lo stato di un task formato dalle seguenti componenti: ! ! valore dei segment registers (CS, DS, SS, ES, FS, GS). ! ! valore dei registri general-purpose. ! ! valore di EFLAGS. ! ! valore di EIP. ! ! valore del control register CR3. ! ! valore del task register (TR). ! ! valore di LDTR. ! ! valori per la gestione dellI/O (contenuti in TSS). ! ! tre stack pointers (contenuti in TSS).

56 di 142

LASSEMBLER DI MINIX. ! Lassembler il linguaggio di programmazione pi vicino al linguaggio macchina. Un ! programma assembler un insieme di direttive, precedute da ., e istruzioni che sono ! eseguite dal processore nellordine in cui sono scritte. Esistono due sintassi possibili: ! ! ! AT&T syntax (objdump, GNU assembler). ! ! ! DOS/Intel syntax (Microsoft assembler, Nasm). ! ! Un programma costituito da quattro sezioni: ! ! ! Code secchino (.sect .text): contiene le istruzioni. ! ! ! Read only data section (.sect .rom): sezione dedicata ai dati non modicabili. ! ! ! Read and writable data section (.sect .data): sezione dati scrivibile. ! ! ! Global read and writable data section (.sect .bss): sezione dedicata a contenere ! ! ! le variabili globali. Il valore di queste variabili inizializzato a zero e se di ! ! ! dichiarano con una valore diverso da zero si incorre in un errore di ! ! ! compilazione. ! Tutte le sezioni vanno dichiarate allinizio di un programma e possono avere un ! qualunque nome preceduto da un punto. Le dichiarazioni standard sono quelle viste ! nellelenco. I contenuti di una sezione non devono necessariamente essere contigui. ! I nomi e gli identicatori possono essere formati utilizzando . , _ e caratteri ! alfanumerici. Le stringhe devono essere racchiuse tra apici singoli o doppi; le parentesi ! tonde vengono utilizzate per lidirizzamento indiretto. Le stringhe o16 e o32 inserite ! prima di unistruzione indicano che gli operandi di quella istruzione sono a 16/32 bit, ! mentre la stringa rep pressa ad unistruzione indica che quelloperazione va eseguita ! nch il valore nel registro CX diventa zero. ! Segment prex: la stringa cseg/dseg/eseg/fseg/gseg/sseg pressa ad una istruzione ! indica che nellindirizzamento vanno usati i registri cd/ds/es/fs/gs/ss. Quando si usano ! registri di 8 bit si deve aggiungere allistruzione il sufsso b. ! ES: ! ! Movb (edi), ah ! ! ! ! ! ! ! ! ! ! ! ! ! Esistono anche delle pseudo-istruzioni: ! ! .extern: dichiara luso di una variabile denita in un altro le. ! ! .dene: serve a denire una variabile globale che sar referenziata come extern ! ! in altri le. ! ! .data: denisce una variabile di un byte. ! ! .data2: denisce una variabile di 2 byte. ! ! .data4: denisce una variabile di 4 byte. ! ! .ascii: denisce una stringa di caratteri. ! ! .asciz: denisce una stringa di caratteri terminata da zero. ! ! .align 16: allinea gli indirizzi alla word. ! ! .space NUM: denisce uno spazio di memoria che contiene NUM byte a zero. ! ! .comm VAR NUM: denisce una variabile di nome VAR di NUM byte inzializzati ! ! a zero. ! Comment

! La sintassi generale per le istruzioni assembly la seguente: Label: mnemonic destination, suorce ! ! ! ! ! Gli operandi possono essere: ! ! registri. ! ! locazioni di memoria. ! ! valori immediati. ! ! impliciti.
57 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

fondamentale che gli operandi abbiano la stessa dimensione e inoltre non possibile che entrambi siano locazioni di memoria. Indirizzamento indiretto: mediante questa tecnica possibile utilizzare i registri come dei puntatori; in questo caso la notazione assembler richiede di metter il nome del registro tra parentesi tonde o quadre. Stack: lo stack unarea di memoria strutturata secondo il modello LIFO. Le istruzioni associate allo stack sono: call, ret, push, pop, enter e leave. Queste istruzioni fanno riferimento al registro SS per individuare il segmento di memoria che contiene lo stack da utilizzare (current stack). Il registro ESP contiene lindirizzo dellultimo elemento memorizzato sullo stack. Loperazione push sposta lo stack poiter verso indirizzi ddi memoria pi bassi mentre loperazione pop lo sposta ad indirizzi pi alti. Pi precisamente la push sottrae 4 da ESP ed inserisce 4 byte in (ESP), la pop invece legge 4 byte allindirizzo (ESP) e somma 4 ad ESP. Listruzione call label esegue un salto incondizionato alletichetta label ed esegue una push dellindirizzo dellistruzione successiva alla call. Listruzione ret effettua una pop e salta allindirizzo estratto dallo stack. Solitamente i parametri di una procedura vengono passati attraverso lo stack, mediante un opportuno numero di istruzioni push. Quando la subroutine deve modicare i parametri che le vengono passati necessario eseguire la push dellindirizzo del parametro (similmente al linguaggio C). In questo caso il valore del parametro pu essere recuperato mediante un indirizzamento indiretto: supponendo che lindirizzo del parametro sia stato inserito nello stack alla posizione ESP+4, per acceder al valore del parametro si esegue lespressione (ESP+4). Per facilitare la gestione dello stack, e mantenere traccia dello stack frame della procedura corrente, larchitettura fornisce un ulteriore registro EBX. Prologo: il prologo di una chiamata a procedura una convezione che impone come prima istruzione il salvataggio sullo stack del valore di EBP e successivamente nello stesso registro si memorizza ESP. Le variabili locali saranno allocate subito dopo aver salvato il valore di ESP: a questo punto ESP pu essere modicato a piacere mentre EBP pu essere utilizzato per far riferimento ai parametri della procedura. Epilogo: prima di eseguire listruzione ret necessario ripristinare il vecchio valore di EBP, quello che secondo la convenzione, era stato salvato sullo stack. Quindi, dopo aver deallocato lo stack utilizzato dalla procedura (dati, variabili locali e parametri), eseguendo una pop EBP il ripristino avverr con successo e sar possibile eseguire correttamente la ret. Calling convention: compito della procedura chiamante posizionare i parametri di una procedura sullo stack e solitamente sempre il chiamante a deallocare lo spazio di stack occupato da questi parametri. Inoltre, per quanto riguarda le funzioni C, i parametri devono essere passati in ordine inverso rispetto quello in cui compaiono nel prototipo della funzione. Inne se una subroutine modica i valori dei registri del processore opportuno che li ripristina prima di passare il controllo alla procedura chiamante.

58 di 142

INTERRUPT ED ECCEZIONI IN MINIX. ! Il processore esegue costantemente il seguente ciclo: ! ! ! preleva la prossima istruzione dalla memoria. ! ! ! interpreta listruzione. ! ! ! esegui listruzione. ! ! ! incrementa lIP. ! Possono per vericarsi determinate circostanze per le quali il processore obbligato a ! modicare tale ciclo di esecuzione. Alcuni esempi sono: ! ! ! si rilevano errori durante lesecuzione di unistruzione (dati errati o non ! ! ! disponibili, accessi non autorizzati, ...). ! ! ! un dispositivo esterno richiede lintervento del processore. ! ! ! il processore deve svolgere unattivit diversa da quella programmata. ! Riguardo agli errori esistono diverse tipologie: ! ! ! Fault: quando la CPU rileva, in fase di decode/execute, che unistruzione non ! ! ! pu essere eseguita, il ciclo deve essere interrotto. Il sistema reagisce salvando ! ! ! le informazioni necessarie in opportune zone di memoria e cedendo il controllo ! ! ! ad una routine di fault-handling. ! ! ! Quando possibile rimediare allerrore riscontrato la routine di fault-handling ! ! ! esegue le operazioni del caso e restituisce il controllo al programma in modo ! ! ! che la CPU possa proseguire dal punto in cui si era interrotta. ! ! ! Abort: se la procedura di fault-handling non in grado di risolvere lerrore il ! ! ! programma viene denitivamente interrotto. ! ! ! Trap: in questo caso il programmatore che denisce quando interrompere ! ! ! lesecuzione del programma principale per passare il controllo ad un secondo ! ! ! programma (ad esempio un debugger). Solitamente il controllo viene ceduto ! ! ! solo dopo lincremento di IP e come per i fault il sistema salva i dati necessari ! ! ! per riprendere lesecuzione e cede il controllo al trap handler. ! I fault e i trap sono interruzioni che dipendono solo dal programma e dalle condizioni ! iniziali, quindi sono predicibili e si parla di interruzioni sincrone. Inoltre il sistema ! predispone un meccanismo di risposta molto simile per queste interruzioni anche se le ! informazioni da memorizzare sono differenti: ! ! ! per i fault lindirizzo salvato deve essere quello dellistruzione che ha provocato ! ! ! lerrore, in modo che possa essere ricaricata dopo che il fault handler abbia ! ! ! eseguito le contromisure adeguate. ! ! ! per i trap invece lindirizzo salvato quello dellistruzione successiva a quella ! ! ! che ha provocato il trap. ! ! ! ! ! ! ! ! ! ! ! ! ! ! Nellarchitettura IA-32 i fault e i trap vengono solitamente inseriti nella categoria delle eccezioni. Anche le eccezioni si dividono in tre categorie: ! ! Fault: eccezione dovuta ad un errore che pu essere corretto. Viene salvato ! ! sullo stack lindirizzo dellistruzione che ha generato lerrore e il controllo ritorna ! ! al programma dopo la correzione dellerrore. ! ! Trap: una richiesta esplicita di intervento attraverso unistruzione di trapping ! ! (INT). ! ! Abort: uneccezione che non consente la ripresa dellesecuzione del ! ! programma. Esistono anche le interruzioni asincrone ovvero interruzioni dovute ad eventi esterni che non possono essere previste (non predicibili). Tipicamente i dispositivi esterni al processore e che operano indipendentemente da esso, possono richiedere lintervento della CPU provocando quindi uninterruzione del ciclo di esecuzione. Classici esempi di eventi non predicibili legati alle periferiche sono: terminazione di operazioni richieste dal
59 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

processore per conto di processi, il dispositivo ha assunto un nuovo stato (ready, faulty, ...) ed quindi necessario avvertire il sistema. quindi necessario sviluppare un meccanismo che permetta al processore di gestire eventi che non pu predire e garantire: ! ! lesecuzione di altre attivit mentre il processore in attesa di un evento ! ! asincrono. ! ! veloce gestione dellevento. Polling: questa tecnica prevede la possibilit della CPU di vericare periodicamente se ci sono richieste pendenti da parte dei dispositivi esterni. un metodo svantaggioso in quanto molti cicli di CPU vengono sprecati quando non ci sono richieste pendenti, il tempo medio di risposta pu diventare elevato per la gestione di eventi critici e determinare la periodicit migliore un grosso problema. Interrupt: si permette ai dispositivi esterni di comunicare direttamente con il processore fornendo una connessione sica con esso (interrupt line). Quando la CPU sente un interrupt esegue una routine di risposta chiamata interrupt handler. Il principale vantaggio lassenza di overhead quando non ci sono richieste pendenti.

! ! ! ! ! ! ! ! ! !

Ciascun interrupt ed eccezione identicato da un numero compreso tra 0 e 255 chiamato vettore. I vettori 0-8, 10-14 e 16-19 sono interrupt ed eccezioni predeniti mentre i vettori 32-255 sono a disposizione degli utenti e vengono chiamati maskable interrupt. Il ag IF del registro EFLAGS pu disabilitare il servizio di interrupt mascherabili ricevuto sul pin INTR del processore. Questo ag viene gestito attraverso le istruzioni STI (set interrupt-enable ag) e CLI (celar interrupt-enable ag) eseguibili solo con opportuni privilegi. Per la gestione di diversi segnali di interrupt provenienti dai dispositivi esterni si ricorre allhardware dedicato: linterrupt controller. Questo controllore stabilisce la priorit tra pi interrupt pendenti e segnala al processore quale interrupt servire per primo.

60 di 142

! ! ! ! ! ! !

Interrupt sull80386: questo processore ha una sola linea di interrupt e una linea di acknowledge e utilizza il seguente protocollo per la segnalazione di un interrupt: ! ! linterrupt controller alza il segnale di INT. ! ! il processore sente il segnale su INT e asserisce INTA (acknowledge). ! ! quando il controller sente INTA abbassa il segnale di INT. ! ! successivamente il processore asserisce ancora INTA per segnalare al ! ! controller di caricare sul bus dati il numero del segnale di interrupt da servire.

INTERRUPT HANDLING. ! Il procedimento per la gestione degli interrupt il seguente: ! ! ! linterrupt controller segnala loccorrenza di un interrupt e passa il numero ! ! ! dellinterrupt, ovvero il vettore. ! ! ! il processore usa il vettore dellinterrupt per decidere quale handler attivare. ! ! ! il processore interrompe il processo corrente PROC e ne salva lo stato. ! ! ! la CPU salta ad un interrupt handler. ! ! ! quando linterrupt stato gestito lo stato del processo PROC viene ripristinato e ! ! ! PROC riprende lesecuzione da dove era stato sospeso. ! Vediamo un esempio: ! ! ! la CPU sta eseguendo listruzione 0x100 di un programma mentre un processo ! ! ! P1 acquisisce dei dati in un suo registro di indirizzo 0x8000. ! ! ! P1 asserisce il segnale INT pro richiedere intervento del processore. !

! ! ! !

! dopo aver completato listruzione in esecuzione il processore sente il segnale ! INT asserito, salva il valore di IP e asserisce INTA.

61 di 142

! ! ! ! ! !

! P1 rileva INTA e abbassa in segnale INT. ! il processore riasserisce INTA. ! P1 rileva di nuovo INTA e pone il vettore dellinterrupt (16) sul bus dati.

! ! ! !

! il processore salta allinterrupt handler associato allinterrupt 16. ! lhandler legge il dato da 0x8000, lo modica e lo riscrive allindirizzo 0x8001.

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

! al termine lhandler esegue listruzione RETI che ripristina il valore di EIP a ! 0x100+0x1 = 0x101, da dove il processore riprende lesecuzione del ! programma interrotto.

Per svolgere queste operazioni larchitettura prevede luso delle seguenti strutture dati: ! ! Global Descriptor Table (GDT): denisce i contenuti dei diversi segmenti di ! ! memoria e informazioni per il controllo degli accessi. ! ! Interrupt Decriptor Table (IDT): denisce lindirizzo di inizio delle varie routine ! ! preposte alla gestione di eccezioni ed interrupt. ! ! Task-State Segment (TSS): contiene gli indirizzi che devono essere caricati nei ! ! registri SS e ESP in risposta ad uninterruzione o interrupt. Per fare in modo che il processore possa localizzare le strutture dati GDT e IDT esistono due registri dedicati a 48 bit: GDTR e IDTR. Il valore di questi registri caricato dal SO attraverso le istruzioni privilegiate LGDT e LIDT e possono essere letti in user mode grazie alle istruzioni SGDT e SIDT. La congurazione dei registri GDTR e IDTR : Segment Base Address nei bit 16-47, Segment Limit nei bit 0-15. Anche per TSS esiste un registro dedicato (TR) ed inoltre presente un descrittore di segmento nella GDT per ogni TSS presente nel sistema.

62 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

Pi dettagliatamente alla ricezione di un interrupt o uneccezione il sistema risponde svolgendo le seguenti operazioni: ! ! jump allinterrupt o exception handler di risposta allinterruzione. ! ! salvataggio dello stato del processo, chiamata a procedura save: ! ! ! ! registri generali. ! ! ! ! registri temporanei. ! ! ! ! carica i selettori dei segmenti relativi al kernel. ! ! ! ! lindirizzo della procedura restart viene messo sullo stack. Questa ! ! ! ! procedura permette di ripristinare lo stato del processo nel caso ! ! ! ! debba riprendere la sua esecuzione. ! ! chiamata alla routine di gestione dellinterrupt o eccezione. ! ! ritorno a restart. Inoltre esistono due metodi di trattamento delle interruzioni: senza cambiamento di stack (stesso livello di privilegio), oppure con cambiamento dello stack (diverso livello di privilegio). Nel primo caso le operazioni svolte dal processore sono: ! ! push di EFLAGS, EIP e CS sullo stack. ! ! push di un eventuale codice di errore. ! ! linterrupt gate, prelevato dalla IDT, viene utilizzati per linizializzazione del ! ! nuovo CS e EIP. ! ! a questo punto differenzio lesecuzione: se linterruzione un interrupt il ! ! processore non pu essere di nuovo interrotto quindi mette a zero il campo IF ! ! del registro EFLAGS; invece se linterruzione uneccezione non necessario ! ! disabilitare gli interrupt, quindi metto a 1 il campo IF. ! ! il controllo viene passato al gestore dellinterruzione (CS:EIP). Quando avviene un cambio di stack le operazioni sono leggermente diverse: il punto principale la necessit di trasferire le informazioni dallo stack del processo allo stack del kernel, e proseguire con la gestione rimanendo nellarea del kernel stack. Il procedimento : ! ! salvataggio temporaneo di SS, ESP, EFLAGS, CS e EIP sullo stack. ! ! carico i nuovi SS e ESP dello stack kernel, prelevandoli dal TSS ! ! memorizza sul nuovo stack: ! ! ! ! lo stack segment selector del processo interrotto ! ! ! ! lo stack pointer del programma interrotto. ! ! ! ! EFLAGS, CS, e EIP correnti. ! ! ! ! un eventuale error code provocato da uneccezione. Viene salvato ! ! ! ! sul nuovo stack dop EIP. ! ! ! ! il registro SS con il nuovo selettore di stack prelevato in TSS. ! ! linterrupt gate viene utilizzato per linizializzazione del nuovo CS e EIP. ! ! come prima differenzio lesecuzione: se linterruzione un interrupt il ! ! processore non pu essere di nuovo interrotto quindi mette a zero il campo IF ! ! del registro EFLAGS; invece se linterruzione uneccezione non necessario ! ! disabilitare gli interrupt, quindi metto a 1 il campo IF. ! ! passo il controllo al gestore (CS:EIP). ! ! terminata la gestione necessario ripristinare lo stack frame precedente quindi ! ! bisogna ripristinare i vecchi SS e ESP. ! ! inoltre se il processo non stato sospeso bisogna ripristinare anche il suo ! ! stato, ovvero i valori di CS, EIP e EFLAGS. ! ! a questo punto pu riprendere lesecuzione del processo.

63 di 142

PROTECTED MODE IN MINIX. ! Allavvio del sistema il processore si trova in Real Mode. Per poter passare alla ! modalit Protected necessario che il SW di inizializzazione provveda a predisporre ! una serie di strutture dati e codice necessari per poter operare correttamente in tale ! modalit. Le strutture dati a cui si fa riferimento sono: IDT, GDT, TSS, un segmento ! codice contenente le istruzioni da eseguire dopo laccesso alla modalit protetta, uno o ! pi segmenti in cui sono caricati i gestori di interrupt ed eccezioni, e una LDT se ! necessaria. Inoltre devono essere inizializzati i seguenti registri: GDTR, IDTR ! (inizializzato a interrupt disabilitati), CR1, CR2, CR3 e CR4. ! Le istruzioni per lo switching a protected mode sono: ! ! ! ! PE_BIT EQU 1B ! ! MOV EBX, CR0 ! ! OR EBX, PE_BIT ! ! MOV CR0, EBX ! ! ! ! Dove PE_BIT una variabile pari al valore 0x1B. Le istruzioni quindi non fanno altro che prelevare il contenuto del registro CR0, applicare una maschera di bit del valore 0x1B e ridepositare il nuovo valore nel registro CR0. La seguente immagine mostra la congurazione del registro CR0:

! Quindi considerando un valore qualsiasi nel registro CR0 si ottiene:! Valore in CR0 ! XXXXXXXXXXXXXXXXXXXXXXXX | xxxxxxxx OR

Maschera

00011011

Valore in CR0

XXXXXXXXXXXXXXXXXXXXXXXX | xxx11x11

64 di 142

! Lavvio di minix avviene mediante la funzione cstart(): ! !


! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! Call C startup code to set up a proper environment to run main(). push edx push ebx push SS_SELECTOR push MON_CS_SELECTOR push DS_SELECTOR push CS_SELECTOR call! _cstart! add! esp, 6*4

! ! ! ! ! ! ! ! ! ! !
! ! ! !

Questa funzione viene chiamata passandole i parametri necessari sullo stack e svolge principalmente funzioni di inizializzazione del sistema: congura lil meccanismo di protezione della CPU, copi ai parametri di boot nellarea kernel eseguendo anche particolari controlli su di essi, determina il tipo di video, il bus, di processore, etc. Queste !nformazioni vengono memorizzate in variabili globali in modo che tutte le parti i del kernel possano accedervi. Inoltre richiama a sua volta una subroutine che inizializza le due tabelle GDT e IDT. Al rientro dalla funzione cstart i parametri vengono deallocati. Per rendere utilizzabili le tabelle GDT e IDT necessario che i loro indirizzi vengano caricati allinterno dei registri dedicati GDTR e IDTR: questo avviene grazie alle istruzioni lgdt e lidt nel modo seguente.
! ! ! ! ! Reload gdtr, idtr and the segment registers to global descriptor ! table set up by prot_init(). lgdt! (_gdt+GDT_SELECTOR) lidt! (_gdt+IDT_SELECTOR)

! ! A questo punto GDTR e IDTR puntano ala cella zero della corrispondente tabella ma in ! GDT il primo descrittore riservato e non pu essere utilizzato.

65 di 142

! In Minix ogni descrittore ha la seguente struttura: ! !


struct segdesc_s { ! ! ! ! u16_t limit_low; ! ! ! u16_t base_low; ! ! ! u8_t base_middle; ! ! ! u8_t access; !! ! ! ! ! u8_t granularity; ! ! ! ! ! u8_t base_high; ! ! }; /* segment descriptor for protected mode */

/* |P|DL|1|X|E|R|A| */ /* |G|X|0|A|LIMT|! */

! Essendo la GDT una tabella di descrittori di segmento la sua dichiarazione sar: ! ! ! ! PUBLIC struct segdesc_s gdt[GDT_SIZE]; ! ! ! ! ! Le prime operazioni eseguite sulla GDT servono per caricare al suo interno i puntatori alla GDT stessa e alla IDT. Successivamente si costruiscono i descrittori di segmento per i task e per gli interrupt handlers.
struct desctableptr_s { ! char limit[sizeof(u16_t)];! ! char base[sizeof(u32_t)];! }

! !
! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

/* limit un campo da 16 bit */ /* base un campo da 32 bit */

/* Build gdt and idt pointers in GDT where the BIOS expects them. */ ! dtp= (struct desctableptr_s *) &gdt[GDT_INDEX]; ! * (u16_t *) dtp->limit = (sizeof gdt) - 1; ! * (u32_t *) dtp->base = vir2phys(gdt); ! ! ! dtp= (struct desctableptr_s *) &gdt[IDT_INDEX]; * (u16_t *) dtp->limit = (sizeof idt) - 1; * (u32_t *) dtp->base = vir2phys(idt);

! !

/* Build segment descriptors for tasks and interrupt handlers. */ init_codeseg(&gdt[CS_INDEX], ! kinfo.code_base, kinfo.code_size, INTR_PRIVILEGE); ! init_dataseg(&gdt[DS_INDEX], ! ! kinfo.data_base, kinfo.data_size, INTR_PRIVILEGE); ! init_dataseg(&gdt[ES_INDEX], 0L, 0, TASK_PRIVILEGE);

! ! ! ! ! ! ! ! ! ! !

Listruzione dtp= (struct desctableptr_s *) &gdt[GDT_INDEX]; prende lindirizzo del descrittore situato in gdt allindice GDT_INDEX. Questo lindirizzo di un descrittore quindi di una struttura struct segdesc_s a 64 bit. Ora, eseguendo loperazione di cast, tale indirizzo punter ad una zona di memoria che verr interpretata non pi come una struttura struct segdesc_s bens come una desctableptr_s . Listruzione * (u16_t *) dtp->limit = (sizeof gdt) - 1; , mediante operazione di cast, forza il puntatore dtp a puntare ad una zona di memoria di 16 bit. A questo punto accede al campo limit grazie alloperatore -> ed inne accede allarray dereferenziando il campo limit. In questa posizione si memorizza la grandezza della GDT, ovvero (sizeof gdt) - 1 .

! Listruzione * (u32_t *) dtp->base = vir2phys(gdt); molto simile alla precedente: ! si esegue il cast, si accede al campo base (questa volta di 32 bit) e si dereferenzia. In ! questa posizione scrivo il valore di ritorno della funzione vir2phys che riceve come
66 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! ! !

argomento gtd, ovvero lindirizzo della tabella GTD e ne trasforma lindirizzo da virtuale a sico . NB: le stesse identiche operazioni vengono riprodotte utilizzando idt per generare e caricare in GDT il puntatore alla tabella IDT. I puntatori cos creati verranno a trovarsi nella GDT esattamente allindice in cui il BIOS si aspetta di trovarli. Le tre operazioni di inizializzazione seguenti servono a denire nella GDT i tre segmenti CD, DS e ES: il primo il segmento codice mentre gli altri due sono segmenti dati. Per linizializzazione necessario specicare la posizione nella GTD, ovvero un indice utilizzato per individuare la corretta posizione ove caricare il descrittore di ogni segmento, lindirizzo base del segmento, la dimensione del segmento ed il livello di privilegio. I livelli utilizzati sono 3 (su 4): ! ! intr-privilege: il livello di privilegio del kernel mode. ! ! task-privilege: livello di lavoro in driver mode. ! ! user-privilege: il livello user mode.

! La seguente procedura viene utilizzata per caricare i segmenti codice: ! ! ! ! PUBLIC void init_codeseg(segdp, base, size, privilege)
! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! register struct segdesc_s *segdp; phys_bytes base; vir_bytes size; int privilege; { /* Build descriptor for a code segment. */ ! sdesc(segdp, base, size); ! segdp->access = (privilege << DPL_SHIFT) ! | (PRESENT | SEGMENT | EXECUTABLE | READABLE); ! ! /* CONFORMING = 0, ACCESSED = 0 */ }

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

Valutiamo il valore del campo access : ! DPL_SHIFT = 0x5 ! PRESENT = 0x80 ! SEGMENT = 0x10 ! EXECUTABLE = 0x08 ! READABLE = 0x02 Quindi: ! Privilegi << 5!! = 0000 0000 OR ! PRESENT ! ! = 1000 0000 OR ! SEGMENT ! ! = 0001 0000 OR ! EXECUTABLE != 0000 1000 OR ! READABLE !! = 0000 0010 OR ! ! ! ! ______________ ! ! ! ! 1001 1010 = segdp->access

67 di 142

! Analogamente accade per i segmenti dati, ovvero linizializzazione di TSS in GDT:


! ! ! ! ! ! ! ! ! ! ! PUBLIC void init_dataseg(segdp, base, size, privilege) ! register struct segdesc_s *segdp; ! phys_bytes base; ! vir_bytes size; ! int privilege; ! { ! /* Build descriptor for a data segment. */ ! ! sdesc(segdp, base, size); ! ! segdp->access = (privilege << DPL_SHIFT) ! ! ! | (PRESENT | SEGMENT | WRITEABLE); ! ! /* EXECUTABLE = 0, EXPAND_DOWN = 0, ACCESSED = 0 */ ! }

! Per la IDT la situazione leggermente diversa in quanto il layout dei descrittori ! contenuti in IDT corrispondono alla struttura gate, ovvero la seguente:!

! ! Nella IDT devono essere caricati gli indirizzi di tutte le procedure di risposta agli ! interrupt. ! Caricamento dati in IDT: ! ! /* Build descriptors for interrupt gates in IDT. */
! ! ! ! ! ! for (gtp = &gate_table[0]; gtp < &gate_table[sizeof gate_table / sizeof gate_table[0]]; ++gtp) { int_gate(gtp->vec_nr, (vir_bytes) gtp->gate, !! PRESENT | INT_GATE_TYPE | (gtp->privilege << ! ! DPL_SHIFT)); }

! ! ! !

68 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! !

PRIVATE void int_gate(vec_nr, offset, dpl_type) unsigned vec_nr; vir_bytes offset; unsigned dpl_type; { /* Build descriptor for an interrupt gate. */ ! register struct gatedesc_s *idp; ! idp = &idt[vec_nr]; ! idp->offset_low = offset; ! idp->selector = CS_SELECTOR; ! idp->p_dpl_type = dpl_type; ! #if _WORD_SIZE == 4 ! ! idp->offset_high = offset >> OFFSET_HIGH_SHIFT; ! #endif }

! Di seguito riportato lo schema del TSS e la sua struttura dati in linguaggio C.

69 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

struct tss_s { reg_t backlink; reg_t sp0; reg_t ss0; reg_t sp1; reg_t ss1; reg_t sp2; reg_t ss2; #if _WORD_SIZE == 4 reg_t cr3; #endif reg_t ip; reg_t flags; reg_t ax; reg_t cx; reg_t dx; reg_t bx; reg_t sp; reg_t bp; reg_t si; reg_t di; reg_t es; reg_t cs; reg_t ss; reg_t ds; #if _WORD_SIZE == 4 reg_t fs; reg_t gs; #endif reg_t ldt; #if _WORD_SIZE == 4 u16_t trap; u16_t iobase; /* u8_t iomap[0]; */ #endif };

/* stack pointer to use during interrupt */ /* " segment " " " " */

! La costruzione del TSS avviene mediante le istruzioni:

!
! ! ! ! ! ! ! ! ! ! !

/* Build main TSS. * This is used only to record the stack pointer to be used after an * interrupt. * The pointer is set up so that an interrupt automatically saves the * current process's registers ip:cs:f:sp:ss in the correct slots in the * process table. */ ! tss.ss0 = DS_SELECTOR; ! init_dataseg(&gdt[TSS_INDEX], vir2phys(&tss), sizeof(tss), ! ! ! INTR_PRIVILEGE); ! gdt[TSS_INDEX].access = PRESENT | (INTR_PRIVILEGE << DPL_SHIFT) | ! ! ! ! ! ! TSS_TYPE;! !

70 di 142

CONTEXT SWITCH IN MINIX. ! Prima di una chiamata ad un handler di interrupt/eccezione, il processore esegue uno ! stack switch che consiste in: ! ! ! recuperare dal TSS del task che ha generato linterrupt il selettore di segmento ! ! ! e lo stack pointer per il nuovo stack. ! ! ! su questo nuovo stack vengono memorizzati: ! ! ! ! lo stack segment selector. ! ! ! ! lo stack pointer del programma interrotto. ! ! ! ! EFLAGS, CS e EIP correnti. ! ! ! ! un eventuale error code provocato da uneccezione. Viene salvato ! ! ! ! sullo stack dopo il valore di EIP.

! ! ! ! ! ! ! ! ! ! ! ! !
! ! ! ! ! ! ! ! ! ! !

Dopo aver provveduto al salvataggio parziale del processo interrotto, il processore provvede a caricare in EIP lindirizzo della procedura di risposta che si trova nella IDT. Pi precisamente viene caricato lindirizzo che nella IDT si trova alla posizione indicizzata dal vettore dellinterrupt o dl numero delleccezione. A questo punto il controllo passa allevento handler. necessario un context switch anche per il trasferimento del controllo tra un processo ed un altro: bisogna quindi predisporre una routine, richiamata dal processore, e che possiamo supporre essere avviata da un interrupt di clock. Le operazioni che tale routine dovr essere in grado di svolgere sono: ! ! terminare la fase di salvataggio del contesto del processo corrente. ! ! richiamare il kernel per determinare il prossimo processo da seguire. ! ! caricare nel sistema il contesto del processo selezionato dal kernel. ! ! avviare lesecuzione di questo processo.!
.sect .rom ! ! ! ! .data2 0x526F!! ! ! .sect .bss k_stack:! ! ! ! ! .space K_STACK_BYTES! k_stktop:! ! ! ! ! ! .comm ex_number, 4 ! .comm trap_errno, 4 ! .comm old_eip, 4 ! .comm old_cs, 4 ! .comm old_eflags, 4! ! Before the string table please ! this must be the first data entry (magic #)

! Kernel stack ! Top of kernel stack

71 di 142

! La struttura di un PCB di un processo la seguente:


! ! ! ! ! ! ! ! ! ! ! ! ! ! ! struct proc { struct stackframe_s p_reg;! /* process' registers saved in stack frame*/ #if (CHIP == INTEL) reg_t p_ldt_sel;! ! ! /* selector in gdt with ldt base and limit */ struct segdesc_s p_ldt[2+NR_REMOTE_SEGS];!/*CS,DS and remote segments */ proc_nr_t p_nr; ! ! ! /* number of this process (for fast access) */ struct priv *p_priv; ! ! /* system privileges structure */ short p_rts_flags; ! ! /* process is runnable only if zero */ char p_priority; ! ! ! /* current scheduling priority */ char p_max_priority; ! ! /* maximum scheduling priority */ char p_ticks_left; ! ! /* number of scheduling ticks left */ char p_quantum_size; ! ! /* quantum size in ticks */ struct mem_map p_memmap[NR_LOCAL_SEGS]; ! /* memory map (T, D, S) */ clock_t p_user_time; ! ! /* user time in ticks */ clock_t p_sys_time; ! ! /* sys time in ticks */ struct proc *p_nextready;! /* pointer to next ready process */ }

CONTEXT SWITCH HANDLER.


! ! ! ! ! ! ! ! ! ! ! ! ! Note this is a macro, it just looks like a subroutine. #define hwint00! \ call!save ! ! ! /* save interrupted process state */ call!SISTEMA OPERATIVO! /* viene richiamata unopportuna!/ * ! ! ! ! ! /* routine che individuer il prossimo*/ ! ! ! ! ! /* processo da eseguire, inserendo */ ! ! ! ! ! /* lindirizzo del suo PCB nella */ ! ! ! ! ! /* variabile _next_ptr */ nop ret! ! ! ! ! /* esegui _restart */ ! ! ! ! ! /* la save mette nello stack lidirizzo */ ! ! ! ! ! /* della procedura _restart */

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! Save for protected mode. ! The stack already points into the process table, or has already ! been sitched to the kernel stack. .align 16 save: ! cld! ! ! ! ! ! Set direction flag to a know value ! pushad ! ! ! ! ! Save general registers ! o16 push ds! ! ! ! Save ds ! o16 push es! ! ! ! Save es ! o16 push fs! ! ! ! Save fs ! o16 push gs! ! ! ! Save gs ! mov dx, ss! ! ! ! ss is kernel data segment ! mov ds, dx! ! ! ! Load rest of kernel segments ! mov es, dx! ! ! ! Kernel does not use fs, gs ! mov eax, esp! ! ! ! Prepare to return ! mov esp, k_stktop! ! push _restart!! ! ! Build return address for int handler ! xor ebp, ebp !! ! ! For stacktrace ! jmp RETADR-P_STACKBASE(eax)

72 di 142

! ! ! ! ! ! ! ! !

La save svolge il compito di salvare le informazioni del processo corrente e preparare il nuovo stack: segue una pushad che salva tutti i registri sullo stack, i registri DS, ES, FS e GS non vengono salvati dalla pushad e quindi devono essere inseriti nello stack manualmente, carica lo stack segment (SS) dello stack del kernel e predisone DS e ES per il cambio di stack; memorizza in EAX il vecchio valore di ESP e sostituisce ESP con lindirizzo del top dello stack del kernel: da questo punto si lavora sullo stack kernel. Quindi nel kernel stack viene pushato lindirizzo della procedura restart e si esegue un salto incondizionato lungo lo stack kernel. Questo salto composto dallindirizzo memorizzato in EAX dalla procedura save e da un opportuno offset (RETADR! P_STACKBASE). !

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

! Restart the current process or the next process if it is set. _restart: ! cmp! (_next_ptr), 0 ! ! See if another process is scheduled ! jz 0f! ! ! ! ! Jump foreward ! mov eax, (_next_ptr) ! mov (_proc_ptr), eax! ! Schedule new process ! mov (_next_ptr), 0 0:! mov esp, (_proc_ptr)! ! Will assume P_STACKBASE == 0 ! lldt P_LDT_SEL(esp)! ! Enable process segment descriptors ! lea eax, P_STACKTOP(esp)! ! arrange for next interrupt ! mov (t_ss+TSS3_S_SP0), eax! ! To save state in process table ! o16 pop gs ! o16 pop fs ! o16 pop es ! o16 pop ds ! popad ! add esp, 4! ! ! ! Skip return address ! iretd! ! ! ! ! Continue process

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

La procedura restart si occupa di determinare il processo che deve riprendere lesecuzione e predisporre lambiente (ripristino registri, stack, segmenti..) per la corretta ripresa. La prima cosa che fa questa procedura controllare se stato schedulato un nuovo processo: questo lo fa controllando il valore di _next_ptr , infatti se tale valore zero non ci sono altri processi da eseguire e si salta la sezione di switch tra il processo corrente ed il processo schedulato. Altrimenti il valore del processo corrente viene associato al processo appena schedulato. Listruzione lldt P_LDT_SEL(esp) permette di caricare la LDT del processo. A questo punto non sono in grado di scrivere direttamente dallo stack alla TSS: quindi carico in EAX lindirizzo dello stack pointer e con listruzione successiva lo scrivo allinterno della TSS. A questo punto necessario ripulire lo stack dai dati memorizzati dalla save: quindi si estraggono prima i valori dei registri di segmento e successivamente sei segue una popad per ripristinare gli altri registri. Inne si dealloca lo stack e si esegue la iretq per proseguire lesecuzione del processo corrente.

73 di 142

GESTIONE DELLE ECCEZIONI IN MINIX. ! Le eccezioni sono eventi che generano uninterruzione del ciclo Fetch-Decode-Exeute ! della CPU. Questi eventi sono detti interruzioni sincrone in quanto dipendono dal ! programma che le genera e dalle condizioni iniziali. Generalmente le interruzioni ! denominate Fault e Trap rientrano nella categoria delle eccezioni proprio per il fatto di ! essere interruzioni sincrone. Possiamo quindi suddividere la classe delle eccezioni in ! tre categorie: ! ! ! Fault: eccezione dovuta ad un errore che pu essere corretto. Viene salvato ! ! ! sullo stack lindirizzo dellistruzione che ha generato lerrore e il controllo ritorna ! ! ! al programma dopo la correzione dellerrore. ! ! ! Trap: una richiesta esplicita di intervento attraverso unistruzione di trapping ! ! ! (INT). ! ! ! Abort: uneccezione che non consente la ripresa dellesecuzione del ! ! ! programma. ! Quando uneccezione, di qualunque tipo, viene generata, a livello hardware signica ! che viene attivato il bit di interrupt. A questo punto linterrupt controller, che ha il compito ! di controllare costantemente una maschera di bit contenente anche il bit di interrupt, ! sente leccezione ed informa la CPU che dovr eseguire le seguenti operazioni: ! ! ! salvare il contesto del processo interrotto (esattamente come nella prima fase ! ! ! del context switch). ! ! ! richiamare lexception handler il cui indirizzo contenuto nella IDT. ! quindi fondamentale che i progettisti si occupino della inizializzazione della IDT e dei ! gestori delle eccezioni tenendo conto del fatto che pu vericarsi uneccezione anche ! durante lesecuzione del kernel. ! La IDT anchessa una tabella di descrittori di segmento come la GTD con la differenza ! che i descrittori della IDT hanno una struttura diversa e vengono chiamati gate ! descriptor.

! !
! ! ! ! ! !

struct gatedesc_s { ! u16_t offset_low; ! u16_t selector; ! u8_t pad;! ! /* |000|XXXXX| ig & trpg, |XXXXXXXX| task g */ ! u8_t p_dpl_type; /* |P|DL|0|TYPE| */ ! u16_t offset_high; };

! La cotruzione della IDT avviene come la GDT ovvero denendo una tabella di gate ! descriptor: ! !
PUBLIC struct gatedesc_s idt[IDT_SIZE];

74 di 142

! ! ! ! ! ! ! ! ! ! ! !

Quando si verica uninterruzione lHW effettua alcune operazioni preliminari: ! ! usa il vettore dellinterruzione come indice per accedere alla IDT. ! ! il vettore di interrupt indicizza un selettore di segmento contenuto nella IDT. ! ! questo selettore viene utilizzato come indice per accedere alla GDT. ! ! si accede alla GDT e si seleziona il descrittore di segmento indicizzato dal ! ! selettore. ! ! si ricava lindirizzo lineare al quale risiede la procedura di gestione ! ! dellinterruzione sollevata. Questo avviene prelevando dal descrittore di ! ! segmento lindirizzo base e sommando tale base alloffset contenuto ! ! nellinterrupt gate (quello della IDT, selezionato al secondo punto). ! ! una volta ottenuto lidirizzo della procedura pu iniziare la fase di gestione ! ! dellinterruzione che prosegue a livello SW.

! ! ! ! ! ! ! ! ! ! ! !

quindi opportuno costruire ed inizializzare la IDT in fase di boot (di caricamento del SO). Abbiamo gai visto come viene creata la IDT mediante array di strutture gate; per riempirla adeguatamente necessario innanzitutto denire le costanti numeriche associate ad ogni interrupt/eccezione. Queste costanti verranno poi utilizzate come indice per accedere alla IDT e recuperare, in base al processo visto poco sopra, gli indirizzi delle procedure di gestione. Dopo questa fase di denizione possibile passare al riempimento della tabella: ogni riga composta da un vettore di tre campi, il primo lindirizzo del gestore delleccezione, il secondo individua il numero delleccezione, il terzo indica il livello di privilegio delleccezione. Gli indirizzi funzionano come dei puntatori a funzione. Tra le routine predenite (le posizioni non a disposizione dellutente) tutte quelle che iniziano con hwint sono procedure per la gestione di interrupt mentre le precedenti sono procedure di gestione delle eccezioni.

75 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

gate_table[] = { { divide_error, DIVIDE_VECTOR, INTR_PRIVILEGE }, { single_step_exception, DEBUG_VECTOR, INTR_PRIVILEGE }, { nmi, NMI_VECTOR, INTR_PRIVILEGE }, { breakpoint_exception, BREAKPOINT_VECTOR, USER_PRIVILEGE }, { overflow, OVERFLOW_VECTOR, USER_PRIVILEGE }, { bounds_check, BOUNDS_VECTOR, INTR_PRIVILEGE }, ! { inval_opcode, INVAL_OP_VECTOR, INTR_PRIVILEGE }, { copr_not_available, COPROC_NOT_VECTOR, INTR_PRIVILEGE }, { double_fault, DOUBLE_FAULT_VECTOR, INTR_PRIVILEGE }, { copr_seg_overrun, COPROC_SEG_VECTOR, INTR_PRIVILEGE }, { inval_tss, INVAL_TSS_VECTOR, INTR_PRIVILEGE }, { segment_not_present, SEG_NOT_VECTOR, INTR_PRIVILEGE }, { stack_exception, STACK_FAULT_VECTOR, INTR_PRIVILEGE }, { general_protection, PROTECTION_VECTOR, INTR_PRIVILEGE }, #if _WORD_SIZE == 4 { page_fault, PAGE_FAULT_VECTOR, INTR_PRIVILEGE }, { copr_error, COPROC_ERR_VECTOR, INTR_PRIVILEGE }, #endif { hwint00, VECTOR( 0), INTR_PRIVILEGE }, { hwint01, VECTOR( 1), INTR_PRIVILEGE }, { hwint02, VECTOR( 2), INTR_PRIVILEGE }, { hwint03, VECTOR( 3), INTR_PRIVILEGE }, { hwint04, VECTOR( 4), INTR_PRIVILEGE }, { hwint05, VECTOR( 5), INTR_PRIVILEGE }, { hwint06, VECTOR( 6), INTR_PRIVILEGE }, ! { hwint07, VECTOR( 7), INTR_PRIVILEGE }, { hwint08, VECTOR( 8), INTR_PRIVILEGE }, { hwint09, VECTOR( 9), INTR_PRIVILEGE }, { hwint10, VECTOR(10), INTR_PRIVILEGE }, { hwint11, VECTOR(11), INTR_PRIVILEGE }, { hwint12, VECTOR(12), INTR_PRIVILEGE }, { hwint13, VECTOR(13), INTR_PRIVILEGE }, { hwint14, VECTOR(14), INTR_PRIVILEGE }, { hwint15, VECTOR(15), INTR_PRIVILEGE }, #if _WORD_SIZE == 2 { p_s_call, SYS_VECTOR, USER_PRIVILEGE }, /* 286 system call */ #else { s_call, SYS386_VECTOR, USER_PRIVILEGE }, /* 386 system call */ #endif { level0_call, LEVEL0_VECTOR, TASK_PRIVILEGE }, };

! NB: level0_call sono task_privilege in quanto si tratta di chiamate al sistema. ! Dopo la gestione delleccezione il SO restituisce il controllo al programma se si ! trattato! di un Trap o un Fault, altrimenti termina il programma a seguito del vericarsi di ! una Abort. ! NB: lindirizzo dello stack utente in Minix si trova nella UDT, mentre lindrizzo del kernel ! stack situato nella GDT. La GDT e la IDT sono raggiungibili mediante i registri GDTR ! e IDTR, la TSS raggiungibile tramite il registro dedicato TR.

76 di 142

! Possiamo riassumere la situazione del sistema attraverso il seguente schema: TR UDT TSS GDT

STACK UTENTE

STACK INTERMEDIO

STACK KERNEL

SS

ESP

CODICE

CS

PCB

! Quando viene sollevata uneccezione il usso di esecuzione salta allexception handler:


! !*=================================================================* ! !*! ! ! ! exception handlers! ! ! ! * ! !*=================================================================* ! ! _divide_error: ! ! ! push ! DIVIDE_VECTOR ! ! ! jmp !! exception ! ! ! _single_step_exception: ! ! ! push ! DEBUG_VECTOR ! ! ! jmp !! exception ! ! ! _nmi: ! ! ! push ! NMI_VECTOR ! ! ! jmp !! exception ! ! 77 di 142

! ! ! ! ! ! ! ! ! ! ! !

! ! ! ! ! ! ! ! !

_breakpoint_exception: ! push ! BREAKPOINT_VECTOR ! jmp !! exception _segment_not_present: ! push ! SEG_NOT_VECTOR ! jmp !! errexception _stack_exception: ! push ! STACK_FAULT_VECTOR ! jmp !! errexception

! ! ! ! ! !

Lexception handler effettua una push con cui inserisce nello stack intermedio il codice delleccezione sollevata dopodich effettua una jump alla routine di gestione: nel caso in cui leccezione possa generare un codice di errore si salta alla routine errexception altrimenti il salto verso la routine exception .

!*=================================================================* ! !*! ! ! ! ! exception! ! ! ! ! * ! !*=================================================================* ! !This is called for all exceptions which do not push an error code. ! ! ! ! ! .align 16 excpetion: ! sseg mov! ! sseg pop! ! ! jmp!

(trap_errno), 0! ! (ex_number) exception1

! Clear trap_errno

! ! ! ! ! ! ! ! ! ! ! ! ! ! !

!*=================================================================* !*! ! ! ! ! errexception! ! ! ! * !*=================================================================* !This is called for all exceptions which push an error code. .align 16 errexception: ! sseg pop! (ex_number) ! sseg pop! (trap_errno) exception1:! ! ! ! ! Common for all exceptions ! push! eax! ! ! Eax is scratch register ! mov ! eax, 0+4(esp)! ! Old eip ! sseg mov ! (old_eip), eax! ! movzx! eax, 4+4(esp)! ! Old cs ! sseg mov ! (old_cs), eax! ! mov! eax, 8+4(esp)! ! Old eflags! ! sseg mov ! (old_eflags), eax! ! pop ! eax ! call ! save ! push ! (old_eflags) ! push ! (old_cs) ! push ! (old_eip) ! push ! (trap_errno) ! push ! (ex_number) ! call ! _exception!! !(ex_number, trap_errno, old_eip, ! ! ! ! ! ! ! old_eflags) ! add! esp, 5*4 ! ret

!
! ! ! ! ! ! ! ! ! ! !

old_cs,

78 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! ! !

La procedura exception viene chiamata in caso di eccezione senza codice di errore: questa routine mette a zero il campo trap_errno e pusha nello stack il numero identicativo delleccezione. A questo punto salta a exception1 che una parte della routine errexception: questo signica che sia le eccezione con codice di errore che quelle senza vengono trattate allo stesso modo, con lunica differenza di prelevare dallo stack il codice dellerrore generato. Al sollevamento delleccezione larchitettura esegue le seguenti operazioni: ! ! tramite TR raggiunge il TSS e copia SS, ESP, EFLAGS, CS, EIP e ERR_CODE ! ! nello stack intermedio, quello puntato dal TSS. ! ! In questo momento ESP e SS puntano sulla stessa porzione di stack. ! ! tramite IDTR raggiunge la IDT, preleva lindirizzo della procedura di gestione ! ! delleccezione e lo inserisce in EIP. ! ! a questo punto parte una delle routine di risposta viste nellexception handler. Quando la procedura exception1 inizia la sua esecuzione lo stack ha quindi la seguente congurazione: SS ESP EFLAGS CS EIP ESP

! ! ! ! ! ! ! ! ! ! !
! ! ! ! ! ! ! ! ! ! ! ! ! ! !

NB: ERR_CODE e il codice delleccezione sono stati gai prelevati dallo stack dalle routine exception e errexception. A questo punto vengono eseguite le seguenti operazioni: ! ! si mette eax nello stack in quanto necessario un registro di supporto. ! ! si preleva dallo stack il valore di EIP salvato dallHW. ! ! si salva tale valore in una variabile dedicata di nome old_eip. ! ! si esegue la medesima operazione anche per CS e EFLAGS. ! ! si ripristina il valore di eax prelevandolo dallo stack. Ora i registri del processore si trovano ancora nella stato in cui erano prima dellinterruzione dellesecuzione del programma principale: devo salvare lo stato del processore per poterlo preservare, quindi chiamo la save.
.align 16 save: ! cld! ! ! ! pushad ! ! ! o16 push ds! ! o16 push es! ! o16 push fs! ! o16 push gs! ! mov dx, ss! ! mov ds, dx! ! mov es, dx! ! mov eax, esp! ! ! !

! ! ! ! ! ! ! ! ! !

! ! ! ! ! ! ! ! ! !

! ! ! ! ! ! ! ! ! !

Set direction flag to a know value Save general registers Save ds Save es Save fs Save gs ss is kernel data segment Load rest of kernel segments Kernel does not use fs, gs Prepare to return

incb (_k_reenter)! ! jnz set_restart1! ! mov esp, k_stktop!

! From -1 if not reentering ! Stack is arredai kernel stack

79 di 142

! ! ! ! ! !

! push _restart!! ! ! Build return address for int handler ! xor ebp, ebp !! ! ! For stacktrace ! jmp RETADR-P_STACKBASE(eax) set_restart1: ! push restart1 ! jmp RETADR-P_STACKBASE(eax)

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

Questa save analoga a quella vista per il context switch con la sola differenza che deve trattare il caso in cui venga sollevata uneccezione durante la gestione di uneccezione: questo possibile in quanto durante la gestione di un eccezione il decimo bit di EFLAGS, ovvero il bit IF, viene settato a uno e quindi il sistema pu sentire gli interrupt. In particolare necessario controllare, quando si cerca di gestire uneccezione, in quale stack si sta lavorando, in quanto se si gai nello stack del kernel non necessario eseguire di nuovo lo stack switch. Questo ci che fanno le istruzioni incb e jnz in grassetto: se il valore indica che lo stack gai stato inizializzato non necessario operare alcuna modica e i pu saltare direttamente alle istruzioni di rientro. Quindi le operazioni eseguite dalla save sono: ! ! salva nello stack lo stato di tutti i registri del processore. ! ! salva lo stato di tutti i registri dei segmenti, non inclusi nella pushad. ! ! setta i segmenti necessari ad SS, dove SS il segmento kernel. ! ! se non ci si trova ancora sullo stack del kernel sposta in ESP lindirizzo del ! ! kernel stack, altrimenti salta al rientro (set_restart1). ! ! se il salto avvenuto inserisci nello stack lindirizzo return1, altrimenti inserisci ! ! lindirizzo _return. ! ! esegui il rientro dalla routine save: siccome i dati presenti sullo stack sono ! ! necessari per operazioni che verranno eseguite successivamente non ! ! possibile eseguire una ret (lo stack non stato pulito e chiameremmo _restart ! ! o restart1), si preferisce un far jump che ritorna allistruzione successiva alla ! ! save, nella procedura exception1. ! ! ! ! ! ! ! ! ! ! NB: SS il segmento kernel perch in realt lo stack kernel e lo stack intermedio sono !lo stesso segmento ma sono costituiti da parti di stack differenti. Per questo motivo il controllo dello stack attuale si fa solo prima della modica di ESP.

SS ESP EFLAGS CS EIP RET ALL REG DS ESP FS GS

jmp RETADR-P_STACKBASE(eax)

_restart

ESP - kernel stack


80 di 142

! Al rientro dalla save lo stack sul quale si lavora cambiato, avvenuto lo switch tra ! stack intermedio e stack del kernel. A questo punto si caricano sul nuovo stack tutti i ! dati necessari per la gestione delle eccezioni e si chiama la procedura _exception.

! ! ! ! ! ! ! ! ! !

/*==========================================================================* * exception * *==========================================================================*/ PUBLIC void exception(vec_nr) unsigned vec_nr; { /* An exception or unexpected interrupt has occurred. */ struct ex_s { char *msg; int signum; int minprocessor; }; static struct ex_s ex_data[] = { { "Divide error", SIGFPE, 86 }, { "Debug exception", SIGTRAP, 86 }, { "Nonmaskable interrupt", SIGBUS, 86 }, { "Breakpoint", SIGEMT, 86 }, { "Overflow", SIGFPE, 86 }, { "Bounds check", SIGFPE, 186 }, { "Invalid opcode", SIGILL, 186 }, { "Coprocessor not available", SIGFPE, 186 }, { "Double fault", SIGBUS, 286 }, { "Copressor segment overrun", SIGSEGV, 286 }, { "Invalid TSS", SIGSEGV, 286 }, { "Segment not present", SIGSEGV, 286 }, { "Stack exception", SIGSEGV, 286 },/* STACK_FAULT already used */ { "General protection", SIGSEGV, 286 }, { "Page fault", SIGSEGV, 386 }, /* not close */ { NIL_PTR, SIGILL, 0 }, /* probably software trap */ { "Coprocessor error", SIGFPE, 386 }, }; register struct ex_s *ep; struct proc *saved_proc; /* Save proc_ptr, because it may be changed by debug statements. */ saved_proc = proc_ptr; ep = &ex_data[vec_nr]; if (vec_nr == 2) { /* spurious NMI on some machines */ kprintf("got spurious NMI\n"); return; } /* If an exception occurs while running a process, the k_reenter * variable will be zero. Exceptions in interrupt handlers or system * traps will make k_reenter larger than zero. */ if (k_reenter == 0 && ! iskernelp(saved_proc)) { cause_sig(proc_nr(saved_proc), ep->signum); return; }

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

81 di 142

! ! ! ! ! ! ! ! ! ! ! !

La procedura _exception contiene usa struttura dati contenente i messaggi di errore e il numero delle signal da inviare. La gestione delle eccezioni si risolve sostanzialmente nellinviare una signal al processo che stato interrotto. La procedura cause_sig in grado di eseguire unanalisi delleccezione e determinare se il processo che lha sollevata in grado di proseguire la sua elaborazione oppure deve essere terminato. Lultimo if controlla se gi in corso la gestione di unaltra eccezione, nel qual caso non viene permesso di chiamare la funzione cause_sig. Rientrati dalla procedura _exception rima solo listruzione di gestione dello stack da eseguire, la quale dealloca lo spazio utilizzato dalla procedura exception1 ed esegue la ret. Ci troviamo ancora nello stack del kernel quindi lindirizzo prelevato dalla ret sar quello pushato dalla save, ovvero _restart/restart1.

! ! _restart: ! ! ! ! !Restart the current process or the next process if it is set ! ! cmp! (_next_ptr), 0 ! ! jz ! 0f ! ! mov !eax,(_next_ptr) ! ! mov !(_proc_ptr), eax ! ! mov !(_next_ptr), 0 ! 0: !mov !esp, (_proc_ptr) ! ! lldt P_LDT_SEL(esp) ! ! lea eax, P_STACKTOP(esp) ! ! mov (_tss+TSS3_S_SP0), eax ! ! restart1: ! ! decb (_k_reenter) ! ! o16 pop gs ! ! o16 pop fs ! ! o16 pop es ! ! o16 pop ds ! ! popad ! ! add esp,4 ! ! iretd

! ! ! ! ! ! ! !

Se il processo che ha sollevato leccezione deve essere sospeso la procedura _exception pone nel campo _next_ptr una valore diverso da zero che identica il successivo processo da eseguire. Quindi il primo controllo serve per stabilire se il processo che ha sollevato leccezione deve essere terminato o meno: se si, viene caricato laltro processo da eseguire, altrimenti si salta alletichetta 0. A questo punto si eseguono le operazioni necessarie per riprendere lesecuzione del processo: si carica la LDT, si aggiornano alcuni campi del TSS (stack switch), si pulisce lo stack e si rientra nel punto il cui il processo era stato interrotto.

82 di 142

GESTIONE INTERRUPT IN MINIX. ! Come abbiamo visto, gli interrupt sono lunico mezzo di cui il sistema dispone per poter ! noticare alla CPU il vericarsi di un evento ad essa esterno. Ci signica che le ! periferiche possono comunicare con il processore solo mediante gli interrupt ed ! essendo, dal punto di vista della CPU, eventi asincroni, stato necessario modicare il ! ciclo di esecuzione del processore per poter rilevare linterrupt. IF interrupt THEN {...} ELSE ! ! ! ! ! FETCH DECODE EXECUTE

I punti chiave di questo meccanismo di comunicazione sono: ! ! la periferica invia un segnale elettrico. ! ! lInterrupt Controller sente il segnale alzato dalla periferica. ! ! lInterrupt Controller invia tale segnale al processore.

! ! ! ! ! ! ! ! ! ! ! !

NB: ! in questo schema IRQ sono i segnali di interrupt delle periferiche. possibile che ! vengano inviate pi richieste di interrupt contemporaneamente, questo perch si ! tratta di un segnale generato dalle periferiche la cui esecuzione indipendente dal ! processore. Per quanto riguarda le eccezioni invece la situazione diversa: un ! processore pu incontrare una sola eccezione per volta, al massimo pu capitare ! di sollevare uneccezione durante la gestione di unaltra eccezione ma non ! vengono mai sollevate due eccezioni contemporaneamente. Il chip dellIC a sua volta costituito da altre quattro componenti: ! ! Interrupt Mask Register. ! ! Interrupt Request Register. ! ! Priorit Resolver. ! ! In Service Register.

83 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

Il registro IMR verica se le linee di interrupt sono state mascherate: se il bit di interrupt non settato, la periferica continua ad inviare il segnale nch non viene servita. Se invece linterrupt non mascherato viene registrato allinterno di IRR dove viene mantenuto no allavvio della sua gestione. La componente PR una logica che stabilisce una priorit tra interrupt che arrivano simultaneamente e seleziona il segnale con priorit maggiore, il quale verr gestito per primo. Il segnale individuato da PR viene memorizzato allinterno di ISR in attesa di essere gestito. Appena PR seleziona linterrupt da gestire invia un segnale INT al processore, il quale, appena sentito il segnale, risponde con INTA (Interrupt Acknowledge). Appena sentito INTA il Programmable Interrupt Controller (PIC) abbassa INT, inserisce in ISR il segnale IRQ selezionato da PR, resetta il bit corrispondente ad IRQ allinterno di IRR e disabilita tutti gli interrupt di priorit minore o uguale a quella dellinterrupt in corso. A questo punto il processore invia un altro segnale INTA a seguito del quale il PIC provvede a caricare sul bus dati un valore di 8 bit che corrisponde al vettore dellinterrupt. Si parla di Programmable Interrupt Controller perch il controllore degli interrupt programmabile predisponendo una serie di comandi allinterno di alcuni registri in grado di regolare il comportamento dellInterrupt Controller. Solitamente allinterno di un PC esistono due PIC: il primo, che controlla gli IRQ da 0 al 7, situato allindirizzo 0x20H, il secondo, al quale sono assegnati gli IRQ da 8 a 15, si trova allindirizzo 0xA0. Sostanzialmente questi indirizzi, che vengono chiamati registri, sono locazioni di memoria a cui i controlli degli interrupt fanno riferimento per sapere come si devono comportare. Laccesso a questi registri avviene mediante due istruzioni assembler dedicate: in e out. NB: queste istruzioni servono per caricare/leggere dati in particolari registri ai quali non si avrebbe accesso utilizzando una normale istruzione mov.

! ES:! con le istruzioni in e out (e le giuste maschere) possibile abilitare/disabilitare i ! ! segnali di interrupt direttamente sul PIC invece che sul processore. Il seguente ! ! codice abilita IRQ4:

84 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

! ! ! ! ! ! ! ! ! ! ! !

! in ! ! and ! ! out ! ! ! ! ! ! ! ! !

al, 0x21! al, 0xef! 0x21, al!

! Read existing bits. ! Turn on IRQ4 (COM1) ! Write result back to PIC

La prima istruzione carica il contenuto del byte all'indirizzo 0x21 nei primi 8 bit di eax. Supponiamo che 0x21 contenga la seguente maschera di bit: 10110011. Quindi: al = 10110011. Successivamente viene eseguito un and logico con 0xef = 11101111. Quindi (al & 0xef) = 10100011 = al. A questo punto il valore di al viene riposto all'indirizzo 0x21 mediante listruzione out.

NB: la maschera deve essere tale da non modicare i bit che non ci interessano.

ESERCIZIO: disabilitare IRQ2: ! Considerando la stessa maschera del esempio precedente (10110011) dobbiamo ! mandare a uno il terzo bit. quindi necessario un or logico tra la maschera e il ! valore 0x04: ! ! in ! al, 0x21 ! ! or! al, 0x04 ! ! out ! 0x21, al Unaltra istruzione fondamentale per la gestione degli interrupt la EOI (End Of Interrupt). Deve essere seguita dal gestore dellinterrupt al termine della sua esecuzione e quindi viene inviata dalla CPU verso il PIC. Questa istruzione serve a segnalare al controller il termine della gestione dellinterrupt e consente la riabilitazione, a livello PIC, degli interrupt. ! ! ! ! ! mov ! al, 0x20 ! out ! 0x20, al ! ! NB: 0x20 un comando che viene inserito all'indirizzo 0x20

Minix in grado di gestire al massimo 16 vettori di interrupt (clock, tastiera, cascade irq, HD, oppy, ...). Come abbiamo visto i vettori di interrupt sono situati nella IDT ma la posizione 0 di questa tabella riservata: c' quindi uno sfasamento tra il numero del vettore e la corrispondente posizione nella IDT. Per questo motivo IRQ viene letto e poi spiazzato: ! ! #dene IRQ0_VECTOR! 0x50 ! #dene IRQ8_VECTOR! 0x70 ! #dene VECTOR(irq) ! //irq il numero dell'interrupt hardware ! ! (((irq < 8) ? IRQ0_VECTOR : IRQ8_VECTOR) + ((irq) & 0x07) ! ! ! ! Se IRQ vale 1, VECTOR(1) restituisce 0x51: ! ! 1<8 quindi seleziono IRQ0_VECTOR che vale 0x50, ! ! 0x01 & 0x07 = 0x01 quindi 0x50 + 0x01 = 0x51. ! ! ! ! Se IRQ vale 12 VECTOR(12) restituisce 0x74.

85 di 142

! Quindi la macro VECTOR(irq) serve per spiazzare il numero del vettore HW ottenendo il ! numero del vettore SW. ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! Quando la CPU riceve il vettore interrupt esegue le seguenti operazioni: ! ! disabilita gli interrupt. ! ! recupera il selettore di segmento e lo stack pointer dal TSS del task che ha ! ! generato linterrupt. ! ! sul nuovo stack memorizza: ! ! ! stack segment selector. ! ! ! stack pointer del processo interrotto. ! ! ! EFLAGS, CS e EIP correnti. ! ! accede alla tabella IDT nella posizione indicata dal vettore e richiama il gestore ! ! dellinterrupt. compito del sistemista decidere quando riabilitare gli interrupt: in minix questo avviene al termine della gestione dellinterrupt ma nei sistemi real-time ad esempio un interrupt di importanza maggiore pu interrompere la gestione di un altro interrupt di minor importanza. Per permettere la gestione di interrupt innestati bisogna introdurre ulteriori strutture dati e porzioni di stack. Il principio di funzionamento del meccanismo di risposta ad un interrupt riassunto dal seguente schema:

! ! ! ! ! ! ! ! ! ! ! ! !

C per un problema: linterrupt handler richiama il device driver il quale deve poter accedere a particolari strutture dati del kernel ma, data la struttura microkernelizzati del SO Minix, questi driver girano in User Space e non possono accedere direttamente si controller delle periferiche. Per ovviare a questo problema sono state adottate due tecniche: ! ! predisporre una serie di syscall usate dai driver per chiedere al kernel di ! ! operare sulle opportune strutture dati. Questo per implica un context switch ! ! per ogni syscall: per ridurre il numero degli switch si introduce il generic handler. ! ! Generic Handler: le operazioni generali di gestione degli interrupt vengono ! ! estrapolate dai driver ed accorpate in ununica procedura di livello kernel che ! ! prende il nome di generic handler. Questa procedura esegue tutte le operazioni ! ! preliminari di risposta ad interrupt, senza context switch, e solo nellultima fase ! ! passa il controllo ai driver.
86 di 142

! ! ! ! !
! ! ! ! ! ! ! ! ! ! ! !

Quindi per tutti i device, ad eccezione del clock, viene richiamato il generic_handler (chiamato anche intr_handler) il quale esegue il controllo dellinterrupt e dello stato di tutte le periferiche, e quando individua la corretta periferica richiama il driver corretto. Per risvegliare il device driver di competenza, il generic handler si avvale del valore del campo proc_nr_e della struttura dati hook.
typedef unsigned long irq_policy_t; typedef unsigned long irq_id_t; typedef struct irq_hook { ! struct irq_hook *next;! ! ! ! /* next hook in chain */ ! int (*handler)(struct irq_hook *); ! /* interrupt handler */ ! int irq;! ! ! ! ! ! ! /* IRQ vector number */ ! int id;! ! ! ! ! ! ! /* id of this hook */ ! int proc_nr_e;! ! ! ! /* (endpoint) NONE if not in use */ ! irq_id_t notify_id;! ! ! ! /* id to return on interrupt */ ! irq_policy_t policy;! ! ! ! /* bit mask for policy */ } irq_hook_t; typedef int (*irq_handler_t)(struct irq_hook *);

! ! ! ! ! ! !

Il caricamento di questa struttura dati viene effettuato dalla procedura put_irq_handler su richiesta dei singoli driver. NB: esiste almeno un elemento irq_hook_t per ogni tipo di interrupt. Lhook una struttura dati contenente, tra le altre cose, un puntatore allinterrupt handler, il pid del driver ed un puntatore al prossimo hook. Esiste quindi una lista di strutture hook: questo stato fatto perch 15 vettori di interrupt sono troppo pochi e con la lista possibile mantenere pi hook sulla stessa linea.

/*===========================================================================* *! put_irq_handler!! ! * *===========================================================================*/ ! PUBLIC void put_irq_handler(hook, irq, handler) ! irq_hook_t *hook; ! int irq; ! irq_handler_t handler; ! { ! ! /* Register an interrupt handler. */ ! ! int id; ! ! irq_hook_t **line; ! ! if (irq < 0 || irq >= NR_IRQ_VECTORS) ! ! ! panic("invalid call to put_irq_handler", irq); ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! line = &irq_handlers[irq]; id = 1; while (*line != NULL) { ! if (hook == *line) return; /* extra initialization */ ! line = &(*line)->next; ! id <<= 1; } if (id == 0) panic("Too many handlers for irq", irq); hook->next = NULL; hook->handler = handler; hook->irq = irq; hook->id = id; *line = hook; irq_use |= 1 << irq; 87 di 142

! ! ! ! ! ! ! !

/* Install the handler. */ ! hook_ptr->proc_nr_e = m_ptr->m_source;! /* process to notify */ ! hook_ptr->notify_id = notify_id;!! ! /* identifier to pass */ ! hook_ptr->policy = m_ptr->IRQ_POLICY; ! /*policy interrupts */ ! put_irq_handler(hook_ptr, irq_vec, generic_handler); /* Return index of the IRQ hook in use. */ ! m_ptr->IRQ_HOOK_ID = irq_hook_id + 1;

! ! Sostanzialmente la struttura dati che il SO ha creato la seguente:


IRQ_HANDLERS Hook 1 Hook 2 Hook 3 Hook 4

! CONTENUTO IDT.
! ! !! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! { hwint00, VECTOR( 0), INTR_PRIVILEGE }, { hwint01, VECTOR( 1), INTR_PRIVILEGE }, { hwint02, VECTOR( 2), INTR_PRIVILEGE }, { hwint03, VECTOR( 3), INTR_PRIVILEGE }, { hwint04, VECTOR( 4), INTR_PRIVILEGE }, { hwint05, VECTOR( 5), INTR_PRIVILEGE }, { hwint06, VECTOR( 6), INTR_PRIVILEGE }, { hwint07, VECTOR( 7), INTR_PRIVILEGE }, { hwint08, VECTOR( 8), INTR_PRIVILEGE }, { hwint09, VECTOR( 9), INTR_PRIVILEGE }, { hwint10, VECTOR(10), INTR_PRIVILEGE }, { hwint11, VECTOR(11), INTR_PRIVILEGE }, { hwint12, VECTOR(12), INTR_PRIVILEGE }, { hwint13, VECTOR(13), INTR_PRIVILEGE }, { hwint14, VECTOR(14), INTR_PRIVILEGE }, { hwint15, VECTOR(15), INTR_PRIVILEGE }, #if _WORD_SIZE == 2 ! { p_s_call, SYS_VECTOR, USER_PRIVILEGE }, /* 286 system call */ #else ! { s_call, SYS386_VECTOR, USER_PRIVILEGE },/* 386 system call */ #endif { level0_call, LEVEL0_VECTOR, TASK_PRIVILEGE }, };

88 di 142

! hwintXX il nome di una routine di risposta ad un interrupt. In Minix tutte le procedure ! hwint richiamano la medesima routine di gestione interrupt passandogli un differente ! argomento:
! ! ! ! ! ! ! ! ! ! ! ! ! ! .align 16 _hwint00: ! hwint_master(0)! ! .align 16 _hwint01: ! hwint_master(1)! ! Interrupt routine for IRQ 0 (the clock)

! Interrupt routine for IRQ 1 (keyboard)

! ! ! !

hwint_master la procedura di gestione che viene chiamata ad ogni interrupt: una macro che provvede a salvare il contesto del sistema, effettua il cambio di stack (al rientro lo stack operativo quello del kernel), pusha lindirizzo dellhook che risponde allinterrupt e chiama linterrupt handler passandogli il puntatore allhook.
!*==========================================================================* !* ! ! ! ! ! ! hwint00 - 07! ! * !*==========================================================================* !Note this is a macro, it just looks like a subroutine. #define hwint_master(irq)! \ ! call save! ! ! ! ! /* save interrupt process state */;\ ! push (_irq_handlers+4*irq)!! /* irq_handlers[irq] */!! ;\ ! call _intr_handle ecx ; ! /* intr_handler(irq_handlers[irq])*/ ;\ ! pop ecx! ! ! ! ! ! ! ! ! ! ! ! ;\ ! cmp (_irq_actids+4*irq), 0 ! /* interrupt still active? */ ;\ ! jz 0f ! ! ! ! ! ! ! ! ! ! ! ! ;\ ! inb INT_CTLMASK !! ! ! /* get current mask */! ! ;\ ! orb al, [1<<irq] ! ! ! /* mask irq */ ! ! ;\ ! outb INTC_TLMASK! ! ! ! /* disable the irq che saranno ! ! ! ! ! ! ! ! riabilitati dai driver */ ;\ 0: movb al, END_OF_INT ! ! ! ! ! ! ! ! ! ;\ ! outb INT_CTL!! ! ! ! /* reenable master 8259 */! ;\ ! ret !! ! ! ! ! ! /* restart (another) process */ ;\

! ! ! ! ! ! ! ! ! ! ! ! ! ! !

! NB: irq_actids un array di 16 interi dove ogni intero una maschera che rappresenta i ! bit attivi relativamente a quel livello di interrupt. !
/*===========================================================================* *! ! ! ! ! ! ! intr_handle! * *===========================================================================*/ PUBLIC void intr_handle(hook) irq_hook_t *hook; { /* Call the interrupt handlers for an interrupt with the given hook list. * The assembly part of the handler has already masked the IRQ, reenabled the * controller(s) and enabled interrupts. */ /* Call list of handlers for an IRQ. */ while (hook != NULL) { ! ! /* For each handler in the list, mark it active by setting its ID bit, * call the function, and unmark it if the function returns true. ! */ ! irq_actids[hook->irq] |= hook->id; ! if ((*hook->handler)(hook)) irq_actids[hook->irq] &= ~hook->id; ! hook = hook->next; } /* The assembly code will now disable interrupts, unmask the IRQ if and only * if all active ID bits are cleared, and restart a process. */

! ! ! ! !

} 89 di 142

! ! ! ! ! ! ! ! ! ! ! !
! ! ! ! ! ! !

La procedura intr_handler riceve come parametro il puntatore alla struttura dati hook ed esegue due operazioni fondamentali: ! ! irq_actids[hook->irq] |= hook->id determina il driver da contattare. ! ! (*hook->handler)(hook) chiama il generic handler. ! ! se il generic handler restituisce qualcosa diverso da zero vado a vedere il ! ! successivo driver: hook = hook->next. ! ! se il generic handler restituisce zero si riporta irq_actids alla situazione iniziale. Il generic handler sveglia il driver legato allinterrupt in oggetto, il cui valore specicato nellhook passato come argomento. Questa procedura restituisce TRUE se al termine della sua esecuzione possibile riabilitare gli interrupt, altrimenti restituisce FALSE ed in questo caso gli interrupt devono essere riabilitati dal driver appena risvegliato.
PRIVATE int generic_handler(hook) irq_hook_t *hook; { /* This function handles hardware interrupt in a simple and generic way. * lAl interrupts are transformed into messages to a driver. The IRQ line * will be reenabled if the policy says so. */ int proc; /* As a side-effect, the interrupt handler gathers random information by * timestamping the interrupt events. This is used for /dev/random. */ get_randomness(hook->irq); /* Check if the handler is still alive. If not, forget about the * interrupt. This should never happen, as processes that die * automatically get their interrupt hooks unhooked. */ if(!isokendpt(hook->proc_nr_e, &proc)) { hook->proc_nr_e = NONE; return 0; } /* Add a bit for this interrupt to the process' pending interrupts. When * sending the notification message, this bit map will be magically set * as an argument. */ priv(proc_addr(proc))->s_int_pending |= (1 << hook->notify_id); /* Build notification message and return. */ lock_notify(HARDWARE, hook->proc_nr_e); return(hook->policy & IRQ_REENABLE); }

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

! ! ! ! ! ! ! ! ! !

Pi nel dettaglio il generic handler disabilita gli interrupt di IRQ e verica se il driver da risvegliare attivo o meno: se attivo va a modicare la maschera dei bit relativa agli interrupt pendenti (modica la struttura dati del processo del driver) e successivamente fa partire il driver. Al termine restituisce una maschera di bit che denisce una policy riguardante la riabilitazione degli interrupt. Questa maschera viene prelevata dalla struttura dati dell hook. Finita la chiamata al generic handler si rientra in hwintXX: in questo momento non sappiamo se il driver ha terminato di gestire linterrupt o meno. Al rientro dalla procedura hwintXX si torna su _restart che mander in esecuzione il processo interrotto o un altro processo a seconda del valore di _next_ptr.
90 di 142

IPC IN MINIX. ! Il sistema operativo Minix distingue tra due forme di meccanismo di comunicazione tra ! processi: ! ! ! IPC tra processi utente: Minix implementa i sistemi standard di Unix. ! ! ! IPC tra processi e kernel: si utilizzano le system calls. ! Quindi per la comunicazione in User Space vengono utilizzati pipe anonime, le, nane ! pipe, socket, signal ! Riguardo alla comunicazione tra kernel e processi invece sono presenti diverse ! complicazioni derivanti dal fatto che alcuni processi di sistema, quali drivers e servers, ! operano un User Space e si devono occupare di permettere la comunicazione tra ! kernel e processi nonostante non operino a livello di privilegio elevato. Infatti, a causa ! della struttura microkernelizzata del SO Minix, le applicazioni utente possono ! comunicare solo allo strato SW immediatamente inferiore, costituito, appunto, dai ! server.

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

In Minix distinguiamo tra: ! ! system call: sono tutte le chiamate di sistema POSIX effettuate dalle ! ! applicazioni verso i server di sistema. ! ! kernel call: sono le chiamate rivolte al System_Task eseguite esclusivamente ! ! dai server per richiedere la realizzazione delle system call. ! ! IPC call: sono le chiamate che il System_Task inoltra al kernel per lesecuzione ! ! delle syscall. Le system call possono essere implementate sulla base di diverse metodologie. Con l'introduzione dei sistemi microkernelizzati le system call vengono implementate mediante scambio di messaggi. Questo anche l'approccio scelto da Minix. Tale scelta dovuta al fatto che non disponendo memoria condivisa non possibile implementare un meccanismo diverso dallo scambio di messaggi. Generalmente un applicazione che deve inviare una system call, trasmette un messaggio o al MM (prima veniva chiamato PM, Process Manager) o al FS, i quali provvederanno a contattare gli strati inferiori del sistema. Lunica primitiva a disposizione delle applicazioni utente utilizzabile per inviare/riceve messaggi viene la procedura sendrec(int src_dest, message *m_ptr).
91 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

Questo sistema prevede, come meccanismo di comunicazione, il rendez-vous: quando un processo invia un messaggio, resta bloccato nch il processo destinatario non legge il messaggio, inoltre un processo che effettua una receive si blocca sempre, a meno che non sia gai presente un messaggio da leggere. La scelta di utilizzare il rendez-vous come modalit di comunicazione volta ad evitare problemi connessi al buffering. Altre primitive messe previste dal SO sono: ! ! echo(message *m_ptr); ! ! notify(int dest); ! ! send(int src, message *m_ptr); ! ! receive(int dest, message *m_ptr); La procedura send viene utilizzata per inviare un messaggio e ha due parametri: il PID del destinatario ed un puntatore a un messaggio. La receive serve per riceve un messaggio e, analogamente alla send, riceve come argomenti il PID del processo mittente ed un puntatore ad un messaggio. La sendrec accorpa le operazioni delle due funzioni precedenti in quanto invia un messaggio al processo avente PID uguale al primo argomento richiesto e inoltre sospende il sender (ovvero il processo che ha chiamato sendrec) in attesa della risposta del receiver. La notify invece un meccanismo di comunicazione asincrono utilizzato solo dalle componenti di sistema: il sender invia il messaggio ma non si blocca in attesa della risposta del receiver. La echo una procedura utilizzata per il trasferimento di messaggi da una zona di memoria ad unaltra. Quando un processo effettua una send il kernel verica se il destinatario in attesa di un messaggio dal mittente o da qualsiasi processo (ANY). Per effettuare questa operazione necessaria la presenza di specici campi nel PCB del destinatario in grado di contenere linformazione necessaria: ! ! un campo per indicare se il processo in attesa di una receive (p_rts_ags) ! ! un campo che indica il numero del processo da cui si aspetta un messaggio ! ! (p_getfrom). Nel caso in cui la verica dia esito positivo, il messaggio viene copiato da un buffer del sender ad un buffer del receiver ed il ricevente viene messo in ready queue. Se lesito negativo il sender deve essere bloccato e bisogna: ! ! memorizzare nel PCB del sender il numero del processo destinatario. Esiste ! ! quindi un campo del PCB, chiamato p_sendto, dedicato a contenere ! ! questinformazione. ! ! tenere traccia dei processi bloccati sulla send verso uno specico receiver ! ! (questo utile, ad esempio, quando si verica un errore nel receiver che implica ! ! lo sblocco di tutti i processi bloccati in send verso di lui).

92 di 142

! ! ! ! ! ! ! ! ! ! !
! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

Quando un processo effettua una receive necessario vericare se il processo che deve inviare il messaggio (src) in attesa di inviare tale messaggio a P. Per fare questo bisogna disporre di : ! ! un campo nel PCB del mittente che indichi se src in attesa di inviare un ! ! messaggio (p_rts_ags). ! ! un campo contenente il PID del destinatario (p_sendto). Nel caso la varica dia esito positivo il messaggio viene copiato dal buffer del sender al buffer del receiver, ovvero il processo P. Altrimenti, se nessun processo in attesa di inviare messaggi a P, il ricevente viene bloccato sino a quando arriva un messaggio. In questo caso necessario memorizzare nel PCB di P lidenticativo del processo mittente.
struct proc { ! struct stackframe_s p_reg; /* process' registers saved in stack frame */ ! reg_t p_ldt_sel; ! ! /* selector in gdt with ldt base and limit */ ! struct segdesc_s p_ldt[2+NR_REMOTE_SEGS]; /* CS,DS and remote segments */ ! proc_nr_t p_nr; ! ! ! /* number of this process (for fast access) */ ! struct priv *p_priv; ! /* system privileges structure */ ! char p_rts_flags; ! ! /* SENDING, RECEIVING, etc. */ ! char p_priority; ! ! /* current scheduling priority */ ! char p_max_priority; ! /* maximum scheduling priority */ ! char p_ticks_left; ! ! /* number of scheduling ticks left */ ! char p_quantum_size; ! /* quantum size in ticks */ ! struct mem_map p_memmap[NR_LOCAL_SEGS]; /* memory map (T, D, S) */ ! clock_t p_user_time; ! /* user time in ticks */ ! clock_t p_sys_time; !! /* sys time in ticks */ ! struct proc *p_nextready; /* pointer to next ready process */ struct proc *p_caller_q;! /* head of list of procs wishing to send */ ! struct proc *p_q_link; ! /* link to next proc wishing to send */ ! message *p_messbuf; !! /* pointer to passed message buffer */ ! proc_nr_t p_getfrom; ! /* from whom does process want to receive? */ ! proc_nr_t p_sendto; !! /* to whom does process want to send? */ ! sigset_t p_pending; !! /* bit map for pending kernel signals */ ! char p_name[P_NAME_LEN]; ! /* name of the process, including \0*/ };

! Ia struttura del PCB deve quindi ospitare tutti i campi necessari per la comunicazione ! mediante messaggi (i campi segnati in rosso). Inoltre esistono due maschere di bit con ! lo scopo di indicare se il processo si trova in stato di sending o di receiving:
! ! ! ! #define SENDING 0x04 ! /* process blocked trying to send */ #define RECEIVING 0x08 ! /* process blocked trying to receive */

! Esempio sendrec. ! Processo A Sendrec(PIDB, msg) Processo B

! ! ! ! ! !

! il processo A esegue un sendrec sul processo B. ! la send deve essere eseguita dal kernel, il quale vede una richiesta di ! trasferimento di un messaggio.

93 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

! ! il SO verica se il processo B in attesa di riceve un messaggio dal processo A ! ! o da qualsiasi processo (ANY): per fare questo controlla i campi p_rts_ags e ! ! p_getfrom e controlla la lista di processi bloccati tramite il campo p_caller_q. ! ! se lesito del controllo positivo il messaggio viene trasferito: si utilizza il campo ! ! p_messbuf del PCB del processo A per individuare il messaggio e lo si copia al ! ! medesimo campo del PCB del processo B. ! ! se lesito del controllo negativo il processo A deve essere bloccato. In questo ! ! caso opportuno: ! ! ! ! memorizzare nel processo A il PID del processo B. ! ! ! ! aggiungere il processo A alla lista dei processi in attesa di una ! ! ! ! receive da B: si aggiornano i campi p_q_link e p_caller_q . ! ! ! ! A questo punto il processo A in waiting. ! ! dopo linvio del messaggio la sendrec impone che il chiamante esegua una ! ! receive verso il destinatario: il kernel quindi vede anche una receive dal ! ! processo A verso il processo B. ! ! si controllano quindi i campi p_rts_ags e p_sendto di B. ! ! se lesito positivo il messaggio viene trasferito dal processo B al processo A. ! ! altrimenti il processo A deve essere bloccato: si memorizza nel PCB del ! ! processo A il PID del processo B. ! ! In minix la struttura dei messaggi deve rispettare alcuni modelli predeniti: in questo modo la componente che riceve il messaggio in grado, a seconda del tipo del messaggio, di conoscere a priori i vari campi da cui costituito tale messaggio, quanti sono e di quale tipo.

94 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

/*=====================================================* * ! ! ! Types relating to messages.!! ! * *=====================================================* #define M1 1 #define M3 3 #define M4 4 #define M3_STRING 14 typedef struct {int m1i1, m1i2, m1i3; ! ! ! char *m1p1, *m1p2, *m1p3;}mess_1; typedef struct {int m2i1, m2i2, m2i3; ! ! ! long m2l1, m2l2; ! ! ! char *m2p1;} mess_2; typedef struct {int m3i1, m3i2; ! ! ! char *m3p1; ! ! ! char m3ca1[M3_STRING];} mess_3; typedef struct {long m4l1, m4l2, m4l3, m4l4, m4l5;} mess_4; typedef struct {short m5c1, m5c2; ! ! ! int m5i1, m5i2; ! ! ! long m5l1, m5l2, m5l3;} mess_5; typedef struct {int m7i1, m7i2, m7i3, m7i4; ! ! ! char *m7p1, *m7p2;} mess_7; typedef struct {int m8i1, m8i2; ! ! ! char *m8p1, *m8p2, *m8p3, *m8p4;} mess_8;

! Dichiarazione messaggi: !
! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! typedef struct { ! int m_suorce;!! ! int m_type;! ! ! union { ! ! mess_1 m_m1; ! ! mess_2 m_m2; ! ! mess_3 m_m3; ! ! mess_4 m_m4; ! ! mess_5 m_m5; ! ! mess_7 m_m7; ! ! mess_8 m_m8; ! } m_u; }message; ! ! /* who sent the message */ /* what kinf of message is it */

/* the following defines provide names for useful members */ #define m1_i1!! m_u.m_m1.m1i1 #define m1_i2!! m_u.m_m1.m1i2 #define m1_i3!! m_u.m_m1.m1i3 #define m1_p1!! m_u.m_m1.m1p1 #define m1_p2!! m_u.m_m1.m1p2 #define m1_p3!! m_u.m_m1.m1p3 #define #define #define #define m3_i1!! m3_i2!! m3_p1!! m3_ca1! m_u.m_m3.m1i1 m_u.m_m3.m1i2 m_u.m_m3.m1p1 m_u.m_m3.m1ca1 95 di 142

! I valori deniti in questo modo devono essere interpretati come segue: ! ! ! ! ! ! ! ! ! ! m1_i3 ! ! m3_ca1! messaggio di tipo 1, campo di tipo int numero 3. messaggio di tipo 3, campo di tipo array di caratteri numero 1.

Le dene invece forniscono identicativi di comodo utilizzo per laccesso ai campi della struttura del messaggio. Le seguenti scritture sono equivalenti: ! ! ! ! msg.m_u.m_m1.m1i3 = 3; ! ! msg.m.m1_13 = 3;

! La struttura priv contiene le maschere di visibilit di alcune componenti del sistema ed ! inoltre memorizza, mediante un campo interno, tutte le notiche pendenti. ! struct priv {!
! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! proc_nr_t s_proc_nr; ! ! sys_id_t s_id;! ! ! short s_flags;! ! ! short s_trap_mask; ! ! sys_map_t s_ipc_from; ! ! sys_map_t s_ipc_to; ! ! long s_call_mask;! ! ! ! ! ! ! ! ! ! /* number of associated process */ /* index of this system structure */ /* PREEMTIBLE, BILLABLE, etc. */ /* /* /* /* allowed allowed allowed allowed system call traps */ callers to receive from */ destination processes */ kernel calls */

sys_map_t s_notify_pending; /* bit map with pending notifications */ irq_id_t s_int_pending; ! ! /* pending hardware interrupts */ sigset_t s_sig_pending; ! ! /* pending signals */ timer_t s_alarm_timer; ! ! ! /* synchronous alarm timer */ struct far_mem s_farmem[NR_REMOTE_SEGS]; /* remote memory map */! reg_t *s_stack_guard;! ! ! /* stack guard word for kernel tasks*/

! Vediamo ora le effettive procedure che permettono lIPC in Minix. ! Le costanti utilizzate sono:

!
! ! ! ! !

SEND ! = 1; RECEIVE ! = 2; BOTH ! = 3; SYSVEC ! = 33; SRCDEST ! = 8; MESSAGE ! = 12;

! ! La procedura che permette linvio dei messaggi la send:


! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! _send(), _receive(), all save ebp, .define __send, __receive, __sendrec .sect! .text __send: ! push ! ebp ! mov !! ebp, esp ! push ! ebx ! mov !! eax, SRCDEST(ebp)! ! ! mov !! ebx, MESSAGE(ebp)! ! ! mov !! ecx, SEND! ! ! ! ! int !! SYSVEC! ! ! ! ! pop !! ebx ! pop !! ebp ! ret but destroy eax and ecx.

! ! ! !

eax = dest-src ebx = message pointer _send(dest,ptr) Trap to kernel

96 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

Quindi la procedura send esegue le seguanti operazioni: ! ! prologo. ! ! salva sullo stack il valore contenuto in ebx. ! ! muove in eax il valore contenuto in ebp+SRCDEST = ebp+8. ! ! A questa posizione dello stack dovr essere contenuto il PID del processo ! ! destinatario del messaggio.! ! ! muove in eax il valore contenuto in ebp+MESSAGE = ebp+12. ! ! In questa posizione si aspetta di trovare un puntatore al messaggio da inviare. ! ! sposta in ecx il codice della primitiva da eseguire, ovvero la SEND. Quindi ecx ! ! assume valore 1. ! ! termina la fase si passaggio dei parametri; viene generato un interrupt SW, ! ! mediante listruzione int, il cui numero di vettore SYSVEC, ovvero 33. ! ! al rientro dallistruzione int lo stack viene ripulito, i valori di ebx ed ebp ! ! vengono ripristinati e si esegue ret. Allesecuzione dellistruzione int si avvia il meccanismo di gestione delle eccezioni o interrupt ed il programma applicativo cede il controllo momentaneamente al kernel. La procedura che implementa loperazione receive analoga alla send; cambia solamente il valore del registro ecx prima della generazione dellinterrupt SW.
__receive: ! push ! ! mov !! ! push ! ! mov !! ! mov !! ! mov !! ! int !! ! pop !! ! pop !! ! ret

!
! ! ! ! ! ! ! ! ! !

ebp ebp, esp ebx eax, SRCDEST(ebp)! ebx, MESSAGE(ebp)! ecx, RECEIVE! ! SYSVEC! ! ! ebx ebp

! ! ! !

! ! ! !

eax = dest-src ebx = message pointer _receive(src,ptr) Trap to kernel

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

! Quindi la procedura send esegue le seguenti operazioni: ! ! prologo. ! ! salva sullo stack il valore contenuto in ebx. ! ! muove in eax il valore contenuto in ebp+SRCDEST = ebp+8. ! ! A questa posizione dello stack dovr essere contenuto il PID del processo ! ! mittente del messaggio.! ! ! muove in eax il valore contenuto in ebp+MESSAGE = ebp+12. ! ! In questa posizione si aspetta di trovare un puntatore al messaggio. ! ! sposta in ecx il codice della primitiva da eseguire, in questo caso RECEIVE. ! ! Quindi ecx assume valore 2. ! ! termina la fase si passaggio dei parametri; viene generato un interrupt SW, ! ! mediante listruzione int, il cui numero di vettore SYSVEC, ovvero 33. ! ! al rientro dallistruzione int lo stack viene ripulito, i valori di ebx ed ebp ! ! vengono ripristinati e si esegue ret. Sostanzialmente le procedure __send e __receive hanno il solo compito di predisporre i corretti parametri per linterrupt SYSVEC e, ovviamente, generare linterruzione. Nella posizione 33 della IDT contenuto il vettore corrispondente alla routine di gestione denominata s_call (oppure p_s_call per questioni di compatibilit con le precedenti verisioni di Minix).

97 di 142

! ! ! ! !

#if _WORD_SIZE == 2 ! { p_s_call, SYS_VECTOR, USER_PRIVILEGE }, /* 286 system call */ #else ! { s_call, SYS386_VECTOR, USER_PRIVILEGE },/* 386 system call */ #endif

! Quindi entrambe le procedure chiamano s_call :


! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! _s_call: ! _p_s_call: ! ! ! cld! ! ! ! ! ! ! ! sub! ! esp, 6*4! ! ! ! ! push!! ebp! ! ! ! ! ! push!! esi ! ! push!! edi ! ! o16 push ! ds! ! ! o16 push ! es! ! ! o16 push ! fs! ! ! o16 push! gs ! ! mov! ! dx, ss ! ! mov! ! ds, dx ! ! mov! ! es, dx ! ! incb ! (_k_reenter) ! ! mov! ! esi, esp! ! ! ! ! mov! ! esp, k_stktop ! ! xor! ! ebp, ebp! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! push!! ebx !! ! ! ! ! push!! eax !! ! ! ! ! push!! ecx !! ! ! ! ! call!! _sys_call ! ! ! ! ! ! ! ! ! ! ! ! mov! ! AXREG(esi), eax !

! set direction flag to a known value ! skip RETADR, eax, ecx, edx, ebx, est ! stack already points into proc table

! assumes P_STACKBASE == 0 ! ! ! ! ! ! ! ! ! for stacktrace end of inline save now set up parameters for sys_call() pointer to user message src/dest SEND/RECEIVE/BOTH sys_call(function, src_dest, m_ptr) caller is now explicitly in proc_ptr sys_call MUST PRESERVE si

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

Le operazioni eseguite da questa procedura sono: ! ! i registri general purpose non vengo salvati sullo stack, tuttavia lo spazio che ! ! sarebbe stato occupato da tali valori deve essere comunque allocato per ! ! rispettare il layout dello stack. Lo stack pointer viene dunque decremento. ! ! vengono memorizzati sullo stack i segmenti che lHW non salva ! ! automaticamente. ! ! si esegue lo switch dello stack, passando al kernel stack, controllando che ! ! questa operazione non sia gi stata eseguita precedentemente. ! ! si pushano sullo stack i parametri della procedura send/receive predisposti ! ! precedentemente dal chiamante (__send o __receive) nei registri ebx, eax, ed ! ! ecx. ! ! viene chiamata la procedura _sys_call: sys_call(function, src_dest, m_ptr). NB: ! nel momento in cui il processo ha richiesto la syscall si trova ancora in stato ! Running. La routine _s_call chiama quindi la _sys_call che a sua volta chiama mini_send oppure mini_receive in base al primo parametro della _sys_call . Al termine dellesecuzione di una delle due routine mini il controllo ritorna a _sys_call , poi a _s_call e conseguentemente si passa allesecuzione della procedura _restart in quanto il codice di questa procedura situato consecutivamente alla _s_call .
98 di 142

/*===============================================* * !! ! ! ! sys_call * *===============================================*/ PUBLIC int sys_call(call_nr, src_dst_e, m_ptr, bit_map) ! int call_nr; !! ! /* system call number and flags */ ! int src_dst_e; ! ! /* src to receive from or dst to send to */ ! message *m_ptr; ! ! /* pointer to message in the caller's space */ ! long bit_map; ! ! /* notification event set or flags */ ! { ! /* System calls are done by trapping to the kernel with an INT instruction. ! * The trap is caught and sys_call() is called to send or receive a message ! * (both). The caller is always given by 'proc_ptr'. ! */ ! ! /* Controlli sui parametri passati alla funzione */ [...] switch(function) { ! case SENDREC: ! ! /* A flag is set so that notifications cannot interrupt SENDREC.*/ ! ! caller_ptr->p_misc_flags |= REPLY_PENDING; ! ! /* fall through */ ! ! case SEND: ! ! result = mini_send(caller_ptr, src_dst_e, m_ptr, flags); ! ! if (function == SEND || result != OK) { ! ! break; ! ! ! ! ! ! /* done, or SEND failed */ ! ! } ! ! /* fall through for SENDREC */ ! ! case RECEIVE: ! ! if (function == RECEIVE) ! ! ! caller_ptr->p_misc_flags &= ~REPLY_PENDING; ! ! ! result = mini_receive(caller_ptr, src_dst_e, m_ptr, flags); ! ! ! break; ! ! ! ! ! ! ! ! ! case NOTIFY: ! result = mini_notify(caller_ptr, src_dst); ! break; case ! ! ! ECHO: CopyMess(caller_ptr->p_nr, caller_ptr, m_ptr, caller_ptr, m_ptr); result = OK; break;

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

default: ! result = EBADCALL; !

/* illegal system call */

! ! ! ! ! ! ! ! ! ! !

La proceura sys_call esegue inizialmente una serie di veriche sui parametri passati: ! ! controlla che gli indenticativi dei processi sender/receiver siano corretti. ! ! verica che il chiamante abbia i necessari privilegi per eseguire la syscall ! ! richiesta. Ad esempio i processi utente possono richiedere servizi al kernel solo ! ! mediante sendrec al ne di evitare che il kernel resti bloccato a causa di un ! ! processo che non esegue la receive sopo aver eseguito una send. ! ! verica se il puntatore del messaggio valido. Una volta superati questi controlli si determina il tipo di procedura che si vuole eseguire: la struttura switch case permette di identicare se loperazione una SEND, una RECEVIVE, una SENDREC, una NOTIFY o una ECHO.
99 di 142

! ! ! !

SENDREC: in questo caso viene settata una ag nel PCB del processo chiamante, e successivamente verranno eseguite sia la porzione di codice della SEND, sia quella della RECEIVE in quanto la condizione function == SEND || result != OK risulter falsa il break; non verr eseguito.

! SEND: in questo caso viene chiamata la procedura mini_send la quale provvede ! alleffettiva consegna del messaggio. ! RECEIVE: in caso di receive di chiama la routine mini_receive . ! ! Alle procedure mini vengono passati gli stessi parametri della sys_call .
/*===========================================================================* * mini_send * *===========================================================================*/ PRIVATE int mini_send(caller_ptr, dst, m_ptr, flags) ! register struct proc *caller_ptr; /* who is trying to send a message? */ ! int dst; /* to whom is message being sent? */ ! message *m_ptr; /* pointer to message buffer */ ! unsigned flags; /* system call flags */ ! { /* Send a message from 'caller_ptr' to 'dst'. If 'dst' is blocked waiting * for this message, copy the message to it and unblock 'dst'. If 'dst' is * not waiting at all, or is waiting for another source, queue 'caller_ptr'. */ ! register struct proc *dst_ptr = proc_addr(dst); ! register struct proc **xpp; ! register struct proc *xp; /* Check for deadlock by caller_ptr and dst ending to each other */ ! ! ! ! ! while(xp->p_rts_flags & SENDING){! ! ! xp = proc_addr(xp->p_sendto);!! ! if(xp == caller_ptr)! ! ! ! ! return (ELOCKED);!! ! } /* check while ending */ /* get xps destination */ /* deadlock if cyclic */

! ! !

/* Check if 'dst' is blocked waiting for this message. The destination's * SENDING flag may be set when its SENDREC call blocked while sending. */ ! if ((dst_ptr->p_rts_flags & (RECEIVING | SENDING)) == RECEIVING && (dst_ptr->p_getfrom == ANY || dst_ptr->p_getfrom == caller_ptr->p_nr)) ! { ! /* Destination is indeed waiting for this message. */ ! CopyMess(caller_ptr->p_nr, caller_ptr, m_ptr, dst_ptr, ! ! dst_ptr->p_messbuf); ! /* Risveglia il processo se era sospeso in receive */ ! if ((dst_ptr->p_rts_flags &= ~RECEIVING) == 0) ! ! enqueue(dst_ptr); ! ! } else if ( ! (flags & NON_BLOCKING)) { ! ! /* Destination is not waiting. Block and dequeue caller. */ ! caller_ptr->p_messbuf = m_ptr; ! if (caller_ptr->p_rts_flags == 0) ! ! dequeue(caller_ptr); ! caller_ptr->p_rts_flags |= SENDING; ! caller_ptr->p_sendto = dst;

100 di 142

! ! ! ! !

/* Process is now blocked. Put in on the destination's queue. */ xpp = &dst_ptr->p_caller_q; /* find end of list */ while (*xpp != NIL_PROC) ! ! xpp = &(*xpp)->p_q_link; ! *xpp = caller_ptr; /* add caller to end */ ! caller_ptr->p_q_link = NIL_PROC; /* mark new end of list */ ! } ! else { ! ! return(ENOTREADY); ! } return(OK); }

! ! !

! ! ! ! ! ! ! ! ! ! ! ! ! !

La prima operazione che esegue la mini_send controllare se si in presenza di deadlock o meno, infatti proprio a causa dellutilizzo del meccanismo di comunicazione randez-vous possono vericarsi situazioni distallo. Per capire meglio questo concetto vediamo un esempio: ! ! un processo A esegue la send(B, msg). ! ! un processo B esegue la send(A, msg1). ! ! i due processi si sospendono in attesa della receive del corrispondente ! ! processo. ! ! siamo in presenza di un deadlock in quanto entrambi i processi sono bloccati in ! ! receive luno verso laltro e nessuno dei due potr inviare la receive che ! ! lunico fattore sbloccante. NB: questo particolare deadlock pu vericarsi anche tra pi di due processi. Si pensi ad esempio a ci che accade nei RAG quando pi processi cercano di accedere alle medesime risorse. In questo caso si avrebbe una situazione simile alla seguente: AsendB, BsendC, CsendD, DsendA deadlock Quindi il ciclo while iniziale serve proprio ad individuare queste situazioni di deadlock xp un puntatore inizializzato con lindirizzo del PCB del destinatario; tramite questo puntatore si verica se il destinatario in sending ed in questo caso si determina verso quale processo ha eseguito la send. Se il destinatario ha eseguito una send verso lattuale chiamante siamo in una situazione di deadlock in quanto abbiamo individuato due processi che hanno eseguito una send luno verso laltro. Inoltre queste istruzioni sono situate allinterno di un ciclo while perch in questo modo possibile individuare i deadlock presenti su richieste di send che coinvolgono pi di due processi. Il ciclo termina se viene individuato un deadlock o se un processo non sta inviando un messaggio. In poche parole come se si scorresse una history della comunicazione vericando che tutti i ! rocessi coinvolti non cerchino di eseguire send gli uni verso gli p altri. Dopo questo controllo si va a vedere se il destinatario sospeso in receiving: ! ! ! ! ! ! ! ! RECEIVING: ! 0000 1000 OR SENDING:! ! 0000 0100 _________________________ ! ! ! 0000 1100 AND p_rts_ags:! ! xxxx xxxx _________________________ ! ! ! 0000 xx00! <== ! se uguale a RECEIVING! signica che il ! ! ! ! ! ! processo bloccato in receiving

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

! A questo punto verichiamo se il ricevente bloccato sul processo che sta eseguendo ! la mini_send , ovvero se il receiver sospeso in receive sul processo che sta
101 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

eseguendo in questo momento la send. Per fare questo si controlla se il campo p_getfrom del receiver uguale al PID del processo sender. Ovviamente bisogna considerare anche il caso in cui il ricevente stia aspettando un messaggio da ANY. Superato questo controllo si pu eseguire la copia del messaggio byte per byte. Dopo questa operazione bisogna risvegliare il processo ricevente nel caso questo fosse in attesa di riceve solo questo messaggio, cio in attesa di riceve un messaggio dal processo che ha eseguito la mini_send . Risvegliare un processo signica richiamare la funzione enqueue() su quel processo in modo che possa essere inserito nella Ready queue. Ora analizziamo le operazioni da eseguire nel caso in cui il processo destinatario del messaggio non sia in waiting sul mittente. In questa sezione lavoriamo sul PCB del chiamante: ! ! salviamo il messaggio nel campo p_messbuf del PCB. ! ! sospendo con la dequeue() il mittente: tolgo il processo dallo stato Running. ! ! NB: no a questo momento il chiamante si trovato in Running. ! ! aggiorno il campo p_rts_ags specicando il motivo della sospensione del ! ! processo: in questo caso indichiamo che il processo bloccato in SENDING. ! ! aggiorniamo il campo p_sendto inserendo il PID del processo al quale si ! ! desidera inviare il messaggio, ovvero il PID del processo verso cui si sospesi ! ! in sending. ! ! a questo punto il sender deve aggiungere lindirizzo del suo PCB nella lista dei ! ! processi sospesi in sending allinterno del PCB del receiver. Sappiamo che nel ! ! PCB del destinatario c il puntatore alla lista dei processi che hanno fatto una ! ! send sul receiver: xpp punta alla testa di questa lista, quindi sufciente ! ! scorrerla tutta ed inserire caller_ptr in coda (caller_ptr punta al chiamante, il ! ! processo che sta eseguendo la send).

! ! ! !

Quando la mini_send termina il messaggio pu essere stato consegnato oppure no: in questo caso il sender stato bloccato e, rientriando nella _s_call, si prosegue con lesecuzione della _restart la quale provveder a mandare in esecuzione un altro processo.

102 di 142

/*===========================================================================* * mini_receive * *===========================================================================*/ PRIVATE int mini_receive(caller_ptr, src_e, m_ptr, flags) ! register struct proc *caller_ptr;! /* process trying to get message */ ! int src_e; ! ! /* which message source is wanted */ ! message *m_ptr; ! /* pointer to message buffer */ ! unsigned flags; ! /* system call flags */ ! { /* A process or task wants to get a message. If a message is already queued, * acquire it and deblock the sender. If no message from the desired source * is available block the caller, unless the flags don't allow blocking. */ ! register struct proc **xpp; ! register struct notification **ntf_q_pp; ! message m; ! int bit_nr; ! sys_map_t *map; ! bitchunk_t *chunk; ! int i, src_id, src_proc_nr, src_p; ! ! ! ! ! ! if(src_e == ANY) src_p = ANY; else { okendpt(src_e, &src_p); if (proc_addr(src_p)->p_rts_flags & NO_ENDPOINT) return ESRCDIED; }

/* Check to see if a message from desired source is already available. * The caller's SENDING flag may be set if SENDREC couldn't send. If it is * set, the process should be blocked. */ ! if (!(caller_ptr->p_rts_flags & SENDING)) { ! /* Check if there are pending notifications, except for SENDREC. */ ! if (! (caller_ptr->p_misc_flags & REPLY_PENDING)) { map = &priv(caller_ptr)->s_notify_pending; for (chunk=&map->chunk[0]; chunk<&map->chunk[NR_SYS_CHUNKS]; chunk++) { /* Find a pending notification from the requested source. */ if (! *chunk) continue; /* no bits in chunk */ for (i=0; ! (*chunk & (1<<i)); ++i) {} /* look up the bit */ src_id = (chunk - &map->chunk[0]) * BITCHUNK_BITS + i; if (src_id >= NR_SYS_PROCS) break; /* out of range */ src_proc_nr = id_to_nr(src_id); /* get source proc */ #if DEBUG_ENABLE_IPC_WARNINGS if(src_proc_nr == NONE) { kprintf("mini_receive: sending notify from NONE\n"); } #endif if (src_e!=ANY && src_p != src_proc_nr) continue;/* source not ok */ *chunk &= ~(1 << i); /* no longer pending */ /* Found a suitable source, deliver the notification message. */ BuildMess(&m, src_proc_nr, caller_ptr); /* assemble message */ CopyMess(src_proc_nr, proc_addr(HARDWARE), &m, caller_ptr, m_ptr); return(OK); /* report success */ } ! } 103 di 142

/* Check caller queue. Use pointer pointers to keep code simple. */ xpp = &caller_ptr->p_caller_q; while (*xpp != NIL_PROC) { if (src_e == ANY || src_p == proc_nr(*xpp)) { #if 0 if ((*xpp)->p_rts_flags & SLOT_FREE) { kprintf("listening to the dead?!?\n"); return EINVAL; } #endif /* Found acceptable message. Copy it and update status. */ CopyMess((*xpp)->p_nr, *xpp, (*xpp)->p_messbuf, caller_ptr, m_ptr); if (((*xpp)->p_rts_flags &= ~SENDING) == 0) enqueue(*xpp); *xpp = (*xpp)->p_q_link; /* remove from queue */ return(OK); /* report success */ } xpp = &(*xpp)->p_q_link; /* proceed to next */ } } /* No suitable message is available or the caller couldn't send in SENDREC. * Block the process trying to receive, unless the flags tell otherwise. */ if ( ! (flags & NON_BLOCKING)) { caller_ptr->p_getfrom_e = src_e; caller_ptr->p_messbuf = m_ptr; if (caller_ptr->p_rts_flags == 0) dequeue(caller_ptr); caller_ptr->p_rts_flags |= RECEIVING; return(OK); } else { return(ENOTREADY); } }

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

La proceura mini_receive esegue le seguenti operazioni: ! ! verica se sono presenti notiche da consegnare. ! ! verica se il processo sorgente, da quale si vuole ricevere il messaggio, in ! ! attesa di consegnare il suo messaggio al processo che ha chiamato la receive. ! ! in caso affermativo avvine la copia del messaggio dal sorgente al processo ! ! chiamante e viene bloccato il sender. ! ! altrimenti, se non c messaggio da consegnare, il processo receiver viene ! ! messo in attesa. ! ! nel campo p_getfrom del PCB del processo chiamante viene inserito il PID del ! ! processo verso il quale stata eseguita la receive. La notify lunica primitiva di comunicazione non bloccante per lIPC in Minix: questo signica che il sender prosegue la sua esecuzione anche se il mittente non in attesa di ricevere un messaggio. Il questo caso la notify non viene persa infatti le notiche vengono consegnate ogni volta che un processo effettua una receive, prima della consegna dei messaggi ordinari. Questa funzione viene utilizzata in Minix esclusivamente allinterno del kernel.

104 di 142

/*===========================================================================* * mini_notify * *===========================================================================*/ PRIVATE int mini_notify(caller_ptr, dst) ! register struct proc *caller_ptr; /* sender of the notification */ ! int dst; /* which process to notify */ ! { ! register struct proc *dst_ptr = proc_addr(dst); ! int src_id; /* source id for late delivery */ ! message m; /* the notification message */ ! /* Check to see if target is blocked waiting for this message. A process * can be both sending and receiving during a SENDREC system call. */ ! if ((dst_ptr->p_rts_flags & (RECEIVING|SENDING)) == RECEIVING && ! ! (dst_ptr->p_misc_flags & REPLY_PENDING) && ! (dst_ptr->p_getfrom_e == ANY || ! dst_ptr->p_getfrom_e == caller_ptr->p_endpoint)) { /* Destination is indeed waiting for a message. Assemble a notification * message and deliver it. Copy from pseudo-source HARDWARE, since the * message is in the kernel's address space. */ ! BuildMess(&m, proc_nr(caller_ptr), dst_ptr); ! CopyMess(proc_nr(caller_ptr), proc_addr(HARDWARE), &m, ! dst_ptr, dst_ptr->p_messbuf); ! dst_ptr->p_rts_flags &= ~RECEIVING; /* deblock destination */ ! if (dst_ptr->p_rts_flags == 0) enqueue(dst_ptr); ! return(OK); }!

/* Destination is not ready to receive the notification. Add it to the * bit map with pending notifications. Note the indirectness: the system id * instead of the process number is used in the pending bit map. */ ! src_id = priv(caller_ptr)->s_id; ! set_sys_bit(priv(dst_ptr)->s_notify_pending, src_id); ! return(OK); }

! ! ! ! ! ! ! ! ! ! !

La mini_notify esegue le seguenti operazioni: ! ! controlla se ci sono processi in attesa di una notify. ! ! se il controllo da esito positivo il messaggio viene consegnato ed il receiver ! ! deve essere risvegliato. ! ! altrimenti viene predisposta una maschera di due bit per avvertire il destinatario ! ! della notify pendente. ! ! successivamente il sender prosegue la sua esecuzione. A volte pu capitare di trovare la chiamata alla procedura lock_notify : questa routine sostanzialmente una notify eseguita ad interrupt disabilitati. Viene spesso utilizzata durante fasi di I/O, per essere certi che la notify non venga interrotta, oppure durante gli aggiornamenti dei PCB.

/*===========================================================================* * lock_notify * *===========================================================================*/ PUBLIC int lock_notify(src_e, dst_e) ! int src_e; ! /* (endpoint) sender of the notification */ ! int dst_e; ! /* (endpoint) who is to be notified */ 105 di 142

! { /* Safe gateway to mini_notify() for tasks and interrupt handlers. The sender * is explicitely given to prevent confusion where the call comes from. MINIX * kernel is not reentrant, which means to interrupts are disabled after * the first kernel entry (hardware interrupt, trap, or exception). Locking * is done by temporarily disabling interrupts. */ ! int result, src, dst; ! ! if(!isokendpt(src_e, &src) || !isokendpt(dst_e, &dst)) return EDEADSRCDST;

/* Exception or interrupt occurred, thus already locked. */ ! if (k_reenter >= 0) { ! result = mini_notify(proc_addr(src), dst); ! } /* Call from task level, locking is required. */ ! else { ! lock(0, "notify"); ! result = mini_notify(proc_addr(src), dst); ! unlock(0); ! } ! return(result); ! }

! MESSAGGI E SYSCALL. ! Le applicazioni utente in Minix possono comunicare solo con lo strato di sistema ! immediatamente inferiore, ovvero i server. In particolare possono comunicare con il ! Processo Manager (PM) e il File System (FS). Lapplicazione si rivolge quindi a questi ! server per richiedere lesecuzione di syscall: essendo la comunicazione implementata ! mediante messaggi, la richiesta di esecuzione di una syscall si traduce nellinvio di un ! messaggio dall applicazione al server. ! Alla ricezione del messaggio, nel server si attiva un handler in grado di selezionare la ! procedura corrispondente al messaggio ricevuto. Quindi un server non altro che ! unentit perennemente in attesa di ricevere messaggi, in grado di riconoscere il tipo di ! messaggio ricevuto e il tipo di syscall richiesta dallapplicazione. Il ciclo di esecuzione di ! un server quindi: ! ! ! rimane in attesa di ricevere un messaggio da qualunque processo. ! ! ! alla ricezione del messaggio determina il tipo e preleva il numero della syscall. ! ! ! chiama lhandler corrispondente al numero di syscall. ! ! ! ritorna in attesa di ricevere un messaggio. SIGNAL. ! Le signal sono un insieme predenito di segnali che pocessi, kernel e servers possono ! inviarsi reciprocamente. Il loro scopo quello di segnalare il vericarsi di eventi ! sincroni/asincroni allinterno del sistema. Esistono diversi tipi di signals ed ognuna ! denisce un evento. Ad ogno evento sono associate delle operazioni di default e ! quando un processo riceve una signal in grado di determinare di che segnale si tratta ! e qual la procedura di default da applicare. POSIX prevede delle signal generali che ! devono essere presenti in tutti i sistemi che intendono adottare quello standard, inoltre ! ogni sistema pu implementare delle proprie signal e permetterne la creazione anche ! allutente.
106 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

Ogni processo ha la possibilit di specicare procedure alternative per la gestione di una determinato segnale ed in questo senso denisce un signal handler per la gestione delle signal. Altrimenti esiste anche la possibilit di ignorare un certo tipo di segnale, disabilitando la ricezione di quel particolare tipo di signal. Ovviamente esistono segnali, ad esempio la SIGILLI, che non possono essere mascherati. Inne tutte le procedure di gestione di una signal devono terminare con listruzione sigreturn. Esistono due syscall che permettono di denire il comportamento di un processo riguardo alla signal: ! ! sigaction: viene usata per specicare i segnali che si intendono ignorare, i ! ! signal handler che si intende adottare, o per ripristinare le operazioni di default ! ! su un segnle.! ! ! sigprocmask: pu essere utilizzata per bloccare un segnale per un determinato ! ! periodo di tempo, dopo il quale il processo stesso riabiliter la signl mediante ! ! listruzione sigaction. Diversamente dalle eccezioni le signal sono locali al processo nel senso che ogni processo pu decidere come gestirle. Quando un processo riceve una signal (il processo riceve la signal quando in ready o quando bloccato, perch gli viene inviata dal processo che in esecuzione) dobbiamo fare in modo che appena torna in running possa partire l'esecuzione dell'handler della signal. Nel PCB presente lo stato del processo tra cui l'EIP e lo stack: quello che vogliamo ottenere che, al ritorno in stato di Running, il usso di esecuzione si trovi nell'handler corrispondente alla signal. Per fare questo necessario modicare il contenuto dello stack in modo che il processo riprenda la sa esecuzione eseguendo il codice dellhandler. Nello stack si salva il contesto del processore, si inseriscono gli indirizzi di ritorno e le variabili locali dellhandler e della sigreturn: in questo modo il processo esegue lhandler senza accorgersi di nulla. Al termine dell'escuzione dell'handler si salta a sigreturn che ripristina lo stato originale dello stack permettendo al processo di ripartire avendo eseguito la procedura di risposta alla signal, e dallo stesso punto in cui era stato interrotto.

107 di 142

SYSCALL IN MINIX. ! Come abbiamo visto le system calls sono uninterfaccia tra le applicazioni ed il SO che ! permette lo svolgimento di operazioni che coinvolgono dispositivi o strutture di !m emoria ! gestite dal SO stesso. Sono di fondamentale importanza in quanto facilitano la ! programmazione, incrementano la sicurezza del sistema e rendono il codice pi ! portabile. Una delle pi famose interfacce la Portable Operating System Interface ! (POSIX). ! Ogni syscall contraddistinta da un numero ed in minix tale denizione si trova nel le ! callnr.h. Questo valore numerico viene inserito in un opportuno campo del messaggio ! che verr inviato al server. !
! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! #define NCALLS 95 /* number of system calls allowed */ #define EXIT 1 #define FORK 2 #define READ 3 #define WRITE 4 #define OPEN 5 #define CLOSE 6 #define WAIT 7 #define CREAT 8 #define LINK 9 #define UNLINK 10 #define WAITPID 11 #define CHDIR 12 #define TIME 13 #define MKNOD 14 #define CHMOD 15 #define CHOWN 16 #define BRK 17 //... /* The following are not system calls, but are processed like them. */ #define UNPAUSE 65 /* to MM or FS: check for EINTR */ #define REVIVE 67 /* to FS: revive a sleeping process */ #define TASK_REPLY 68 /* to FS: reply code from tty task */ /* Posix signal handling. */ #define SIGACTION 71 #define SIGSUSPEND 72 #define SIGPENDING 73 #define SIGPROCMASK 74 #define SIGRETURN 75! #define REBOOT 76 /* to PM */ /* MINIX specific calls, e.g., to support system services. */ #define SVRCTL 77 #define PROCSTAT 78 /* to PM */ #define GETSYSINFO 79 /* to PM or FS */ #define GETPROCNR 80 /* to PM */ #define DEVCTL 81 /* to FS */ #define FSTATFS 82 /* to FS */ #define ALLOCMEM 83 /* to PM */ #define FREEMEM 84 /* to PM */ #define SELECT 85 /* to FS */ #define FCHDIR 86 /* to FS */ #define FSYNC 87 /* to FS */ #define GETPRIORITY 88 /* to PM */ #define SETPRIORITY 89 /* to PM */ #define GETTIMEOFDAY 90 /* to PM */ #define SETEUID 91 /* to PM */ #define SETEGID 92 /* to PM */ #define TRUNCATE 93 /* to FS */ #define FTRUNCATE 94 /* to FS */ 108 di 142

! Una volta ricevuta la richiesta di esecuzione si una system call, ogni server ispeziona la ! struttura dati call_vec per determinare le attivit da fare.
! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! _PROTOTYPE (int (*call_vec[NCALLS]), (void)) = { ! no_sys, ! ! ! ! /* 0 = unused */ ! do_pm_exit, ! ! ! /* 1 = exit ! */ ! do_fork, ! ! ! ! /* 2 = fork ! */ ! no_sys, ! ! ! ! /* 3 = read ! */ ! no_sys, ! ! ! ! /* 4 = write !*/ ! no_sys, ! ! ! ! /* 5 = open ! */ ! no_sys, ! ! ! ! /* 6 = close! */ ! do_waitpid, ! ! ! /* 7 = wait! */ ! no_sys, ! ! ! ! /* 8 = creat! */ ! no_sys, ! ! ! ! /* 9 = link! */ ! no_sys, ! ! ! ! /* 10= unlink */ ! do_waitpid, ! ! ! /* 11= waitpid*/ ! No_sys, ! ! ! ! /* 12= chdir! */ ! do_time, ! ! ! ! /* 13= time ! */ ! // ! };

! Ogni server dispone di un proprio handler specico per gestire ogni singola syscall: ! questi handler hanno solitamente un nome tipo do_nomesyscall. ! La struttra generale di un server la seguente:
! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! while( TRUE ) { ! get_work(); /* wait for an MM system call */ ! /* ! * ... ! */ ! result = (*call_vec [call_nr]) (); ! reply(...); }

! ! ! ! !
! ! ! ! ! ! ! ! ! !

Ogni server rimane costantemente in attesa di riceve un messaggio e quando questo accade viene interpretato il tipo di messaggio e viene prelevato il numero della syscall da utilizzare per chiamare il corretto handler di risposta. Vediamo nello specico la procedura get_work() :
PRIVATE void get_work() { /* Wait for the next message and extract useful information from it. */ if (receive(ANY, &m_in) != OK) ! panic(__FILE__,"PM receive error", NO_NUM); who_e = m_in.m_source;! ! ! ! ! /* who sent the message */

if(pm_isokendpt(who_e, &who_p) != OK) ! panic(__FILE__, "PM got message from invalid endpoint", who_e); call_nr = m_in.m_type; ! ! ! ! ! /* system call number */

! ! ! ! ! ! !

Quindi appena viene ricevuto il messaggio la procedura get_work() non fa altro che eseguire minimi controlli e determinare il mittente del messaggio e il numero della syscall. La replay la risposta che il server invia al sender del messaggio e contiene il risultato della syscall. A questo punto il server provvede a chiamare la kernel call adeguata eseguendo una sendrec al system task (sys_task). Dentro a questa kernel call si trover la chiamata allhandler effettivo della syscall.
109 di 142

/*=================================================================* *! ! ! SYSTASK request types and field nane! ! * *=================================================================*/ ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! #define #define #define #define #define #define #define #define #define #define #define #define KERNEL_CALL ! SYS_FORK! ! SYS_EXEC! ! SYS_EXIT! ! SYS_NICE! ! SYS_PRIVCTL! SYS_TRACE!! SYS_KILL! ! SYS_GETKSIG! SYS_ENDKDIG! SYS_SIGSEND! SYS_SIGRETURN! 0X600!! /* base for kernel calls to SYSTEM */ (KERNEL_CALL+0)! ! /* sys_fork() ! */ (KERNEL_CALL+1)! ! /* sys_exec() ! */ (KERNEL_CALL+2)! ! /* sys_exit() ! */ (KERNEL_CALL+3)! ! /* sys_nice() ! */ (KERNEL_CALL+4)! ! /* sys_privctl() !*/ (KERNEL_CALL+5)! ! /* sys_trace() ! */ (KERNEL_CALL+6)! ! /* sys_kill() ! */ (KERNEL_CALL+7)! ! /* sys_getsig() ! */ (KERNEL_CALL+8)! ! /* sys_endsig() ! */ (KERNEL_CALL+9)! ! /* sys_sigsend()! */ (KERNEL_CALL+10)! ! /* sys_sigreturn()*/

//... ! /* Process management. */ map(SYS_FORK, do_fork); ! ! map(SYS_EXEC, do_exec); ! map(SYS_EXIT, do_exit); ! map(SYS_NICE, do_nice); ! map(SYS_PRIVCTL, do_privctl); ! map(SYS_TRACE, do_trace); ! /* Signal handling. */ map(SYS_KILL, do_kill); ! map(SYS_GETKSIG, do_getksig); ! map(SYS_ENDKSIG, do_endksig); ! map(SYS_SIGSEND, do_sigsend); ! map(SYS_SIGRETURN, do_sigreturn);! ! /* Device I/O. */ map(SYS_IRQCTL, do_irqctl); ! map(SYS_DEVIO, do_devio); ! map(SYS_SDEVIO, do_sdevio); ! map(SYS_VDEVIO, do_vdevio); ! map(SYS_INT86, do_int86); !

/* /* /* /* /* /*

a process forked a new process update process after execute clean up after process exit set scheduling priority system privileges control request a trace operation

*/ */ */ */ */ */

/* /* /* /* /*

cause a process to be signaled PM checks for pending signals PM finished processing signal start POSIX-style signal return from POSIX-style signal

*/ */ */ */ */

/* /* /* /* /*

interrupt control operations */ inb, inw, inl, outb, outw, outl */ phys_insb, _insw, _outsb, _outsw */ vector with devio requests */ real-mode BIOS calls */

! Il syscall handler ha la seguente struttura:


! ! PUBLIC int (*call_vec[NR_SYS_CALLS])(message *m_ptr); ! ! #define map(call_nr, handler) \ ! ! ! {extern int dummy[NR_SYS_CALLS>(unsigned)(call_nr-KERNEL_CALL) ?\ ! ! ! 1:-1];} ! ! call_vec[(call_nr-KERNEL_CALL)] = (handler) !

ESECUZIONE DELLA SYSCALL FORK. ! A grandi linee lesecuzione della fork() si pu riassumere in quattro passaggi: ! ! ! lapplicazione invia un messaggio FORK al memory manager richiedendo la ! ! ! creazione di un nuovo processo. ! ! ! il memory manager alloca la memoria per il nuovo processo ed informa il ! ! ! system task della richiesta di creazione inviando un messaggio SYS_FORK. ! ! ! anche il le system deve essere messo al corrente della richiesta quindi ! ! ! necessario che gli venga inviato un messaggio (di nuovo il messaggio FORK).
110 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

! al termine della creazione del processo il memory manager invia due messaggi ! reply: uno viene recapitato al processo genitore, e conterr il PID del glio, ! laltro viene recapitato al processo glio e conterr il valore zero.

Pi dettagliatamente, una chiamata a funzione fork() viene tradotta dal compilatore in una chiamata alla procedura fork.s, presente nella libreria lib/syscall. Questa procedura esegua un salto incondizionato a __fork , ovvero chiama la procedura fork() scritta in C e contenuta in s/s/src/lib/posix. Questa funzione richiama, a sua volta, la procedura _syscall che riceve tre parametri: ! ! il primo parametro il destinatario della richiesta: nel nostro caso parliamo del ! ! server PM. ! ! il secondo campo il valore numerico della syscall, che passiamo mediante ! ! una macro. ! ! lultimo parametro deve contenere lindirizzo del messaggio che vogliamo ! ! inviare al server. Dopo la chiamata, la _syscall inserisce nel messaggio il numero di syscall che si vuole eseguire ed esegue una _sendrec verso il server (PM nel nostro caso), inviando il messaggio appena modicato. Come abbiamo visto la _sendrec predispone alcuni parametri e lancia un interrupt software che porta allesecuzione della _s_call : a questo punto, dopo lo stack switch, viene chiamata _sys_call con gli stessi parametri predisposti dalla _sendrec .

/*========================================================================* * sys_call * *========================================================================*/ PUBLIC int sys_call(call_nr, src_dst_e, m_ptr, bit_map) int call_nr; /* system call number and flags */ int src_dst_e; /* src to receive from or dst to send to */ message *m_ptr; /* pointer to message in the caller's space */ long bit_map; /* notification event set or flags */ { /* System calls are done by trapping to the kernel with an INT instruction. * The trap is caught and sys_call() is called to send or receive a message * (or both). The caller is always given by 'proc_ptr'. */ register struct proc *caller_ptr = proc_ptr; /* get pointer to caller */ int function = call_nr & SYSCALL_FUNC; /* get system call function */ unsigned flags = call_nr & SYSCALL_FLAGS; /* get flags */ int mask_entry; /* bit to check in send mask */ int group_size; /* used for deadlock check */ int result; /* the system call's result */ int src_dst; vir_clicks vlo, vhi; /* virtual clicks containing message to send */ /* Require a valid source and/or destination process, unless echoing. */ if (src_dst_e != ANY && function != ECHO) { if(!isokendpt(src_dst_e, &src_dst)) { return EDEADSRCDST; } } else src_dst = src_dst_e; /* Check if the process has privileges for the requested call. Calls to the * kernel may only be SENDREC, because tasks always reply and may not block * if the caller doesn't do receive(). */ if (! (priv(caller_ptr)->s_trap_mask & (1 << function)) || (iskerneln(src_dst) && function != SENDREC && function != RECEIVE)) { 111 di 142

return(ETRAPDENIED); }

/* trap denied by mask or kernel */

/* If the call involves a message buffer, i.e., for SEND, RECEIVE, SENDREC, * or ECHO, check the message pointer. This check allows a message to be * anywhere in data or stack or gap. It will have to be made more elaborate * for machines which don't have the gap mapped. */ if (function & CHECK_PTR) { vlo = (vir_bytes) m_ptr >> CLICK_SHIFT; vhi = ((vir_bytes) m_ptr + MESS_SIZE - 1) >> CLICK_SHIFT; if (vlo < caller_ptr->p_memmap[D].mem_vir || vlo > vhi || vhi >= caller_ptr->p_memmap[S].mem_vir + caller_ptr->p_memmap[S].mem_len) { return(EFAULT); /* invalid message pointer */ } } /* If the call is to send to a process, i.e., for SEND, SENDREC or NOTIFY, * verify that the caller is allowed to send to the given destination. */ if (function & CHECK_DST) { if (! get_sys_bit(priv(caller_ptr)->s_ipc_to, nr_to_id(src_dst))) { return(ECALLDENIED); /* call denied by ipc mask */ } } /* Check for a possible deadlock for blocking SEND(REC) and RECEIVE. */ if (function & CHECK_DEADLOCK) { if (group_size = deadlock(function, caller_ptr, src_dst)) { return(ELOCKED); } } /* Now check if the call is known and try to perform the request. The only * system calls that exist in MINIX are sending and receiving messages. * - SENDREC: combines SEND and RECEIVE in a single system call * - SEND: sender blocks until its message has been delivered * - RECEIVE: receiver blocks until an acceptable message has arrived * - NOTIFY: nonblocking call; deliver notification or mark pending * - ECHO: nonblocking call; directly echo back the message */ switch(function) { case SENDREC: /* A flag is set so that notifications cannot interrupt SENDREC. */ caller_ptr->p_misc_flags |= REPLY_PENDING; /* fall through */ case SEND: result = mini_send(caller_ptr, src_dst_e, m_ptr, flags); if (function == SEND || result != OK) { break; /* done, or SEND failed */ } /* fall through for SENDREC */ case RECEIVE: if (function == RECEIVE) caller_ptr->p_misc_flags &= ~REPLY_PENDING; result = mini_receive(caller_ptr, src_dst_e, m_ptr, flags); break; case NOTIFY: result = mini_notify(caller_ptr, src_dst); break; case ECHO: 112 di 142

CopyMess(caller_ptr->p_nr, caller_ptr, m_ptr, caller_ptr, m_ptr); result = OK; break; default: result = EBADCALL; /* illegal system call */ } /* Now, return the result of the system call to the caller. */ return(result); }

! ! ! ! ! ! !

Quindi, dopo che la procedura sys_call ha terminato la fase di controllo, si passa allinvio vero e proprio del messaggio mediante le procedure viste precedentemente, quando abbiamo trattato lIPC di Minix. Ad un certo punto il server PM eseguir una receive e otterr il messaggio di richiesta di esecuzione di una fork(): nel le include/minix/calar.h contenuta la macro FORK, corrispondente al valore 2, mentre nel le pm/table.c possiamo vedere che in posizione 2 associata la procedura do_fork.

/*===========================================================================* * PM/do_fork * *===========================================================================*/ PUBLIC int do_fork() ! {! /* The process pointed to by 'mp' has forked. Create a child process. */ ! register struct mproc *rmp; ! /* pointer to parent */ ! register struct mproc *rmc; ! /* pointer to child */ ! int child_nr, s; ! phys_clicks prog_clicks, child_base; ! phys_bytes prog_bytes, parent_abs, child_abs; /* Intel only */ ! pid_t new_pid; ! static int next_child; ! int n = 0, r; /* If tables might fill up during FORK, don't even start since recovery half * way through is such a nuisance. */ ! rmp = mp; ! if ((procs_in_use == NR_PROCS) || (procs_in_use >= NR_PROCS-LAST_FEW && rmp->mp_effuid != 0)) ! { ! printf("PM: warning, process table is full!\n"); ! return(EAGAIN); ! } ! /* Determine how much memory to allocate. Only the data and stack need to * be copied, because the text segment is either shared or of zero length. */ ! prog_clicks = (phys_clicks) rmp->mp_seg[S].mem_len; ! prog_clicks += (rmp->mp_seg[S].mem_vir - rmp->mp_seg[D].mem_vir); ! prog_bytes = (phys_bytes) prog_clicks << CLICK_SHIFT; ! if ( (child_base = alloc_mem(prog_clicks)) == NO_MEM) return(ENOMEM); /* Create a copy of the parent's core image for the child. */ ! child_abs = (phys_bytes) child_base << CLICK_SHIFT; ! parent_abs = (phys_bytes) rmp->mp_seg[D].mem_phys << CLICK_SHIFT; ! s = sys_abscopy(parent_abs, child_abs, prog_bytes); ! if (s < 0) panic(__FILE__,"do_fork can't copy", s); /* Find a slot in 'mproc' for the child process. A slot must exist. */ 113 di 142

! ! ! ! ! ! ! !

do { next_child = (next_child+1) % NR_PROCS; n++; } while((mproc[next_child].mp_flags & IN_USE) && n <= NR_PROCS); if(n > NR_PROCS) panic(__FILE__,"do_fork can't find child slot", NO_NUM); if(next_child < 0 || next_child >= NR_PROCS || (mproc[next_child].mp_flags & IN_USE)) ! panic(__FILE__,"do_fork finds wrong child slot", next_child);

! rmc = &mproc[next_child]; /* Set up the child and its memory map; copy its 'mproc' slot from parent. */ ! child_nr = (int)(rmc - mproc); /* slot number of the child */ ! procs_in_use++; ! *rmc = *rmp; /* copy parent's process slot to child's */ ! rmc->mp_parent = who_p; /* record child's parent */ /* inherit only these flags */ ! rmc->mp_flags &= (IN_USE|SEPARATE|PRIV_PROC|DONT_SWAP); ! rmc->mp_child_utime = 0; /* reset administration */ ! rmc->mp_child_stime = 0; /* reset administration */ /* A separate I&D child keeps the parents text segment. The data and stack * segments must refer to the new copy. */ ! if (!(rmc->mp_flags & SEPARATE)) rmc->mp_seg[T].mem_phys = child_base; ! rmc->mp_seg[D].mem_phys = child_base; ! rmc->mp_seg[S].mem_phys = rmc->mp_seg[D].mem_phys + (rmp->mp_seg[S].mem_vir - rmp->mp_seg[D].mem_vir); ! rmc->mp_exitstatus = 0; ! rmc->mp_sigstatus = 0; /* Find a free pid for the child and put it in the table. */ ! new_pid = get_free_pid(); ! rmc->mp_pid = new_pid; /* assign pid to child */ /* Tell kernel and file system about the (now successful) FORK. */ ! if((r=sys_fork(who_e, child_nr, &rmc->mp_endpoint)) != OK) { ! panic(__FILE__,"do_fork can't sys_fork", r); } ! tell_fs(FORK, who_e, rmc->mp_endpoint, rmc->mp_pid); /* Report child's memory map to kernel. */ ! if((r=sys_newmap(rmc->mp_endpoint, rmc->mp_seg)) != OK) { ! panic(__FILE__,"do_fork can't sys_newmap", r); } /* Reply to child to wake it up. */ ! setreply(child_nr, 0); ! ! rmp->mp_reply.endpt = rmc->mp_endpoint; ! ! } return(new_pid); /* child's pid */

/* only parent gets details */ /* child's process number */

! ! ! ! ! ! !

Questa funzione legge il valore di mp che punta al PCB del processo che ha richiesto lesecuzione della fork(). Il campo mp contenuto allinterno del server PM. Successivamente verica se ci sono posizioni libere nella tabella dei processi mproc: questa struttura dati contiene uno slot per ogni processo ed in queste posizioni sono contenute tutte le informazioni necessarie alla gestione di tutti i processi. Quindi, se sono presenti posizioni libere, si determina la quantit di memoria necessaria per la generazione del processo glio (devono essere replicati solo segmento stack e
114 di 142

! ! ! ! !
! ! ! ! ! !

dati). Dopodich di copia il PCB del padre nella posizione libera di mproc e si assegna al processo glio un nuovo PID. Lultima operazione da fare linvio dei messaggi di risposta al sys_task e a FS: il messaggio a FS viene inviato tramite la funzione tell_fs , mentre per il sys_task (kernel) si utilizza la procedura sys_fork .
PUBLIC int sys_fork(parent, child, child_endpoint) ! int parent; !/* process doing the fork */ ! int child; !/* which proc has been created by the fork */ ! int *child_endpoint; ! { ! /* A process has forked. Tell the kernel. */ ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! message m; int r; m.PR_ENDPT = parent; m.PR_SLOT = child; r = _taskcall(SYSTASK, SYS_FORK, &m); *child_endpoint = m.PR_ENDPT; return r; }

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

Questa procedura provvede a creare un messaggio contenente i PID dei processi coinvolti nella fork, ovvero il padre e il glio, e inviarlo al sys_task mediante la chiamata a _taskcall. La funzione _taskcall riceve tre parametri: ! ! il destinatario del messaggio. ! ! il numero della syscall. ! ! un puntatore al messaggio. Il suo compito solo quello di inoltrare il messaggio al destinatario, dopo averlo aggiornato inserendo il numero della syscall che ha ricevuto come secondo parametro. Linvio del messaggio viene eseguito tramite sendrec perci la procedura _taskcall rester in attesa di riceve un messaggio dal receiver. Nel nostro caso il destinatario il server sys_task: questo server analogo agli altri, infatti resta perennemente in attesa di ricevere messaggi e, quando questo accade, recupera lhandler di risposta per la syscall ricevuta, prelevandolo dalla struttura call_vec . Quindi il sys_task prelever dal messaggio il numero dellhandler per la syscall fork() e mander in esecuzione tale gestore: la funzione chiamata la do_fork del server sys_task.

/*===========================================================================* * do_fork * *===========================================================================*/ PUBLIC int do_fork(m_ptr) ! register message *m_ptr; /* pointer to request message */ ! { ! /* Handle sys_fork(). PR_ENDPT has forked. The child is PR_SLOT. */ ! #if (CHIP == INTEL) ! reg_t old_ldt_sel; ! #endif ! register struct proc *rpc; /* child process pointer */ ! struct proc *rpp; /* parent process pointer */ ! int i, gen; ! int p_proc; ! ! if(!isokendpt(m_ptr->PR_ENDPT, &p_proc)) 115 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

return EINVAL; rpp = proc_addr(p_proc); rpc = proc_addr(m_ptr->PR_SLOT); if (isemptyp(rpp) || ! isemptyp(rpc)) return(EINVAL); /* Copy parent 'proc' struct to child. And reinitialize some fields. */ gen = _ENDPOINT_G(rpc->p_endpoint); #if (CHIP == INTEL)! ! old_ldt_sel = rpc->p_ldt_sel; /* backup local descriptors */ *rpc = *rpp; /* copy 'proc' struct */ rpc->p_ldt_sel = old_ldt_sel; /* restore descriptors */ #else *rpc = *rpp; /* copy 'proc' struct */ #endif if(++gen >= _ENDPOINT_MAX_GENERATION) /* increase generation */ gen = 1; /* generation number wraparound */ rpc->p_nr = m_ptr->PR_SLOT; /* this was obliterated by copy */ rpc->p_endpoint = _ENDPOINT(gen, rpc->p_nr); /* new endpoint of slot */ /* Only one in group should have SIGNALED, child doesn't inherit tracing. */ rpc->p_rts_flags |= NO_MAP; /* inhibit process from running */ rpc->p_rts_flags &= ~(SIGNALED | SIG_PENDING | P_STOP); sigemptyset(&rpc->p_pending); rpc->p_reg.retreg = 0; rpc->p_user_time = 0; rpc->p_sys_time = 0; /* child sees pid = 0 to know it is child */ /* set all the accounting times to 0 */

! ! ! ! !

/* Parent and child have to share the quantum that the forked process had, * so that queued processes do not have to wait longer because of the fork. * If the time left is odd, the child gets an extra tick. */ rpc->p_ticks_left = (rpc->p_ticks_left + 1) / 2; rpp->p_ticks_left = rpp->p_ticks_left / 2; /* If the parent is a privileged process, take away the privileges from the * child process and inhibit it from running by setting the NO_PRIV flag. * The caller should explicitely set the new privileges before executing. */ if (priv(rpp)->s_flags & SYS_PROC) { rpc->p_priv = priv_addr(USER_PRIV_ID); rpc->p_rts_flags |= NO_PRIV; } /* Calculate endpoint identifier, so caller knows what it is. */ m_ptr->PR_ENDPT = rpc->p_endpoint;

! ! ! ! ! ! !

! ! }

return(OK);

! ! ! ! ! ! ! !

Questa routine provvede a concludere linizializzazione del nuovo processo. Le principali operazioni svolte sono: ! ! copia nel processo glio alcune porzioni delle strutture del padre. ! ! inizializzazione di alcuni campi. ! ! aggiornamento dei ticks dei due processi. ! ! impostazione dei privilegi del processo creato.

116 di 142

! ! ! ! ! ! ! ! ! !

A questo punto la procedura do_fork del server sys_task ha terminato e passa il controllo al sys_task: il server quindi esegue una send verso _taskcall la quale era infatti bloccata in receiving. _taskcall pu concludere la sua computazione ed il controllo rientra nuovamente in sys_task che pu anchesso terminare rientrando su PM/do_fork. Questultima funzione provvede ad inviare una reply al processo glio in quanto anchesso, essendo la copia del padre, bloccato in receiving. Terminata la procedura do_fork del server PM si rientra dentro a PM, esattamente nel main: a questo punto lultima operazione da eseguire linvio di una reply allapplicazione che originariamente ha richiesto la fork.

117 di 142

GESTIONE DEI PROCESSI IN MINIX. ! Lo scheduler di Minix lavora con un algoritmo tipo RR a 16 code di priorit. Ogni ! processo possiede un quanto di tempo per il quale presente un campo nel PCB. Se ! un processo viene bloccato prima del termine del suo quanto di tempo, appena ! possibile sbloccarlo viene messo in testa invece che in coda: siccome il processo non ! ha esaurito il suo quanto di tempo, viene dotato di una sorta di precedenza rispetto ! agli altri processi in Ready queue. ! L'algoritmo di scheduling seleziona la lista, non vuota, di priorit pi alta (lista 0) e ! manda in esecuzione il processo in testa alla coda. fondamentale garantire la corretta ! terminazione di questo algoritmo, ovvero indispensabile la presenza di almeno un ! processo in Ready queue. A questo scopo stato introdotto un nto processo, ! denominato idle, che sempre presente nella coda di priorit pi bassa (15) e che ! esegue una computazione innita: questo permette allo scheduler di individuare sempre ! un processo da assegnare alla CPU, assicurando la corretta esecuzione dellalgoritmo. ! Data la sua locazione lungo le code, il processo idle non incider negativamente ! sullutilizzo del processore in quanto verr mandato in esecuzione solo nel caso in cui ! non sia presente nessun altro processo in Ready queue: questo signica che la CPU ! eseguir idle al posto di rimanere inutilizzata. ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! Quasi tutte le strutture dati necessarie allimplementazione del meccanismo di scheduling sono presenti allinterno dei PCB dei processi, pi precisamente nella sezione associata al kernel: ! ! tabella dei processi composta dai PCB di tutti i processi in esecuzione. ! ! un puntatore al PCB del processo precedentemente eseguito. ! ! un puntatore al PCB del processo in esecuzione. ! ! un puntatore al PCB del processo da eseguire. NB: ! le informazioni contenute nei PCB sono divise in tre sezioni: Process Manager, ! File Manager e Kernel. La componente assegnata al kernel contiene ! principalmente informazioni relative allo scheduling. Sempre allinterno dei PCB sono presenti i campi necessari per limplementazione delle code, e altre variabili connesse allo scheduler: ! ! struct proc *p_nextready : un puntatore al PCB del prossimo elemento della ! ! lista. NULL nel caso in cui il processo associato a quel PCB lultimo della ! ! lista. ! ! p_priority : Minix implementa una forma di aging; questo campo serve a ! ! mantere la priorit del processo a cui associato il PCB. ! ! p_max_priority : indica il valore di priorit pi basso assegnabile al processo, ! ! al di sotto del quale non possibile andare. ! ! p_ticks_left : mantiene il numero di ticks associati al processo; serve a ! ! determinare quando il processo esaurisce il suo quanto di tempo. ! ! p_quantum_size : indica le dimensioni del quanto di tempi misurato in ticks. ! ! p_user_time : indica il numero di ticks spesi dal processo in User Space. ! ! p_sys_time : indica il numero di ticks spesi dal processo in Kernel Mode. ! ! sono presenti due puntatori per ogni coda di processi Ready: ! ! ! ! rdy_head[indice_di_coda] punta alla testa della coda. ! ! ! ! rdy_tail[indice_di_coda] punta alla ne della coda.

118 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! !

In Minix i processi sono preemptible, ovvero possono essere sospesi in favore dell'esecuzione di altri processi. Il tempo di esecuzione dei processi kernel che svolgono servizi per processi utente viene addebitato al processo utente: in questo caso si dice che il processo utente e billable. In pr_number sono presenti i valori numerici assegnati ai processi di sistema (driver e server) e ai processi kernel: i processi di sistema hanno valori positivi mentre quelli di kernel sono negativi. NB: ! il kernel non un processo, non presente in nessuna Ready queue. Solo in ! determinate situazioni possibile saltare nella sezione kernel ma il kernel non ! viene mai schedulato ne esiste un PCB del kernel. Ad ogni processo sono inoltre assegnate ags e traps: le prime sono costituite dalle propriet preemptible (P), billable (B) e system (S), mentre le traps deniscono la capacit di comunicazione del processo e sono echo (E), send (S), receive (R), both (B) e notication (N).

119 di 142

! ! ! ! ! ! ! ! ! !

Come si vede dallimmagine, il processo idle billable: in questo caso il signicato che il tempo che gli viene attribuito corrisponde al tempo totale in cui la CPU non ha eseguito operazioni, o meglio ha eseguito solo operazioni del processo idle in quanto non cera nientaltro da fare. In fase di boot viene inizializzata la tabella dei processi con i PCB dei processi di sistema. Il primo processo ad essere eseguito in User Space il server PM mentre il primo processo utente ad andare in esecuzione init. Questo processo il padre di tutti i processo in quanto tutti i processi utente in esecuzione nel sistema forkano a partire da init.

IMPLEMENTAZIONE DELLO SCHEDULER. ! Lo scheduler di Minix composto da quarto proedure: ! ! ! enqueue : aggiunge un processo alla Ready queue. ! ! ! dequeue : rimuove un processo dalla Ready queue. ! ! ! sched : determina la coda in cui inserire un processo. ! ! ! pick_proc : seleziona il processo da mandare in Running.
/*===========================================================================* * enqueue * *===========================================================================*/ PRIVATE void enqueue(rp) ! register struct proc *rp; /* this process is now runnable */ ! { /* Add 'rp' to one of the queues of runnable processes. This function is * responsible for inserting a process into one of the scheduling queues. * The mechanism is implemented here. The actual scheduling policy is * defined in sched() and pick_proc(). */ ! int q; !/* scheduling queue to use */ ! int front; !/* add to front or back */ /* Determine where to insert to process. */ ! sched(rp, &q, &front); /* Now add the process to the queue. */ ! if (rdy_head[q] == NIL_PROC) { ! rdy_head[q] = rdy_tail[q] = rp; ! rp->p_nextready = NIL_PROC; ! } ! ! else if (front) { ! rp->p_nextready = rdy_head[q]; ! rdy_head[q] = rp; ! } ! else { ! rdy_tail[q]->p_nextready = rp; ! rdy_tail[q] = rp; ! ! rp->p_nextready = NIL_PROC; ! } /* Now select the next process to run. */ ! pick_proc(); ! }

!/* add to empty queue */ ! /* create a new queue */ ! /* mark new end */ ! /* add to head of queue */ ! /* chain head of queue */ !/* set new queue head */ !/* !/* /* !/* add to tail of queue */ chain tail of queue */ set new queue tail */ mark new end */

! La procedure enqueue prende, come unico parametro, il puntatore al PCB del processo ! in stato Running, il quale deve essere inserito in una delle code. La procedura deve ! quindi vericare alcune informazioni contenute nel PCB del processo, determinare la
120 di 142

! ! ! ! ! ! ! ! !

corretta coda a cui assegnare il processo, ed eseguire linserimento. Quindi la prima operazione da fare richiamare la procedura sched: questa procedura fornisce il puntatore alla Ready queue dinteresse q per il processo puntato da rp, ed un puntatore alla testa/coda di questa lista. A questo punto si verica che la coda restituita dalla procedura sched non sia vuota: se questo accade necessario creare una nuova lista, quindi sia rdy_head[q] sia rdy_tail[q] punteranno al PCB del processo associato ad rp. Altrimenti sar sufciente eseguire un inserimento in testa o in coda alla lista, a seconda del valore fornito dalla procedura sched. Al termine viene chiamata la procedura pick_proc che seleziona il prossimo processo da mandare in esecuzione.

/*===========================================================================* * dequeue * *===========================================================================*/ PRIVATE void dequeue(rp) ! register struct proc *rp; /* this process is no longer runnable */ ! { /* A process must be removed from the scheduling queues, for example, because * it has blocked. If the currently active process is removed, a new process * is picked to run by calling pick_proc(). */ ! register int q = rp->p_priority; /* queue to use */ ! register struct proc **xpp; /* iterate over queue */ ! register struct proc *prev_xp; /* Side-effect for kernel: check if the task's stack still is ok? */ ! if (iskernelp(rp)) { ! if (*priv(rp)->s_stack_guard != STACK_GUARD) ! panic("stack overrun by task", proc_nr(rp)); } /* Now make sure that the process is not in its ready queue. Remove the * process if it is found. A process can be made unready even if it is not * running by being sent a signal that kills it. */ ! prev_xp = NIL_PROC; ! for (xpp = &rdy_head[q]; *xpp != NIL_PROC; xpp = &(*xpp)->p_nextready) { ! if (*xpp == rp) { ! ! *xpp = (*xpp)->p_nextready; ! if (rp == rdy_tail[q]) ! rdy_tail[q] = prev_xp; ! if (rp == proc_ptr || rp == next_ptr) ! pick_proc(); ! break; } prev_xp = *xpp; /* /* /* /* /* /* found process to remove */ replace with next chain */ queue tail removed */ set new tail */ active process removed */ pick new process to run */

! !

! ! } }

/* save previous in chain */

! ! ! ! ! ! ! !

Come abbiamo visto lo scopo della procedura dequeue quello di prelevare un processo dalla Ready queue impedendo in questo modo la sua schedulazione. Questa operazione viene eseguita tipicamente quando un processo deve essere bloccato per qualche ragione, ad esempio quando riceve una SIGKILL o a seguito dellesecuzione di send/receive non corrisposte. Questa routine riceve come parametro il puntatore rp al PCB del processo da bloccare e tramite questo preleva la coda di Ready q su cui lavorare. Successivamente si controlla, tramite una costante sullo stack del kernel, se c stato un buffer overow e nel qual caso si genera un kernel panic. Altrimenti il
121 di 142

! ! ! ! !

procedimento continua con lo scorrimento della lista e la cancellazione del processo puntato da rp dalla coda q. Dopo la rimozione necessario vericare se il processo rimosso era quello in esecuzione in quel momento ed in caso affermativo sar necessario richiamare la procedura pick_proc per mandare in esecuzione un altro processo.

/*===========================================================================* * sched * *===========================================================================*/ PRIVATE void sched(rp, queue, front) ! register struct proc *rp; !/* process to be scheduled */ ! int *queue; !/* return: queue to use */ ! int *front; ! /* return: front or back */ ! { ! static struct proc *prev_ptr = NIL_PROC; ! int penality = 0; /* This function determines the scheduling policy. It is called whenever a * process must be added to one of the scheduling queues to decide where to * insert it. As a side-effect the process' priority may be updated. */ ! int time_left = (rp->p_ticks_left > 0); !/* quantum fully consumed */ /* Check whether the process has time left. Otherwise give a new quantum * and lower the process' priority, unless the process already is in the * lowest queue. */ ! if (! time_left) { ! /* quantum consumed ? */ ! rp->p_ticks_left = rp->p_quantum_size; /* give new quantum */ ! if (prev_ptr == rp) penality++; ! ! else penality--; ! ! prev_ptr = rp; ! } ! if (penalty != 0 && ! iskernelp(rp)) { ! ! rp->p_priority += penalty; ! ! ! /* update with penalty */ ! ! if (rp->p_priority < rp->p_max_priority)! /* check upper bound */ ! ! ! ! rp->p_priority=rp->p_max_priority; ! ! else if (rp->p_priority > IDLE_Q-1) ! /* check lower bound */ ! ! ! ! rp->p_priority = IDLE_Q-1; ! } /* If there is time left, the process is added to the front of its queue, * so that it can immediately run. The queue to use simply is always the * process' current priority. */ ! *queue = rp->p_priority; ! *front = time_left; ! }

! ! ! ! ! ! ! ! ! ! !

La procedura sched , come prima operazione, verica se durante lultimo quanto di esecuzione il processo ha utilizzato tutti i ticks a sua disposizione. Se time_left diverso da zero allora il processo non ha ancora esaurito il suo quanto e deve riperdere lesecuzione con i ticks rimasti. Nel caso in cui il processo venga tolto dalla CPU al termine del quanto a disposizione, necessario ripristinare i ticks prima del reinserimento di tale processo in una Ready queue. Questoperazione di assegnamento di un nuovo quanto di tempo viene eseguita allinterno dell if della procedura sched. Sempre in questa sezione di codice si gestisce la penality: la penalit un fattore inuenzante nella determinazione della coda di priorit cui assegnare il processo. Lottica di Minix riguardo alla penalit prevede che un processo
122 di 142

! ! ! ! ! ! ! ! ! ! ! ! !

che utilizza la CPU due volte consecutivamente, utilizzando tutto il quanto di tempo, venga penalizzato con un abbassamento della priorit: questo si fa in quanto un comportamento come quello appena descritto lascia presupporre che tale processo stia eseguendo un ciclo innito o altre operazioni sospette. Quindi si preferisce dare precedenza ad altri processi che hanno un comportamento pi normale decrementando la penality ad essi associata. Nella determinazione della priorit in funzione anche della penality, lalgoritmo tiene conto del limite massimo e minimo di priorit associabile al processo in oggetto in quel momento. Inne la procedura determina la coda cui assegnare il processo e la posizione di inserimento, head o tail. NB: ! i processi di kernel o clock non possono subire una penalizzazione. ! Per un processo che non ha esaurito il suo quanto di tempo la coda rimane la ! stessa, mentre linserimento dovr avvenire in testa e non in coda.

/*===========================================================================* * pick_proc * *===========================================================================*/ PRIVATE void pick_proc() ! { /* Decide who to run now. A new process is selected by setting 'next_ptr'. * When a billable process is selected, record it in 'bill_ptr', so that the * clock task can tell who to bill for system time. */ ! register struct proc *rp; /* process to run */ ! int q; /* iterate over queues */ /* Check each of the scheduling queues for ready processes. The number of * queues is defined in proc.h, and priorities are set in the task table. * The lowest queue contains IDLE, which is always ready. */ ! for (q=0; q < NR_SCHED_QUEUES; q++) { ! if ( (rp = rdy_head[q]) != NIL_PROC) { ! ! next_ptr = rp; /* run process 'rp' next */ ! ! if (priv(rp)->s_flags & BILLABLE) ! ! bill_ptr = rp; /* bill for system time */ ! ! return; ! } ! } ! }

! ! ! ! ! ! ! ! ! !

La procedura pick_proc seleziona il primo processo disponibile nella coda a priorit pi alta non vuota e lo assegna a next_ptr. Questo il puntatore utilizzato dalla _restart per determinare quale processo assegnare alla CPU. La funzione esegue quindi le seguenti operazioni: ! ! scorre tutte le liste nch non individua un processo. ! ! controlla se quel processo p billable e mette il suo indirizzo in next_ptr. ! ! se il processo billable medesimo indirizzo viene posto in bill_ptr per i ! ! controlli sul tempo di esecuzione. NB:! possibile eseguire il ciclo senza ulteriori controlli sulle code in quanto siamo certi ! di individuare almeno un processo che nel caso peggiore sar il processo idle.

! Quindi lesecuzione effettiva di un processo demandata alla routine _restart che ! costituisce la componente terminale del processo di risposta ad interrupt e syscall.

123 di 142

IDLE_TASK ! Il processo idle presente in tutti i sistemi operativi ed il processo che serve a tenere ! occupata la CPU quando nn ci sono altre operazioni da eseguire. Inizialmente questo ! processo eseguiva solo un while(TRUE) ed veniva interrotto solo mediante interrupt, ! allarrivo di altri processi. Con l'avvento dei portatili stato necessario modicare idle in ! modo da non forzare il processore ad eseguire operazioni, sprecando energia. ! stata introdotta l'istruzione hlt che mette il processore in uno stato dormiente in attesa ! che accada qualcosa nel sistema che necessiti della CPU. L'istruzione hlt privilegiata ! e quindi pu essere eseguita solo in kernel mode: in minix solo il kernel, il system task ! e clock task vengono eseguiti in KM. Si dovuto perci trovare un meccanismo in ! grado di permettere al processo idle di eseguire un'istruzione privilegiata. ! Solitamente mediante syscall chiediamo al SO di eseguire una procedura, invece ! questo meccanismo prevede che all'interno di idle_task, che opera con ! TASK_PRIVILEGE, sia possibile utilizzare l'istruzione privilegiata hlt ad i ! NTR_PRIVILEGE.
!*===========================================================================* !*idle_task * !*===========================================================================* !*Thistaskiscalledwhenthesystemhasnothingelsetodo.TheHLT !*instructionputstheprocessorinastatewhereitdrawsminimumpower. !*PUBLICvoidhalt_cpu(void); !*reanablesinterruptsandputsthecpuinthehaltsstate.Onceaninterrupt !*ishandledtheexecutionresumesbydisablinginterruptsandcontinues !* ! _idle_task: ! ! pushhalt ! ! call_level0/*level0(halt)*/ ! ! ! pop! eax ! ! jmp_idle_task ! halt: ! ! sti ! ! hlt ! ! cli ! ! ret

! La routine inizia posizionando sullo stack lindirizzo delletichetta halt e ! successivamente viene chiamata _level0 che, implicitamente pusha sullo stack ! lindirizzo dellistruzione pop eax. La congurazione dello stack quindi la seguente:

halt ret esp

124 di 142

!*===========================================================================* !*level0 * !*===========================================================================* !*PUBLIC void leve0l(void(*func)(void)) !*Call a function at permission level 0. This allows kernel task to do things !*that are only possibile at the most privileged CPU level. !* ! ! _level0: ! ! ! mov ! eax, 4(esp) ! ! ! mov ! (_level0_func), eax ! ! ! int ! LEVEL0_VECTOR ! ! ! ret

! La _level0 posizione in eax lindirizzo corrispondente alletichetta halt, sposta tale ! valore allindirizzo contenuto in _level0_func e lancia leccezione SW corrispondente a ! LEVEL0_VECTOR. A questa operazione segue lesecuzione della procedura level0_call:
!*===========================================================================* !*level0 * !*===========================================================================* ! ! ! ! ! ! _leve0l_call: ! call !save ! jmp! (_level0_func)

! ! ! ! ! ! ! ! ! ! ! ! !

Questa routine chiama la save quindi salva il contesto del processo idle ed esegue lo stack switch passando al kernel. A questo punto ci si trova in Kernel Mode ed quindi possibile eseguire listruzione hlt: si salta quindi a (_level0_func) che contiene letichetta halt, si disabilitano alcuni interrupt e si esegue hlt. Non possibile disabilitare tutti gli interrupt perch altrimenti non si potrebbe pi risvegliare il processore. Quando eseguiamo listruzione ret di idle_task ci troviamo sullo stack del kernel a seguito della chiamata alla procedura save: letichetta a cui saltiamo quindi _restart. Quando il processore in stato dormiente e riceve un interrupt viene chiamata la procedura save: in questo caso per siamo gi sullo stack del kernel quindi si salta la sezione di codice che permette lo switch del kernel e viene salvata _restart1 sullo stack. Al termine dellinterrupt possibile che venga ripresa lesecuzione di idle oppure di un altro processo (ad esempio se linterrupt ha chiamato lo scheduler).

125 di 142

GESTIONE DEL CLOCK IN MINIX. ! In questa sezione per clock non si intende il clock di esecuzione della CPU, bens una ! periferica del sistema in grado di inviare interrupt. Questa componente costituita da ! un cristallo di quarzo oscillante sottoposto a tensione elettrica: le oscillazioni ! permettono di scandire il decremento di un contatore che viene poi ripristinato una volta ! raggiunto il valore zero. Il contatore ha lo scopo di denire la frequenza di invio ! dellinterrupt di clock.

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

Il cristallo di quarzo pu generare un segnale con frequenza variabile tra i 5 MHz e i 200 MHz. Quando il contatore raggiunge il valore zero vengono eseguite le seguenti operazioni: ! ! la periferica invia un interrupt HW (IRQ 8). ! ! il contatore viene reinizializzato. NB:! lintervallo di tempo intercorso tra un interrupt di clock e il successivo viene ! chiamato clock tick. Esempio: ! Consideriamo un contatore a 16 bit e un clock a 5 MHz. ! Il clock tick generabile compreso tra 0,2 microsecondi e 13,1 millisecondi a ! seconda del valore del contatore: ! ! ! contatore = 1 ! ! Periodo = 1/(5*10^6) = 2*10^-7 = 0,2 micorsecondi ! ! ! contatore = 65536/(5*10^6) = 0,0131 secondi = 13,1 millisecondi. Le principali funzionalit svolte dal clock sono: ! ! mantenimento dellorario e della data del giorno: a questo scopo si sconsiglia di ! ! utilizzare registri a 64 bit in quanto comportano operazioni lente. ! ! determinazione della scadenza dei quanti di tempo dei processi. ! ! mantiene il tempo delle connessioni: ad esempio viene utilizzato per stabilire e ! ! vericare un determinato tempo do timeout per una trasmissione. ! ! gestisce segnali a tempo per conto dei processi eseguiti nel sistema. Si ! ! considerino i processi che basano le loro azioni sul vericarsi o meno di ! ! determinati eventi (ACK TCP). ! ! Watchdog timers: il clock si occupa di mandare in esecuzione determinati ! ! processi entro/ad un determinato orario. Un esempio di questo tipo ! ! lesecuzione i backup programmati. ! ! Prolino: raccoglie i tempi di esecuzione di diverse componenti del sistema e ! ! dei processi utente.

126 di 142

IMPLEMENTAZIONE DEL CLOCK. ! Le componenti coinvolte in questo ambito sono: ! ! ! interrupt handler. ! ! ! clock driver. ! ! ! routine di servizio (lettura del clock, dellorario, dei registri della periferica, ...). ! Linterrupt di clock richiama il driver solo quando devono essere eseguite operazioni di ! una certa complessit, ad esempio un context switch. Lhandler invece viene richiamato ! molte volte al secondo e deve quindi essere estremamente efciente. ! ! ! ! ! Il driver la prima componente del clock ad essere avviata sul sistema quindi deve provvedere allinizializzazione del dispositivo. Viene infatti avviata in fase di boot e dopo linizializzazione si mette in attesa di ricevere un messaggio di tipo HARD_INT generato dallinterrupt handler.

/*=========================================================================* * clock_task * *===========================================================================*/ PUBLIC void clock_task() ! { /* Main program of clock task. * If the call is not HARD_INT it is an error. */ ! message m; /* message buffer for both input and output */ ! int result; /* result returned by the handler */ ! init_clock(); /* initialize clock task */ /* Main loop of the clock task. Get work, process it. Never reply. */ ! while (TRUE) { ! ! ! ! ! ! ! /* Go get a message. */ receive(ANY, &m);

! ! ! ! ! ! } }

/* Handle the request. Only clock ticks are expected. */ switch (m.m_type) { ! case HARD_INT: ! ! ! result = do_clocktick(&m); /* handle clock tick */ ! ! ! break; ! default: /* illegal request type */ ! ! ! kprintf("CLOCK: illegal request %d from %d.\n", ! ! ! ! m.m_type,m.m_source); }

! ! ! ! ! ! ! ! ! ! !

La procedura di inizializzazione della componente clock esegue le seguenti operazioni: ! ! inizializza alcuni registri della periferica. ! ! predispone la periferica inserendo una serie di comandi. ! ! setta il valore del contatore principale. ! ! carica il valore dellhandler della periferica clock nella struttura hook (CLOCK ! ! una costante che denisce un numero di processo: -3). ! ! abilita il clock interrupt. Terminata lesecuzione di init_clock(), dopo 16.67 millisecondi verr generato il primo interrupt clock. Dopo questa fase il driver si sospende in receive: la sospensione inevitabile in quanto essendo stato avviato in fase di boot questo driver non potr avere messaggi pendenti alla prima esecuzione della receive.

127 di 142

! Quando il messaggio arriva si controlla se proviene effettivamente dallinterrupt ! handler: se ci vericato si prosegue chiamando do_clocktick altrimenti si in ! presenza di una illegal request.
/*===========================================================================* * clock_handler * *===========================================================================*/ PRIVATE int clock_handler(hook) ! irq_hook_t *hook; ! { /* This executes on each clock tick (i.e., every time the timer chip generates * an interrupt). It does a little bit of work so the clock task does not have * to be called on every tick. The clock task is called when: * * (1) the scheduling quantum of the running process has expired, or * (2) a timer has expired and the watchdog function should be run. * * Many global global and static variables are accessed here. The safety of * this must be justified. All scheduling and message passing code acquires a * lock by temporarily disabling interrupts, so no conflicts with calls from * the task level can occur. Furthermore, interrupts are not reentrant, the * interrupt handler cannot be bothered by other interrupts. * * Variables that are updated in the clock's interrupt handler: * lost_ticks: * Clock ticks counted outside the clock task. This for example * is used when the boot monitor processes a real mode interrupt. * realtime: * The current uptime is incremented with all outstanding ticks. * proc_ptr, bill_ptr: * These are used for accounting. It does not matter if proc.c * is changing them, provided they are always valid pointers, * since at worst the previous process would be billed. */ register unsigned ticks; /* Acknowledge the PS/2 clock interrupt. */ ! if (machine.ps_mca) outb(PORT_B, inb(PORT_B) | CLOCK_ACK_BIT); /* Get number of ticks and update realtime. */ ! ticks = lost_ticks + 1; ! lost_ticks = 0; ! realtime += ticks; /* Update user and system accounting times. Charge the current process for * user time. If the current process is not billable, that is, if a non-user * process is running, charge the billable process for system time as well. * Thus the unbillable process' user time is the billable user's system time. */ ! proc_ptr->p_user_time += ticks; ! if (priv(proc_ptr)->s_flags & PREEMPTIBLE) { ! proc_ptr->p_ticks_left -= ticks; ! } ! if (! (priv(proc_ptr)->s_flags & BILLABLE)) { ! bill_ptr->p_sys_time += ticks; ! bill_ptr->p_ticks_left -= ticks; ! } /* Update load average. */ ! load_update(); 128 di 142

/* Check if do_clocktick() must be called. Done for alarms and scheduling. * Some processes, such as the kernel tasks, cannot be preempted. */ ! if ((next_timeout <= realtime) || (proc_ptr->p_ticks_left <= 0)) { ! prev_ptr = proc_ptr; /* store running process */ ! lock_notify(HARDWARE, CLOCK); /* send notification */ ! } ! return(1); /* reenable interrupts */ ! }

! ! ! ! ! ! ! ! ! ! ! ! ! !

Il clock_handler lhandler che viene richiamato ogni volta che il processore riceve un interrupt di clock. Une delle prime operazioni eseguite laggiornamento del realtime ovvero il contatore che mantiene il time-of-the-day. La viariabile lost_ticks un contatore globale che mantiene il numero di ticks persi nelle fasi in cui gli interrupt sono disabilitati: in questi momenti linterrupt di clock non viene sentito dal processore e quindi i campi relativi al clock non possono essere aggiornati; questa variabile permette di tenere traccia dei ticks non ancora aggiunti. Successivamente vengono aggiornati i ticks a disposizione dei processi e si controlla se il processo corrente billable: in caso affermativo devono essere aggiornati i ticks del processo e quelli di sistema in modo da effettuare laddebito al processo utente dei ticks spesi dai processi di sistema. A questo punto si verica se c qualche processo da risvegliare (es: watchdog) oppure se il processo corrente ha terminato il quanto a disposizione: se uno dei casi si verica bisogna chiamare do_clocktick quindi si mette in prev_ptr il processo corrente e si invia una notify al clock driver.

! Il clock driver era sospeso in receiving e quando arriva il messaggio va in Ready queue ! sulla coda di livello 0. Lo scheduler esegue il suo normale algoritmo e se viene mandato ! in esecuzione il clock driver ha inizio la procedura di gestione.
/*===========================================================================* * do_clocktick * *===========================================================================*/ PRIVATE int do_clocktick(m_ptr) ! message *m_ptr; /* pointer to request message */ ! { /* Despite its name, this routine is not called on every clock tick. It * is called on those clock ticks when a lot of work needs to be done. * A process used up a full quantum. The interrupt handler stored this * process in 'prev_ptr'. First make sure that the process is not on the * scheduling queues. Then announce the process ready again. Since it has * no more time left, it gets a new quantum and is inserted at the right * place in the queues. As a side-effect a new process will be scheduled. */ ! if (prev_ptr->p_ticks_left <= 0 && priv(prev_ptr)->s_flags & PREEMPTIBLE){ ! lock_dequeue(prev_ptr); /* take it off the queues */ ! lock_enqueue(prev_ptr); /* and reinsert it again */ ! } /* Check if a clock timer expired and run its watchdog function. */ ! if (next_timeout <= realtime) { ! tmrs_exptimers(&clock_timers, realtime, NULL); ! next_timeout = clock_timers == NULL ? ! TMR_NEVER : clock_timers->tmr_exp_time; ! } /* Inhibit sending a reply. */ ! return(EDONTREPLY); ! } 129 di 142

! ! ! ! ! ! !

La procedura do_clocktick controlla innanzitutto se stata chiamata a seguito della terminazione del quanto di tempo di un processo. In questo caso il processo ancora in Running ed quindi necessario toglierlo dalla Ready queue per poi essere reinserito mediante la funzione enqueue (applicando le regole del RR e laging tramite penality). A questo punto si controlla se c qualche watchdog scaduto e se necessario avviare le funzioni associate. Se non scaduto nessun timer e se non ci sono segnali particolari da gestire si rientra tramite listruzione return.

130 di 142

GESTIONE DELLA MEMORIA. ! La memoria una componente fondamentale dellarchitettura di un computer: ! ! ! un programma, per essere eseguito, deve risiedere in memoria centrale. ! ! ! per questioni prestazionali necessario che la memoria sia in grado di ! ! ! contenere pi di un programma contemporaneamente. ! ! ! laccesso alla memoria centrale una delle operazioni pi frequenti eseguite ! ! ! dal processore (accesso ai dati e al codice). ! Le nuove tecnologie hanno introdotto un grande divario tra la velocit del processore e ! quella della memoria: solitamente quando il processore accede alla memoria, passando ! alcuni cicli di CPU prima che il dato sia disponibile. Per questa ragione fondamentale ! la multiprogrammazione: si consideri il caso di un processo che esegue molte ! operazioni di I/O, la presenza di altri programmi in memoria, che possono essere ! eseguiti, permette di sfruttare i tempi morti del processo principalmente I/O bound. !

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

Quindi i punti fondamentali sono due: ! ! disporre di meccanismi molto efcienti di gestione della memoria. ! ! permettere la multiprogrammazione per ottimizzare lutilizzo della CPU. Ovviamente, quando si in presenza di un sistema multiprogrammato, necessario che il gestore della memoria fornisca delle garanzie riguardo la corretta esecuzione dei programmi. NB:! se i programmi sono prettamente I/O bound, per aumentare lutilizzo della CPU ! necessario incrementare il numero di programmi in esecuzione ! contemporaneamente. importante per notare che laumento del livello di ! multiprogrammazione non direttamente proporzionale allaumento di utilizzo ! della CPU. Dal graco questo rapporto evidente. Dal punto di vista HW la memoria non altro che un enorme insieme (array) di byte, dove ogni byte possiede un indirizzo. colui che si occupa di implementare il MM (Memory Manager) che da signicato alla memoria centrale e ai suoi indirizzi. Oltre a questo il MM di deve occupare di fornire adeguati servizi riguardo alla memoria e alla sua interazione con i processi in esecuzione nel sistema: ! ! allocazione di spazi di memoria. ! ! condivisione di spazi di memoria.
131 di 142

! ! ! ! ! !

! ! protezione degli spazi di memoria. ! ! randomizzazione della memoria allocata, ovvero quando un processo va in ! ! esecuzione non deve sempre essere posizionato nelle stesse porzioni di ! ! memoria. Per rilocazione si intende il processo di trasformazione dello spazio di indirizzamento simbolico di un programma nello spazio di indirizzamento sico: Nomi simbolici Nomi logici Nomi sici

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

Lo spazio di indirizzamento di un programma costituito da nomi simbolici, cio gli identicatori che il programmatore assegna a variabili, label e procedure, e dalle zone di dati dinamiche e stack. Questo spazio di indirizzamento deve esser trasformato in spazio logico, ovvero si passa ad indirizzi comprensibili allHW. Tipicamente questo processo si traduce nella sostituzione dei nomi simbolici con dei valori numerici mappati in uno spazio di memoria sico partendo dall'indirizzo zero. I compilatori sono basati su questo presupposto, ovvero considerano gli indirizzi del programma a partire dall'indirizzo zero: quindi i valori generati non hanno alcuna relazione con i veri indirizzi della memoria centrale e devono esistere altre componenti che operino la conversione da indirizzi logici a indirizzi sici. Il passaggio da livello logico a sico viene detto mappatura in quanto gli indirizzi logici vengono mappati sulla memoria centrale determinando gli indirizzi che conterranno sicamente i dati e le istruzioni del programma. In generale deniamo: ! ! logici: sono gli indirizzi generati dalla CPU in seguito allelaborazione (decode) ! ! degli indirizzi degli operandi presenti in unistruzione. ! ! sici: indirizzi che giungono al bus dati della memoria centrale e specicano una ! ! precisa cella. Loperazione di generazione degli indirizzi logici viene eseguita una sola volta dal compilatore; la generazione degli indirizzi sici invece avviene ad ogni esecuzione del programma: gli indirizzi logici vengono inviati ad ununit specica, lMMU, che elabora questo input generando gli indirizzi sici associati alla memoria centrale. Raramente accade che gli indirizzi logici coincidano con quelli sici, quindi necessario eseguire unassociazione tra gli uni e gli altri: questo processo chiamato binding. La rilocazione un processo mediante il quale si assegna uno spazio dindirizzamento ad un programma, ovvero si assegnano gli indirizzi logici o sici. Loperazione di binding viene eseguita durante il procedimento di rilocazione e consiste nello assegnamento, a dati e istruzioni, di un determinato valore nel nuovo spazio di indirizzamento. Il binding pu essere effettuato in diversi momenti: ! ! compile time: se in fase di compilazione fosse gi noto lo spazio di memoria ! ! che dovr contenere il processo, possibile generare codice assoluto. I ! ! problemi di questa tecnica sono: ! ! ! ! nel caso in cui lo spazio sico dovesse cambiare, sarebbe ! ! ! ! necessario ricompilare lintero codice. ! ! ! ! bisogna conoscere a priori la posizione corretta in cui posizionare ! ! ! ! il programma in memoria durante lesecuzione (possibile solo su ! ! ! ! sistemi dedicati). ! ! ! se lo spazio sico che dovr contenere il programma non noto a priori, il ! ! compilatore assocer al programma uno spazio logico (rilocabile) che potr ! ! essere mappato sullo spazio sico. Questultima fase pu essere eseguita in ! ! due momenti distinti:
132 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

! ! ! ! ! ! ! !

! ! ! ! ! ! ! !

! ! ! ! ! ! ! !

! ! ! ! ! ! ! !

Load time: durante il caricamento del programma da memoria di massa a memoria centrale. Execution time: durante la fase di esecuzione di unistruzione si provvede a rilocare opportunamente gli indirizzi in essa contenuti. In questo caso necessario un supporto HW per lo svolgimento delloperazione: questo perch listruzione gi in fase di decode e quindi non si pu interrompere la CPU per forzarla ad eseguire il mapping.

Il binding a compile time viene anche detto Early binding e produce codice efciente, consentendo anche di anticipare in fase di compilazione una serie di veriche sugli indirizzi. Il binding a load time viene detto Delayed binding (linker/loader) e anchesso produce codice efciente, consente compilazioni separate e facilita la portabilit del codice. Inne il binding ad execution time, o Late binding (VM, linker/loader dinamici, overlay), quello che produce codice meno efciente, necessita di check a runtime ed meno essibile degli altri modelli. Il supporto HW necessario al Late binding una componente che prende il nome di MMU, Memory Management Unti. LMMU riceve in input informazioni dal SO e lindirizzo logico generato dalla CPU: mediante questi dati ricava lindirizzo sico della memoria centrale che verr inviato sul bus dati. Questa operazione viene eseguita ogni volta che la CPU deve elaborare un indirizzo logico. La mappatura tramite MMU avviene come ultima fase di elaborazione di un indirizzo: tutte le operazione preliminari vengono eseguite dalla CPU e direttamente sullindirizzo logico.

In questa struttura esiste per un problema di protezione. Consideriamo in seguente programma: ! ! ! ! ! mov ! eax, end ! ! loop: ! ! ! mov! (eax), 0 ! ! ! add! eax, 4 ! ! ! jmp! loop ! ! end: ! ! ! ret

133 di 142

! ! ! ! ! ! ! ! ! ! ! ! !

Il programma appena descritto azzera tutta la memoria del sistema, ad eccezione delle sue prima quattro istruzioni. Quindi quando eseguiamo un programma dobbiamo controllare che gli indirizzo generati da questo programma corrispondano al suo spazio di indirizzamento. lMMU ad occuparsi di questo aspetto: se un processo genera indirizzi oltre il suo spazio di indirizzamento viene automaticamente sollevata uneccezione. Inoltre necessario individuare i processi che utilizzano memoria condivisa: in questo caso bisogna consentire la generazioni di indirizzi compresi nellarea condivisa, predisponendo tutti i meccanismi di mutua esclusione per garantire la correttezza dei dati. Anche per quanto riguarda le sezioni di codice opportuno che il MM sia in grado di consentire la condivisione del codice tra pi processi piuttosto che forzarne la copia per tutti i processi che intendono utilizzarlo.

Un ulteriore compito del del MM riguarda lallocazione della memoria: ! ! ! la memoria viene divisa in porzioni che corrispondono alla dimensione minima ! ! ! allocabile. ! ! ! ad ogni porzione viene associata una bitmap utilizzata per indicare se quella ! ! ! specica porzione gi allocata oppure libera. ! ! ! per eseguire unallocazione necessario ricercare, tra le bitmap, una sezione di ! ! ! memoria libera sufcientemente grande per ospitare il programma da eseguire. ! ! ! se la sezione viene individuata opportuno aggiornare il PCB del processo ! ! ! associato a quella porzione di memoria. !

134 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

Nellultima immagine la lettera P indica che la sezione occupata da un processo mentre la lettera H indica un Hole, ovvero un buco libero di memoria. I due valori numerici successivi invece indicano la posizione di partenza della sezione di memoria e la lunghezza della porzione. ES: ! ! ! ! ! P|0|5 Il processo P inizia allindirizzo 0 e termina allindirizzo 5. H|5|3 La porzione libera inizia allindirizzo 5 e termina allindirizzo 8. Quando un processo termina si compattano tutte le sezioni di memoria libere adiacenti. A questo scopo pi essere pi efciente utilizzare delle liste bidirezionali per la gestione delle porzioni allocate.

Il principale problema in questo aspetto della gestione della memoria individuare un meccanismo in grado di mantenere sezioni di memoria libera di grande dimensioni, ovvero evitare di ottenere una memoria libera splittata in sezioni di piccole dimensioni non consecutive. Le strategie di allocazioni pi utilizzate sono: ! ! First Fit: viene associato il primo Hole sufcientemente grande per contenere il ! ! processo. ! ! Best Fit: viene associato il pi piccolo Hole sufcientemente grande pro ! ! contenere il processo. ! ! Worst Fit: viene associato il pi grande buco che possa contenere il processo.

! Mentre in questo graco possiamo vedere le tecniche di gestione della memoria pi ! utilizzate:

! Allocazione contigua: il programma viene allocato consecutivamente in memoria ! (bisogna scegliere porzioni di memoria sufcientemente ampie). ! Allocazione non contigua: il programma viene caricato in memoria spezzato in pi parti ! ( necessario il supporto dellHW). ! Il modello a partizioni sse/variabili venne adottato nei primi PC ed oggi utilizzato solo ! in sistemi dedicati. I meccanismi implementati dai moderni SO sono orientati verso la ! paginazione e la segmentazione.

135 di 142

! ! ! ! ! ! ! ! !

Nella struttura a partizioni multiple sse la memoria viene suddivisa in partizioni la cui dimensione stabilita dal sistemista. possibile disporre di code separate per ogni partizione, sulle quali si accumulano i processi. I processi vengono inseriti in una coda piuttosto che in unaltra a seconda della dimensione della partizione e della dimensione del processo. In base al criterio di accodamento dei processi pu capitare di avere alcuni processi bloccati in coda su una partizione di dimensione N mentre la coda di una partizione pi grande di N vuota. Una situazione del genere porta ad un sottoutilizzo del sistema. Il mid-term scheduler ha il compito di assegnare il processo alla coda adeguata.

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

Unaltra versione di questo meccanismo prevede una singola coda di processi ed un meccanismo di assegnamento di una partizione ad un processo sempre basato sulla dimensione che il processo deve occupare e la dimensione della partizione. Il problema dellimplementazione mediante partizioni sse la necessit di conoscere molto bene il sistema ed i processi che possono andare in esecuzione. Con lavvento dei sostenni rime sharing il numero degli utenti molto pi alto e variabile, inoltre non pi possibile determinare a priori quali programmi possono andare in esecuzione. Inoltre i programmi applicativi avevano, nel complesso, una dimensione maggiore di quella della memoria centrale e anche considerati singolarmente avevano dimensioni fortemente variabili. Nei primi sistemi time-sharing venne introdotta la nozione di partizioni variabili: la memoria viene inizialmente considerata come ununica partizione non allocata, nel momento in cui necessario assegnare una porzione di memoria per un processo, si crea una partizione della dimensione necessaria. La dimensione della partizione a volte maggiore di quella effettivamente richiesta dal processo in quanto si considera la possibilit che il processo possa crescere leggermente durante la sua esecuzione. Alla terminazione dellesecuzione di un processo, la memoria che gli era stata assegnata viene deallocata e la partizione viene distrutta.

136 di 142

! ! ! ! ! ! ! ! ! ! ! ! !

In questi sistemi viene introdotto per la prima volta il concetto di swapping, ovvero il procedimento mediante il quale un processo in esecuzione viene fermato e depositato su disco nella swap area. ES:! ! Un processo A nella partizione P si blocca in I/O: questo processo viene tolto dallo ! stato running e depositato su disco (swap out), mentre un secondo processo B ! pu andare in esecuzione. Quando il processo A viene ricaricato da disco si parla ! di swap in. Quindi la struttura interna della memoria muta notevolmente con il progredire dello stato del sistema: numero, dimensione e posizione delle partizioni variano in funzione del numero di processi in esecuzione. Inoltre se un processo lascia la memoria centrale, ad esempio a seguito di uno swap out, al suo rientro non deve necessariamente riprendere la posizione che aveva prima dello swap out.

! ! ! ! ! ! ! ! ! !

Tutta questa entropia genera unalta frammentazione della memoria: pu capitare di avere 100K di memoria liberi, splittati in porzioni da 2K che non sono sufcienti a contenere nessuno dei programmi in Ready. Questo aspetto prende il nome di fenomeno della frammentazione esterna. Per recuperare memoria necessario scaricare la memoria centrale, mediante uno swap out, e ricaricare il tutto cercando di compattare il pi possibile. Queste tecniche di compressione della memoria centrale risultano comunque poco efcienti. Inoltre va considerato anche il fatto che le operazioni su disco sono molto costose in termini di tempo.
137 di 142

! ! ! ! ! ! ! ! ! ! !

Con gli schemi a partizioni possibile caricare un processo in memoria solo quando disponibile una porzione di memoria contigua sufcientemente ampia da contenere lintero processo. Da questo segue che il procedimento di allocazione pu richiedere parecchio tempo. Unottima soluzione a questo problema consiste nella tecnica chiamata paginazione. La paginazione rompe il dogma dellallocazione di memoria contigua: il processo viene caricato in pezzi separati di memoria. Per fare questo necessario suddividere in blocchi (solitamente da 1024 byte) sia la memoria logica sia quella sica. I blocchi di memoria logica prendono il nome di pagine mentre i blocchi della memoria sica vengono chiamati frame. In questo schema possibile caricare un processo in memoria quando si ha a disposizione un numero di pagine sufciente a contenerlo.

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

Mediante la paginazione possibile sfruttare ogni singolo Kilobyte di memoria, anche se questo comporta un costo in termini di tempo. In un sistema paginato gli indirizzi logici di memoria sono espressi tramite una coppia di valori <p,d> alla quale va associato, in fase di esecuzione, un indirizzo sico lineare che esprime la locazione esatta del dato o istruzione in memoria centrale. La coppia <p,d> deve essere interpretata come il numero di pagine per lo spiazzamento allinterno della pagina: il valore p quindi il numero di pagina mentre d indica loffset tra linizio della pagina e lindirizzo logico identicato da <p,d>. A questo punto, se si conosce il frame associato alla pagina p possibile ricavare lindirizzo sico corrispondente allindirizzo logico <p,d>. Vediamo un esempio: ! Consideriamo un sistema paginato con pagine da 256 byte. ! Lindirizzo 26251 sar rappresentato dalla coppia: ! ! ! ! p = (26251 / 256) = 102 ! ! d = (26251 % 256) = 139 ! ! ! ! Per conoscere lindirizzo sico associato alla locazione logica <102,139> bisogna conoscere il frame nel quale stata caricata la pagina 102: ! Indirizzo sico = (Nframe * 256) + d

! Per implementare questo meccanismo per necessario mantenere da qualche parte ! le associazioni tra pagina e frame, oltre alla informazioni generali presenti anche nella ! tecnica del partizionamento (ad esempio le bitmap).
138 di 142

! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

Queste informazioni son contenute allinterno della tabella delle pagine. Quando si utilizzano pagine di dimensione 2k si pu sfruttare lindirizzo logico per calcolare quello sico: ! ! i k bit meno signicativi dellindirizzo logico individuano lo spiazzamento ! ! allinterno della pagina. ! ! i restanti bit pi signicativi individuano il numero della pagina. ! ! il numero di pagina si usa come indice nella tabella delle pagine. ! ! il valore trovato in corrispondenza dellindice il numero del frame associato ! ! alla pagina. ! ! se la pagina presente in memoria, il numero di page frame copiato nei bit ! ! alti del registro di output e concatenato ai k bit bassi dellindirizzo logico ! ! (lindirizzo sico composto dallo stesso offset di quello sico e dal numero di ! ! frame). Quindi la paginazione consente lo spezzettamento di un programma in pagine e la distribuzione di queste parti all'interno della memoria centrale nei corretti frame. Con questo metodo si sfrutta al massimo la memoria disponibile anche se esiste comunque un fenomeno di frammentazione, che prende il nome di frammentazione interna (ad esempio quando un programma ha una lunghezza di N pagine e mezzo). La tabella delle pagine viene compilata dal SO, il quale ha il compito di ricercare i frame liberi e mantenere aggiornata la tabella. L'HW invece deve essere in grado di rilocare a runtime i vari indirizzi delle pagine. Questa operazione svolta dallMMU: ! ! il processore legge gli indirizzi logici e li passa all MMU. ! ! l'MMU genera l'indirizzo sico sostituendo al numero di pagina logica il ! ! corrispondente numero del frame sico. ! ! l'offset che l'MMU riceve come indirizzo logico rimane uguale anche nella ! ! generazione dell'indirizzo sico.

139 di 142

Il supporto dellHW in questa fase fondamentale in quanto le operazioni di accesso alla memoria sono molto frequenti, quindi disporre di un dispositivo hardware veloce rende il sistema molto pi efciente. Il caricamento di un programma in memoria avviene secondo la seguente procedura: ! ! ! il long-term scheduler verica la disponibilit dei frame in memoria centrale. ! ! ! il processo viene caricato in memoria centrale, la sua tabella viene compilata ed ! ! ! associata al suo PCB. ! ! ! quando il processo va in esecuzione la sua tabella delle pagine viene caricata ! ! ! in quella del sistema. Quindi un sistema paginato deve disporre delle seguenti strutture dati: ! ! ! tabella dei frame, utilizzata dal SO per conoscere i frame liberi e quelli occupati. ! ! ! tabella delle pagine che denisce lassociazione Npagina logica <---> Nframe ! ! ! sico. Deve essere presente una tabella per ogni processo in esecuzione o in ! ! ! Ready.

La tabella delle pagine contiene anche ulteriori informazioni: livello di protezione della pagina (read only, writable, ...), se una pagina stata modicata dal momento del caricamento, se una pagina stata letta almeno una volta dal caricamento, Consideriamo un sistema ad indirizzamento di memoria a 32 bit: ! ! ! le pagine sono di 4 Kb (212 bit). ! ! ! la memoria al massimo ampia 232 bit. Posso disporre al massimo di 220 pagine (220 = 232 / 212), quindi ogni tabella deve avere 220 elementi. Ne consegue che ogni tabella avr una dimensione media di 4 Mb: se ho 100 processi in esecuzione sar costretto a riservare 400 Mb di memoria solo per contenere le tabelle della pagine. Inoltre siccome la tabella delle pagine risiede anchessa in memoria centrale, per recuperare lindirizzo logico necessario eseguire prima un accesso alla memoria, successivamente si calcola lindirizzo sico e si esegue un secondo accesso per reperire il dato o listruzione. Per risolvere questi problemi stato introdotto un sistema a due livelli: esistono due tabelle delle pagine, una di primo livello e laltra di secondo livello. Il concetto principale di questo metodo consiste nel paginare la tabella delle pagine: la tabella di primo livello contiene campi che puntano ad insiemi di pagine. Ad esempio il primo elemento della tabella di primo livello punta alle prime 1024 pagine, il secondo campo punta alle altr2 1024, e cos via. In memoria si tengono solo le parti della tabella delle pagine che sono necessarie e, siccome generalmente servono in memoria pochi blocchi della tabella, si risparmia molto spazio.

140 di 142

A seguito di questo l'indirizzamento della pagina logica diventa:


<10 bit per l'indicizzazione nella tabella 1> <10 bit per la tabella di secondo livello> <12 bit per l'offset>

A questo punto per per ogni accesso in memoria paghiamo due ulteriori accessi: il primo per accedere alla tabella di livello 1, il secondo per accedere alla seconda tabella e solo al terzo accesso si reperisce il dato di cui si aveva bisogno.

Unaltra soluzione consiste nellutilizzare una tabella delle pagine invertita, ovvero al posto di essere organizzata per numero i indirizzi logici, la tabella indicizzata dal numero di frame. Il problema qui che la CPU genera sempre indirizzi logici: prima si usava il numero di pagina come indice di accesso alla tabella e si determinava l'indirizzo sico, adesso l'indirizzo logico all'interno della tabella e l'indice di accesso il frame quindi per individuare l'indirizzo logico nella bisogna eseguire una ricerca. Questa ricerca pu essere sequenziale o pi efcientemente un hash: ovvero il PID e l'indirizzo logico vengono elaborati in modo da generare un hash che cosrrisponda all'indice della tabella. Si possono per vericare situazioni di collisione ovvero quando combinazioni diverse di PID e indirizzo logico generano lo stesso indice o hash. Questo problema pu essere risolto utilizzando sempre lhash come indice si accesso alla tabella, ma introducendo delle liste di record <PID,pagina logica> puntate dai compi allinterno della tabella dei frame. Il seguente schema chiarisce la struttura:

141 di 142

inoltre possibile dotare queste implementazioni di una cache delle pagine. Questa cache prende il nome di Traslation Lookaside Buffer e permette di mantenere in memoria i riferimenti alle tabelle pi utilizzate o utilizzate di recente. La TLB si basa sul principio di localit.

142 di 142

Anda mungkin juga menyukai