Ponieważ Linux działa w trybie chronionym, więc cały poniższy opis dotyczy właśnie tego trybu.
Przerwaniami i wyjątkami (ang. interrupts and exceptions)
nazywamy takie zmiany przepływu sterowania
w programie, które powodują wywołanie specjalnej, wspólnej dla wszystkich
programów, procedury obsługi (ang. handler). Nie są to więc lokalne
procedury, lecz globalne metody obsługi pewnych specjalnych sytuacji.
Zasadniczo przerwania i wyjątki mogą mieć trzy przyczyny:
Każde przerwanie lub wyjątek ma przypisaną jednobajtową nieujemną liczbę całkowitą, zwaną wektorem (ang. interrupt vector). Wektor służy do identyfikacji przerwania lub wyjątku.
Stare procesory Intela (w tym niektóre modele Pentium) miały
następujące przyporządkowanie źródeł przerwań zewnętrznych:
Przerwanie NMI nie może zostać zablokowane przez procesor (stąd nazwa). Związany jest z nim wektor 2. Wystąpienie przerwania NMI oznacza bardzo poważne kłopoty.
Przerwania sygnalizowane na INTR pochodzą ze sterownika przerwań zewnętrznych, tak zwanego PIC (Programmable Interrupt Controller). W komputerach typu PC AT używany jest układ 8259A, podłączony do INTR na procesorze. Do tego układu jest podłączony drugi, również 8259A. W komputerach PC XT znajduje się tylko jeden taki układ.
Przerwania pochodzące z INTR są maskowalne, tzn. procesor może odmówić przyjmowania tych przerwań (w szczególności jeśli flaga IF jest wyzerowana, to tak się dzieje). Jeżeli PIC chce przekazać procesorowi przerwanie, to wysyła mu również wektor tego przerwania. teoretycznie ten wektor może być dowolną liczbą od 0 do 255. W praktyce przerwań sprzętowych jest 16 i są im przypisane konkretne numery (opisuję to później).
Przerwanie może zostać wywołane przez program, gdy wykona on instrukcję
INT n,
gdzie n jest dowolnym wektorem. Instrukcję tę można wykonać niezależnie
od wartości flagi IF. W wypadku wywołania z wektorem przerwania NMI
wołana jest procedura obsługi tego przerwania, ale nie są wykorzystywane
żadne specjalne mechanizmy sprzętowe normalnie używane przy NMI.
Wyjątki dzieli się na trzy rodzaje, przy których angielskich nazwach
pozostanę: faults,traps i aborts. Różnią się one
zachowaniem procesora podczas ich wystąpienia. Wyjątki mają na stałe
przyporządkowane numery, tak więc można zawsze powiedzieć, jakiego wyjątek
o numerze n jest rodzaju. Oto różnice między rodzajami wyjątków:
Wektor | Skrót | Nazwa | Rodzaj | Kod błędu | Źródło |
---|---|---|---|---|---|
0 | #DE | Błąd dzielenia | Fault | - | DIV i IDIV |
1 | #DB | Debug | Fault/Trap | - | Różnie |
2 | -- | NMI | Przerwanie | - | sygnał NMI |
3 | #BP | Pułapka | Trap | - | INT 3 |
4 | #OF | Nadmiar | Trap | - | INTO |
5 | #BR | Indeksowanie poza tablicę | Fault | - | BOUND |
6 | #UD | Niezdefiniowana instrukcja | Fault | - | UD2 (Pentium) lub niedozwolona instrukcja |
7 | #NM | Brak koprocesora | Fault | - | Instrukcja zmiennopozycyjna lub WAIT |
8 | #DF | Błąd podwójny | Abort | + | Różnie |
9 | -- | ??? | Fault | - | Instrukcja zmiennopozycyjna |
10 | #TS | Błąd w TSS | Fault | + | Zmiana zadania lub odwołanie do TSS |
11 | #NP | Brak segmentu | Fault | + | Ładowanie rejestrów segmentowych itp. |
12 | #SS | Błąd segmentu stosu | Fault | + | Operacje na stosie lub SS |
13 | #GP | Błąd ochrony | Fault | + | Nieuprawniony dostęp |
14 | #PF | Błąd stronicowania | Fault | + | Odwołanie do pamięci |
15 | -- | zarezerwowane | -- | - | |
16 | #MF | Błąd zmiennopozycyjny | Fault | - | Instrukcja zmiennopozycyjna lub WAIT |
17 | #AC | ???Alignment Check | Fault | + | Odwołanie do pamięci |
18 | #MC | ???Machine Check | Abort | - | Zależy od modelu |
19-31 | -- | zarezerwowane | |||
32-255 | -- | Przerwania niezarezerwowane | Przerwanie | Sterownik 8259A lub INT n |
Maskować (blokować) można jedynie przerwania otrzymywane przez procesor poprzez INTR. Jeżeli flaga IF jest wyzerowana, to przerwania pochodzące z INTR są ignorowane. Natomiast wyjątki generowane przez procesor oraz przerwanie NMI nie są blokowane. Również instrukcja INT n wykonuje się normalnie. Teoretycznie z INTR procesor może otrzymać wektor, który odpowiada wyjątkowi, a nie przerwaniu (np. 14). Takiej sytuacji nie nazywamy wyjątkiem, lecz przerwaniem - takie przerwanie jest maskowalne i nie wykonuje się jak wyjątek (tzn. kod błędu nie jest umieszczany na stosie).
Przerwanie NMI nie jest maskowalne, jednak procesor automatycznie blokuje dalsze przerwania NMI do wykonania najbliższej instrukcji IRET. Dlatego NMI należy obsługiwać z IF=0 - inaczej IRET od zwykłego przerwania ponowanie odblokuje NMI.
Istnieje jeszcze flaga RF (ang. Resume Flag), która maskuje wyjątek #DB, ale nie będę jej tu omawiał.
IDT, czyli Interrupt Descriptor Table jest tablicą systemową, w której każdemu z 256 wektorów odpowiada jeden deskryptor bramy. W rejestrze IDTR znajduje się adres IDT (tzn. 32 bity adresu bazowego i 16 bitów ograniczenia). Jeżeli procesor ma obsłużyć przerwanie lub wyjątek o wektorze x, to po wykonaniu czynności wstępnych (np. umieszczeniu kodu błędu na stosie), znajduje początek IDT patrząc na IDTR, potem dodaje do tego 8*x (8 jest rozmiarem deskryptora) i przechodzi przez bramę określoną przez ten deskryptor.
Deskryptory bram dzielą się na trzy rodzaje:
Procesor, chcąc wywołać przerwanie lub wyjątek x robi co następuje:
Powrót z procedury obsługi następuje przez instrukcję IRET. Wykonuje ona zwykły powrót zdejmując jeszcze na koniec flagi ze stosu. Jeżeli jest to konieczne, to następuje ponowna zmiana stosów. Zdjęcie ze stosu kodu błędu nie następuje automatycznie - procedura obsługi musi go sama usunąć.
Błąd podwójny jest błędem typu Abort. Występuje on, gdy procesor wygeneruje jedno przerwanie lub wyjątek podczas obsługiwania drugiego. Zwykle taka sytuacja nie powoduje błędu, jednak na przykład podniesienie #PF (Page Fault podczas obsługiwania #PF generuje #DF. Po wygenerowaniu #DF w zasadzie nic się już nie da zrobić, można jedynie wyświetlić przykry komunikat.
W komputerach typu PC XT przerwania od urządzeń zewnętrznych były przekazywane do układu 8259, który następnie mógł je przekazać procesorowi. Układ ten, zwany PIC, był przyłączony do wejścia INTR procesora i przez to wejście sygnalizował żądanie obsługi przerwania. Do PIC można było przyłączyć 8 urządzeń, z których każde generowało inne przerwanie. Tych 8 wejść PIC, do których można przyłączyć urządzenia, nazywa się liniami IRQ (ang. Interrupt Request Lines). Tak więc na PC XT można było obsługiwać co najwyżej 8 różnych przerwań sprzętowych.
Do układu 8259A może być podłączonych 8 linii IRQ. Sygnały z tych linii przechodzą przez ESR (Edge Sense Register) do IRR (Interrupt Request Register). Niektóre z nich mogą być w danym momencie zamaskowane (jeżeli system operacyjne wysłał do PIC odpowiednia komendę). W rejestrze IMR (Interrupt Mask Register) znajduje się maska przerwań. Jeżeli bit x jest ustawiony, to linia IRQ o numerze x jest blokowana (tzn. przerwania z niej nie są przyjmowane). PIC posiada jeszcze jeden rejestr - bit x jest w nim ustawiony, jeśli PIC wysłał do na swoje wyjście (czyli zwykle do procesora) żądanie przerwania, a nie otrzymał jeszcze sygnału EOI, oznaczającego zakończenie obsługi przerwania. Ten rejestr nazywa się ISR (Interrupt Service Register).
Przerwania na PIC mają swoje priorytety - przerwania przyłączone do linii IRQ o niższych numerach mają wyższy priorytet. Linie IRQ numeruje się od zera do siedmiu (IRQ0-IRQ7).
Jako pierwszy przykład zobaczmy, co się dzieje, jeśli jesteśmy na PC XT, żadne przerwania nie są ani zamaskowane, ani obsługiwane, ani aktywne:
Procesor otrzymuje od PIC sygnały przez wejście INTR i reaguje na nie automatycznie. Jeżeli jednak chcemy wysłać do PIC sygnał EOI albo zamaskować jakieś przerwanie, to jest nam potrzebna jakaś droga komunikacji. Tą drogą są specjalne porty wejścia-wyjścia, przez które można kontrolować PIC. Dla PC XT są to 0x20 i 0x21. Służą one m.in. do inicjalizacji PIC, ale o tym nie będę pisał.
Chcąc zamaskować przerwanie IRQ o numerze x musimy mieć dostęp do rejestru IMR układu 8259A. Rejestr ten znajduje się pod portem wejścia-wyjścia o numerze 0x21. Jedynka w tej masce oznacza, że przerwanie jest zamaskowane. Jeżeli nie wiemy, jak wygląda teraz maska, a chcemy zamaskować przerwanie IRQ x, to musimy najpierw odczytać ten port, a następnie zapisać do tego portu (wartość odczytana OR 1<<x).
Z kolei komendy do PIC wysyłamy do portu 0x20, gdzie znajduje się tzw. Interrupt Command Register. Przykładowo tzw. komenda non-specific End of Interrupt ma numerz 0x20, zatem wysyłamy ją pisząc do portu 0x20 liczbę 0x20.
Sygnał End of Interrupt jest bardzo ważny - jeżeli nie zostanie wysłany, to żadne przerwanie o priorytecie mniejszym równym od tego, na które nie odpowiedzielismy EOI, nie zostanie obsłużone. Wysłanie EOI powoduje ponowne odblokowanie tych przerwań.
Chcąc pisać do lub czytać z rejestrów PIC musimy wcześniej zablokować przerwania instrukcją cli. Ponieważ w momencie otrzymania przerwania, które jest obsługiwane przez bramę przerwań, procesor sam zeruje flagę IF, więc przy wejściu do procedury obsługi możemy od razu działać na portach PIC.
Ponieważ na PC XT było tylko 8 linii przerwań, więc szybko zaczęto odczuwać brak wolnych linii. W związku z tym połączono ze sobą dwa układy 8259A, które łącznie zapewniały obsługę 16 linii przerwań. Ponieważ wejście INTR na procesorze pozostało tylko jedno, więc połączono je w łańcuch - drugi PIC jest podłączony do pierwszego, które jest podłączony do INTR na procesorze. Pierwszy PIC jest nazywany Master PIC, drugi Slave PIC. Drugi PIC jest na komputerach AT podłączony do linii IRQ2 w pierwszym PIC. W związku z tym przerwanie IRQ2 nie może być już wykorzystane przez żadne inne urządzenie (czyli mamy naprawdę 15 wolnych linii przerwań).
Jak już wspominałem, priorytet mają przerwania o niższych numerach. Ze względu na podłączenie drugiego PIC do IRQ2, najwyższy priorytet mają przerwania IRQ0, IRQ1 (pierwszy PIC), dalej IRQ8-IRQ15 (drugi PIC), a na koniec IRQ3-IRQ7 (pierwszy PIC). Skutkiem użycia dwóch układów 8259A mamy też kolejne dwa porty wejścia-wyjścia do obsłużenia:
Niektórym numerom IRQ przypisane są standardowe funkcje. Funkcje te opisane są w następującej tabeli:
Numer przerwania | Numer sterownika | Funkcje |
---|---|---|
IRQ0 | 1 | Zegar systemowy - wyjście 0 |
IRQ1 | 1 | Klawiatura |
IRQ2 | 1 | "Cascade" - połączenie z drugim PIC |
IRQ3 | 1 | Port szeregowy, inne |
IRQ4 | 1 | Port szeregowy, inne |
IRQ5 | 1 | Audio, inne |
IRQ6 | 1 | Stacja dyskietek |
IRQ7 | 1 | Port równoległy |
IRQ8 | 2 | Zegar czasu rzeczywistego |
IRQ9-12 | 2 | Różnie |
IRQ13 | 2 | Koprocesor matematyczny |
IRQ14 | 2 | Twardy dysk |
IRQ15 | 2 | Drugi twardy dysk |
W dzisiejszych komputerach i procesorach architektura się poważnie zmieniła (nie ma już wejść INTR i NMI, na szynie PCI obsługa przerwań jest inna), ale zachowano kompatybilność z opisanym tu modelem - nowe, lepsze rozwiązania potrafią symulować stare - w systemie Linux nie korzysta się chyba z tych nowych możliwości.
Obsługa przerwań sprzętowych w systemie Linux jest rozrzucona po paru plikach:
Kod źródłowy w języku C zawierający wstawki w GNU asemblerze jest ohydny z wyglądu. Dodatkowo jest całkowicie niekompatybilny ze zwykłym asemblerem intelowskim:
Zasadniczo przerwania sprzętowe w systemie Linux są obsługiwane przez bramy przerwań. Przerwaniom IRQ0-IRQ15 odpowiadają wektory 0x20-0x2F - tak jest inicjalizowany układ 8259A. Nową niskopoziomową procedurę przerwania rejestruje się w IDT za pomocą makra set_intr_gate(n, addr), gdzie n oznacza wektor przerwania, a addr adres procedury obsługi. Makro to jest zdefiniowane w pliku system.h.
Przerwanie IRQx może być obsługiwane przez trzy rodzaje niskopoziomowych procedur: IRQx_interrupt, fast_IRQx_interrupt oraz bad_IRQx_interrupt. Pierwsza z nich jest używana dla przerwań obsługiwanych bez flagi SA_INTERRUPT, druga dla tych z flagą SA_INTERRUPT, trzecia dla przerwań w ogóle nie zarejestrowanych. Procedury te są faktycznie zdefiniowane w irq.c za pomocą makr z irq.h. W pierwszym z tych plików są też zadeklarowane trzy statyczne tablice, w których znajdują się adresy tych procedur.
Podczas obsługi przerwań używa się wielokrotnie makr zachowujących rejestry:
W Linuxie cały czas mamy pod ręką informację, które przerwania są zamaskowane: wartości rejestrów IMR obu sterowników znajdują się w zmiennych cache_21, cache_A1. Dzięki temu nie musimy odczytywać portów 0x21 i 0xA1.
Ogólny algorytm obsługi przerwań jest następujący (x - numer przerwania):
Jeżeli nikt nie zarejestrował własnej procedury obsługi IRQx, to jest ona obsługiwana przez bad_IRQx_interrupt. Ta procedura robi następujące rzeczy:
Do rejestrowania nowych procedur obsługi służy funkcja request_irq, do odrejestrowywania funkcja free_irq. Funkcje te dodają po prostu nową strukturę do listy lub usuwają tę strukturę z listy. Jeżeli jednak przerwanie było dotychczas nie obsługiwane, to trzeba jeszcze wywołać set_intr_gate, by przerwanie było obsługiwane przez (fast_)IRQx_interrupt, a nie bad_IRQx_interrupt. Odwrotne działanie następuje w momencie odrejestrowywania ostatniej procedury obsługi dla danej linii IRQ.
Inicjalizacja przerwań sprzętowych polega na wywołaniu set_interrupt_gate dla każdego przerwania z procedurą obsługi bad_IRQx_interrupt. Dodatkowo jeszcze inicjalizuje się zegar i ustanawia standardowe procedury obsługi przerwań IRQ2 ("cascade") i IRQ13("math error").
Przerwania "wolne" (czyli zwykłe) obsługuje się bardzo podobnie do "szybkich":
W wypadku przerwań zachowanie przy wywołaniu funkcji ret_from_sys_call zależy od wartości zmiennej intr_count. Jeżeli zmienna ta ma wartość większą od zera, to znaczy, że wracamy z obsługi przerwania, które nastąpiło podczas obsługi innego przerwania. W takiej sytuacji wykonuje się jedynie makro RESTORE_ALL. Jeżeli jednak intr_count jest zero, to wykonuje się pełny powrót z funkcji systemowej: obsługuje się tzw. dolne połowy, sygnały i wykonuje ewentualnie reschedule.
Przerwanie zegarowe jest obsługiwane tak, jak inne, z małym wyjątkiem: przerwania są podczas jego obsługi zawsze zablokowane. Wynika to stąd, że zmienna jiffies jest używana przez niektóre przerwania do obliczania opóźnień itp. Gdyby jakieś przerwanie było wywołane podczas obsługi przerwania zegarowego, to zmienna jiffies by się nie zmieniła - przerwanie zegara byłoby bowiem zamaskowane (jako jedyne).
Jeżeli chcemy znaleźć IRQ, na którym siedzi nasze urządzenie, to według rad z pliku interrupt.h powinniśmy zrobić co następuje:
Funkcje do_IRQ oraz do_fast_IRQ wykonują jeszcze jedną, dotychczas przeze mnie nie opisaną funkcję. Otóż jeżeli którakolwiek spośród procedur obsługi znajdujących się na liście odpowiadającej przerwaniu IRQx ma ustawioną flagę SA_SAMPLE_RANDOM, to funkcje te wywołują add_interrupt_randomness. To wywołanie powoduje zwiększenie "entropii" generatora i , w konsekwencji, prowadzi do uzyskiwania liczb losowych, a nie jedynie pseudolosowych.