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.
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
handler
flags
device
/proc/interrupts
;
dev_id
request_irq
w nowszych
wersjach Linuxa (okolo 1.3.70), jest zdefiniowany tylko dla urzadzen podlaczanych
do szyny PCI (pozostale przekazuja NULL);
irq
poza przedzialem 0-15 lub -EBUSY jesli
ta linia jest juz obslugiwana.
handler
to :
dev_id
sluzacy do
zidentyfikowania uzadzenia, jesli linia przerwania jest dzielona miedzy kilka
(patrz nizej),
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.
dev_id
.
unsigned long probe_irq_on(void)
int probe_irq_of(unsigned long)
{
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.
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:
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
/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.void queue_task( struct tq_struct *bh_pointer,task_queue *bh_list)
tq_struct
do listy bh_list
,
void run_task_queue( task_queue *list)
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.
#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.