- Determinare quali interrupt possono bloccare l'esecuzione corrente e determinare un context-switch verso la propria routine di gestione.
- Determinare quali operazioni in Kernel-mode sono disponibili durante l'esecuzione di una routine.
- Determinare la priorità tra i vari device-interrupt (interrupt generati dall'hardware).
L'architettura di Windows NT ( a detta del DDK ) segue l'idea di un sistema Always-preemptible and interruptible. Con ciò si vuole indicare che durante l'esecuzione di un qualsiasi thread può verificarsi un evento (interrupt) che può bloccare la CPU, determinare un context-switch ed eseguire la routine di gestione dell'interrupt che si è verificato. Chiaramente l'architettura NT definisce in modo rigoroso come vengono trattati i vari interrupt.
2) Cosa significa pre-emptive?
La descrizione di pre-emptie riportanta in questo post è abbastanza superficiale perchè dobbiamo concentrarci sugli IRQL e non su come funzionano gli interrupt. Per maggiori informazioni utilizzate il link http://www.itis.mn.it/inform/gestint/index.htm
Gli odierni calcolatori ( quasi tutti ) sono strutturati con un'architettura event-driven. (guidata da eventi).
Esistono un numero finito di interrupt hardware che possono verificarsi durante il ciclo di operatività di una macchina.Sono in numero finito perchè sono legati alla CPU. Nelle architetture intel, ad esempio, troviamo la tavola degli interrupt (http://www.itis.mn.it/inform/gestint/tabint.htm)
(Ogni sistema operativo DEVE saper gestire tutti questi interrupt.)
Durante il ciclo di operatività di una macchina ci saranno X processi in esecuzione. Essendoci una sola CPU ( oggi più di una ma il discorso è lo stesso ) tutti i processi CONCORRERANNO ( si scrive così????) per utilizzarla.
Nei vecchi sistemi operativi, progettati per hardware non event-driven, il Kernel assegnava la CPU a un processo e attendeva che quest'ultimo gli ritornasse il controllo, prima di passare al processo successivo.
Ogni processo doveva essere scritto in modo da ritornare dopo un determinato tempo T per poter rendere il sistema simil-multitasking. Se un processo si bloccava, o era malizioso, poteva non ritornare e bloccare il pc.
Un sistema pre-emptive, invece, non aspetta che i programmi cedano volontariamente il controllo del processore al Sistema operativo. Prima che la CPU esegua il codice di un thread viene assegnato un tempo T in un registro del processore, si passa in user-mode, e solo dopo sia avvia l'esecuzione. Allo scadere del tempo T la CPU riceverà un interrupt hardware. Nella tavola degli interrupt ( che viene preventivamente scritta in fase di boot ) si recupererà la routine di gestione dell'interrupt, si passerà in user-mode e, quindi, il controllo ritornerà al kernel.
La prelazione (preemptible) è l'atto di interrompere un programma a prescindere dalla volontà del programma stesso, ciò avviene grazie a delle particolari strutture hardware integrate nel microprocessore che automatizzano il cambio di contesto (context switch):
in questo caso non solo lo scheduler interviene nelle circostanze previste da uno scheduler senza prelazione, ma anche in
casi quali:il passaggio di un programma dallo stato di esecuzione allo stato di pronto per essere eseguito;il passaggio di un programma dallo stato di attesa allo stato di pronto per essere eseguito;Un multitasking con prelazione (preemptive) non può quindi essere implementato se la piattaforma hardware non mette a disposizione gli strumenti necessari, ma in compenso, grazie all'hardware, il cambio di contesto è molto più efficiente favorendo l'adozione di quanti di tempo regolari e una esecuzione più "fluida" dei vari processi. Il preemptive multitask è stato adottato dalla maggior parte dei sistemi operativi
3) Cosa dice il DDK sugli IRQL
L'obbiettivo di un'architettura always preemptible-interruptible è quella di massimizzare le performance di sistema. Ogni thread può essere interrotto da un thread di priorità superiore così come ogni routine di gestione di interrupt e ogni routine di gestione degli interrupt dei driver (ISR) possono essere interrotte da routine di priorità superiore.
I componenti del kernel (scheduler, dispatcher) determinano quando un thread deve essere eseguito e quando deve essere interrotto, in base ai criteri descritti di seguito.
1° criterio : Lo schema di gestione delle priorità del kernel
(l'algoritmo usato dallo scheduler)
Ogni thread, in esecuzione nel sistema, ha associato un attributo priorità.
Generalmente molti thread hanno questo attributo con valore variabile. Questi tipi di thread sono sempre interrompibili e sono schedulati con un algoritmo di tipo round-robin. Concorrono tuttiall'utilizzo del processore con lo stesso livello di priorità e il sistema opeartivo cerca di garantire ad ognuno di essi la stessa quantità di tempo CPU.
Altri thread invece hanno priorità real-time.
E' bene fare una distinzione tra i diversi tipi di real-time:
- Hard real-time : Un processo hard real-time DEVE ESSERE ESEGUITO e COMPLETATO in un tempo STABILITO. Se il sistema NON PUO' ASSICURARE il completamento del thread, nel tempo limite, fallisce. (crash).
Un esempio di sistema realtime è la catena di montaggio delle auto. Un braccio meccanico DEVE essere sincronizzato perfettamente... oppure deve bloccare l'intera catena per evitare danni sulle parti successive.
- Soft real-time : Un processo soft real-time DOVREBBE ESSERE ESEGUITO e COMPLETATO in un tempo STABILITO. Tuttavia se il sistema non riesce ad assicurarne il completamento, nel tempo limite, non fallisce. L'architettura NT supporta questo tipo di real-time ( da task-manager può essere applicata questa priorità ad un processo) ma non supporta l'hard real-time.
I thread che girano a livello real-time (soft, parliamo di windows adesso) dovrebbero essere eseguiti fino al completamento nel minor tempo possibile a meno che non vengano interrotti da thread di livello real-time ancora superiore.
2° criterio : Il valore IRQL assegnato al codice in esecuzione.
Il kernel assegna una priorità elevata agli interrupt Hardware(interrupt,exception,fail) e software(i trap), associandoli ad un IRQL alto,in modo che, il codice di gestione operante in kernel-mode venga eseguito più celermente rispetto agli altri thread in user-mode.
Il livello di IRQL che il sistema associa ai vari interrupt ne determina quindi anche la priorità. L'interrupt che segnala
alla CPU una caduta di tensione è chiaramente più importante di un'interrupt che segnala la pressione di un tasto. Se la CPU è quindi occupata a gestire la pressione di un tasto SARA' interrotta se va via la luce.
Per schermare le routine di priorità massima dall'essere interrotte da routine di priorità più bassa si usa la tecnica della mascherazione gli interrupt. Durante l'esecuzione di una routine con IRQL=X il kernel maschera tutti gli interrupt di livello IRQL < X.
Il codice in Kernel-Mode è sempre interrompibile. Un interrupt di livello IRQL più alto può verificarsi in ogni momento e ciò determinerà un context-switch verso la routine di gestione dell'interrupt stesso.
Il livello più basso è chiamato PASSIVE_LEVEL. A questo livello nessun interrupt è mascherato. Il codice esguito a questo
livello potrà essere sempre interrotto da interrupt di livello superiore. Generalmente, tutti i thread in user-mode, come le applicazioni che utilizziamo normalmente, operano a questo livello.
I tre livelli immeditamente superiori a PASSIVE_LEVEL sono quelli riservati agli interrupt software ( INT xx, SystemCALL).
- APC_LEVEL
- DISPATCH_LEVEL
- WAKE_LEVEL: Utilizzato per il debug del kernel.
Ancora più in alto nella scala delle priorità troviamo gli interrupt associati alle periferiche identificati dal nome DIRQL. DIRQL in effetti identifica un range di interrupt e non un interrupt singolo. Il kernel riserva i valori IRQL più alti per associarli agli interrupt critici come ad esempio : SYSTEM CLOCK, BUS ERROR, POWER DOWN e così via.
Alcune routine fornite dal kernel operano in PASSIVE_LEVEL per uno dei seguenti motivi:
- perchè sono implementate come paginabili
- perchè accedono a memoria paginata
- perchè necessitano di avviare qualche thread.
Ricordiamo che in PASSIVE_LEVEL è possibile accedere alla memoria paginata mentre dal livello DISPATCH_LEVEL in poi NO.
In modo simile alcune routine dei driver operano in PASSIVE_LEVEL, come ad esempio le routine di inizializzazione quale DriverEntry. Tuttavia diverse routine dei driver operano a DISPACH_LEVEL e in caso di driver di basso livello che operano direttamente con l'hardware direttamente a livello DIRQL.
Ogni routine, in un driver, è interrompibile incluse tutte le routine che girano ad un livello superiore a PASSIVE_LEVEL. Chiaramente a causa della interrupt mask vista in precedenza ogni routine potrà essere interrotta da un interrupt di livello IRQL superiore. La routine, quindi, mantiene il controllo del processore finchè non arriva un interrupt che chiaramente prima o poi arriverà perchè il sistema è pre-emptive. L'interrupt che segnala al processore di fermare un processo è iniziarne un altro gira chiaramente con un IRQL superiore ai processi stessi.
A differenza dei driver che venivano sviluppati per i vecchi sistemi operativi, un windows una routine di gestione degli interrupt dei driver (da ora ISR) non è mai nè grande ne complessa ne fà tutto il lavoro di I/O. Questo perchè ogni ISR può essere interrotto da un altra ISR che lavora ad un IRQL superiore.
Così gli ISR non devono necessariamente mantenere il controllo della CPU, ininterrottamente, dall'inizio alla fine del loro lavoro. In un driver per windows una ISR salva lo stato corrente (per driver, delle periferiche etc) mette in coda una DPC ed esce immediatamente.
Successivamente il sistema recupera dalla coda la DPC e permette al driver di completare l'operazione di I/O ad un livello IRQL più basso ( in DISPATCH_LEVEL ).
Per avere buone prestazioni di sistema, tutte le routine che girano a livelli IRQL alti devono rilasciare il controllo della CPU il più velocemente possibile. (ecco perchè vengono scritte in assembler o in C ottimizzato al massimo.)
In windows, tutti i thread hanno un thread-context ( contesto ). Questo contesto consiste nelle informazioni che identificano il processo padre, lo stato, altre caratteristiche e i diritti di accesso.
In generale solo i driver di alto livello ( di più alto livello) vengono chiamati nel contesto del thread che ha richiesto l'operazione di I/O.
PENSIERO PERSONALE:
"Questa affermazione è importantissima. Nel primo tutorial abbiamo visto 3 metodi di I/O. Il terzo metodo era identificato come ne diretto ne bufferizzato e presumeva che il driver stesse girando nel contesto dell'applicazione user-mode. Infatti era presente un'istruzione try catch e delle istruzioni di probe per determinare se era possibile leggere e scrivere. Quella modalità di I/O funziona solo se il driver sta girando nello stesso contesto del processo chiamante"
Driver di livello medio-basso o driver di basso livello NON DEVONO MAI!!! assumere di essere nel contesto del thread che ha richiesto l'I/O.
Questa non sono riuscita a tradurla :(
Consequently, driver routines usually execute in an arbitrary thread
context—the context of whatever thread is current when a standard driver
routine is called.
Per motivi di performance (per evitare context-switch che fanno perdere tempo) quasi nessun driver utilizza thread secondari.
3) Come vengono gestiste le priorità hardware dal Kernel?
Il livello IRQL al quale una routine gira determina quali Api Kernel-Mode possono essere chiamate. Ad esempio alcune API in KernelMode richiedono che il thread chiamate stia girando ad IRQL = DISPATCH_LEVEL. Altre non possono essere chiamate in modo sicuro se il chiamante non si trova ad un livello più di PASSIVE_LEVEL.
Di seguito è riportata una lista di livelli IRQL nei quali molte Api Kernel possono essere chiamate. Gli IRQL sono riportati dalla priorità più bassa a quella più alta:
- PASSIVE_LEVEL
- Interrupt Mascherati: Nessuno
Driver Routines chiamate in PASSIVE_LEVEL — DriverEntry, AddDevice, Reinitialize, Unload routines, molte routine di tipo dispatch, driver-created threads, worker-thread callbacks.
- APC_LEVEL
- Interrupt Mascherati: APC_LEVEL interrupt.
Driver Routines chiamate in APC_LEVEL — Alcune routine di dispacth (Dispatch Routines and IRQLs).
- DISPATCH_LEVEL
- Interrupt Mascherati: DISPATCH_LEVEL e APC_LEVEL Device, clock, e power failure interrupts possono verificarsi.
Driver Routines chiamate in DISPATCH_LEVEL — StartIo, AdapterControl, AdapterListControl,ControllerControl, IoTimer, Cancel (while holding the cancel spin lock), DpcForIsr, CustomTimerDpc,CustomDpc routines.
- DIRQL
- Interrupt Mascherati: Tutti gli interrupt con IRQL<= DIRQL del driver che ha generato l'interrupt.
- Device interrupts con DIRQL più alti possono verificarsi come clock e power failure interrupts.
Driver Routines chiamate in DIRQL — InterruptService, SynchCritSection routines.
L'unica differenza tra APC_LEVEL e PASSIVE_LEVEL è che i processi che vengono eseguiti in APC_LEVEL non possono essere interrotti da altri processi in APC_LEVEL. Entrambi i livelli tuttavia implicano il fatto di trovarsi nello stesso contesto del chiamate ed implicano che il codice possa essere paginato.
I driver di basso livello processano gli IRP mentre girano ad uno di questi livelli:
I driver di alto livello, processano in genere gli IRP ai livelli:
In alcune circostanze driver di livello medio o basso, come ad esempio i driver per i device mass-storage(hd,memory) vengono chiamati al livello APC_LEVEL. In particolare questa situazione può verificarsi durante un errore di pagina, quando un driver del filesystem invia una richiesta di IRP_MJ_READ ad un driver di livello inferiore.
Molte SDR girano in un IRQL che permette di chiamare in modo semplice le API Kernel. Ad esempio un driver deve chiamare AllocateAdapterChannel mentre sta girando in DISPATCH_LEVEL. Molti driver infatti chiamano questa funzione nella routine standard (SDR) StartIo che effetivamente gira a DISPACH_LEVEL.
Un driver che non implemeta la routine StartIO perchè gestisce da sè la propria coda di IRP potrebbe non portarsi mai a DISPATCH_LEVEL. Se il driver vuole chiamare AllocateAdapterChannel deve in qualche modo arrivare a DISPATCH_LEVEL altrimenti la chiamata non andrà a buon fine. Per fare ciò esistono due funzioni che permettono di salire e scendere di IRQL. KeRaiseIRQL (il Ke sta per Kernel) e KeLowerIrql. La prima aumenta mentre la seconda ripristina l'IRQL originale.
Prima di utilizzare queste due funzioni è necessario ricordare che:
- KeRaiseIrql non può essere richiamata con un valore NewIRQL minore di quello corrente altrimenti si avrà un errore fatale.
- KeLowerIrql deve essere richiamata dopo la Raise altrimenti causa un errore fatale.
- Durante l'esecuzione ad un IRQL >= DISPATCH_LEVEL chiamare KeWaitForSingleObject oppure KeWaitForMultipleObject per oggetti definiti dal kernel, per intervalli non zero, causa errori fatali.
- Le uniche routine che possono attendere eventi, semafori, mutex o timer sono quelle routine che girano in contesti non arbitrari al livello PASSIVE_LEVEL come ad esempio thread creati dai driver, DriverEntry, Reinialize e routine di dispatch inerenti operazioni di I/O sincrone.
- Anche durante l'esecuzione in PASSIVE_LEVEL codice segnato come paginabile non deve mai chiamare le funzioni KeSetEvent, KeReleaseSemaphore oppure KeReleaseMutex con il parametro wait settato a TRUE. Ciò potrebbe causare un errore page-fault.
- Tutte le routine che girano ad un livello maggiore di APC_LEVEL non possono ne allocare memoria dal pool paginabile ne accedere alla memoria paginata. Se una routine gira in APC_LEVEL e causa un page-fault tale errore viene trattato come fatale. (BSOD)
- Un driver deve girare in DISPATCH_LEVEL prima di richiamare KeAcquireSpinLockAtDpcLevel e KeReleaseSpinLockFromDpcLevel.
- Un driver che gira con un IRQL <= DISPATCH_LEVEL può richiamare KeAcquireSpinLock ma deve rilasciarlo richiamando KeReleaseSpinLock. In altre parole, è un errore di programmazione rilasciare uno spin lock preso con la funzione KeAcquireSpinLock tramite la funzione KeReleaseSpinLockFromDpcLevel.
- Un driver non deve mai chiamare KeAcquireSpinLockAtDpcLevel, KeReleaseSpinLockFromDpcLevel, KeAcquireSpinLock, KeReleaseSpinLock mentre gira con IRQL>=DISPATCH_LEVEL
- Chiamare una routine di supporto che usa gli spin lock, come ad esempio una routine ExInterlockedXXX aumenta il livello IRQL a DISPATCH_LEVEL o DIRQL se il chiamante non gira già al livello esatto.
- Il codice dei driver che gira ad un IRQL > PASSIVE_LEVEL deve essere eseguito il più presto possibile. Più alto è il valore IRQL dove gira la routine più importate è avere performance ottimali per eseguire il codice il più presto possibile. Ad esempio un driver che chiama KeRaiseIrql deve richiamare KeLowerIrql il più presto possibile.
PENSIERO PERSONALE:
"Questo post termina qui. Ho tentato di tradurre correttamente le due sezioni di introduzione del DDK che introducono il concetto di IRQL. Eventuali segnalazioni di errori sintattici, semantici, grammaticali, di traduzione sono graditi in modo da rendere le informazioni quanto più complete possibili"