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