O přerušení a jeho obsluze
Zařízení má k dispozici data, která se musí hned zpracovat, aby se uvolnilo místo pro další data. Vyvolá proto přerušení, které se (na základě reakce procesoru) dostane až do jádra systému. Jádro přeruší činnost, která se zrovna provádí (pokud to není zakázáno) a začne vykonávat obsluhu přerušení. Po dokončení obsluhy se systém vrátí k původní činnosti.
Na první pohled to vypadá velice jednoduše, ale je tu několik úskalí. Především je potřeba obsluhu vykonat co nejrychleji, jednak kvůli zařízení samotnému, a také aby se nebrzdila běžná činnost systému. Pak jde také o to, co dělat, vyvolá-li se během této obsluhy další přerušení. A konečně poslední problém spočívá ve specifičnosti využití sdílených prostředků, alokace paměti a podobných operací. Ke všemu se dostaneme.
Nastavení obsluhy přerušení
Do systému přidáme nějaké nové zařízení, které bude poskytovat data. Pro jednoduchost můžeme uvažovat třeba teploměr (v podobě karty na některé sběrnici, např. PCI), snímající pravidelně (podle svých vnitřních hodin) teplotu v místnosti a poskytující tuto hodnotu systému. Vždy, když bude k dispozici naměřená hodnota, teploměr vyvolá přerušení a my si tuto hodnotu odebereme.
První věc, o které se zmíním, je číslo přerušení (kanál, IRQ), které je pro zařízení v podstatě pevně dáno (může být nastavitelné). Je to samozřejmě silně platformně závislá věc, ale obecně platí, že jich je poměrně malý počet a zařízení je mohou mít samostatně nebo je sdílet. Sdílení pak pochopitelně vyžaduje, abychom v obslužné rutině ověřili, zda se jedná o "naše" přerušení a jen v tomto případě ho obsloužili. Dobrým zvykem však je provádět tuto kontrolu i v případě, že přerušení s nikým nesdílíme.
Existují metody, jak zjišťovat správné číslo přerušení (IRQ) přímo v jádře bez nutnosti ručního nastavení. Ne pro každé zařízení to však lze, někdy je to poněkud náročnější a ne úplně bezpečné. Ideální samozřejmě je, když zařízení číslo IRQ samo poskytne.
Obsluha přerušení se nastaví tak, že požádáme jádro o registraci obslužné rutiny pro daný kanál. Pro náš teploměr by to mohlo vypadat třeba takto:
int res = request_irq(9, thermo_intr_handler, SA_SHIRQ, "thermo", ptr);
Zde žádáme o registraci přerušení na kanálu 9, obsluhovat ho bude funkce thermo_intr_handler() (viz dále), kanál lze sdílet, registrujeme s názvem "thermo" a předáváme ukazatel, podle něhož se bude rozlišovat, zda je přerušení naše. Při úspěšném volání funkce vrací 0 (jinak záporný kód chyby). V příznacích lze (kromě volby sdílení) specifikovat také, zda jde o tzv. rychlé přerušení (s maximální prioritou, obsluhu nelze přerušit) a zda se má toto přerušení použít jako zdroj entropie (to by se mělo používat jen v případech, kdy vznik přerušení nemá žádný řád - tedy rozhodně ne u nějakého časovače, ale nevhodné je to třeba i u síťových zařízení).
Je logické, že na závěr musíme bezpodmínečně obsluhu odregistrovat (free_irq()). Ovšem pozor, pokud nebyla obsluha zaregistrována, může pokus o uvolnění způsobit poškození struktur v jádře!
Obslužná rutina
Sice jsme nastavili obsluhu, ale příslušnou funkci zatím nemáme. Náš teploměr přerušením oznamuje, že je k dispozici naměřená hodnota. Tu si vyzvedneme přečtením z portu a uložíme ji do kruhového bufferu. Odtud si ji pak bude vyzvedávat klientský proces, ale to nás nyní nezajímá. Zde je ukázka, jak se to dá udělat:
#define THERMO_RB_SIZE 16 volatile u32 thermo_rb[THERMO_RB_SIZE]; volatile int thermo_rb_next = 0; volatile int thermo_rb_cnt = 0; irqreturn_t thermo_intr_handler(int irq, void* dev_id, struct pt_regs* regs) { if (irq != 9 || dev_id != ptr) return IRQ_NONE; spin_lock_irq(&thermo_lock); thermo_rb[thermo_rb_next] = inl(0x400); if (thermo_rb_cnt > THERMO_RB_SIZE) thermo_rb_cnt++; if (thermo_rb_next == THERMO_RB_SIZE) thermo_rb_next = 0; spin_unlock_irq(&thermo_unlock); tasklet_schedule(&thermo_tasklet); return IRQ_HANDLED; }
Pokud při obsluze přerušení nebo v taskletu používáme spinlock, musíme správně zvolit zamykací funkci. Obyčejné volání spin_lock() způsobí pouze zamčení, spin_lock_bh() zakáže softwarová přerušení (hodí se pro tasklet), spin_lock_irq() zakáže všechna přerušení a spin_lock_irqsave() umožňuje navíc uložit aktuální stav zákazu přerušení. Právě tato poslední funkce je pro obsluhu přerušení nejbezpečnější, protože máme vždy jistotu, že nepovolíme přerušení v situaci, kdy by povoleno být nemělo.
Obsluhu začínáme testem, zda je to vůbec naše přerušení. Pak přeneseme hodnotu z I/O portu do kruhového bufferu, inkrementujeme počet dostupných hodnot (pokud už buffer není plný) a posuneme ukazatel na příští pozici v bufferu. Na závěr naplánujeme tzv. tasklet (viz dále), kde se budou provádět činnosti, které příliš nespěchají. A nakonec dáme jádru na vědomí, že bylo přerušení úspěšně obslouženo. Všimněte si zamykání pomocí spinlocku - slouží k zamčení přístupu do pole a k indexům. V kontextu procesu pak musíme patřičnou sekci zamknout též.
Připomínám, že je potřeba dodržovat přísnou disciplinu, jak už jsem na to upozorňoval dříve. Tzn. nikde se nezdržovat, hlídat přístup ke sdíleným prostředkům (pozor hlavně na víceprocesorové stroje!) a nepoužívat funkce, které mohou blokovat. Zdlouhavější činnosti je lépe odložit na později.
Tasklet
Uvedený příklad obsluhy přerušení skončil naplánováním taskletu. Tasklet je jednou z metod tzv. odloženého zpracování. Běží s nižší prioritou než obsluha přerušení, ale s vyšší než uživatelské procesy. Při vykonávání taskletu tedy může jádro normálně obsluhovat přerušení, pokud mu to nezakážeme.
Tasklety mohou běžet na dvou úrovních priority, jako "normální" a "rychlé". Většinou si vystačíme s těmi obyčejnými. Pro příklad taskletu jsem si nechal část zpracování naměřené hodnoty z teploměru. Tasklet udělá jedinou věc, a sice probuzení procesu/vlákna čekajícího na naměřenou hodnotu (pokud čeká).
void thermo_tasklet_fnc(unsigned long data) { wake_up(&thermo_wq); } DECLARE_TASKLET(thermo_tasklet, thermo_tasklet_fnc, 0);
Funkce pro probuzení dostane jediný parametr, a to ukazatel na čekací frontu. O čekání a probouzení ještě bude řeč později. Jako obvykle lze tasklet inicializovat staticky (při deklaraci - viz příklad) nebo dynamicky. V deklaraci lze předat i parametr, který pak taskletová funkce obdrží.
Pracovní fronty
Než přejdeme k problematice časovačů, ještě se krátce zmíním o další formě odloženého zpracování. Jsou to tzv. pracovní fronty (workqueues). V nich se zpracování dat provádí v rámci zvláštního procesu vlákna, tedy s obdobnými podmínkami, jako v případě jiných procesů. To znamená, že lze používat i časově náročnější nebo blokující operace. Ovšem i tady je potřeba určité opatrnosti, hned se o tom zmíním. Pracovní fronty se hodí pro zdlouhavé činnosti, u kterých na rychlosti nezáleží.
Ve většině případů si lze vystačit se společnou frontou, kterou sdílí celé jádro. Jen pokud potřebujeme provádět něco skutečně časově náročného, je vhodnější použít samostatnou frontu, abychom neblokovali jiné moduly v jádře. Zejména se to týká situace, kdy se musí na něco delší dobu čekat.
Použití pracovní fronty není nic těžkého. Příklad ukazuje, jak na to:
void thermo_work_fnc(void* ptr) { ... } DECLARE_WORK(thermo_work, thermo_work_fnc, NULL); if (schedule_work(&thermo_work) != 0) ...
Podobně jako u taskletu se vytvoří příslušná funkce, ta se inicializuje a pak se přidá do fronty. Příklad ukazuje využití společné fronty, pokud bychom pracovali se samostatnou, bylo by to podobné (museli bychom ji ale samozřejmě vytvořit a později zrušit).
Časové prodlevy a časovače
Často potřebujeme, aby se něco provedlo (nebo opakovaně provádělo) v určitý okamžik nebo za určitou dobu. Máme celou řadu možností, tu správnou zvolíme podle situace, kterou řešíme. Špatná volba má za následek přinejmenším pokles výkonu, v horším případě i tuhnutí celého systému nebo jeho pád.
Činné čekání
Pokud potřebujeme počkat velice krátkou dobu, můžeme zůstat v činné smyčce. V jádře jsou k dispozici funkce, které to zařídí. Činné čekání má význam v případech, kdy se opravdu čeká jen krátce, takže se nevyplatí přepínat kontext (přepnutí má poměrně značnou režii). Zde je příklad (čeká se pět mikrosekund): udelay(5); Lze takto čekat i mimo kontext procesu, což by se ovšem - kromě zcela výjimečných případů - nemělo nikdy dělat.
Uspání běhu
Pokud se čeká déle (např. v řádu milisekund), je výhodnější běh uspat, aby mohl plánovač přepnout kontext a mohl mezitím běžet jiný proces. Někdy stačí proces prostě znovu naplánovat, většinou je ale nutná i jistá minimální prodleva. Obojí ukazuje následující příklad:
schedule(); set_current_state(TASK_INTERRUPTIBLE); schedule_timeout(50); msleep_interruptible(20);
První příkaz pouze odevzdá procesor a způsobí nové naplánování. Další dvojice příkazů uspí proces (v přerušitelném stavu) na 50 jiffies. A konečně třetí způsobí uspání (opět přerušitelné) na 20 ms.
Jiffie je základní časovová jednotka používaná v jádře. Počet jiffies za sekundu odpovídá konstantě HZ, kterou lze nastavit v konfiguraci jádra před kompilací. Nejčastěji se dnes pro HZ používá hodnota 250, u serverů někdy 100, u desktopů naopak 1000. Vyšší hodnota poskytuje rychlejší odezvu procesů a větší plynulost, ale spotřebuje více času procesoru na režii přepínání kontextu.
Čekání na událost
Někdy se čekání váže na nějakou událost, splnění určité podmínky. Pak použijeme některé z maker určených pro tento účel. Můžeme čekat časově omezenou nebo neomezenou dobu. Proces je potřeba explicitně probouzet (funkcí wake_up() apod.) ke kontrole podmínky. Probuzení je vyvoláno - pro přerušitelné uspání - také příchodem signálu, na který bychom měli správně reagovat (tedy obvykle opuštěním systémového volání a vrácení -ERESTARTSYS, resp. -EINTR). Totéž samozřejmě platí i u předchozího příkladu, kde jsem to pro stručnost vynechal.
wait_queue_head_t thermo_wq; init_waitqueue_head(&thermo_wq); int res = wait_event_interruptible_timeout(thermo_wq, cond, 100); if (res > 0) return -EINTR;
V příkladě se přerušitelně čeká na nenulovost proměnné cond, a to max. 100 jiffies. Při přerušení signálem se vyskočí z funkce s návratovou hodnotou -EINTR.
Časovače
Výše uvedené postupy se nehodí pro práci mimo kontext procesu a také v případech, kdy pracujeme s pravidelnou periodou. Proto použijeme vhodnější mechanismus - časovače. Časovač je ve své podstatě ovládán přerušením, proto se při obsluze jím spouštěné události musíme chovat stejně jako při obsluze přerušení. Vazbu na kontext procesu lze vytvořit snadno, např. s využitím výše popsaného čekání na událost.
Časovač (jeho datová struktura) se musí nejprve inicializovat (staticky nebo dynamicky), pak se nastaví (přiřazením hodnot) a nakonec spustí. Je vždy jednorázový, periodicity dosáhneme tak, že se v obslužné rutině časovač ihned znovu odstartuje. Již běžící časovače lze přenastavovat, nevypršené časovače můžeme rušit. Jsou dvě funkce na rušení časovačů del_timer() (asynchronní) a del_timer_sync() (synchronní). Pokud není zvláštní důvod činit jinak, používáme vždy synchronní verzi. Příklad časovače:
void thermo_timer_fnc(unsigned long data) { ... } struct timer_list thermo_timer; init_timer(&thermo_timer); thermo_timer.function = thermo_timer_fnc; thermo_timer.expires = jiffies + HZ; add_timer(&thermo_timer);
Objektový model
I když jsme zdaleka všechno nevyčerpali, prostorové možnosti bohužel nedovolí více se věnovat práci mimo kontext procesu a souvisejícím věcem. Posuneme se tedy zase dál. Příště bude na programu objektový model jádra. Je v jádře relativně nový (v této podobě až v řadě 2.6), ale nesmírně silný a užitečný. Umožňuje elegantní manipulaci s daty a jejich snadnou výměnu s uživatelským prostorem. Jeho použití výrazně usnadňuje činnosti spojené se sledováním modulů a jejich konfigurací.