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]):

W dalszej czesci tekstu poruszone zostana nastepujace zagadnienia:

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:

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.


Algorytm system_call()

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.


Algorytm ret_from_sys_call

{ 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:


Obsluga przerwan sprzetowych

Podstawowe informacje dotyczace przerwan sprzetowych:

Z powyzszego zgrubnego opisu wspolpracy CPU i sterownika przerwan wynikaja dla tworcow procedur obslugi nastepujace reguly, ktorych wplyw widac w kodzie Linuxa:


W pliku irq.c makro BUILD_IRQ() tworzy po trzy podprogramy obslugujace kazde przerwanie (nazwijmy je FOO):

Uwaga: wyjatkiem jest przerwanie zegarowe, ktore obslugiwane jest w sposob specjalny.


Funkcja do_IRQ()

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:

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.


Funkcja do_timer()

{ 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:


Bibliografia

  1. Pliki zrodlowe Linuxa
  2. The Linux Kernel Hacker's Guide
  3. [GoTu] Goszczynski R., Tuszynski M., Mikroprocesory 80286, 80386 i i486, Help 1991


Autor: Jan Koslacz