do spisu tresci tematu 7

7.1.4 Przerwania sprzetowe


Spis tresci


Wprowadzenie

Jedynym sposobem, w jaki urzadzenie moze zwrocic na siebie uwage zapracowanego procesora jest wywolanie przerwania sprzetowego. Przerwanie sprzetowe to asynchroniczne zdarzenie podobne do sygnalu - tak jak jadro wysyla sygnaly do procesow uzytkownika informujac je o sytuacjach wyjatkowych i wymagajacych podjecia natychmiastowej akcji, tak urzadzenie generuje przerwanie, zeby zawiadomic program obslugi np. o zakonczeniu operacji wejscia-wyjscia, bledzie. Przerwania generuje rowniez zegar. Podobny jest sposob obslugi tych dwoch rodzajow zdarzen.


Obsluga przerwan

Zeby moc zareagowac na przerwanie, nalezy zarejestrowac procedure obslugi jednej z 16 linii przerwan (IRQ - Interrupt Request Lines). Mozna to zrobic przy pomocy zdefiniowanej w pliku arch/i386/kernel/irq.c funkcji:


int request_irq( unsigned int irq,
	void (*handler)(int , void *, struct pt_regs *),
	unsigned long flags,
	const char *device,
	void *dev_id)

irq
numer przerwania, ktore checemy obslugiwac ( 0 - 15 )
handler
wskaznik do procedury obslugi przerwania,
flags
flagi: SA_INTERRUPT, SA_SHIRQ lub 0,
device
nazwa urzadzenia, ktora mozemy sobie obejrzec czytajac plik /proc/interrupts;
dev_id
ten parametr zostal dodany do wywolania request_irq w nowszych wersjach Linuxa (okolo 1.3.70), jest zdefiniowany tylko dla urzadzen podlaczanych do szyny PCI (pozostale przekazuja NULL);
Wynik:
0, -EINVAL jesli irq poza przedzialem 0-15 lub -EBUSY jesli ta linia jest juz obslugiwana.
Parametry z ktorymi wywolywana jest funkcja handler to : Znaczenie flag:
SA_INTERRUPT
W czasie wykonywania procedury obslugi przerwania wlaczone sa wszytkie inne przerwania, a po powrocie z procedury obslugi przerwania sprawdzane jest ustawienie globalnej flagi need_resched. Jesli jest ona ustawiona, to wywolywana jest funkcja szeregujaca (schedule()). Wywolujac request_irq z flaga SA_INTERRUPT definiujemy "szybka" procedure obslugi, w czasie ktorej zamaskowane sa przerwania i po ktorej nie nastepuje szeregowanie.
SA_SHIRQ
Poniewaz linii przerwan jest tylko 16, w Linuxie 1.3.70 pojawila sie mozliwosc dzielenia linii przerwania przez kilka programow obslugi urzadzen, kazde z nich musi uzyc flagi SA_SHIRQ i przekazac identyfikator dev_id.
Programy obslugi urzadzen korzystajacych z przerwan musza czesto szukac "na oslep" linii przerwania uzywanej przez fizyczne urzadzenie. Przydaja sie do tego dwie funkcje:
unsigned long probe_irq_on(void)
zwraca maske z ustawionymi bitami odpowiadajacymi przerwaniom dla ktorych zarejestrowano procedure obslugi
int probe_irq_of(unsigned long)
bierze maske jako parametr i zwraca numer przerwania ktore nastapilo, 0 jesli nie nastapilo przerwanie lub liczbe ujemna jesli nastapilo wiele przerwan
Algorytm poszukiwania linii przerwania moze wygladac tak:

{
    sti();
    /* pobierz nieuzywane numery linii: */
    maska = irq_probe_on();
    kaz urzadzeniu wywolac przerwanie; /* uzywajac portow wejscia-wyjscia */
    przeczekaj czas wystarczajacy na wywolanie przerwania;
    przerwanie = irq_probe_off( maska );	
}

Funkcja free_irq( unsigned int irq ) usuwa obsluge przerwania.


Dolne polowy i kolejki zadan

Procedura obslugi przerwania musi nieraz wykonywac dosyc zlozone i czasochlonne operacje, na przyklad reorganizowac kolejke zadan dostepu do urzadzenia, budzic czekajace na jakies zdarzenie procesy, przepisywac dane z portow do buforow (patrz program obslugi dysku). Podczas wykonywania procedury obslugujacej przerwanie zostaje ono (a nieraz takze inne przerwania) zamaskowane, na przyklad aby uniknac uszkodzenia roznych waznych struktur danych. Jesli obsluga przerwania nie jest dosc szybka to nastepne przerwanie moze nie zostac obsluzone, w efekcie mozemy co najmniej utracic jakies dane, jesli nie stanie sie cos gorszego.

Aby zmniejszyc ryzyko takiego zdarzenia, mozna podzielic procedure obslugi przerwania na dwie czesci:

Jadro Linuxa umozliwia wywolywanie i wykonanie "bottom half" (bede poslugiwac sie oryginalnym terminem z braku zgrabnego tlumaczenia) poza kontekstem konkretnego procesu - podczas szeregowania procesow lub wychodzenia z funkcji systemowej. W tym celu jadro utrzymuje tablice bh_base 32 wskaznikow do funkcji typu "bottom half" dla najwazniejszych urzadzen: zegara, konsoli, klawiatury i paru innych - 20 wpisow pozostaje wolnych (plik include/linux/interrupt.h). Funkcja init_bh pobiera jako parametr numer w tablicy bh_base i wskaznik na funkcje "bottom half" ktory do tablicy wstawia. Podczas obslugi przrwania funkcja "top half" zwieksza licznik ilosci wywolan funkcji "bottom half" znajdujacej sie w bh_base wywolujac funkcje mark_bh z argumentem bedacym indeksem w tablicy. Przejrzenie tablicy bh_base i wywolanie zaznaczonych funkcji nastapi po obsludze wszystkich przerwan, na poczatku najblizszego wykonania schedule() lub return_from_sys_call(). Najpierw wywolywane sa funkcje majace nizszy numer w tablicy

W ten sposob z obsluga jednego przerwania moze byc zwiazanych wiele funkcji "bottom half". Funkcja "top half" moze wybrac jedna lub wiele z nich. Ograniczeniem jest jednak niewielki rozmiar tablicy bh_base (okolo 20 wolnych pozycji). Rozwiazaniem tego problemu sa

Kolejki zadan (Task queues)

Kolejki zadan zdefiniowane sa w pliku /include/linux/tqueue.h.

struct tq_struct {
	struct tq_struct * next;
	int sync;	
	void (* routine)(void *);
	void *data;
}

typedef struct tq_struct * task_queue;

Kolejka zadan to po prostu lista struktur tq_struct zawierajacych wskazniki do funkcji "bottom half" i do ich argumentow.
Funkcje do obslugi kolejek zadan:
void queue_task( struct tq_struct *bh_pointer,task_queue *bh_list)
ta funkcja dolacza nowa stukture tq_struct do listy bh_list,
void run_task_queue( task_queue *list)
wywoluje wszystkie funkcje z listy list poczynajac od ostatnio wlozonej. Kazda funkcja zostaje najpierw zdjeta z listy a potem wywolana, dzieki czemu moze wykonujac sie wlozyc sie (albo inna funkcje) na liste.
Podzielone na "gorna" i "dolna" czesc procedury obslugi przerwan moga teraz wygladac tak:

#define NUMER  20  /* indeks w tablicy bh_base pod  */
   /* ktory wstawimy wskaznik do funkcji dolna_polowa() */
task_queue kolejka = NULL;
struct tq_struct zadanie_1 = { 0,0,funkcja_1,arg_1 };
struct tq_struct zadanie_2 = { 0,0,funkcja_2,arg_2 };

/* "dolna polowa" uruchamia tylko kolejke */
void dolna_polowa()
{
    run_task_queue( &kolejka );
}

/* "gorna polowa" procedury obslugi przerwania */
void gorna_polowa( int irq, void *dev_id, struct pt_regs *regs )
{
    sprawdz stan urzadzenia;
    przeslij dane;
    if( warunek1 ) {
        queue_task( &zadanie_1, &kolejka );
        mark_bh( NUMER );
    }
    if( warunek2 ) {
        queue_task( &zadanie_2, &kolejka );
	mark_bh( NUMER );
    }  
}

int inicjalizacja_sterownika()
{
    ...
    init_bh( NUMER, dolna_polowa ); 
}

Zamiast wstawiac funkcje dolna_polowa do tablicy bh_base i uaktywniac je przy pomocy mark_bh( NUMER ) mozna dodawac je do zdefiniowanej w pliku kerne/sched.c kolejki tq_immediate, ktora jest uruchamiana przy przegladaniu tablicy bh_base i ma w tej tablicy dosyc wysoki numer (7) dzieki czemu umieszczone w niej "dolne polowy" wykonuja sie wczesnie.


Zrodla informacji

  1. Kod zrodlowy Linuxa, pliki:
  2. Linux Journal - artykuly z dzialu Kernel Korner, szczegolnie numer 26,
  3. Michael K. Johnson: Artykuly dotyczace programow obslugi przerwan z Kernel Hackers' Guide,
  4. Michael K. Johnson: Writing Linux Device Drivers


Artur Zawlocki