System calls
Home Linux Kernel

Il sito si č trasferito a qui

the site has been moved here

 

System calls

L'infrastruttura delle system calls spiegato step by step.

Questo articolo spiega come aggiungere al nostro kernel l'infrastruttura per il supporto delle system calls. 

 

a cura di Salvatore D’Angelo – koala.gnu@tiscalinet.it

 

Introduzione  

In questo articolo, verrà mostrato come aggiungere al nostro kernel didattico l' infrastruttura necessaria per il supporto delle system calls. Una system call, come vedremo più avanti, non è altro che un servizio che un processo utente richiede kernel. Oltre alle system calls, questo articolo riprende il discorso sugli interrupts già affrontato in parte negli articoli [1] e [2].
Approfondiremo meglio il concetto di interrupt e come i processori della famiglia Intel li gestiscono. Vedremo, inoltre, come migliorare nel nostro kernel il supporto per la loro gestione, in modo da semplificare lo sviluppo di futuri driver.

 

Interrupts & Eccezioni

Interrupts & Eccezioni: che cos'č un interrupt

Interrupts & Eccezioni: Programmable Interrupt Controller (PIC) & IDT

Interrupts & Eccezioni: call gates & IDT descriptors

Interrupts & Eccezioni: controllo dei privilegi

Interrupts & Eccezioni: stack layout

Interrupts & Eccezioni: step 0

Interrupts & Eccezioni: step 1

Programmable Interrupt Timer (PIT) 8253/8254 CMOS

PIT: step 2

Memoria CMOS

Memoria CMOS: step 3

System calls

System calls: step 4

Conclusioni

Bibliografia

 

Clicca qui per scaricare il codice sorgente.

Interrupts & Eccezioni

Interrupts & Eccezioni: che cos'č un interrupt

I processori della famiglia Intel fornisco due meccanismi per segnalare la verifica di un dato evento a un programma in esecuzione: interrupts e eccezioni.
Un interrupt è un evento asincrono generato tipicamente da un device di I/O o dallo interval timer. Generalmente gli interrupt si dividono in due categorie:

Interrupt mascherabili, cioè gli interrupt provenienti dai PICs e notificati alla CPU attraverso il piedino INTR. Questi interrupts possono essere disabilitati attraverso il flag IF del registro EFLAGS.

Interrupt non mascherabili (NMI), questo genere di interrupts vengono inviati alla CPU attraverso il piedino NMI per segnalare gravi condizioni di errore come hardware failures.

Una eccezione è un evento sincrono che viene generato quando il processore individua una o più condizioni di programming error, oppure su richiesta di una applicazione che gira in user mode. Il termine sincrono sta ad indicare, in questo caso, che l' evento viene segnalato al termine dell' esecuzione dell'istruzione che lo ha generato. Questa categoria di eventi si divide in due categorie: processor-detected exceptions e programmed exceptions.

Processor-detected exceptions

A questa categoria appartengono gli eventi causati da condizioni di programming error o anomalie. Essa si suddivide in tre ulteriori sottocategorie: fault, trap e abort.

Fault, generalmente indica una condizione di errore recuperabile. Un esempio tipico di faults si verifica quando un processo richiede una data pagina di memoria che, però, è stata swappata su disco. In tale caso, il valore del registro eip (che punta all' istruzione che ha generato il faults) viene salvato e un apposito handler viene eseguito per rimediare alla condizione di errore. Terminata l' esecuzione di questo handler, il kernel può riprovare a rieseguire l' istruzione che aveva generato il fault.

Traps, generalmente usato per informare un debugger che una data istruzione è stata eseguita. Quando viene generato un evento di questo tipo, viene salvata la locazione dell' istruzione che segue quella che ha generato il trap. Dopo l' esecuzione dell' handler il processo che era in esecucione riprenderà da quest'ultima.

Abort, un evento di questo tipo indica un errore serio in qualche struttura dati del kernel, un eventuale hardware failure e così via. In genere l' handler di un evento di questo tipo effettua un kill del processo che ha generato la condizione di errore.

Programmed exceptions

A questa categoria appartengono gli eventi generati su richiesta di una applicazione che gira in user mode, attraverso una delle istruzioni: int3, int, into o bound.

La gestione degli interrupts e eccezioni è uno dei tasks più complessi che un kernel possa gestire. Un progettista di sistemi operativi deve tener conto dei seguenti aspetti quando si trova ad affrontare questo tema:

quando si verifica un evento di interrupt (o eccezione), la CPU è costretta a interrompere il lavoro attualmente in corso per gestire tale evento. Poichè è obiettivo della CPU di portare al termine il task precedentemente in corso quanto prima possibile, è ovvio che risulta indispensabile gestire quell' evento in un tempo relativamente piccolo. Linux per garantire questa proprietà, spesso divide la gestione dell' evento in due parti: top half and bottom half. La prima parte viene eseguita immediatamente, mentre la seconda parte può anche essere rimandata ad un successivo istante. In questo articolo non affronteremo il tema dei bottom halves, che magari verrà discussa in successivi articoli, insieme ad altre tecniche utilizzate negli ultimi kernel della serie 2.4 e 2.6.

Se un evento di interrupt (o eccezione) può verificarsi in qualsiasi istante è chiaro che esso possa verificarsi mentre è in esecuzione la gestione di un altro interrupt (o eccezione). Generalmente quando il kernel gestisce un interrupt mascherabile, disabilita il flag IF del registro EFLAGS in modo che la CPU ignori ulteriori interrupts di questo tipo. Viceversa, quando il kernel si trova a gestire una eccezione, questa può essere interrotta da un ulteriore evento di interrupt o eccezione. In quest'ultimo caso è indispensabile che gli handler delle eccezioni vengano scritti in modo tale che si possano eseguire handler innestati.

Se è vero che il kernel in alcune circostanze deve garantire l' esecuzione innestata di eventi di interrupts o eccezione è chiaro che ci potrebbero essere delle regioni critiche da sincronizzare. E' importante che il progettista di sistemi operativi riduca al minimo il numero di queste regioni.

Interrupts & Eccezioni: Programmable Interrupt Controller (PIC) & IDT

In [1] abbiamo visto come i dispositivi di I/O sono collegati a un particolare chip detto Programmable Interrupt Controller (PIC) attraverso alcune linee di collegamento dette IRQs. Abbiamo visto che, generalmente, su un PC vengono montati due PICs collegati in cascata. Quando si collega un device ad un PC è necessario associare ad esso una linea di IRQ e un range di indirizzi di I/O. Il primo serve a segnalare al PIC eventuali richieste di I/O, mentre il secondo serve alla CPU per leggere le informazioni provenienti dal device. Quando un device deve notificare alla CPU un dato evento, invia un segnale al PIC attraverso la linea IRQ ad esso assegnato. Compito del PIC è quello di tradurre questo segnale di IRQ in un indice nella Interrupt Descriptor Table (IDT) che conterrà l' indirizzo della routine che gestirà l' interrupt. In [1] abbiamo visto che questa tabella viene inizializzata in modo tale che ogni entry (o vettore) punti a un null handler il cui compito è quello di visualizzare un messaggio di errore. Durante la fase di inizializzazione del kernel le entries di questa tabella vengono aggiornate in modo tale che ogni vettore punti ad uno specifico handler. l' associazione tra vettore di interrupt e IRQ viene effettuata attraverso il comando ICW2 del PIC (vedi [1]). Nel file setup.S abbiamo il seguente codice:

                     # Comando ICW2. (out[0x21] = 0x20, Master).
                    movb $0x20, %al

                    # IRQ0 -> 20h, IRQ1 -> 21h, and so on
                    outb %al, $0x21
                    call delay

                    # Comando ICW2. (out[0xA1] = 0x28, Slave).
                    movb $0x28, %al
                    outb %al, $0xa1
                    # IRQ8 -> 28h, IRQ9 -> 29h, and so on
                    call delay

con il quale si associa IRQ0 al vettore di interrupt 0x20, IRQ al vettore 0x21 e così via fino a IRQ15 associato al vettore 0x2F.
Quindi se il device collegato a IRQ1 (es. tastiera) invia un segnale di IRQ al PIC, questi provvederà a calcolare la corrispondente entry nella IDT che, nel nostro caso è 0x21. Per segnalare questa informazione alla CPU, il PIC scrive 0x21 in una delle sue porte di I/O ed invia un segnale di interruzione alla CPU attraverso il piedino INTR. La CPU a questo punto leggerà il valore 0x21 e manderà al PIC un segnale di acknowlegement.
E' importante ricordare che il PIC può essere programmato in modo tale da abilitare/disabilitare la ricezione di segnali da specifiche linee IRQs. Disabilitare una linea di IRQ è diverso rispetto all' operazione di azzerare il bit IF del registro EFLAGS. Quando disabilitiamo una linea IRQ, i segnali di interrupts non vengono persi, bensì vengono recuperati non appena viene riabilitata la linea. Viceversa, azzerando il bit IF del registro EFLAGS, non si fa altro che dire alla CPU di ignorare gli interrupts mascherabili.

La Fig. 1 illustra con un semplice schema il processo sopra descritto, ulteriori chiarimenti è possibile ottenerli leggendo il capitolo 4 di [5] e il capitolo 6 di [12].

 

               

A questo punto il lettore si porrà sicuramente le seguenti domande: come sono fatte le entries della IDT? Cosa contengono con esattezza tali entries?

Iniziamo con il rispondere alla seconda domanda. Come già visto sopra, i vettori 0x20-0x2F contengono gli indirizzi degli handler degli interrupts mascherabili. Le entries 0x00-0x19 sono riservate alle exceptions, eccetto:

0x02, contenente l' handler dell' unico interrupt non mascherabile (NMI);

0x03, contenente l' handler di int3;

0x05, contenente l' handler di bound;

0x12-0x19, che sono riservati.

I vettori nel range 0x30-0xFF non vengono usati ad eccezione di 0x80, utilizzato da Linux per la gestione delle system calls (come vedremo più dettagliatamente in seguito).

l' altra domanda a cui bisogna rispondere, è come sono fatte le entries di una IDT.

Prima di rispondere a questa domanda, però, è necessario introdurre il concetto di call gate.

Interrupts & Eccezioni: call gates & IDT descriptors

Sappiamo che l' istruzione assembler CALL può essere utilizzata per effettuare chiamate a procedure all' interno del segmento di memoria corrente (near call) o a procedure presenti in un altro segmento (far call). Le chiamate near calls vengono generalmente usate per chiamati locali al task corrente, mentre le far calls vengono utilizzate per invocare routines di sistema o routines di altri tasks.
L' architettura dei processori Intel prevede 4 livelli di protezione numerati da 0 a 3. 0 è il livello con i massimi privilegi, al contrario 3 è il livello di protezione meno restrittivo. La ragione principale per cui sono stati introdotti questi livelli di protezione è quella di fornire un sistema più affidabile in cui difficilmente un'applicazione possa compromettere l' esecuzione dell' intero sistema.

La Fig. 2 rappresenta i quattro livelli di protezione come cerchi concentrici in cui quello più interno (livello 0) conterrà il codice più critico dell' intero sistema (il kernel), mentre in quello più esterno, generalmente, girano le applicazioni utente.

E' chiaro che una routine presente al livello i può accedere senza problemi ad una porzione di codice presente al livello j, con j > i e i,j in [0 .. 3]. Viceversa, se un modulo di codice al livello i vuole accedere ad una porzione di codice al livello j, con j<i e i,j in [0 .. 3], lo può fare a patto di farlo attraverso una opportuna interfaccia di protezione chiamata gate. Se si tenta di violare questa regola la CPU genera una general-protection exception.

Una chiamata a procedura ad un livello di protezione più alto può essere fatta attraverso una istruzione del tipo CALL (selettore, offset), dove selector punta a un descrittore chiamato call gate descriptor il quale contiene:

                             

informazioni di protezione;

selettore di segmento contenente la routine da invocare;

offset nel segmento in cui è presente la routine.

Dopo questa breve introduzione sul concetto di call gate, finalmente possiamo rispondere alla domanda sul formato delle entries della IDT.

                          

La IDT può contenere tre tipi di call gate descriptor:

task gate descriptor, non usato da Linux;

interrupt gate descriptor, contiene l' indirizzo logico (selettore, offset) dell' handler utilizzato per la gestione dell' interrupt. Prima di trasferire il controllo all' handler, il processore azzera il bit IF del registro EFLAGS disabilitando ulteriori interrupts mascherabili. Questo tipo di descriptor viene utilizzato per gestire interrupt mascherabili;

trap gate descriptor, ha un funzionamento analogo all' interrupt gate descriptor, con la differenza che il bit IF del registro EFLAGS non viene azzerato, lasciando così abilitati gli interrupts mascherabili. Questo genere di descrittore viene usato per puntare ad handler di eccezioni che, come già anticipato prima, possono essere interrotti da altri interrupts (mascherabili e non).

La Fig. 3 mostra il formato dei tre descrittori sopra citati.

Interrupts & Eccezioni: controllo privilegi

Quando la CPU invoca l' handler di un interrupt, la prima cosa che fa è controllare se la sorgente che ha generato l' interruzione ha i privilegi per farlo. Per far ciò confronta il campo CPL contenuto (nei due bit meno significativi) nel selettore del call gate descriptor, con il campo DPL del descrittore della IDT, se quest'ultimo ha un valore maggiore rispetto al primo allora viene lanciato un errore di protezione generale.

Negli steps 1, 2 e 3, vedremo come i call gate descriptors associati agli interrupts non mascherabili (quelli provenienti dai device di I/O tramite linee IRQs) avranno un DPL=0 e gli handler risiederanno nel kernel segment avente CPL=0. Al contrario, per le system call il valore DPL=3.

Interrupts & Eccezioni: stack layout

Un altro aspetto fondamentale da prendere in considerazione nella gestione degli interrupts, è il layout dello stack. I processori utilizzano uno stack per ogni livello di protezione. Quindi ogni processo Linux utilizza due stacks, uno per quando gira in user mode ed un altro per quando gira in kernel mode.

Quando si verifica un interrupt il processore interrompe il processo correntemente in esecuzione in user mode (livello di protezione 3) ed esegue un handler in kernel mode (livello di protezione 0). Nel fare ciò vengono eseguiti i seguenti passi:

effettua i controlli di protezione (privilege check);

salva temporaneamente i registri SS, ESP, EFLAGS, CS e EIP;

carica il selettore e lo stack pointer del nuovo stack (quello relativo al nuovo livello di protezione) prendendoli dal TSS del processo corrente e mette queste informazioni in SS e ESP; con questa semplice operazione avviene lo switch dello stack;

effettua il push, nel nuovo stack, dei valori di SS e ESP precedentemente salvati (SS, ESP, EFLAGS, CS e EIP);

copia i parametri dallo stack della procedura chiamante al nuovo stack;

carica il selettore e l' instruction pointer dal call gate nei registri CS e EIP;

esegue l' handler.

La Fig. 4 mostra il layout del kernel e user stack durante l' esecuzione di un interrupt.

                                       

Il ritorno da un interrupt inizia con l' esecuzione dell' istruzione IRET che, a differenza dell' istruzione RET, recupera anche il contenuto del registro EFLAGS. I passi eseguiti sono quindi:

effettua i controlli di protezione (privilege check);

recupera i valori dei registri CS e EIP;

recupera i valori dei registri EFLAGS;

recupera i valori che i registri SS e ESP avevano prima dell' interrupt; questo comporta lo switch allo user stack;

riprende l' esecuzione del processo;

A questo punto, dopo tutta questa teoria, iniziamo a vedere in pratica alcuni argomenti finora discussi. Nell' articolo [2] abbiamo visto come scrivere un driver per la tastiera e, come bisogna configurare opportunamente il PIC affinchè gli eventi di tale periferica venissero correttamente segnalati alla CPU. Tuttavia, non abbiamo ancora introdotto una infrastruttura adeguata, che ci consenta di associare un IRQ ad un dato handler di periferica attraverso una interfaccia semplice e chiara. Obiettivo dei prossimi 2 steps è proprio quello di introdurre tale infrastruttura e consentire di associare un IRQ ad un handler semplicemente con una chiamata a funzione:

add_irq_handler(<num irq>, <handler>).

In successivi articoli, mostreremo anche come aggiungere l' infrastruttura per la gestione delle eccezioni e come gestire quelle predefinite dal processore.

Interrupts & Eccezioni: step 0

In questo step non faremo nulla di nuovo se non quello di spostare il codice di inizializzazione del PIC dal file setup.S nel nuovo file irq.c. Questo file conterrà da adesso in avanti tutto il codice inerente alla programmazione degli IRQs.
Uno dei motivi per cui ho deciso di fare ciò è quello di diminuire quanto più possibile il codice in assembler. Un altro motivo è che in questo modo si evita di disperdere in più files il codice per la gestione degli IRQs.

Quindi definiamo in irq.c la routine init_irq() che conterrà la traduzione in C del codice di setup del PIC presente in setup.S.

#define INT_CTL 0x20
#define INT_CTLMASK 0x21
#define INT2_CTL 0xA0
#define INT2_CTLMASK 0xA1

// IRQ 2 on Master is cascaded
static unsigned char cache_21 = 0xFB;
static unsigned char cache_A1 = 0xFF;

void init_irq(void) {

// OCW1 command. Mask all the IRQ on Master
outb(0xff, INT_CTLMASK);
// OCW1 command. Mask all the IRQ on Slave
outb(0xff, INT2_CTLMASK);

// set up PIC
// ICW1 command.
outb_p(0x11, INT_CTL);
// ICW4 will be sent and size of entry in interrupt vector is 4 bytes.
outb_p(0x11, INT2_CTL);

outb_p(0x20, INT_CTLMASK);

// ICW2 command.
outb_p(0x28, INT2_CTLMASK);

// IRQ0 -> 20h, IRQ1 -> 21h, and so on
// IRQ8 -> 28h, IRQ9 -> 29h, and so on

// ICW3 command.
outb_p (0x04, INT_CTLMASK);
outb_p (0x02, INT2_CTLMASK);

// The controller master is attached to
// the slave through IRQ12 (IRQ4 on slave).
// The controller slave is attached to
// the master through IRQ2.


outb_p (0x01, INT_CTLMASK);

// ICW4 command.
outb_p (0x01, INT2_CTLMASK);

// restore masks. OCW1 command.
outb(cache_21, INT_CTLMASK);

// Mask all interrupt for controller Slave
outb(cache_A1, INT2_CTLMASK);
// Mask all interrupt except IRQ2

}

Questi comandi sono del tutto equivalenti a quelli presenti nel file setup.S, dove abbiamo lasciato solo i comandi che mascherano tutti gli IRQs, in modo tale che al boot si evita che la CPU riceva eventi dal PIC, poi man mano che si procede nel codice di inizializzazione (quello in C per intenderci) si abiliteranno gli IRQs necessari (tastiera, timer, ecc.).

Evitiamo in questa sede di spiegare i comandi sopra riportati, perchè già sono stati trattati in [1].

Affinchè questo codice di inizializzazione venga invocato prima del setup della tastiera, è necessario invocare tale funzione in start_kernel prima del setup della tty.

void start_kernel(void) {

init_irq();
tty_init();
...

}

Infine, modifichiamo il Makefile affinchè anche il file irq.c venga compilato.

KERNEL_OBJ=head.o main.o tty_io.o keyboard.o console.o asm.o vsprintf.o irq.o

La sola aggiunta di irq.o alla riga di sopra comporterà la compilazione del file irq.c e la sua inclusione nell' immagine del kernel.

Interrupts & Eccezioni: step 1

In questo articolo vedremo in dettaglio l' infrastruttura per la gestione degli IRQs e vedremo anche come utilizzarla per il driver della tastiera che al momento ha un codice di gestione IRQ ad hoc senza una interfaccia chiara.

Iniziamo con il definire, nel file irq.c, una tabella di 16 elementi ognuno dei quali conterrà un puntatore all' handler della relativa linea IRQ.

volatile void *IRQ_TABLE[16];

Introduciamo ora la funzione da usare per associare un handler ad una data linea IRQ.

void add_irq_handler(unsigned int irq_nr, void *handler) {

long flags;
save_flags(flags); cli();
if (is_valid_irq(irq_nr)) {

IRQ_TABLE[irq_nr] = handler;
enable_irq(irq_nr);

}
restore_flags(flags);

}

Come si evince dal codice, quello che viene fatto è di aggiungere l' handler alla tabella sopra citata (a patto che l' irq in input sia valido, cioè compreso tra 0 e 15) con gli interrupts disabilitati. Dopo che l' handler è stato aggiunto alla tabella viene abilitata la linea IRQ attraverso una nuova funzione enable_irq che sotto andremo ad analizzare.

E' chiaro, quindi, che invocando add_irq_handler(irq, handler), si abilita l' IRQ irq e ogni volta che arriverà un evento da tale linea, la funzione handler verrà eseguita.

La funzione enable_irq è dichiarata statica perchè è usata solo da add_irq_handler e quindi non fa parte dell' interfaccia verso l' esterno. In [1] abbiamo visto che il PIC comunica con la CPU attraverso due porte: 0x20 e 0x21 per il PIC Master e 0xA0 e 0xA1 per quello Slave. Abbiamo inoltre visto come, grazie al comando OCW1 si possa abilitare/disabilitare le linee IRQs. In irq.c definiamo due importanti variabili globali al file che conterranno ciascuna la maschera degli IRQs abilitati su ciascun PIC.

static unsigned char cache_21 = 0xFB; // IRQ 2 on Master is cascaded
static unsigned char cache_A1 = 0xFF;

Sappiamo che se il bit i del comando OCW1 è impostato a 1, la relativa linea IRQ viene diabilitata. La variabile cache_21 mantiene il valore in memoria degli IRQs abilitati sul PIC Master, mentre cache_A1 tiene traccia delle linee IRQs abilitate sullo Slave. Nel file setup.S abbiamo disabilitato tutte le linee tranne IRQ2 sul PIC Master, infatti i valori con cui inizializzeremo queste variabili tendono a rappresentare proprio questo stato.

A questo punto vediamo come queste due variabili vengono usati all' interno della funzione enable_irq.

static void enable_irq(unsigned int irq_nr) {

unsigned char mask;

mask = ~(1 << (irq_nr & 7));
if (irq_nr < 8) {

cache_21 &= mask;
outb(cache_21,0x21);

} else {

cache_A1 &= mask;
outb(cache_A1,0xA1);

}

}

Per prima cosa la funzione calcola la maschera che consente l' abilitazione della linea IRQ irq_nr. Se irq_nr == i, allora l' i-esimo bit della maschera è 0 (0 indica l' abilitazione della linea IRQ). Se irq_nr è compreso tra 0 e 7 si modifica lo stato del PIC Master, altrimenti quello dello Slave. Si aggiorna il valore cache_XX ponendolo in and con la maschera precedentemente calcolata e si invia un comando OCW1 al PIC.

A questo punto, bisogna analizzare il codice che consente l' esecuzione dell' handler corretto quando proviene un evento da una data linea IRQ.

Iniziamo con l' introdurre un nuovo file
traps.c in cui definiamo una nuova routine init_traps a cui delegheremo il compito di associare a ciascuna entry della IDT un ben preciso handler.

Alle prime 32 entries (0-31) verrà associato il null handler, mentre alle successive 16 entries si associeranno le routine che gestiranno gli eventi provenienti dalle linee IRQs. Le restanti entries possono essere utilizzate dal kernel come software interrupt. Nello step 4, vedremo che il kernel utilizzerà solo l' interrupt 0x80 per la gestione delle system call.

void init_traps(void) {

int i;

// exception: 0x0 - 0x10. Not implemented yet
for (i=0x0; i<0x20; i++)

set_intr_gate(i, &_unhand_int);

set_intr_gate(0x20, &_hwint0);
set_intr_gate(0x21, &_hwint1);
set_intr_gate(0x22, &_hwint2);
set_intr_gate(0x23, &_hwint3);

set_intr_gate(0x24, &_hwint4);
set_intr_gate(0x25, &_hwint5);
set_intr_gate(0x26, &_hwint6);
set_intr_gate(0x27, &_hwint7);
set_intr_gate(0x28, &_hwint8);
set_intr_gate(0x29, &_hwint9);
set_intr_gate(0x2A, &_hwint10);
set_intr_gate(0x2B, &_hwint11);
set_intr_gate(0x2C, &_hwint12);
set_intr_gate(0x2D, &_hwint13);
set_intr_gate(0x2E, &_hwint14);
set_intr_gate(0x2F, &_hwint15);

// system call: 0x80. Not implemented yet
for (i=0x30; i<IDT_ELEMENTS; i++) {

set_intr_gate(i, &_unhand_int);

}

}

Il null handler _unhand_int non fa altro che stampare un messaggio di errore e ciclare in modo da bloccare il sistema (più avanti magari vedremo approcci di gestione più adeguati). Le routine _hwint0/_hwint15, verranno usati per gestire gli eventi provenienti dagli IRQs e li analizzeremo meglio più avanti in questo paragrafo.

Com'è facile osservare dal codice sopra riportato, abbiamo usato la macro set_intr_gate (introdotta per la prima volta nello step 0 di [2]) per definire in ciascuna entry della IDT un interrupt call gate.

Le routines _hwint1/_hwint7, utilizzate per gestire gli eventi segnalati dal PIC master, sono definite nel file asm.S, attraverso la seguente macro:

mentre le routines _hwint8/_hwint15, utilizzate per gestire gli eventi segnalati dal PIC slave, sono definite con quest'altra macro:

#define HWINT_SLAVE(irq) \

ENTRY(_hwint##irq) \
SAVE_ALL; \
inb $INT2_CTLMASK, %al; \
orb $1<<(irq-8), %al; \
outb %al, $INT2_CTLMASK; \
movb $ENABLE, %al; \
outb %al, $INT_CTL; \
jmp .+2; \
outb %al, $INT2_CTL; \
STI; \
call *(IRQ_TABLE+4*irq); \
CLI; \
inb $INT2_CTLMASK, %al; \
andb $~(1<<(irq-8)), %al; \

outb %al, $INT2_CTLMASK; \
RET

Le due macro sono praticamente simili eccetto qualche lieve differenza dovuto al fatto che indichiamo gli IRQs sullo slave con i numeri 8/15 anzichè 0/7, quindi analizzeremo in dettaglio solo la prima.

Per chi ha poca conoscenza di assembler, traduco la macro in un pseudo codice C.

salva tutti i registri sul kernel stack compreso l' indirizzo della routine ret_from_intr

mask1 = maschera con il bit irq a 1

outb[inb[0x21] | mask1, 0x21]

outb[0x20, 0x20]

abilita interrupt mascherabili

esegui l' handler irq_table[irq]

disabilita interrupt mascherabili

mask2 = maschera con il bit irq a 0

outb[inb[0x21] & mask2, 0x21]

jump alla routine ret_from_intr

Per prima cosa vengono salvati i registri nel kernel stack (oltre a quelli già salvati in automatico dal processore come spiegato sopra) attraverso la macro SAVE_ALL. Al top dello stack ci sarà l' indirizzo della routine ret_from_intr che gestirà, come già visto in [2] il ritorno dall' interrupt. l' istruzione outb[inb[0x21] | mask1, 0x21], non è altro che un comando OCW1 inviato al PIC per indicare che successivi eventi devono attendere in coda per essere elaborati (vedi [1]).
Il comando outb[0x20, 0x20] segnala al PIC la fine dell' interrupt e, con il successivo comando STI, si fa in modo che la CPU non ignori successivi interrupt mascherabili.

A questo punto, viene eseguito l' handler dell' interrupt puntato da IRQ_TABLE[irq], terminato il quale viene riabilitata la linea IRQ in questione con il comando outb[inb[0x21] & mask2, 0x21] e si salta alla routine ret_from_intr.

La routine ret_from_intr non fa altro che ripristinare i valori dei registri dal kernel stack e chiamare l' istruzione IRET.

ret_from_intr:

cli
popw %gs
popw %fs
popw %es
popw %ds
popa
iret

A questo punto, è pronta la nostra infrastruttura per la gestione degli IRQs e possiamo iniziare a provarla proprio con il nostro keyboard driver. Sostituiamo, quindi, in keyboard_init (nel file keyboard.c) le righe:

set_intr_gate(0x21, &kb_intr);
outb_p(inb_p(0x21) & 0xfd, 0x21);

con la seguente nuova riga.

add_irq_handler (1, &keyboard_interrupt);

D'ora in avanti basterà utilizzare questa funzione per associare un handler ad una specifica linea IRQ.

Programmable Interrupt Timer (PIT) 8253/8254 CMOS

Molte attività di un sistema operativo sono strettamente legate al tempo. Basta pensare ad esempio a screen saver che si attivano dopo un periodo di inutilizzo dei dispositivi d'input da parte dell' utente. Generalmente, le attività legate al tempo che un sistema operativo si trova a svolgere, sono raggruppabili in due categorie:

tener traccia dell' ora corrente in modo da restituirla a processi utente su richiesta o per marcare timestamp di specifiche risorse;

segnalare al kernel o a un'applicazione utente che un certo periodo di tempo è ormai trascorso.


Affinchè un kernel possa gestire queste due categorie di tasks, è necessario che l' hardware su cui esso si poggia supporti tali funzionalità. Su architetture Intel un kernel può interagire con tre tipi di dispositivi: Real Time Clock, Time Stamp Counter, e il PIT. I primi due vengono usati per tener traccia del' ora corrente, mentre l' ultimo serve per inviare segnali al kernel o ad applicazioni utente allo scadere di un certo intervallo di tempo. Su architetture Intel, generalmente il PIT viene implementato attraverso il CMOS chip 8253 su PC & XT e 8254 su AT+.

Il CMOS PIT 8253/8254 ha 3 canali interni detti: timer 0, timer 1 e timer 2. Il primo viene usato come Timer Clock, il secondo per il refresh della RAM e il terzo per emettere suoni attraverso lo speaker. Tutti e tre i canali hanno un oscillatore associato che lavora ad una frequenza base di 1,193182 Mhz. E' possibile programmare ciascun canale, rispettivamente, attraverso le porte di I/O: 0x40, 0x41 e 0x42. In questo paragrafo utilizzeremo solo la prima per fare in modo che dal canale 0 fuoriesca un segnale di interrupt sulla linea IRQ 0 ad una frequenza di 100 Hz. Il PIT utilizza, infine, un ultimo registro (Mode Control Register) associato alla porta 0x43 il cui formato è il seguente:

7

6

5

4

3

2

1

0

 

bit 6-7 : timer channel

bit 4-5 : LSB (11) o MSB

bit 3-1 : counter mode (011=square wave rate generator)

bit 0 : count format (0 = binary);

Ad esempio, volendo che il PIT fornisca attraverso il channel 0 (porta 0x40) un segnale di interrupt con frequenza di 100 Hz, bisogna scrivere un pezzo di codice come il seguente:

#define HZ 100
#define TIMER_FREQ 1193182
#define TIMER_COUNT (TIMER_FREQ /HZ)

outb_p(0x36, 0x43);
outb_p(TIMER_COUNT, 0x40);
outb_p(TIMER_COUNT>>8, 0x40);

si osservi come i bits 6-7 del mode control register sono stati impostati a 0 per selezionare il canale 0. I bits 4-5 indicano la modalità di lettura/scrittura. Poichè alle porte 0x40, 0x41 e 0x42 è possibile inviare singoli byte, nel momento in cui è necessario inviare delle word bisogna specificare in che ordine questi byte verranno inviati. Specificando 1 per entrambi i bits non facciamo altro che informare il PIT che invieremo prima il byte meno significativo e poi quello più significativo. Attraverso i bits 1-3 si informa il PIT il tipo di timing che si desidera. Con i due bytes da noi inviati sulla porta 0x40 diciamo al PIT di generare un segnale ad onda quandra ad una frequenza di 100 Hz (ossia ogni 10 ms). Questo segnale arriverà al PIC attraverso la linea IRQ 0.

La scelta della frequenza del segnale di output non è casuale, perchè esa influenza enormemente i tempi di risposta del sistema. Generalmente, frequenze alte consentono migliori tempi di risposta, a patto che il processore sia sufficientemente potente da consentirlo. Infatti su processori Alpha Linux usa una frequenza di 1024 Hz. I tempi di risposta di un sistema spesso dipendono da quanto velocemente un processo viene prelazionato da uno ad alta priorità. Aumentando la frequenza delle interruzioni del timer interrupt aumenta il numero di volte in cui si richiama lo scheduling e quindi la probabilità che un processo venga prelazionato. Tuttavia, tempi frequenze troppo alte implicano anche un maggiore tempo di esecuzione in kernel mode con conseguente rallentamento delle applicazioni user space. Quindi aumentare questo parametro ha senso solo su processori sufficientemente potenti.

PIT: step 2

In questo step programmeremo il chip CMOS PIT 8253/8254 affinchè ad una frequenza di 100 Hz invii al PIC un segnale di interrupt sulla linea IRQ0. Introduciamo, innanzittutto, un primo file chiamato sched.h, che conterrà la define HZ della frequenza del segnale in uscita dal canale 0 del PIT.

#define HZ 100

Questo file, conterrà in futuro importanti strutture dati per la gestione dei processi. E' necessario definire un altro nuovo file, chiamato time.c, è necessario definire affinchè contenga tutto il codice relativo alla gestione del timer.

Definiamo in esso la funzione di inizializzazione del timer che, oltre ad avere il codice già analizzato sopra, associa anche l' handler
timer_interrupt all' IRQ 0.

#define TIMER_FREQ 1193182L
#define TIMER_COUNT ((unsigned) (TIMER_FREQ/HZ))

#define TIMER_MODE 0x43
#define TIMER_0 0x40
#define SQUARE_WAVE 0x36

static void setup_timer(void) {

outb_p (SQUARE_WAVE, TIMER_MODE);
outb_p ((unsigned char)TIMER_COUNT, TIMER_0);
outb_p ((unsigned char)(TIMER_COUNT>>8), TIMER_0);

add_irq_handler (0, &timer_interrupt);

}

void time_init(void) {

setup_timer();

}

l' handler timer_interrupt, in questo step, non fa altro che incrementare una variabile jiffies e stampare un punto a video, in successivi articoli vedremo come questo handler sarà fondamentale per implementare un efficiente scheduler.

unsigned long volatile jiffies = 0;

static void timer_interrupt(void) {

++jiffies;
printk(".");

}

In base a quanto visto nello step precedente, un segnale sulla linea IRQ 0 attiverà la routine _hwint0 in asm.S che avrà un codice diverso rispetto alle restanti linee IRQs:

ENTRY(_hwint0)

SAVE_ALL
call *(IRQ_TABLE)
movb $ENABLE, %al
outb $al, INT_CTL
RET

in quanto non disabilita/abilita la linea IRQ ed esegue il comando EOI dopo l' esecuzione dell' handler.

Infine, è importante richiamare nella funzione main di main.c la routine time_init.

void start_kernel(void) {

init_traps();
init_irq();
tty_init();
time_init();
sti();
while(1);

}

Memoria CMOS

Visto che nello step precedente abbiamo programmato il PIT affinchè generasse un intervallo con una frequenza di 100 Hz, in questo step utilizzeremo la memoria CMOS per far stampare, in fase di boot, la data e ora corrente.

La memoria CMOS è una memoria tampone che contiene alcune importanti informazioni sulla configurazione della macchina. Queste informazioni vengono mantenute anche se il PC è spento per un lungo periodo, essendo la memoria alimentata da una batteria che si trova sulla scheda madre.

l' accesso alla memoria CMOS avviene tramite due porte di I/O. La porta 0x70 utilizzata per indirizzare il byte interessato, e la porta 0x71 per leggere o modificare il relativo byte. Non vi è nessun controllo sul byte indirizzato nella memoria CMOS, infatti la porta 0x70 è una porta a sola scrittura, quindi prima di leggere o modificare qualsiasi byte contenuto nella memoria è importante disabilitare gli interrupt attraverso l' istruzione CLI e riabilitarli successivamente attraverso l' istruzione STI. Questa coppia di istruzioni, purtroppo, non ha nessun effetto sugli interrupt NMI.

La memoria CMOS ha una dimensione standard di 64 bytes, ma col passare del tempo i produttori di schede madri ne hanno aumentato la capacità a 128 bytes.

Di seguito è possibile osservare una tabella riassuntiva del contenuto della CMOS.

Indirizzo

Lunghezza

Contenuto

0x00

0x0E

Real Time clock information

0x0E

0x01

Diagnostic state bye

0x0F

0x01

Shutdown state bye

0x10

0x01

Tipo di floppy disk drive

0x11

0x01

Riservato

0x12

0x01

Tipo di Hard Disk drive

0x13

0x01

Riservato

0x14

0x01

Equipment set byte

0x15

0x02

Dimensione memoria base

0x17

0x02

Dimensione memoria estesa (superiore a 1Mb)

0x19

0x01

Tipo di Hard Disk drive C

0x1A

0x01

Tipo di Hard Disk drive D

0x1B

0x06

Riservato

0x21

0x0D

Riservato

0x2E

0x02

CMOS checksum

0x30

0x02

Dimensione memoria estesa superiore a 1Mb

0x32

0x01

Secolo corrente

0x33

0x01

Informazioni addizionali

0x34

0x0C

Riservato

0x40

0x40

Estensioni della memoria CMOS da parte dei produttori di schede madri

Memoria CMOS: step 3

Dopo questa breve panoramica sulla memoria CMOS estratta da [8], vediamo come sfruttare queste informazioni per stampare, in fase di boot, l' ora e data corrente. Definiamo, innanzittutto, un nuovo file mktime.h che conterrà la definizione di una importante struttura dati di questo step:

struct mktime {

int sec;
int min;
int hour;
int day;
int mon;
int year;

};

Questa struttura conterrà data e ora fino alla risoluzione del secondo. Creiamo un altro file, chiamato mc146818rtc.h, che conterrà alcune importanti macro. La prima che andiamo ad esaminare è la macro che consente la lettura di un byte dalla CMOS.

#define CMOS_READ(addr) ({ \

outb_p(addr|0x80,0x70); \
inb_p(0x71); \
})

Si osservi come prima si specifica l' indirizzo da leggere scrivendolo sulla porta 0x70 e poi la si legge effettivamente dalla porta 0x71.

Nella routine time_init(), prima del setup dell' handler, leggiamo l' orario dalla CMOS e memorizziamolo in una variabile di tipo struct mktime.

void time_init(void) {

struct mktime time;

do {

time.sec = CMOS_READ(RTC_SECONDS);
time.min = CMOS_READ(RTC_MINUTES);
time.hour = CMOS_READ(RTC_HOURS);
time.day = CMOS_READ(RTC_DAY_OF_MONTH);
time.mon = CMOS_READ(RTC_MONTH);
time.year = CMOS_READ(RTC_YEAR);

} while (time.sec != CMOS_READ(RTC_SECONDS));
....

}

Gli indirizzi della CMOS dove è memorizzata la data e ora corrente sono i primi 0x0E bytes (vedi tabella sopra) e le macro di questi offset (RTC_SECONDS, RTC_MINUTES, ecc.), sono definiti nel file mc146818rt.h nel modo seguente.

#define RTC_SECONDS 0
#define RTC_MINUTES 2
#define RTC_HOURS 4
#define RTC_DAY_OF_MONTH 7
#define RTC_MONTH 8
#define RTC_YEAR 9

Poichè questi dati sono espressi in BCD, si effettua una opportuna conversione prima di stampare i dati a video.

if (!(CMOS_READ(RTC_CONTROL) & RTC_DM_BINARY) ||
RTC_ALWAYS_BCD{

BCD_TO_BIN(time.sec);
BCD_TO_BIN(time.min);
BCD_TO_BIN(time.hour);
BCD_TO_BIN(time.day);
BCD_TO_BIN(time.mon);
BCD_TO_BIN(time.year);

}

Con la macro BCD_TO_BIN definita nel file mc146818rt.h. A questo punto siamo pronti a stampare l' orario a video.

printk("Date (mm/dd/yy): %02d/%02d/%02d\n", time.mon, time.day, time.year);
printk("Time (hh:mm:ss): %02d:%02d:%02d\n",
time.hour, time.min, time.sec);

System calls

Lo standard Posix definisce uno strato software che consente a qualsiasi processo in user mode di richiedere servizi al kernel. Questo strato consente di accedere a risorse hardware come stampanti, dischi ecc., senza dover conoscere i dettagli di funzionamento di tali risorse. Questo offre un notevole vantaggio da un punto di vista della portabilità dei programmi. Un altro vantaggio è sicuramente la security, perchè ovviamente i processi girando in user mode avranno potere limitato e le richieste fatte al kernel possono essere opportunamente controllate prima di essere eseguite.

Una system call non è altro che una richiesta fatta da un processo al kernel via software interrupt. Queste system calls vengono invocate attraverso delle wrapper routines definite nella libreria standard libc. Ad esempio la chiamata di sistema fork per il forking di processi è una wrapper routine che richiede l' esecuzione del servizio di forking al kernel attraverso un opportuno interrupt software (0x80 su Linux) e settando opportunamente alcuni registri.

#define _syscall0(type,name) \

extern inline type name(void) \
{ \

type __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name)); \
if (__res >= 0) \

return __res; \

errno = -__res; \
return -1; \

}

_syscall0(int, fork);

Si osservi come venga invocato l' interrupt software 0x80 settando il registro EAX con l' identificativo che il kernel ha assegnato alla system call fork.

Ovviamente in fase di compilazione i programmi dovranno linkarsi alla libc per poter utilizzare questi servizi dei kernels, cosa che viene fatta automaticamente dai compilatori.

La Fig. 5 mostra cosa accade in Linux (e quindi al nostro kernel) quando un processo invoca una wrapper routines come la fork.

        

La wrapper routine, imposta opportunamente alcuni parametri come l' id che il kernel ha associato a quella determinata system call e invoca un software interrupt (0x80 nel nostro caso). Questo provoca lo switch della CPU i kernel mode con la conseguente esecuzione di una routine uguale per qualsiasi system call invocata.

Questa kernel routine salva il contenuto attuale dei registri sul kernel stack, che verranno ripristinati prima che il processo ritornerà in user mode. Tra i registri salvati non compaiono eflags, cs, eip, ss, e esp, che vengono salvati automaticamente dalla CPU quando viene invocato il software interrupt. A questo punto, viene eseguita la routine
sys_xxx che esegue la system call xxx lato kernel. Terminata l' esecuzione di questa funzione, si esegue il pezzo di codice che serve al kernel per riprendere l' esecuzione del processo dal punto in cui aveva lasciato. Questa pezzo di codice che, in genere, viene denominata in Linux con il nome ret_from_sys_call (ret_from_intr nel nostro caso), ripristina i registri precedentemente salvati e chiama l' istruzione IRET.

Ad ogni system call il kernel associa un id che la identifica. Affinchè esso possa associare a ciascuna system call
xxx con id k la rispettiva kernel routine di servizio sys_xxx, viene usata una system call dispatcher table; questa tabella, avrà nella k-esima entry il riferimento alla routine sys_xxx.

Un aspetto molto importante e delicato nella gestione delle system call e il passaggio dei parametri. Abbiamo visto che i programmi accedono alle system calls attraverso delle wrapper routine, le quali settano i valori passati da essi in appositi registri. l' uso dei registri per il passaggio dei parametri pone alcuni limiti:

visto il numero esiguo di registri disponibili su architetture Intel non più di 6 parametri è possibile passare alle system calls, in particolare, si utilizza il registro eax per contenere l' id della system call e ebx, ecx, edx, esi e edi per gli altri parametri. Questi registri, come già visto sopra, verranno salvati sul kernel stack subito dopo l' indirizzo di ritorno. Se si rende necessario passare più di 6 parametri, si può sempre usare un buffer, contenente uno o più parametri, puntato da uno dei registri.

La lunghezza di ciascun parametro non deve essere superiore alla size dei registri (32 bits per processori Intel). Questo, tuttavia, non è un problema visto che lo standard Posix stabilisce che per parametri con size maggiore di 32 bits si deve passare l' indirizzo.

I valori dei parametri memorizzati sullo stack, verranno facilmente prelevati dalla kernel routine che esegue la richiesta di servizio attraverso una semplice dichiarazione come questa:

int sys_xxx(type1 param1, type2 param2, ...) {

...

}

delegando così al compilatore il ripristino dei parametri dal kernel stack. Per ragioni di sicurezza la prima cosa che dovrebbe fare questa routine è quella di controllare la validità dei parametri per evitare che una irregolare richiesta blocchi l' intero sistema.

Come già anticipato precedentemente, ad ogni system call è associata una wrapper routine definita attraverso una delle macro _syscallx, con x che può assumere un valore da 0 a 5. Questo valore indica il numero di parametri che accetta la routine. I primi due parametri di questa macro sono il tipo di ritorno della system call e il suo nome. Successive coppie di parametri indicano il tipo e nome del parametro della system call.

All' inizio di questo paragrafo abbiamo visto la dichiarazione della macro _syscall0 che, espansa con i parametri (int, fork) da vita alla wrapper routine della system call fork.

extern inline int fork(void) \

{ \

type __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_fork)); \
if (__res >= 0) \

return __res; \

errno = -__res; \
return -1; \

}

__NR_fork denota l' entry della system call dispatcher table che punta alla routine di servizio sys_fork.

System calls: step4

In questo step implementeremo l' infrastruttura necessaria per la gestione delle system calls. Per scopi di test definiremo una system call di debug, che chiameremo print, per la stampa di messaggi a video.

int print(char *msg);

Iniziamo con il creare due nuovi files sys.h e sys.c. Nel primo file definiremo la size max della system call dispatcher table

#define NR_SYSCALLS 1

mentre nel secondo definiremo la tabella vera e propria.

extern asmlinkage int sys_print(char *msg);
void *sys_call_table[] = { sys_print };

Com'è facile osservare, sys_print sarà la kernel routine che gestirà il servizio offerto dalla system call print. l' implementazione di questa routine di servizio si trova nel file console.c ed esegue una semplice printk. In questo step la system call print verrà invocata dal processo idle in kernel mode, quindi è sufficiente la printk per implementarla.

int sys_print(char *msg) {

printk(msg);
return 0;

}

A questo punto si crea il file unistd.h che conterrà la definizione delle macro per le wrapper routines.

#define __NR_print 0

#define _syscall1(type,name,atype,a) \

type name(atype a) \
{ \

type __res; \
__asm__ volatile ("int $0x80" \

: "=a" (__res) \
: "0" (__NR_##name),"b" (a)); \

if (__res >= 0) \

return __res; \

errno = -__res; \
return -1; \

}

extern int errno;
int print(char *msg);


Si osservi come alla system call sia stato associato l' id __NR_print = 0. A questo punto, quando un processo user mode invoca la wrapper routine print, imposterà il registro eax con l' id 0, il registro ebx con l' indirizzo del messaggio e invocherà il software interrupt 0x80.

A questo interrupt si associerà un trap gate nel file traps.c attraverso la seguente riga di codice.

set_system_gate(0x80, &system_call);

Questa routine imposterà l' entry 0x80 della IDT nel modo seguente:

selettore = kernel code segment;

offset = indirizzo della routine system_call;

type = 15 per indicare che si tratta di una trap, cioè non disabilita gli interrupts mascherabili;

DPL=3 questo permette ai processi user mode di invocare l' exception handler.

Il codice dell' handler system_call, definito nel file asm.S, è il seguente.

.extern sys_call_table
ENTRY(system_call)

SAVE_ALL
STI

cmpl $NR_SYSCALLS-1, %eax
ja 1f

pushl %edx
pushl %ecx
pushl %ebx

call *sys_call_table(,%eax,4)
addl $3*4, %esp

1:

RET

Innanzittutto vengono salvati tutti i registri attraverso la consueta macro SAVE_ALL. Poi si controlla se l' id della system call in eax è valido, si salvano i restanti registri non salvati con la precedente macro e si invoca la routine di servizio attraverso la system call dispatcher table. Terminata quest'ultima si esegue la RET che preleva l' indirizzo al top dello stack e fa un salto ad esso. Come per magia al top dello stack è presente l' indirizzo della routine ret_from_intr dichiarata sempre nello stesso file.

ret_from_intr:

CLI
popw %gs
popw %fs
popw %es
popw %ds
popa

IRET

Questa routine non fa altro che ripristinare tutti i registri e invocare l' istruzione IRET.

Infine testiamo tutto il nostro lavoro invocando la system call mediante il processo idle nella routine start_kernel del file main.c.

void start_kernel(void) {

....
#ifdef DEBUG
print("System call: ok!\n");
#endif

while(1);

}

Affinchè questa system call funzioni anche da processi in user mode, è necessario copiare il messaggio da user space a kernel space, cosa che vedremo nel successivo articolo.

Conclusioni

Con questo articolo penso che abbiamo aggiunto al nostro kernel un altro importante tassello: la gestione degli interrupt & system calls. Nel prossimo articolo inizieremo ad introdurre le strutture dati principali per la gestione dei processi, compresa la realizzazione di uno scheduler e una gestione della memoria fisica minimale.

Bibliografia

[1] Linux Boot Process

[2] Keyboard & Console Programming

[3] Codice sorgente del kernel Pablox
http://www.xoom.virgilio.it/javaman

[4] Codice sorgente del kernel Scaraos
http://www.scaramanga.co.uk/scaraOS

[5] Understanding Linux Kernel
Bovet & Cesati
O' Reilly

[6] Linux Internals
Second Edition
Michael Beck & Harold Boheme
Addison Wesley

[7] INTEL CORPORATION - “80386 Programmer's Manual”

[8]
http://www.programmazione.it – Articolo sul sistema operativo Prometeus di Antonio Mazzeo

[9] Linux source code 0.96a
http://www.kernel.org

[10] Linux source code 1.0
http://www.kernel.org

[11] Progettazione e sviluppo dei sistemi operativi
Edizione Italiana
Andrew S Tanembaum
Paolo Ciancarino

Gruppo Editoriale Jackson

[12] IA-32 Intel Architecture
Software Developer's Manual
Volume 1: Basic Architecture
http://www.intel.com

[13] IA-32 Intel Architecture
Software Developer's Manual
Volume 3: System Programming Guide
http://www.intel.com