Do spisu tresci tematu 1.
1.6 Zegar i przerwania
Spis tresci
Wprowadzenie
Obsluga przerwan, w tym przerwania zegarowego, w sposob naturalny
mocno uzalezniona jest od architektury komputera goszczacego system
operacyjny. Poniewaz Linuxa zaimplementowano poczatkowo na zwyklym
klonie IBM PC z jednym procesorem 80386 Intela, skupie sie na tej wlasnie
platformie (jest to zreszta jedyny sprzet, jaki w miare dobrze znam).
Choc materialy, ktore wymieniam w
bibliografii dotyczyc moga roznych wersji
Linuxa, podstawowym zrodlem informacji byl dla mnie kod jadra w wersji
2.0.20 dostepny na serwerze zodiac1.
Uzywac bede nastepujacej terminologii dotyczacej przerwan (vide [GoTu]):
- przerwania sprzetowe (hardware interrupts) zglaszane sa przez
sterownik przerwan na zadanie jakiegos urzadzenia (np. zegara lub
klawiatury)
- wyjatki (exceptions) generowane sa przez sam procesor; dziela
sie na:
- niepowodzenia (faults), zglaszane przed pelnym
wykonaniem instrukcji i umozliwiajace jej powtorzenie; dobrym
przykladem jest tu wyjatek #PF (Page Fault), powstajacy przy
odwolaniu do strony nieobecnej w pamieci
- potrzaski (traps), sygnalizowane po wykonaniu instrukcji,
gdy spelnione sa wyzwalajace je warunki; przykladem jest instrukcja
int, ktorej wykonanie zawsze podnosi wyjatek
- zalamania (aborts), powstajace w wyniku powaznych i
nienaprawialnych bledow; na przyklad wyjatek #DF (Double Fault)
podnoszony jest wtedy, gdy przed zakonczeniem obslugi niepowodzenia
zgloszone zostaje nastepne.
W dalszej czesci tekstu poruszone zostana nastepujace zagadnienia:
- Podprogramy obslugujace przerwania zglaszane sa procesorowi przez
jadro podczas ladowania systemu.
- Jeden z potrzaskow (wyzwalany przez int 0x80) zwiazany jest z
mechanizmem wolania funkcji systemowych.
- Obsluga wyjatkow zaszyta jest gleboko w jadrze
Linuxa i nie ma do niej bezposredniego dostepu. Informacje o wystapieniu
niektorych wyjatkow przekazywane sa do procesow uzytkownika w formie
sygnalow.
- Przerwania sprzetowe moga byc obslugiwane
przez funkcje zglaszane w czasie pracy systemu przez sterowniki
urzadzen.
- W specjalny sposob obslugiwane jest przerwanie sprzetowe
zegara systemowego.
Osoby pragnace dowiedziec sie wiecej o technicznych szczegolach obslugi
przerwan, priorytetach przerwan sprzetowych, czynnosciach wykonywanych
przez procesor przy przelaczaniu zadan i innych fascynujacych detalach
odsylam do ksiazki [GoTu].
Ladowanie systemu a przerwania
W trakcie
inicjacji jadra systemu kilkakrotnie zmieniane sa deskryptory
przerwan. Zabawe rozpoczyna kod znajdujacy sie w
arch/i386/kernel/head.s, ktory tworzy tablice deskryptorow
przerwan (IDT - Interrupt Descriptor Table) wypelniona deskryptorami
wskazujacymi funkcje ignore_int() (zadanie tego podprogramu
sprowadza sie do wyswietlenia tekstu "Unknown interrupt").
Znacznie ciekawsza jest funkcja
trap_init(), ustanawiajaca podprogramy
obslugi wyjatkow oraz wypelniajaca globalna i lokalne tablice
deskryptorow przy pomocy makr set_call_gate(),
set_system_gate() i set_trap_gate() zdefiniowanych
w pliku
include/asm-i386/system.h. Jako funkcja obslugi przerwania 0x80
rejestrowany jest takze interfejs do funkcji
systemowych - fragment kodu asemblerowego oznaczony etykieta
system_call.
Pojawiajaca sie w trap_init() funkcja
lcall7() uzywana jest prawdopodobnie tylko przy wykonywaniu
binariow w formacie iBCS2 i zapewne moze byc wolana z procesu
uzytkownika instrukcja call.
W funkcji
init_IRQ() ustawiana jest na okolo 100 Hz czestotliwosc pracy
zegara systemowego, a deskryptory przerwan sprzetowych ustawiane sa
przy pomocy makra set_intr_gate() na procedure-atrape,
konczaca tylko przerwanie (polega to na wyslaniu polecenia EOI, End
Of Interrupt, do odpowiedniego sterownika przerwan).
W funkcji
time_init() rejestrowany jest wreszcie podprogram obslugi
przerwania zegarowego.
Po przeanalizowaniu konfiguracji systemu, inicjalizacji konsoli i
managera pamieci wlaczane sa wreszcie makrem sti() przerwania
maskowalne. Gdzies po drodze do ostatecznego zaladowania systemu
pod przerwania sprzetowe podlaczaja sie jeszcze sterowniki urzadzen
(np. klawiatury czy myszy).
Mechanizm wolania funkcji systemowych
Tablica funkcji systemowych
sys_call_table Linuxa 2.0.20 zawiera 163 pozycje. W kazdej
z nich znajduje sie adres podprogramu z przestrzeni jadra uruchamianego
przez funkcje
system_call() (no, prawie: jest tez jedno zero zastepujace
adres funkcji afs_syscall). Nazwy podprogramow sa z dokladnoscia
do prefiksu sys_ albo old_ takie, jak implementowanych
przez nie funkcji systemowych.
Interakcja miedzy funkcja z przestrzeni adresowej uzytkownika uzywajaca
funkcji systemowej (np. setuid()) a jadrem Linuxa wyglada
nastepujaco:
- Funkcja setuid() jest zdefiniowana w bibliotece libc
przy pomocy makra syscall1() (1 to liczba argumentow funkcji
systemowej). W przypadku funkcji o zmiennej liczbie argumentow (np.
ioctl()), implementacja jest bardziej skomplikowana, lecz
zachowane sa opisane ponizej zasady.
- Makro syscall1() tworzy fragment kodu, ktory odklada na stos
przekazany funkcji argument, dorzuca do niego numer funkcji (23 dla
setuid()) i wykonuje instrukcje int 0x80.
- Sterowanie przekazywane jest automagicznie do algorytmu (dosc ciezko
nazwac go funkcja)
system_call. Przed jego
wykonaniem procesor zmienia tryb pracy z 3 (kod uzytkownika) na 0
(kod nadzorcy), po powrocie z niego wykonywany jest algorytm
ret_from_sys_call
i - znow automagicznie - procesor wraca do trybu uzytkownika.
- Po powrocie z jadra do przestrzeni uzytkownika syscall1()
sprawdza kod powrotu funkcji systemowej. Jesli jest dodatni, od razu
go zwraca. W przeciwnym razie wstawia do errno wartosc
-kod_powrotu i zwraca -1.
Dostep do danych przechowywanych w przestrzeni adresowej uzytkownika
(selektor jego segmentu danych jest w rejestrze FS) zapewniaja m.in.
makra get_user() i put_user() zdefiniowane w pliku
arch/i386/kernel/segment.h. Ich wykonanie moze spowodowac
koniecznosc sciagniecia do pamieci strony, do ktorej sie odwoluja, co
opisane jest
gdzie indziej.
Argument: numer wolanej funkcji systemowej
Wynik: ujemny numer bledu albo nieujemny kod powrotu
{
if(numer_funkcji > NR_syscalls) // NR_syscalls = 256
return(-ENOSYS);
if(sys_call_table[numer_funkcji]==NULL)
return(-ENOSYS);
return((sys_call_table[numer_funkcji])());
}
Uwaga: w powyzszym kodzie moga zostac wykonane rozkazy call
syscall_trace,
ale to juz nie moj rewir.
{
if(intr_count!=0) // obslugujemy jakies przerwanie
return; // wroc do przerwanego procesu
while(czekaja funkcje "bottom half")
{
++intr_count;
do_bottom_half();
--intr_count;
}
if(przerwanym procesem bylo jadro)
return;
if(ustawiona jest flaga need_resched)
{
schedule();
goto ret_from_sys_call;
}
if(biezacy proces to task[0]) // do task[0] nie sa wysylane
return; // zadne sygnaly
if(czekaja jakies sygnaly)
do_signal();
}
Uwagi:
Obsluga wyjatkow
Podprogramy obslugi wyjatkow skladaja sie z dwoch czesci: definiowany w
pliku
entry.s fragmentu kodu przechodzi do trybu jadra, wola funkcje
o nazwie do_nazwa_wyjatku() zdefiniowana
zazwyczaj w pliku
traps.c i wraca do przerwanego procesu, wykonujac algorytm
ret_from_sys_call. (Niektore
podprogramy, np. obslugi wyjatku 0, sa z powodow technicznych bardziej
skomplikowane)
Wiekszosc podprogramow obslugi generowana jest przez makro
DO_ERROR(), ktore wola funkcje
force_sig() z odpowiednim numerem sygnalu (patrz tabelka pod
spodem), po czym sprawdza, czy wyjatku nie spowodowal kod jadra (jesli
tak, zatrzymuje system procedura
die_if_kernel()).
Wyjatek |
Sygnal |
Nazwa podprogramu obslugi |
0 | SIGFPE | do_divide_error |
3 | SIGTRAP | do_int3 |
4 | SIGSEGV | do_overflow |
5 | SIGSEGV | do_bounds |
7 | SIGSEGV | do_device_not_available |
8 | SIGSEGV | do_double_fault |
9 | SIGFPE | do_coprocessor_segment_overrun |
10 | SIGSEGV | do_invalid_TSS |
11 | SIGBUS | do_segment_not_present |
12 | SIGBUS | do_stack_segment |
17 | SIGSEGV | do_alignment_check |
Uwaga: wyjatek 9 obslugiwany jest w kontekscie procesu, ktory ostatnio
uzywal koprocesora (wszystkie pozostale - w kontekscie procesu biezacego).
Poza tym:
- wyjatek 1 (praca krokowa) obsluguje funkcja
do_debug(), ktora wola force_sig(SIGTRAP, current),
po czym - jesli przerwanym procesem byl kod jadra - zeruje rejestr DR7
- wyjatek 2, bedacy w rzeczywistosci przerwaniem
niemaskowalnym (NMI), obsluguje funkcja do_nmi(),
wyswietlajaca komunikat o potencjalnych problemach z pamiecia RAM
- wyjatek 13 obsluguje funkcja
do_general_protection(), w ktorej po magii zwiazanej z trybem
wirtualnym 8086 (byc moze chodzi tu o emulacje MS DOS-u ?) wolana jest
funkcja force_sig(SIGSEGV, current)
- wyjatek 14 obsluguje funkcja
do_page_fault(), ktorej opis powinien znajdowac sie w
temacie 4
- wyjatek 16 obsluguje funkcja
do_coprocessor_error(), ktora po zebraniu argumentow wola
(posrednio) force_sig(SIGFPE) w kontekscie procesu, ktory
ostatni uzywal koprocesora
- wyjatki 15 i 18..47
sa zarezerwowane. Wykonywana po ich zgloszeniu funkcja
do_reserved() wyswietla stosowny komunikat.
Obsluga przerwan sprzetowych
Podstawowe informacje dotyczace przerwan sprzetowych:
- urzadzenie (mowa tu tylko o urzadzeniach z wlasnym IRQ, czyli
podlaczonych do sterownika przerwan) dochodzi do wniosku, ze zaszla
sytuacja, o ktorej powinien dowiedziec sie procesor (np. nacisnieta
zostala litera 'A' na klawiaturze) i zglasza te potrzebe sterownikowi
przerwan
- sterownik sprawdza, czy nie jest przypadkiem obslugiwane przerwanie
o wyzszym lub rownym zgloszonemu priorytecie; jesli jest, czeka na
moment, w ktorym wszystkie takie przerwania zostana obsluzone
(najwyzszy priorytet ma zegar systemowy); jesli obslugiwane jest
przerwanie o priorytecie nizszym, mozna jego obsluge przerwac
- jesli w czasie oczekiwania przerwania o numerze n
na obsluzenie nadejdzie drugie przerwanie o tym samym numerze,
zostanie ono zignorowane (przerwania nie sa zliczane)
- gdy sterownik moze juz zglosic procesorowi przerwanie, musi poczekac
na ustawienie flagi IF procesora, ktora odblokowuje przerwania (jest
ona automatycznie kasowana przez procesor przed rozpoczeciem
wykonywania procedury obslugi przerwania; nalezy ja ustawic recznie
lub po zakonczeniu procedury odtwarzany jest jej poprzedni stan)
- dopoki procedura obslugi przerwania nie wysle do sterownika sygnalu
konca przerwania (EOI, End Of Interrupt), sterownik uwaza przerwanie
za obslugiwane.
Z powyzszego zgrubnego opisu wspolpracy CPU i sterownika przerwan
wynikaja dla tworcow procedur obslugi nastepujace reguly, ktorych
wplyw widac w kodzie Linuxa:
- ustawiaj flage IF dopiero wtedy, gdy nie zaszkodzi ci przerwanie
pracy - ale rob to jak najszybciej, by nie blokowac przerwan o
wyzszym priorytecie
- jesli za szybko wyslesz sygnal EOI do sterownika, mozesz spodziewac
sie nastepnego przerwania o tym samym numerze w najmniej odpowiedniej
chwili; jesli zrobisz to za pozno, mozesz je utracic
- twoja procedura powinna byc jak najszybsza; jesli zwiazane sa z nia
jakies dlugie obliczenia, powinny byc przeprowadzane poza przerwaniem
(koncepcja "bottom halves" - patrz nizej).
W pliku
irq.c makro
BUILD_IRQ() tworzy po trzy podprogramy obslugujace kazde
przerwanie (nazwijmy je FOO):
- FOO_interrupt(): potwierdza otrzymanie przerwania,
inkrementuje zmienna intr_count, ustawia flage IF, wola
do_IRQ() z argumentem rownym
numerowi przerwania, kasuje flage IF, wysyla do sterownika EOI,
dekrementuje zmienna intr_count i wykonuje algorytm
ret_from_sys_call
(podstawowy sposob obslugi przerwan)
- fast_FOO_interrupt(): podobny do FOO_interrupt(),
ale nie ustawia IF (nie musi zatem ruszac intr_count), wola
do_fast_IRQ() i wraca
bezposrednio do przerwanego procesu
- bad_FOO_interrupt(): tylko potwierdza otrzymanie przerwania
i wraca bezposrednio do przerwanego procesu.
Uwaga: wyjatkiem jest przerwanie zegarowe, ktore
obslugiwane jest w sposob specjalny.
Procedura obslugi IRQ, ktore zostalo zainstalowane bez flagi
SA_INTERRUPT (patrz nizej). Uruchamiana z ustawiona flaga IF,
po jej zakonczeniu sterowanie przechodzi do
ret_from_sys_call. Powinna
sie ja stosowac w przypadku przerwan, ktorych obsluga trwa dosc dlugo
(np. przerwanie zegara lub klawiatury).
Argument: irq - numer przerwania
{
inkrementuj licznik obsluzonych przerwan irq;
forEach(funkcja obslugi zarejestrowana dla irq)
wykonaj_funkcje;
if(ktoras z funkcji miala atrybut SA_SAMPLE_RANDOM)
add_interrupt_randomness(irq);
}
Uwaga: funkcja
add_interrupt_randomness() zwieksza entropie wbudowanego w
Linux generatora liczb pseudolosowych (jest to rozwiazanie, ktorym bylem
oczarowany - zazwyczaj o generowanie liczb pseudolosowych musi sie
troszczyc tworca uzywajacego ich oprogramowania).
Funkcja do_fast_IRQ() rozni sie od do_IRQ() jedynie
tym, ze wolanym funkcjom obslugi nie jest przekazywany adres odlozonych
na stosie argumentow.
Rejestrowanie funkcji wolanych przez do_[fast_]IRQ()
Aby do_IRQ() i do_fast_IRQ() mialy co robic, trzeba
najpierw zarejestrowac wolane przez nie funkcje. Wymienie teraz (gdyz
szczegoly, choc ciekawe, nie wydaja mi sie szczegolnie wazne) zwiazane
z tym zadaniem podprogramy:
-
request_irq() probuje zarejestrowac funkcje (nie musi sie
to udac - moze zabraknac pamieci na struct irqaction lub
wybranym przerwaniem nie mozna sie dzielic); jesli przerwanie to
nie jest uzywane, odblokowuje je
-
free_irq() wyrzuca funkcje z listy; jesli lista zostanie
oprozniona, blokuje przerwanie.
Struktura struct irqaction, w ktorej przechowywane sa dane
rejestrowanych funkcji, zdefiniowana jest w pliku
include/linux/interrupt.h:
struct irqaction
{
void (*handler)(int, // numer obslugiwanego IRQ
void *, // tu bedzie irqaction.dev_id
struct pt_regs *); // NULL lub rejestry CPU
unsigned long flags; // flagi SA_*
unsigned long mask; // ???
const char *name; // nazwa funkcji obslugi
void *dev_id; // argument dla handler()
struct irqaction *next; // nastepna zarejestrowana funkcja
}
W polu flags ustawiane sa bity SA_* zdefiniowane w
pliku
asm-i386/signal.h.
Sterowniki urzadzen - "bottom halves"
Aby zminimalizowac czas spedzony w do_IRQ(),
projektanci sterownikow urzadzen wymagajacych czasochlonnej obslugi
powinni podzielic ja na dwie czesci. Szybka funkcja rejestrowana
przez request_irq() jest wolana w chwili nadejscia przerwania.
Wolniejsza funkcja realizujaca druga czesc obslugi wstawiana jest
do tablicy
bh_base; jej szybka siostra w razie potrzeby zaznacza
(funkcja
bh_mark()), ze
do_bottom_half() (uruchamiana w
ret_from_sys_call) ma dokonczyc (przy wlaczonych przerwaniach)
obsluge urzadzenia.
Przerwanie zegarowe
Opisane przy obsludze przerwan sprzetowych
trzy wersje procedur obslugi w przypadku przerwania zegarowego
zastepowane sa jedna (choc znana pod trzema nazwami). Jest ona bardzo
podobna do FOO_interrupt(); jedyna roznica polega na tym, ze
w trakcie jej wykonywania przerwania sa caly czas zablokowane.
Kazde przerwanie zegarowe powoduje wykonanie podlaczonej w
time_init() funkcji
timer_interrupt(). Wola ona najpierw opisana ponizej funkcje
do_timer(), a nastepnie przeprowadza jakies tajemnicze korekty
zegara czasu rzeczywistego.
{
jiffies++; // takty od startu systemu
lost_ticks++; // takty zuzyte przez proces
mark_bh(TIMER_BH); // wykonaj timer_bh()
if(tryb jadra)
{
lost_ticks_system++; // takty zuzyte przez system
if(profilowanie wlaczone
i PID procesu != 0)
gromadz informacje do profilu programu;
}
if(planista czeka na uplyw kwantu czasu)
mark_bh(TQUEUE_BH); // uruchom bottom half planisty
}
Uwagi:
- zmienna jiffies opisana jest wraz z funkcjami systemowymi
dotyczacymi czasu
- funkcja timer_bh() wola update_times() oraz funkcje
zwiazane ze stoperami, ktore juz zakonczyly odliczanie
- sposob wykorzystania zegara przez planiste rozpracowywany byl
przez osoby zajmujace sie tematem 2.;
tam rowniez znajduje sie zapewne opis update_times()
Bibliografia
- Pliki zrodlowe Linuxa
- The Linux Kernel Hacker's Guide
- [GoTu]
Goszczynski R., Tuszynski M.,
Mikroprocesory 80286, 80386 i i486,
Help 1991
Autor: Jan Koslacz