Ecco una vista di alto livello dell'elaborazione di basso livello. Sto descrivendo una semplice architettura tipica, le architetture reali possono essere più complesse o differire in modi che non contano a questo livello di dettaglio.
Quando si verifica un'interruzione, il processore controlla se le interruzioni sono mascherate. Se lo sono, non succede nulla finché non vengono smascherati. Quando gli interrupt vengono smascherati, se ci sono degli interrupt in sospeso, il processore ne sceglie uno.
Quindi il processore esegue l'interruzione ramificandosi verso un particolare indirizzo in memoria. Il codice a quell'indirizzo è chiamato gestore di interrupt. Quando il processore si ramifica lì, maschera gli interrupt (quindi il gestore di interrupt ha il controllo esclusivo) e salva il contenuto di alcuni registri in qualche posto (tipicamente altri registri).
Il gestore di interrupt fa ciò che deve fare, tipicamente comunicando con la periferica che ha attivato l'interrupt per inviare o ricevere dati. Se l'interruzione è stata generata dal timer, il gestore potrebbe attivare lo scheduler del sistema operativo per passare a un thread diverso. Quando il gestore termina l'esecuzione, esegue una speciale istruzione di ritorno dall'interrupt che ripristina i registri salvati e smaschera gli interrupt.
Il gestore di interrupt deve essere eseguito rapidamente, perché impedisce l'esecuzione di qualsiasi altro interrupt. Nel kernel di Linux, l'elaborazione degli interrupt è divisa in due parti:
- La "metà superiore" è il gestore di interrupt. Fa il minimo necessario, in genere comunica con l'hardware e imposta un flag da qualche parte nella memoria del kernel.
- La "metà inferiore" esegue qualsiasi altra elaborazione necessaria, ad esempio copiando i dati nella memoria del processo, aggiornando le strutture dei dati del kernel, ecc. Può richiedere tempo e persino bloccarsi in attesa di qualche altra parte del sistema poiché funziona con gli interrupt abilitato.
Come di consueto su questo argomento, per maggiori informazioni, leggi Linux Device Drivers; il capitolo 10 riguarda gli interrupt.
Gilles ha già descritto il caso generale di un interrupt, quanto segue si applica specificamente a Linux 2.6 su un'architettura Intel (anche parte di questo si basa sulle specifiche di Intel).
Un interrupt è un evento che modifica la sequenza di istruzioni eseguite dal processore.
Ci sono due diversi tipi di interrupt:
- Interrupt sincrono (eccezione) prodotto dalla CPU durante l'elaborazione delle istruzioni
- Interrupt asincrono (Interrupt) emessi da altri dispositivi hardware
Le eccezioni sono causate da errori di programmazione (ad es. Errore di divisione , Errore di pagina , Overflow ) che deve essere gestito dal kernel. Invia un segnale al programma e cerca di recuperare dall'errore.
Le seguenti due eccezioni sono classificate:
- Eccezione rilevata dal processore generato dalla CPU durante il rilevamento di una condizione anomala; divisi in tre gruppi:Guasti generalmente possono essere corretti, Trappole segnalare un'esecuzione, Interruzione sono errori gravi.
- Eccezione programmata richiesto dal programmatore, gestito come una trappola.
Gli interrupt possono essere emessi da dispositivi I/O (tastiera, scheda di rete, ..), timer di intervallo e (su sistemi multiprocessore) altre CPU. Quando si verifica un interrupt, la CPU deve interrompere l'istruzione corrente ed eseguire l'interrupt appena arrivato. Ha bisogno di salvare il vecchio stato del processo interrotto per (probabilmente) riprenderlo dopo che l'interruzione è stata gestita.
La gestione degli interrupt è un compito delicato:
- Gli interrupt possono verificarsi in qualsiasi momento, il kernel cerca di eliminarli il prima possibile
- Un interrupt può essere interrotto da un altro interrupt
- Ci sono regioni nel kernel che non devono essere affatto interrotte
Sono definiti due diversi livelli di interrupt:
- Interruzioni mascherabili emessi dai dispositivi I/O; può essere in due stati, mascherato o non mascherato. Vengono elaborati solo gli interrupt non mascherati.
- Interrupt non mascherabili; malfunzionamenti critici (es. guasti hardware); sempre elaborato dalla CPU.
Ogni dispositivo hardware ha la propria riga IRQ (Interrupt Request). Gli IRQ sono numerati a partire da 0. Tutte le linee IRQ sono collegate a un PIC (Programmable Interrupt Controller). Il PIC ascolta gli IRQ e li assegna alla CPU. È anche possibile disabilitare una linea IRQ specifica.
I moderni sistemi Linux multiprocessing generalmente includono il più recente Advanced PIC (APIC), che distribuisce equamente le richieste IRQ tra le CPU.
Il passaggio intermedio tra un'interruzione o un'eccezione e la sua gestione è l'Interrupt Descriptor Table (IDT). Questa tabella associa ciascun vettore di interrupt o eccezione (un numero) a un gestore specificato (ad es. Errore di divisione viene gestito dalla funzione divide_error()
).
Attraverso l'IDT, il kernel sa esattamente come gestire l'interruzione o l'eccezione avvenuta.
Quindi, cosa fa il kernel quando si verifica un interrupt?
- La CPU controlla dopo ogni istruzione se c'è un IRQ dall'(A)PIC
- In tal caso, consulta l'IDT per mappare il vettore ricevuto a una funzione
- Controlla se l'interruzione è stata emessa da una fonte autorizzata
- Salva i registri del processo interrotto
- Chiama la funzione corrispondente per gestire l'interrupt
- Carica i registri salvati di recente del processo interrotto e prova a riprenderlo
Prima di tutto i partecipanti coinvolti nella gestione degli interrupt sono dispositivi hardware periferici, controller di interrupt, CPU, kernel del sistema operativo e driver. I dispositivi hardware periferici sono responsabili della generazione di interrupt. Asseriscono linee di richiesta di interruzione quando richiedono attenzione dal kernel del sistema operativo. Questi segnali sono multiplexati dal controller di interrupt, che è responsabile della raccolta dei segnali di interrupt. È anche responsabile della determinazione dell'ordine in cui i segnali di interruzione verranno trasmessi alla CPU. Il controller di interrupt è in grado di disabilitare temporaneamente una particolare linea di richiesta di interrupt (IRQL) e riabilitarla nuovamente (mascheramento IRQL). Il controller di interrupt trasmette le richieste di interrupt raccolte alla CPU in sequenza. La CPU dopo il completamento dell'esecuzione di ogni istruzione La CPU controlla se ci sono richieste di interrupt in attesa dal controller di interrupt. Se la CPU rileva una richiesta in attesa E il flag di attivazione dell'interruzione è impostato nel registro di controllo interno della CPU, la CPU avvia la gestione dell'interruzione. Come puoi vedere, manipolando il flag di interrupt nella CPU e comunicando con il controller di interrupt, il kernel Linux è in grado di controllare l'accettazione dell'interrupt. Ad esempio, Linux può disabilitare l'accettazione degli interrupt da un particolare dispositivo o disabilitare del tutto l'accettazione degli interrupt.
Cosa succede quando il processore riceve una richiesta di interrupt? In primo luogo, la CPU disabilita automaticamente gli interrupt reimpostando il flag di interrupt. Verranno riabilitati una volta terminata la gestione degli interrupt. Allo stesso tempo, la CPU effettua una quantità minima di lavoro necessaria per passare dalla modalità utente alla modalità kernel in modo tale da consentirle di riprendere l'esecuzione del codice interrotto. La CPU si consulta con speciali strutture di controllo della CPU riempite dal kernel Linux per trovare un indirizzo di codice a cui verrà passato il controllo. Questo indirizzo è l'indirizzo della prima istruzione del gestore di interrupt, che fa parte del kernel di Linux.
Come primo passo nella gestione dell'interrupt, il kernel identifica il vettore dell'interrupt ricevuto per identificare quale tipo di evento si è verificato nel sistema. Il vettore di interruzione definisce quali azioni intraprenderà Linux per gestirlo. Come secondo passaggio, Linux salva il resto dei registri della CPU (che non sono stati salvati automaticamente dalla CPU) e che potenzialmente possono essere utilizzati dal programma interrotto. Questa è un'azione molto importante, perché consente a Linux di gestire gli interrupt in modo trasparente rispetto al programma interrotto. Come terzo passaggio, Linux esegue il passaggio alla modalità kernel impostando l'ambiente del kernel e impostando lo stato della CPU richiesto per esso. E infine, vector viene chiamato il gestore di interrupt dipendente. (Puoi guardare la macro BUILD_INTERRUPT3 in arch\x86\kernel\entry_32.S per ottenere i dettagli aggiuntivi per l'esempio relativo all'architettura x86) Nel caso di dispositivi periferici questa è una routine do_IRQ(). (Guarda in arch\x86\kernel\irq.c)
Il gestore di interrupt dipendente dal vettore di solito è racchiuso dalle chiamate a irq_enter() e irq_exit(). L'area di codice racchiusa all'interno di una coppia di queste funzioni, è atomica rispetto a qualsiasi altra area simile ed è anche atomica rispetto a coppie di cli/sti. Irq_enter() e irq_exit() catturano anche alcune statistiche relative alla gestione degli interrupt. Infine, il kernel cerca nella tabella vector_irq il numero irq assegnato al vettore dell'interrupt ricevuto e chiama handle_irq() (da arch\x86\kernel \irq_32.c).
A questo punto la parte comune della gestione degli interrupt in Linux termina, perché il kernel cerca la routine del gestore di interrupt dipendente dal dispositivo installata dal driver del dispositivo come parte del descrittore irq e lo invoca. Se tale gestore non è stato installato dal driver, il kernel riconosce semplicemente l'interrupt sul controller di interrupt e va a uscire dal gestore di interrupt generale.
Dopo la fine della gestione degli interrupt, il kernel ripristina lo stato del programma precedentemente interrotto e riprende l'esecuzione di questo programma.