nucleo - Dipartimento di Ingegneria dell`Informazione
by user
Comments
Transcript
nucleo - Dipartimento di Ingegneria dell`Informazione
NUCLEO G. Frosini Slide 1 NUCLEO MLTIPROGRAMMATO (1) • Multiprogrammazione col processore PC: – un processore reale, più processori virtuali; – memoria virtuale paginata; – pagine con livello di privilegio (o stato) utente e sistema. • Processo: – livello di privilegio proprio (o stato) utente o sistema; – spazio di indirizzamento: • comune a tutti i processi e privato per ciascun processo. – processo utente: • spazio comune e spazio privato parte a livello di privilegio sistema e parte a livello di privilegio utente; – processo sistema: • spazio comune e spazio privato tutti a livello di privilegio sistema; – spazio comune a livello di privilegio sistema: • routine e strutture dati del nucleo del sistema operativo. G. Frosini Slide 2 NUCLEO MLTIPROGRAMMATO (2) • Meccanismo di interruzione: – un processo utente può portarsi in stato sistema, per poi tornare nello stato iniziale eseguendo una IRET. • Processo caratterizzato da: – descrittore; – corpo (programma che ne definisce le elaborazioni), costituito da codice e area dati privata; – area dati comune. • Descrittore di processo: – si trova nello spazio comune a livello di privilegio sistema; – contiene, fra l’altro: • se trattasi di un processo utente, il valore iniziale del puntatore della pila sistema; • il contenuto del registro CR3; • il contenuto dei registri generali del processore (fra cui il puntatore di pila). G. Frosini Slide 3 NUCLEO MLTIPROGRAMMATO (3) • Codice di un processo: – può essere condiviso da più processi con lo stesso livello di privilegio; – deve trovarsi nello spazio comune (faremo sempre questa ipotesi). • Area dati privata: – contiene le pile per i due livelli di privilegio se trattasi di un processo utente, o la pila se trattasi di un processo sistema. – ipotesi: area dati privata costituita interamente dalle pile. • Area dati comune: – informazioni comuni; – scambio di informazioni. G. Frosini Slide 4 PROPRIETA’ DEL C++ • Linguaggio C++: – una funzione, anche con parametri formali, può costituire il modello le cui istanze specificano le azioni compiute dai vari processi; – brevemente: • il codice di una funzione può essere condiviso tra più processi; • ciascun processo prevede un’istanza della funzione, con i propri parametri attuali e la propria copia delle variabili locali; – il codice è memorizzato nella zona testo (.text) comune a tutti i processi; – i parametri attuali e le variabili locali (variabili automatiche) sono contenuti nella pila del livello di privilegio proprio di ciascun processo; – la zona dati (.data) comune a tutti i processi, contiene le variabili comuni (variabili statiche), definite al di fuori delle funzioni. • Zona testo e zona dati: – comuni a tutti i processi, e differenti per ogni livello di privilegio. • Pile: – differenti da processo a processo; • un processo ne ha due (se processo utente) o una (se processo sistema). • Corpo di un processo: – istanza della funzione modello; – per semplicità, funzione void con un argomento di tipo intero . G. Frosini Slide 5 OPERAZIONI SUI PROCESSI • Processi: – iniziano e terminano. • Processo che inizia: – vengono creati il suo descrittore e le sue pile (la sua pila se il processo ha livello di privilegio proprio sistema); – il codice può venir creato, oppure, se condiviso, può già esistere. • Processo che termina: – le prime due entità vengono distrutte; – il codice viene distrutto solo se non condiviso. • Altre operazioni su un processo: – coinvolgono il suo descrittore; – per questo motivo il descrittore assume un ruolo fondamentale, e spesso viene a confondersi con il processo che rappresenta. G. Frosini Slide 6 COMMUTAZIONE DI CONTESTO (1) • Evento che produce cambiamento del processo in esecuzione (commutazione di contesto): – interruzione (hardware o software). • Ipotesi: – la commutazione avviene attraverso routine con livello di privilegio sistema (via software); – il gate coinvolto da queste routine è di tipo interrupt: vengono memorizzati in pila sistema i contenuti di EFLAG, CPL ed EIP, che non prevedono spazio nel descrittore di processo. • Commutazione di contesto (che avviene a livello di privilegio sistema) comporta: – memorizzazione dello stato attuale relativo al processo in esecuzione o processo uscente; – la scelta del nuovo processo da mandare in esecuzione (o processo entrante): questo compito viene svolto da una specifica routine detta schedulatore; – il caricamento dello stato relativo al processo entrante. G. Frosini Slide 7 COMMUTAZIONE DI CONTESTO (2) • Commutazione di contesto: – produce il caricamento di un nuovo valore in CR3; • cambiamento dello spazio di indirizzamento privato. • Nuovo valore caricato nel puntatore di pila: – si riferisce alla pila sistema realizzata nello spazio privato del processo entrante. • Nuovo valore caricato nel contatore di programma: – si riferisce al punto di esecuzione del codice del processo entrante, nello spazio comune (il codice può essere anche lo stesso, nel caso in cui il vecchio e il nuovo processo abbiano lo abbiano in comune, ma l’indirizzo di partenza è in genere diverso). G. Frosini Slide 8 COMMUTAZIONE DI CONTESTO (3) • La memorizzazione dello stato attuale consiste comunemente in due azioni: – salvataggio nel descrittore del processo delle informazioni di stato, fra cui il contenuto dei registri del processore; – inserimento dell’identificatore del processo in una determinata lista (qualche volta in nessuna lista). • Liste tipiche: – quella dei processi pronti per l’esecuzione; – quelle dei processi bloccati in attesa che si verifichi un certo evento; – quella relativa al processo in esecuzione (particolare lista, costituita da un solo elemento). • Effettuazione della scelta: – lo schedulatore opera sulla lista dei processi pronti avvalendosi anche di un’informazione di precedenza. G. Frosini Slide 9 COMMUTAZIONE DI CONTESTO (4) • Il caricamento del nuovo stato consiste comunemente in due azioni: – caricamento nei registri del processore delle informazioni contenute nel descrittore del processo entrante; – caricamento nel registro TR dell’identificatore del processo entrante. • Processo: – può trovarsi in ogni istante in uno di questi tre stati: • in esecuzione, quando ha il controllo del processore; • pronto, quando è in attesa di essere mandato in esecuzione (tipicamente dallo schedulatore); • bloccato, quando è in attesa del verificarsi di una certa condizione di attivazione. • Processo che da bloccato diviene pronto: – il suo identificatore viene rimosso dalla lista in cui si trova e inserito nella lista dei processi pronti. G. Frosini Slide 10 SISTEMA OPERATIVO PERSONALE • Nucleo di un sistema operativo multiprogrammato: – costituito da un insieme di routine e di strutture dati in grado di gestire la multiprogrammazione; – queste si trovano nello spazio comune a tutti i processi, e hanno livello di privilegio sistema. • Sistema personale (monoutente ma multitasking): – corpi dei processi costituiti dai vari programmi che l’utente attiva; – quello in esecuzione ha il controllo della tastiera e del video; – lo schedulatore, quando invocato esplicitamente dall’utente (tipicamente attraverso il mouse), seleziona il nuovo processo da mandare in esecuzione in base alle indicazioni fornite dall’utente stesso. G. Frosini Slide 11 SISTEMA OPERATIVO TIME-SHARING • Sistema time-sharing: – molti utenti condividono lo stesso elaboratore in maniera interattiva, tipicamente mediante terminali. • Corpi dei processi: – costituiti dai programmi dei vari utenti. • Processore: – esegue il processo relativo a un certo utente, mentre gli altri utenti stanno (per esempio) battendo i loro comandi, e i processi corrispondenti non sono pertanto pronti. • Schedulatore: – per mezzo di un contatore di tempo, va in esecuzione a intervalli regolari (sfruttando il meccanismo di interruzione); – se più processi sono pronti per l’esecuzione, assegna il processore a un utente alla volta in modo ciclico (la durata di ogni intervallo di tempo (tick) è tipicamente di 50 ms). G. Frosini Slide 12 SISTEMA OPERATIVO IN TEMPO REALE • Sistema in tempo reale: – processi mandati in esecuzione al verificarsi di eventi esterni, con i requisiti temporali imposti dall’applicazione. • Alcuni processi sono più importanti di altri: – ad ogni commutazione di contesto lo schedulatore sceglie il processo a precedenza più alta. • Per il verificarsi di un certo evento, un processo da bloccato diviene pronto: – occorre che anche il processo in esecuzione venga inserito, o sia già inserito, fra i processi pronti; – deve essere invocato lo schedulatore; • può accadere che la precedenza del processo sbloccato sia maggiore di quello in esecuzione, ossia che si abbia preemption (prelazione). • Nel seguito, faremo esplicito riferimento ai sistemi in tempo reale. G. Frosini Slide 13 PRIMITIVE E DRIVER • Routine del nucleo: – primitive, invocate da un processo utente con interruzioni software, che fanno passare il processo stesso da stato utente a stato sistema; – driver, mandati in esecuzione da un’interruzione esterna, che operano in stato sistema. • • Alcune routine possono determinare una commutazione di contesto. Strutture dati più rilevanti: – liste dei processi; – costituite da elementi che contengono identificatori di processo; – ogni lista è individuata da un apposito puntatore (contenuto in una variabile di nucleo). G. Frosini Slide 14 ROUTINE DI NUCLEO E INTERRUZIONI • Routine di nucleo: – molte lavorano sulle liste dei processi. • Liste dei processi: – risorse che si trovano in uno stato consistente solo all’inizio e alla fine di ogni operazione che le manipola. • Interruzione esterna (hardware): – esecuzione di una routine che può lavorare sulle liste dei processi; – tutte le routine che lavorano sulle liste dei processi devono essere eseguite in maniera indivisibile, con le interruzioni (mascherabili) disabilitate; – vedremo che anche le routine che non lavorano sulle liste dei processi devono essere eseguite in maniera indivisibile. • Meccanismo di interruzione (hardware o software) utilizzato per eseguire routine di nucleo: – fa uso di gate di tipo interrupt, che disabilitano automaticamente le interruzioni mascherabili. G. Frosini Slide 15 PROCESSI E INTERRUZIONI • Processo in esecuzione può produrre un cambiamento di contesto: – volontariamente (mediante la chiamata di una primitiva): • effettua delle richieste che non possono essere immediatamente soddisfatte; • il processo diviene bloccato e viene richiamato lo schedulatore; – involontariamente, perché si verifica un’interruzione esterna: • va in esecuzione un driver: – può effettuare le sue elaborazioni e poi terminare (il processo interrotto ritorna in esecuzione alla fine del driver stesso); – può forzare l’esecuzione di un altro processo; – può far divenire pronto un processo bloccato; – può inserire nella lista dei processi pronti il processo in esecuzione e richiamare lo schedulatore. • Cambiamento di contesto involontario: – perché sia possibile, i processi, quando non eseguono routine di nucleo, devono avere le interruzioni (mascherabili) abilitate. G. Frosini Slide 16 TIPI USATI NELLE REALIZZAZIONI • Si utilizzano le seguenti dichiarazioni typedef (le prime 5 sono già state introdotte in precedenza): typedef void* addr; // indirizzo virtuali e indirizzo fisico nello spazio di memoria typedef unsigned short ioaddr; // indirizzo (fisico) nello spazio di I/O typedef unsigned char natb; typedef unsigned short natw; typedef unsigned long natl; typedef void* str; typedef const void* cstr; • In C++: – un indirizzo di memoria (di tipo void*) può essere dereferenziato, previa una conversione di tipo; – un indirizzo di I/O non può essere dereferenziato (lo può essere solo in Assembler). G. Frosini Slide 17 REALIZZAZIONE DELLE PRIMITIVE (1) • Descrizione delle primitive di nucleo: – file scritti in Assembler e file scritti in C++; – due file utente.s e utente.cpp aventi livello di privilegio utente (modulo utente) con invocazione delle primitive; – due file sistema.s e sistema.cpp aventi livello di privilegio sistema (modulo sistema) con il codice delle primitive. • Processo: – può utilizzare le primitive di nucleo, che si trovano a livello sistema. – in C++ non sono possibili chiamate dirette di primitive, poiché non sono previste istruzioni che provocano interruzioni software (INT $tipo); – la primitiva è in realtà un sottoprogramma di interfaccia (scritto in Assembler), che esegue un’interruzione software con esecuzione della primitiva vera e propria (a_primitiva); – questa deve accedere al record di attivazione per il prelievo dei parametri. G. Frosini Slide 18 REALIZZAZIONE DELLE PRIMITIVE (2) • Processo utente: – il record di attivazione della routine di interfaccia primitiva si trova nella pila utente, dalla quale devono essere prelevati dalla a_primitiva, la cui pila è a livello sistema. • a_primitiva: – termina con un’istruzione di ritorno da interruzione; – se non si sono avute commutazioni di contesto, con l’istruzione IRET restituisce il controllo al sottoprogramma di interfaccia; – questo a sua volta termina con l’istruzione RET, con il ritorno al programma che ha chiamato la primitiva. G. Frosini Slide 19 REALIZZAZIONE DELLE PRIMITIVE (3) // utente.cpp extern "C" void primitiva_i (/* parametri formali */); void p (int h) { // … primitiva_i (/* parametri attuali */); // ritp_i … } # utente.s .TEXT .GLOBAL primitiva_i: INT rit_i: # sistema.s .TEXT a_primitiva_i: primitiva_i $tipo_i RET // per semplicità, di tipo void // codice di un processo # sottoprogramma di interfaccia # routine INT $tipo_i # ... IRET G. Frosini Slide 20 REALIZZAZIONE DELLE PRIMITIVE (4) • Processo utente che chiama una routine di interfaccia (in C++), sia primitiva_i: – memorizza in pila utente i parametri attuali e, con l’istruzione di chiamata, il valore di EIP relativo al punto di ritorno ritp_i. • Routine di interfaccia (in assembler) primitiva_i: – invoca la primitiva effettiva (a_primitiva_i) con l’istruzione INT $tipo_i; • Interruzione: – variazione di livello di privilegio, e quindi cambiamento di pila; – prelievo del puntatore della pila sistema dal descrittore del processo in esecuzione (individuato dal contenuto del registro TR); – memorizzazione nella nuova pila del valore del puntatore di pila utente e dei valori dei registri EFLAG, CPL ed EIP (quest’ultimo contiene il valore rit_i). • Moduli di livello utente e Moduli di livello sistema: – non hanno nomi comuni e non richiedono nessuna azione di collegamento. G. Frosini Slide 21 REALIZZAZIONE DELLE PRIMITIVE (5) EIP (ritp_i) parametri attuali ESP ESP EIPu (rit_i) CPLu EFLAGu ESPu spazio riservato ESPs Pila utente Pila sistema G. Frosini Slide 22 REALIZZAZIONE DELLE PRIMITIVE (6) • Invocazione di una primitiva da parte di un processo sistema: – non produce nessun cambiamento di livello di privilegio. ESP EIP (rit_i) CPL EFLAG EIP (rit_pi) Parametri attuali Pila sistema G. Frosini Slide 23 STRUTTURA DI UNA A-PRIMITIVA (1) • a_primitiva: – può provocare o meno una commutazione di contesto. • Se la commutazione di contesto è possibile: – all’inizio, la a_primitiva salva lo stato del processo al momento della esecuzione dell’istruzione INT (il valore dei registri EFLAG, CPL ed EIP (quest’ultimo relativo all’indirizzo rit_i) non fanno parte dello stato che viene salvato, essendo già stati memorizzati nella pila sistema dall’istruzione INT stessa); – alla fine, la a_primitiva carica lo stato del nuovo processo (il nuovo stato può anche coincidere con quello salvato); – dopo il caricamento del nuovo stato, l’istruzione IRET agisce sulla pila sistema del nuovo processo. G. Frosini Slide 24 STRUTTURA DI UNA A-PRIMITIVA (2) • Processi: – hanno la sommità della pila sistema (tre o cinque parole lunghe) nella stessa situazione; – hanno abbandonato l’esecuzione o per effetto di un’interruzione software (invocazione di una a_primitiva), o per effetto di un’interruzione hardware. • Creazione di un processo: – occorre predisporre una situazione iniziale appropriata per la pila sistema. • Istruzione IRET della a_primitiva: – produce il ritorno a un nuovo processo (tipicamente a una nuova routine di interfaccia). • Nota: – quando un processo che invoca una a_primitiva torna di nuovo in esecuzione, riparte dal punto successivo rispetto all’invocazione della a_primitiva stessa (punto successivo rispetto all’istruzione INT $tipo). G. Frosini Slide 25 TIPO DESCRITTORE DI PROCESSO • Tipo descrittore di processo (struttura des_proc): • campo id contenente un identificatore di processo ID (tipicamente del processo che lo ha creato o di quello che lo ha mandato in esecuzione); • campo punt_nucleo contenente il puntatore (valore iniziale) alla pila di nucleo; • campo per il registro CR3; • campo contesto destinato a contenere la copia del contenuto dei registri generali del processore, supponiamo siano N_REG. // sistema.cpp struct des_proc { natl id; addr punt_nucleo; addr riservato; // quattro parole lunghe a disposizione addr cr3; natl contesto[N_REG]; }; G. Frosini Slide 26 DESCRITTORI E PROCESSI • Descrittori dei processi: – variabili definite nel nucleo; – un descrittore è individuato da un identificatore che, tramite la tabella GDT, determina la base e il limite del descrittore stesso. • Processo: – costituito da un identificatore, dal corrispondente descrittore, da un corpo (istanza di una funzione, con pile di dimensioni standard); – nella pila relativa al livello di privilegio proprio si trovano i parametri attuali e le variabili locali di una istanza di funzione; – supponiamo (per semplicità) che il codice di un processo sia una istanza di una funzione C++ con un solo parametro di tipo intero. G. Frosini Slide 27 LISTE DI PROCESSI • Processi; – gestiti attraverso liste, individuate da un puntatore, e costituite da elementi di tipo proc_elem (informazione: identificatore e precedenza). • Puntatori definiti nel nucleo: – esecuzione: puntatore a un elemento (specifica il processo in esecuzione); – pronti: puntatore alla lista dei processi pronti. // sistema.cpp struct proc_elem { natl id; natl precedenza; proc_elem* puntatore; }; extern proc_elem* esecuzione; extern proc_elem* pronti; # sistema.s .DATA .GLOBAL esecuzione: .GLOBAL pronti: esecuzione .LONG 0 pronti .LONG 0 G. Frosini Slide 28 ATTIVAZIONE DI UN PROCESSO UTENTE (1) • Attivazione (creazione) di un processo utente: – nel caso più semplice avviene in fase di inizializzazione (in molti sistemi è consentito che un processo padre crei processi figli); • • • • • selezione di un identificatore; inizializzazione del corrispondente descrittore e delle corrispondenti pile; allocazione in memoria dinamica di un nuovo elemento di tipo proc_elem; inserimento dell’elemento nella lista dei processi pronti. Primitiva di nucleo activate_p(): natl activate_p(void f(int), int a, natl prio, natl liv); – parametri: intestazione di funzione, parametro attuale della funzione, precedenza e livello proprio; – restituisce l’identificatore del processo ovvero 0xFFFFFFFF se l’attivazione non è andata a buon fine. • Attivazione: – richiamo della primitiva activate_p() con parametri attuali p, A, PRIO, LIV; G. Frosini Slide 29 ATTIVAZIONE DI UN PROCESSO UTENTE (2) • 1) Inizializzazione della pila sistema del processo (cinque parole lunghe): – dall’alto in basso: • • • • • • indirizzo della prima istruzione (puntatore a funzione p); livello di privilegio proprio (LIV); costante che rappresenta il contenuto del registro EFLAG; puntatore alla cima della pila utente; parola lunga riservata. 2) Inizializzazione della pila utente (due parole lunghe): – dall’alto in basso: • parola lunga di valore 0; • parametro attuale della funzione p() (A). • 3) Inizializzazione del descrittore del processo: – valore del puntatore della pila sistema relativo a pila vuota e valore del puntatore della pila sistema con le cinque parole lunghe: • inseriti nei campi ESPSv ed ESPv del descrittore stesso. • 4) Allocazione in memoria dinamica di un elemento di tipo proc_elem; • memorizzazione in esso dell’identificatore e della priorità (PRIO); • 5) Inserimento nella lista dei processi pronti. G. Frosini Slide 30 ATTIVAZIONE DI UN PROCESSO UTENTE (3) ESPv EIPu (puntatore p) CPLu (parametro LIV) EFLAGu ESPu Parametro di p() (A) Parola lunga riservata ESPSv Pila utente Pila sistema G. Frosini Slide 31 ATTIVAZIONE DI UN PROCESSO UTENTE (4) • Processo da mandare in esecuzione: – scelta del processo da parte dello schedulatore: – caricamento del suo stato; – esecuzione dell’istruzione IRET (che rappresenta una specie di istruzione di chiamata). • Il processo inizia eseguendo il prologo della funzione puntata da p: – salvataggio di EBP, ricopiamento di ESP in EBP, decremento di ESP per tener conto di variabili locali; – elaborazioni effettuate dalla funzione puntata da p: • il parametro attuale corrisponde (correttamente) all’indirizzo EBP+8. G. Frosini Slide 32 TERMINAZIONE DEI PROCESSI UTENTE • Terminazione di un processo utente: – avviene per mezzo della primitiva terminate_p(), che non ha parametri; – essa richiama semplicemente lo schedulatore. • Chiamata della primitiva terminate_p(): – ultima istruzione di ogni processo. • Fase di inizializzazione: – creazione di un processo start_utente, che richiama la funzione main(); • Mediante tale funzione, il processo start_utente: – attiva tutti i processi utente; – … – termina (come tutti i processi) eseguendo la primitiva terminate_p(). G. Frosini Slide 33 FILE UTENTE.CPP // utente.cpp extern "C" natl activate_p(void f(int), int a, natl prio, natl liv); extern "C" void terminate_p(); natl processo_i; // contiene l’identificatore di un processo void p(int h) // codice di un processo { ... terminate_p(); } int main() { … processo_i = activate_p(p, A, PRIO, LIV); ... terminate_p(); } G. Frosini Slide 34 FILE UTENTE.S e SISTEMA.S # utente.s ... .TEXT .GLOBAL activate_p: .GLOBAL terminate_p: # sistema.s .TEXT ... a_activate_p: a_terminate_p: activate_p INT $tipo_a RET terminate_p INT $tipo_t RET # routine INT $tipo_a # ... IRET # routine INT $tipo_t # ... IRET G. Frosini Slide 35 AREA DATI CONDIVISA • Identificatori dei processi: – variabili condivise (non private dei singoli processi); – un processo può utilizzare primitive che agiscono esplicitamente su un altro processo; – esempio: • un processo può provocare la terminazione forzata di un altro processo. G. Frosini Slide 36 PROCESSO DUMMY (1) • Lista dei processi pronti: – – – – – • processo dummy; precedenza inferiore a quella di tutti gli altri processi utente; va in esecuzione quando tutti i processi utente sono bloccati; ha livello di privilegio sistema (non può essere modificato da un utente); come tutti i processi, gira con le interruzioni abilitate. Processi utente tutti bloccati: – situazione modificabile solo da un’interruzione esterna. • Routine relativa a una tale interruzione esterna: – può far passare un processo da bloccato a pronto; • in questo caso deve anche richiamare lo schedulatore e caricare lo stato del nuovo processo. • Processo dummy: – utilizza la variabile processi, inizializzata a 0, che viene incrementata dalla primitiva activate_p() e decrementata dalla primitiva terminate_p(). G. Frosini Slide 37 PROCESSO DUMMY (2) • Processo dummy: – esamina il valore della variabile processi, e se vale 1 (solo il processo dymmy non è terminato) termina con una primitiva (end_program()) che effettua le operazioni di chiusura. // sistema.cpp extern natl processi; extern "C" void end_program(); void dd(int i) { while(processi !=1) ; end_program(); } • // variabile definita in sistema.s Ciclo: – non viene effettuato se la variabile processi vale 1; – può essere interrotto da un’interruzione esterna, che fa divenire pronto un processo bloccato e richiama lo schedulatore; • il processo sboccato, quando esegue la primitiva terminate_p(), produce la modifica del contenuto della variabile processi. G. Frosini Slide 38 STRUTTURA DELLE PRIMITIVE • Primitiva: – produce l’esecuzione di una a_primitiva, che può provocare o meno una commutazione di contesto; – nel primo caso, all’inizio deve essere effettuato il salvataggio dello stato attuale, e alla fine il caricamento del nuovo stato; – queste due azioni sono espresse utilizzando il linguaggio Assembler, in quanto coinvolgono i registri del processore; – le elaborazioni compiute da una a_primitiva possono essere più comodamente espresse utilizzando il linguaggio C++ (c_primitiva): • vanno rispettati gli standard di aggancio dei sottoprogrammi ad alto livello; • in particolare vanno ricopiati in testa alla pila sistema i parametri che, per i processi utente, si trovano nella pila utente. G. Frosini Slide 39 STRUTTURA DI UNA A_PRIMITIVA # sistema.s .TEXT salva_stato: carica_stato: .EXTERN a_primitiva_i: # ... RET # ... RET c_primitiva_i CALL salva_stato # eventuale verifica e ricopiamento dei parametri CALL c_primitiva_i # ripulitura della pila CALL carica_stato IRET // sistema.cpp extern "C" void c_primitiva_i(/* parametri formali */) { /* ... */ } G. Frosini Slide 40 PARAMETRI E PILA • Realizzazione di una a_primitiva: – il ricopiamento dei parametri avviene dalla pila utente o dalla pila sistema in base al valore del vecchio CPL (memorizzato in pila sistema); – l’eventuale accesso alla pila utente da livello sistema non deve comportare alcuna violazione delle regole di privilegio (livello di privilegio delle pagine contenenti la pila uguale al livello di privilegio del processore): • da livello sistema si accede alla pila utente come insieme di pagine dati, senza utilizzare come registri contenenti indirizzi né ESP né EBP. • Ripulitura della pila: – non è indispensabile; – subito dopo si ha il caricamento di un nuovo stato (precedentemente salvato), e quindi il trasferimento in ESP di un nuovo valore. G. Frosini Slide 41 SOTTOPROGRAMMA SALVA_STATO • Sottoprogramma salva_stato: – memorizza il contenuto dei registri del processore nel descrittore di processo il cui identificatore è contenuto nell’elemento puntato da esecuzione; – i valori dei registri EFLAG, CPL ed EIP si trovano nella pila sistema del processo che ha invocato la a_primitiva e non prevedono spazio nel descrittore di processo; – il salvataggio dei valori di CR3 e di ESP produce il salvataggio dell’intera pila; – il valore di ESP che deve essere salvato non corrisponde al valore attuale (ma questo va opportunamente incrementato), poiché in testa alla pila sistema è memorizzato l’indirizzo di ritorno del sottoprogramma salva_stato. G. Frosini Slide 42 SOTTOPROGRAMMA CARICA_STATO • c_primitiva: – deve lasciare in esecuzione l’indirizzo dell’elemento contenente l’identificatore ID del processo da mandare in esecuzione. • Sottoprogramma carica_stato: – tramite il puntatore esecuzione copia in TR il valore di ID; – tramite la GDT, carica nei registri del processore i contenuti dei corrispondenti registri virtuali prelevati dal descrittore del processo identificato da ID (sia nel caso che vi sia stata una commutazione di contesto come nel caso che non vi sia stata). – poiché può esservi stata una commutazione di contesto, il sottoprogramma carica_stato deve provvedere a trasferire dalla vecchia pila sistema nella nuova pila sistema il suo indirizzo di ritorno, per poter terminare correttamente. G. Frosini Slide 43 PRIMITIVA CHE RESTITUISCE UN VALORE (1) • Supponiamo che la primitiva restituisca un intero: // utente.cpp ... extern "C" int primitiva_i(/* parametri formali */); ... void p/(int h) { ... int risu = primitiva_i(/* parametri attuali */); // ritp_i ... } // codice di un processo – il risultato viene prodotto dalla c_primitiva e lasciato in EAX; – nel caso in cui sia possibile una commutazione di contesto (la c_primitiva può invocare lo schedulatore, che cambia il valore del puntatore esecuzione) olccorre: • preliminarmente salvare l’identificatore del processo che ha invocato la primitiva; • alla fine della c_primitiva, salvare EAX nel descrittore di tale processo. ... G. Frosini Slide 44 PRIMITIVA CHE RESTITUISCE UN VALORE (2) # sistema.s ... .TEXT … .EXTERN a_primitiva_i: c_primitiva_i CALL salva_stato # salvataggio in pila di ID (identificatore del processo in esecuzione) # eventuale verifica e ricopiamento dei parametri CALL c_primitiva_i # ripulitura della pila # prelievo dalla pila di ID e salvataggio di EAX nel suo descrittore CALL carica_stato IRET ... // sistema.cpp .. extern "C" int c_primitiva_i(/* parametri formali */) { /* ... */ } ... G. Frosini Slide 45 PRIMITIVA SENZA COMMUTAZIONE • Primitiva che non effettua mai commutazione di contesto: – salvataggio e ripristino dello stato del processo chiamante viene sostituito con salvataggio e ripristino dei registri utilizzati; • se produce un risultato, non si deve salvare e ripristinare EAX. # sistema.s .TEXT .EXTERN c_primitiva_j a_primitiva_j: # salvataggio dei registri # eventuale verifica e ricopiamento dei parametri CALL c_primitiva_j # ripulitura della pila # ripristino dei registri IRET // sistema.cpp extern "C" void c_primitiva_j(/* parametri formali */) { /* ... */ } G. Frosini Slide 46 FUNZIONI DI UTILITA’ • Descrizione di una c_primitiva: – nel file sistema.cpp vengono utilizzate funzioni di utilità: – void inserimento_lista(proc_elem*& p_lista, proc_elem* p_elem) { /* ... */ } inserisce nella lista prioritaria (precedenza decrescente) puntata da p_lista l’ elemento (contenente identificatore di processo e precedenza) puntato da p_elem ponendolo come ultimo tra quelli aventi uguale precedenza; – void rimozione_lista(proc_elem*& p_lista, proc_elem*& p_elem) { /* ... */ } rimuove dalla lista il primo elemento; – extern "C" void inspronti() { /* ... */ } // utilizzato anche dal file sistema.s inserisce nella lista dei processi puntata da pronti l’elemento puntato da esecuzione. – extern "C" void schedulatore() { /* ... */ } // utilizzato anche dal file sistema.s estrae dalla lista dei processi puntata da pronti il primo elemento (identifica il processo a precedenza più alta) e lo fa puntare da esecuzione. G. Frosini Slide 47 ATOMICITA` • Sistema multiprogrammato: – alcune operazioni, compiute da un processo su una risorsa, debbono avvenire in modo atomico (non divisibile): • azione effettuata completamente, dall’inizio alla fine, senza che un altro processo possa inserirsi e provocare alterazioni dell’azione stessa. • Sezione critica: – operazione (insieme di istruzioni) su una risorsa condivisa che deve essere eseguita da un processo in maniera atomica. • Concetto di atomicità: – non coincide con quello di non interrompibilità: • azione non interrompibile è certamente atomica; • un’azione atomica può anche essere interrompibile: l’interruzione può mandare in esecuzione un nuovo processo, purché questo non compia elaborazioni che arrechino danno all’azione iniziata (ma non terminata) dal processo interrotto. G. Frosini Slide 48 MUTUA ESCLUSIONE • Concetto di atomicità analogo a quello di mutua esclusione; – su una risorsa condivisa possono venir effettuate differenti operazioni da differenti processi; – ognuna di queste, una volta iniziata, deve essere completata prima che ne venga iniziata un’altra. • Sistema monoprogrammato: – un’operazione su una risorsa viene sempre effettuata in modo atomico; – una risorsa è sempre in uno stato consistente. • Sistema multiprogrammato: – un processo può essere interrotto mentre sta compiendo un’operazione su una risorsa, e il nuovo processo può accedere alla stessa risorsa e trovarla in uno stato inconsistente; – le operazioni su una risorsa condivisa possono avvenire in modo non atomico. G. Frosini Slide 49 MUTUA ESCLUSIONE E INTERRUZIONI • Caso semplice: – risorsa protetta agendo direttamente sulle interruzioni (mascherabili): disabilitazione_interruzioni sezione_critica riabilitazione_interruzioni • In una sezione critica: – ipotesi: non c’è sospensione volontaria del processo in esecuzione; – la soluzione precedente assicura l’indivisibilità della sezione critica. • Agendo direttamente sulle interruzioni: – tutte le sezioni critiche (relative a tutte le risorse) divengono mutuamente esclusive. G. Frosini Slide 50 SEZIONI CRITICHE LUNGHE • Agire direttamente sul meccanismo di interruzione: – tecnica utilizzata per sezioni critiche corte, per le quali non è ragionevole ricorrere a soluzioni più sofisticate, e quindi più complesse. • Routine di nucleo: – vengono eseguite tutte con le interruzioni disabilitate; • quelle che lavorano sulle liste dei processi devono comunque avere le interruzioni disabilitate; • quelle che non lavorano sulle liste dei processi rientrano nella categoria delle sezioni critiche corte. • Processo utente: – una sezione critica può essere lunga; – non è ragionevole mantenere a lungo le interruzioni disabilitate. – le sezioni critiche devono essere mutuamente esclusive solo a gruppi, in relazione a una certa risorsa; – tecniche più flessibili, che consentano di risolvere il problema della mutua esclusione senza agire sul meccanismo di interruzione. G. Frosini Slide 51 SEMAFORI • Mutua esclusione: – può essere gestita mediante un semaforo e due primitive di nucleo sem_wait() e di sem_signal(). • Semaforo di mutua esclusione: – protegge una risorsa; – possiede un contatore inizializzato a 1. • Processo che vuole utilizzare una risorsa: – intende compiervi un’operazione; – se trova che la risorsa è occupata perché un altro processo vi sta compiendo un’altra operazione, si blocca volontariamente dando luogo a una commutazione di contesto; – quando la risorsa sarà libera, il processo bloccato diventerà di nuovo pronto per l’esecuzione. G. Frosini Slide 52 PRIMITIVE SEMAFORICHE (1) • Primitiva sem_wait() (parametro: indice del semaforo su cui opera): – – viene decrementato il contatore del semaforo e ne viene poi effettuato il test; se il nuovo valore è minore di 0, il processo che la ha invocata viene bloccato sulla coda del semaforo, viene richiamato lo schedulatore e mandato in esecuzione un nuovo processo. salva_stato contatore = contatore-1 SI contatore < 0 inserimento_lista_semaforo schedulatore NO carica_stato IRET G. Frosini Slide 53 PRIMITIVE SEMAFORICHE (2) • Primitiva sem_signal() (parametro: indice del semaforo su cui opera): – – viene incrementato il contatore del semaforo e ne viene poi effettuato il test; se il nuovo valore è minore o uguale a 0 (vi sono processi bloccati sul semaforo), quello a precedenza più alta viene tolto dalla coda del semaforo e inserito nella coda dei processi pronti, il processo in esecuzione viene inserito nella lista dei processi pronti, viene richiamato lo schedulatore e mandato in esecuzione il nuovo processo salva_stato • Nota: contatore = contatore+1 se il nuovo valore del contatore è minore o uguale a 0, il suo valore assoluto, aumentato di 1, rappresenta il numero dei processi bloccati sul semaforo. NO SI contatore <= 0 rimozione_lista_semaforo inserimento_lista_pronti processi rimosso e processo in esecuzione schedulatore carica_stato G. Frosini Slide 54 PREEMPTION • Si ha preemption (prelazione): – un processo da bloccato diviene pronto e viene messo nella lista dei processi pronti; – il processo in esecuzione viene messo nella lista dei processi pronti; – viene richiamato lo schedulatore. • Il processo sboccato, se ha precedenza maggiore di quello in esecuzione, va in esecuzione al posto di quello attualmente in esecuzione. G. Frosini Slide 55 STATI DI UN PROCESSO • Transizioni di stato di un processo: – processo in esecuzione: può bloccarsi perché esegue una sem_wait(); – processo bloccato: può divenire pronto per effetto di una sem_signal() eseguita da un altro processo; – transizione tra pronto ed esecuzione (e viceversa): può avvenire o quando viene invocato (direttamente o indirettamente) lo schedulatore o per effetto di un’interruzione esterna. processo in esecuzione sem_wait (effettuata dallo stesso processo) terminazione revoca processore assegnazione processore processo pronto attivazione processo bloccato sem_signal (effettuata da un altro processo G. Frosini Slide 56 REALIZZAZIONE DEI SEMAFORI (1) • Tipo descrittore di semaforo (des_sem): – struttura con un campo counter per la gestione del semaforo mediante le primitive sem_wait() e sem_signal(), e un campo pointer a cui si accodano i processi che si bloccano sul semaforo. // sistema.cpp struct des_sem { int counter; proc_elem* pointer; }; • Descrittori di semaforo: – variabili definite nel nucleo, per esempio come un array di descrittori. # sistema.s .DATA .GLOBAL array_dess: array_dess .SPACE BYTE_SEM # MAX_SEM*sizeof(des_sem) // sistema.cpp extern des_sem array_dess[MAX_SEM]; G. Frosini Slide 57 REALIZZAZIONE DEI SEMAFORI (2) • Semaforo: – indice di un descrittore, con valore iniziale nel campo counter; • I semafori vengono inizializzati dal programma main(), utilizzando la primitiva di nucleo sem_ini(): natl sem_ini(int val); – essa ha come parametro il valore iniziale del contatore, e restituisce l’indice selezionato all’interno dell’array dei descrittori di semaforo, oppure 0xFFFFFFFF se non vi sono più elementi. // utente.cpp extern "C" int sem_ini(int val); natl semaforo_i; // memorizza l’indice di un descrittore di semaforo, int main() { … semaforo_i = sem_ini(VALORE); … terminate_p(); } G. Frosini Slide 58 PRIMITIVA SEM_WAIT() (1) • Primitiva sem_wait(): // utente.cpp extern "C" void sem_wait(natl sem); natl proc; natl semaforo; void p_code(int n) { // ... sem_wait(semaforo); // ... } int main() { … semaforo = sem_ini(1) ; proc = activate_p(p_code, A, PRIO, LIV); … } G. Frosini Slide 59 PRIMITIVA SEM_WAIT() (2) # sistema.s .TEXT .EXTERN a_sem_wait: c_sem_wait # routine INT $tipo_w CALL salva_stato # ricopiamento del parametro sem CALL c_sem_wait # ripulitura della pila CALL carica_stato IRET # sistema.cpp extern "C" void c_sem_wait(natl sem) { des_sem* s; s = &array_dess[sem]; (s -> counter) --; if ((s ->counter) < 0) { inserimento_lista(s -> pointer, esecuzione); schedulatore(); } } G. Frosini Slide 60 PRIMITIVA SEM_SIGNAL() (1) • Primitiva sem_signal(): // utente.cpp extern "C" void sem_signal(natl sem); natl proc; natl semaforo; void p_code(int n) { // ... sem_signal(semaforo); // ... } int main() { … semaforo = sem_ini(1) ; proc = activate_p(p_code, A, PRIO, LIV); … } G. Frosini Slide 61 PRIMITIVA SEM_SIGNAL() (2) # sistema.s .TEXT .EXTERN a_sem_signal: c_sem_signal # routine INT $tipo_s CALL salva_stato # ricopiamento del parametro sem CALL c_sem_signal # ripulitura della pila CALL carica_stato IRET // sistema.cpp extern "C" void c_sem_signal(natl sem) { des_sem* s; proc_elem* lavoro; s = &array_dess[sem]; (s ->counter) ++; if ((s ->counter) <= 0) { rimozione_lista(s -> pointer, lavoro); inserimento_lista(pronti, lavoro); inspronti(); // preemption schedulatore; // preemption } } G. Frosini Slide 62 MUTUA ESCLUSIONE • Processi utente: – risorsa su cui i processi compiono le operazioni oper_1, oper_2, ..., oper_n; – semaforo di mutua esclusione, sia mutex, inizializzato a 1. // utente.cpp natl mutex; natl procm_a, procm_b; void pma(int i) { sem_wait(mutex); oper_1; sem_signal(mutex); } void pmb(int i) { sem_wait(mutex); oper_2; sem_signal(mutex); } // identificatori dei processi // i cui corpi sono istanze di pma() e pmb() // sezione critica // sezione critica G. Frosini Slide 63 SINCRONIZZAZIONE • Semaforo con contatore inizializzato a 0: – utilizzato per scambiare segnali temporali tra due processi (sincronizzazione). // utente.cpp natl sincr; natl procs_a, procs_b; // identificatori dei processi i cui corpi sono istanze di pma() e pmb() void psa(int i) { sem_wait(sincr); // ... } void psb(int i) { sem_signal(sincr); // ... } • • Il processo procs_a si blocca quando esegue la sem_wait(), e diviene di nuovo pronto quando il processo procs_b esegue la sem_signal(). Se il processo procs_b esegue la sem_signal() per primo, il processo procs_a non si blocca quando esegue la sem_wait() (l’evento si è già verificato). Questa sincronizzazione non è simmetrica. G. Frosini Slide 64 MEMORIA DINAMICA (1) • Sistema multiprogrammato: – la memoria dinamica deve essere gestita in mutua esclusione; – si fa uso di due funzioni, mem_alloc() e mem_free(): void* mem_alloc(natl dim); • alloca una struttura di dim byte, restituendo il puntatore al primo byte allocato (nel caso di allocazione di un tipo struttura, il numero di byte allocati viene determinato applicando l’operatore sizeof al tipo struttura stesso). void mem_free(void* pv); • libera la struttura indirizzata da pv, che deve essere stata allocata utilizzando la funzione mem_alloc() (e quindi ha associata l’informazione sul numero di byte da liberare). • Mutua esclusione: – utilizzo di un semaforo mem_mutex, inizializzato dal processo main (con il campo contatore che vale 1). G. Frosini Slide 65 MEMORIA DINAMICA (2) • Struttura delle funzioni mem_alloc() e mem_free()): # utente.cpp … natl mem_mutex; … void* mem_alloc(natl dim) { void* p; sem_wait(mem_mutex): … sem_signal(mem_mutex); return p; } void mem_free(void* pv) { sem_wait(mem_mutex): … sem_signal(mem_mutex); } G. Frosini Slide 66 MEMORIA DINAMICA (3) • Processo utente: – per poter utilizzare la funzione mem_alloc(), deve convertire il tipo del risultato, da puntatore a void a puntatore: // utente.cpp … void p(int i) // codice di un processo { struct struttura { /* ... */ }; struttura* ps; ... ps = static_cast<struttura*>(mem_alloc(sizeof(struttura))); mem_free(ps); } G. Frosini Slide 67 MEMORIA DINAMICA (4) • Memoria dinamica: – può essere utilizzata da alcune routine di sistema (che girano con le interruzioni disabilitate, per cui è assicurata la mutua esclusione). • Funzioni che allocano e deallocano memoria dinamica di livello sistema: // sistema.cpp ... void* alloca(natl dim) { /* ... */ } void dealloca(void* p) { /* ... */ } • Non è opportuno utilizzare gli operatori new e delete del C++, in quanto utilizzano il supporto a run-time del linguaggio (ossia alcune primitive del sistema operativo su cui il C++ è installato). G. Frosini Slide 68 INTERFACCIA DI CONTEGGIO • Interfaccia conta-tempi: – possiede un contatore, inizializzato con il contenuto di CTR (di 16 bit), che viene decrementato con la frequenza di pilotaggio; – quando il conteggio arriva a 0, l’interfaccia attiva un piedino di uscita, che viene utilizzato per generare una richiesta di interruzione, e inizializza nuovamente il contatore; – l’interfaccia possiede anche un registro di stato STR che consente di leggere il valore attuale del conteggio, e un registro di controllo CWR che specifica le modalità di funzionamento dell’interfaccia. CTR STR CVR G. Frosini Slide 69 TIMER DI SISTEMA • Interfaccia di conteggio: – predisposta con una costante di tempo (correlata con la frequenza di pilotaggio) in modo da avere una richiesta di interruzione ogni 50 ms. • Sistema in tempo reale: – i processi possono sospendersi per un certo numero di intervalli di tempo, ciascuno lungo 50 ms. • Puntatore p_sospesi: – puntatore a una lista di attesa, composta da elementi che individuano i processi che si sospendono sul timer. • Processo che vuole sospendersi: – invoca la primitiva delay(), specificando il numero di intervalli di tempo in cui intende rimanere bloccato. Trascorso questo tempo, il processo diviene di nuovo pronto. G. Frosini Slide 70 PRIMITIVA DELAY() (1) • Tipo richiesta: // sistema.cpp struct richiesta { natl d_attesa; richiesta* p_rich; proc_elem* pp; }; richiesta* p_sospesi; • // puntatore alla lista dei processi sospesi Primitiva delay(): // utente.cpp extern "C" void delay(natl n); void procd(int i) { // ... delay(N); //... } // codice di un processo G. Frosini Slide 71 FUNZIONE C_DELAY() • Parte della primitiva delay() scritta in C++: // sistema.cpp void inserimento_lista_attesa(richiesta* p); extern "C" void c_delay(natl n) { richiesta* p; p = static_cast<richiesta*>(alloca(sizeof(richiesta))); p -> d_attesa = n; p -> pp = esecuzione; inserimento_lista_attesa(p); schedulatore(); } G. Frosini Slide 72 FUNZIONE INSERIMENTO_LISTA_ATTESA() (1) • Primitiva delay(): – utilizza la funzione di utilità inserimento_lista_attesa(); – questa ha come parametro il puntatore all’elemento (di tipo richiesta) da inserire. • Lista di attesa: – organizzata in modo tale che il numero di intervalli di attesa di un certo elemento è dato dalla somma dei campi d_attesa di tutti gli elementi che precedono e di quello dell’elemento stesso; – questa organizzazione rende più semplice la gestione della lista da parte del driver di interruzione, che si limita a decrementare il contenuto del campo d_attesa del primo elemento in lista e a mettere nella lista dei processi pronti quelli le cui richieste si trovano in testa alla lista e hanno il campo d_attesa uguale a 0. G. Frosini Slide 73 FUNZIONE INSERIMENTO_LISTA_ATTESA() (2) d_attesa Nuovo elemento di tipo richiesta p_ric 5 0 4 0 3 0 8 p_sospesi pp d_attesa 5 p_ric 1 0 p_sospesi pp G. Frosini Slide 74 FUNZIONE INSERIMENTO_LISTA_ATTESA() (3) // sistema.cpp void inserimento_lista_attesa(richiesta* p) { richiesta* r; richiesta* precedente; bool ins; r = p_sospesi; precedente = 0; ins = false; while (r != 0 && !ins) if (p -> d_attesa > r -> d_attesa) { p -> d_attesa -= r -> d_attesa; precedente = r; r = r -> p_rich; else ins = true; p -> p_rich = r; if (precedente != 0) precedente -> p_rich = p; else p_sospesi = p; if (r != 0) r -> d_attesa -= p -> d_attesa; } } G. Frosini Slide 75 INTERRUZIONE DA TIMER • Ogni 50 ms arriva un’interruzione dal timer, che manda in esecuzione uno specifico driver: – esso salva lo stato del processo che è stato interrotto, utilizzando il sottoprogramma salva_stato (il meccanismo di interruzione hardware è uguale a quello software, utilizzato nelle primitive); – pone il processo interrotto nella lista dei processi pronti, utilizzando il sottoprogramma inspronti (senza parametri); – esegue le elaborazioni dovute sulla lista puntata da p_sospesi (decrementa il contenuto del campo d_attesa del primo elemento in lista e immette nella lista dei processi pronti quelli relativi alle richieste che si trovano in testa alla lista e che hanno il campo d_attesa uguale a 0); – richiama lo schedulatore; – carica lo stato del nuovo processo. G. Frosini Slide 76 DRIVER DEL TIMER # sistema.s .TEXT .EXTERN driver_td: c_driver_td CALL salva_stato CALL c_driver_td CALL inviaEOI CALL carica_stato IRET // sistema.cpp extern "C" void c_driver_td(void) { richiesta* p; if (p_sospesi != 0) p_sospesi -> d_attesa --; while (p_sospesi != 0 && p_sospesi -> d_attesa == 0) { inserimento_lista(pronti, p_sospesi -> pp); p = p_sospesi; p_sospesi = p_sospesi -> p_rich; dealloca(p); } inspronti(); // preemption schedulatore(); // preemption } G. Frosini Slide 77