Produkty Novinky Články Návody Kontakty

Inicializace a ukončení - I

Inicializace a ukončení - ta nejzákladnější funcionalita každého ovladače ať už jde o prostý ”Hello kernel!” až po ovladač nějakého složitého blokového zařízení. Pojďme si ukázat, co všechno musíme při inicializaci a ukončování ovladače zvládnout.

Inicializace ovladače

Inicializace modulu je místem, kde si modul alokuje potřebné prostředky: registruje se k major-minor páru (hlavní a vedlejší číslo), vkládá záznam do adresáře /sys a začleňuje se do jádra.
Inicializace modulu se provádí během jeho zavádění do jádra. Po úspěšném dokončení inicializace je modul zaveden do jádra a jeho funkcionalita může být využívána jádrem a uživatelskými aplikacemi.
Inicializační kód modulu umisťujeme do samostatné inicializační funkce. Deklarujeme ji jako statickou a před její název umisťujeme token __init, který zajistí, že kód funkce je po zavedení modulu odstraněn z jádra kvůli úspoře paměti. Inicializační funkci musíme registrovat makrem module_init(nazev_init_fce), které se dává na konci zdrojového kódu. Návratová hodnota inicializační funkce je typu int a v případě chyby je vhodné vrátit chybový kód definovaný v hlavičkovém souboru <linux/errno.h>.
Příklad definice inicializační funkce:
1static int __init mymodule_init(void)
2{
3      /* code */
45      return 0;
6}
78module_init(mymodule_init);
Zavádění, resp. inicializace modulu může s ohledem na alokaci potřebných zdrojů selhat. Důvodů je celá řada - nedostatek paměti, selhání registrace major-minor páru, atd. Programátor se musí rozhodnout, jestli daná chyba způsobí ukončení inicializace nebo jestli je možné v inicializaci pokračovat a dokončit ji, byť bude funkcionalita zavedeného modulu omezená.
Při předčasném ukončení inicializace (z důvodů chyby), musíme zajistit uvolnění již alokovaných prostředků, protože v jádře žádné automatické uvolňování není k dispozici. Jinými slovy, každý alokovaný prostředek musí modul zase poctivě uvolnit a to explicitně. Jádro za nás v tomto směru nic neudělá (zatím, aktivity v tomto směru již probíhají). Je důležité si uvědomit, že neuvolněné zdroje jednak zabírají místo v paměti a jednak zvyšují nestabilitu jádra. Proto pokud se nějaký modulu v tomto ohledu nechová slušně, tak často pak nezbývá nic jiného než restart systému.
Ohledně pořadí uvolňování zdrojů platí jednoduché pravidlo - provádíme je přesně v opačném pořadí než jejich alokaci.
Způsobů jak implementovat uvolňování zdrojů je několik. Buď můžeme použít konstrukci s goto a na konci funkce uvést potřebná návěstí, za nimiž bude kód uvolňující daný alokovaný prostředek. Při chybě v inicializaci pak stačí skočit pomocí goto na příslušné návěstí. Například nějak takto:
1int mymodule_init(void)
2{
3      res = alloc(item1);
4      if (res < 0)
5              goto err1;
67      res = alloc(item2);
8      if (res < 0)
9              goto err2;
1011     return 0;
1213     // uklid pri chybe
14     err2:
15             free(item1);
1617     err1:
18             return res;
19}
Nevýhodou je, že stejná část kódu, která zajišťuje uvolnění všech alokovaných prostředků, musí být jak v inicializační funkci, tak i v ukončovací funkci (viz dále), která se volá při odstraňování modulu z jádra. Musíme si proto hlídat, aby v obou funkcích byla tato část vždy stejná.
Další možností je tuto část kódu vyvést do vyhrazené funkce (nazvěme jí úklidovou funkcí) a tu pak volat jak z inicializační funkce (při selhání), tak i z ukončovací funkce. Musíme si ovšem nějakým způsobem udržovat přehled o úspěšně alokovaných zdrojích. Proto se úklidové funkci předává číslo chyby (vhodné pro jednodušší inicializaci) nebo se používá sada příznaků, které se nastavují po úspěšné alokaci daného zdroje (řešení pro složitější inicializace). První případ s předáváním čísla chyby může vypadat nějak takto:
1int mymodule_init(void)
2{
3      int res = 0;
45      res = alloc(item1);
6      if (res < 0)
7              cleanup(Err1);
8              goto End;
910     res = alloc(item2);
11     if (res < 0)
12             cleanup(Err2);
13             goto End;
1415     End:
16             return res;
17}
1819//——————————————————————--
20void cleanup(int err)
21{
22     switch (err)
23     {
24             case ModuleExit:
25                     free(item2);
2627             case Err2:
28                     free(item1);
2930             case Err1:
31                     break;
3233             default:
34                     break;
35     }
36}
3738//——————————————————————--
39void mymodule_exit(void)
40{
41     cleanup(ModuleExit);
42}
V souvislosti s inicializací modulu je nutné uvést ještě jednu důležitou věc. Od okamžiku, kdy požádáme jádro o začlenění moduluInformationVoláním funkce cdev_add() - ale to předbíháme. (rozuměj zveřejnění funkcí modulu) během vykonávání inicializační funkce, může jádro volat funkce modulu, byť vykonávání inicializační funkce nebylo ještě dokončeno! Proto je nutné, abychom veškeré alokování potřebných zdrojů provedli PŘED voláním funkce, která začlení modul do jádra. Jinak hrozí, že při volání funkcí modulu nebude mít modul všechny zdroje alokované. Následkem může být nestabilita nebo až pád systému.

Ukončení ovladače

Stejně jako je při zavádění modulu volána inicializační funkce, tak je při vyjímání modulu z jádra volán její opak - ukončovací funkce. Logicky je jejím úkolem je uvolnit všechny prostředky alokované v inicializační funkci a provést případně ještě další aktivity související s vyjmutím modulu.
Jak jsme už jsme se zmínili dříve, uvolňování prostředků probíhá přesně v opačném pořadí než jejich alokování. Prvním krokem tedy zpravidla bývá vyčlenění modulu z jádra, které zajistí, že funkce modulu již nejsou dále dostupné, nejsou veřejné. Pak se pokračuje postupně s uvolňováním dalších prostředků.
Ukončovací funkce je podobně jako inicializační deklarována jako statická a její název uvozujeme tokenem __exit, který zajistí, že tato funkce může být volána jen a pouze při vyjímání modulu. Registruje se makrem module_exit(nazev_exit_fce), které se spolu s makrem module_init() uvádí na konci zdrojového kódu. Pokud není zaregistrována žádná funkce jako ukončovací, jádro nepovolí vyjmutí modulu z jádra. Zajímavostí je, že jestliže je modul do jádra zakompilován jako součást monolitu jádra, tak jsou ukončovací funkce označené tokenem __exit vyjmuty z kompilace jádra a jádro je vůbec neobsahuje.
Příklad definice ukončovací funkce:
1static void __exit mymodule_exit(void)
2{
3      /* code */
4}
56module_exit(mymodule_exit);

Inicializační funkce

Teorii už máme za sebou, takže je nejvyšší čas se přesunout ke kostře našeho modulu a doplnit ji o čerstvě nabyté znalosti - přidat do ní inicializační a ukončovací funkci.
Nejprve doplníme hlavičku zdrojového souboru (výpis 9): přidáme hlavičkový soubor s definicí struktury cdev (řádek 2), která v jádře reprezentuje znakové zařízení, a pak hlavičkový soubor s definicí chybových kódů (řádek 3), které funkce modulu vrací jako návratové hodnoty. Následuje definice chyb, které se využívají při volání úklidové funkce (řádek 6 - 13) jako indikace místa, kde alokace zdrojů v průběhu inicializace selhala.
Na řádcích 16 - 22 se nachází definice struktury dskel_t, která slouží jako jakýsi kontejner všech proměnných důležitých pro běh modulu. Postupně do ní budeme přidávat další a další potřebné členy. První člen dev_num v sobě drží major-minor pár získaný od jádra při dynamické registraci major-minor páru. Do druhého členu cdev_ptr, typu ukazatel na strukturu cdev, budeme ukládat ukazatel na instanci struktury cdev, která v jádře reprezentuje naše znakové zařízení, resp. náš modul dskel.
Statická proměnná dskel (řádky 24 - 28) je instancí struktury dskel_t. Při její deklaraci zároveň nastavíme i výchozí hodnoty jednotlivých členů struktury dskel_t. Člen dev_num nastavíme pomocí makra MKDEV(major, minor), které slouží pro převedení číselného vyjádření major-minor čísla na speciální typ dev_t používaný řadou funkcí v jádře. A člen cdev_ptr nastavíme na NULL, protože platná hodnota se nastavuje až během inicializace modulu.
1/* ######## puvodni vkladane soubory ######## */
2#include <linux/cdev.h>
3#include <linux/errno.h>
45// ——————————————————————-
6#define DEV_NAME               "dskel"
7#define DEV_COUNT             1
89//——————————————————————-- 
10enum
11{
12     ModuleExit = 0,
1314     Failed_ChrdevRegion,
15     Failed_CdevAlloc,
16     Failed_CdevAdd,
17};
1819// ——————————————————————-
20// parametry ovladace
21struct dskel_t
22{
23     // major-minor par
24     dev_t                   dev_num;
25     // ukazatel na strukturu cdev reprezentujici znakove zarizeni
26     struct cdev *   cdev_ptr;
27};
2829static struct dskel_t dskel = 
30{
31     .dev_num = MKDEV(0, 0),
32     .cdev_ptr = NULL
33};
Výpis zdrojového kódu ovladače - hlavička (doplněk)
Struktura cdev je definována v hlavičkovém souboru <linux/cdev.h>. Z jejích členů nás bude zajímat jen člen .owner, a člen .ops, více se o jejich hodnotách dozvíme později.
Nyní přesuneme naši pozornost k první funkci modulu dskel - inicializační funkci dskel_init() (výpis 9). Můžeme ji v podstatě rozdělit na čtyři části: registrace major-minor páru, alokace paměti pro strukturu cdev, nastavení vlastníka a seznamu souborových operací v instanci struktury cdev a začlenění ovladače do jádra (zveřejnění jeho funkcionality).
Dynamickou registraci major-minor páru (řádek 7 - 12) zajišťuje funkce alloc_chrdev_region(). Předáme ji ukazatel na proměnou myDevNum typu dev_t, kterou vyplní přiděleným hlavním a vedlejším číslem (major-minor pár). Mimo to ji předáme první číslo rozsahu vedlejších čísel, které bude náš modul používat, a pak počet vedlejších čísel. Obvykle je prvním vedlejším číslem 0 (vedlejší číslo získáme zpět z typu dev_t makrem MINOR()) a pokud nehodláme obsluhovat více jak jedno zařízení, tak počet vedlejších čísel bude 1. V případě, že se registrace major-minor páru podaří, funkce vrátí 0, jinak zápornou hodnotu, kterou můžeme použít jako návratovou hodnotu inicializační funkce.
Pro instanci struktury cdev musíme alokovat paměť (řádek 15 - 21) a to pomocí funkce cdev_alloc(), která v případě úspěchu vrátí ukazatel na prázdnou instanci nebo NULL, pokud alokace paměti selhala.
Jakmile máme prázdnou instanci struktury cdev, nastavíme jí dva důležité atributy (řádek 24 a 25): vlastníka modulu na obligátní hodnotu THIS_MODULE a ukazatel na seznam souborových operací (proměnná typu struktura file_operations). Nastavení vlastníka modulu nesmíme vynechat, protože umožňuje jádru sledovat využití modulu. Pokud by vlastník nebyl nastaven, tak se může stát, že jádro modul automaticky vyjme.
Na závěr pak už jen modul začleníme do jádra, tj. zveřejníme jej. To má za úkol funkce cdev_add(), které předáme ukazatel na instanci struktury cdev, registrovaný major-minor pár a počet vedlejších čísel (= obsluhovaných zařízení). Ihned po úspěšném návratu z volání funkce cdev_add() může být modul používán jádrem či uživatelskými aplikacemi. Tehdy funkce vrátí 0, jinak vrátí zápornou hodnotu, kterou může inicializační funkce použít jako návratovou hodnotu.
Inicializační funkci musíme zaregistrovat makrem module_init(), kterému předáme název inicializační funkce - řádek 39.
1static int __init dskel_init(void)
2{
3      int result = 0;
45      // registrace k major-minor paru
6      result = alloc_chrdev_region(&dskel.dev_num, MINOR(dskel.dev_num),
7                                                               DEV_COUNT, DEV_NAME);
8      if ( result < 0 )
9      {               cleanup(Failed_ChrdevRegion);
10             goto End;
11     }
1213     // alokace pameti pro strukturu cdev
14     dskel.cdev_ptr = cdev_alloc();
15     if ( dskel.cdev_ptr == NULL )
16     {
17             result = -ENOMEM;
18             cleanup(Failed_CdevAlloc);
19             goto End;
20     }
2122     // nastaveni vlastnika a seznamu souborovych operaci
23     dskel.cdev_ptr->owner = THIS_MODULE;
24     dskel.cdev_ptr->ops = &dskel_file_ops;
2526     //  — pridani ovladace do jadra —
27     result = cdev_add(dskel.cdev_ptr, dskel.dev_num, DEV_COUNT);
28     if ( result < 0 )
29     {
30             cleanup(Failed_CdevAdd);
31             goto End;
32     }
3334End:
35     pr_alert("dskel_init(): %d\n", result);
36
                                                37     return result;
38}
3940module_init(dskel_init);
Výpis zdrojového kódu ovladače - funkce init()

Ukončovací funkce

Říká se to nejlepší nakonec. Náš výklad o zavádění a vyjímání modulu uzavřeme ukončovací funkcí, kterou si přidáme do našeho modulu dskel. Na výpisu 10 vidíme, že ukončovací funkce dskel_exit() je velmi jednoduchá - volá se jen úklidová funkce cleanup() se speciální hodnotou, která zajistí uvolnění všech alokovaných prostředků (řádek 4). Podobně jako u inicializační funkce, tak i ukončovací funkci musíme zaregistrovat makrem module_exit(), kde jako parametr vložíme název ukončovací funkce (řádek 7).
Naší pozornosti by neměla uniknout ani úklidová funkce cleanup(), která dělá veškerou ”špinavou” práci (řečeno samozřejmě s notnou dávkou nadsázky). Úklidová funkce se skládá z jednoho switche. Používá se propadání jednotlivých případů (case), takže případ ModuleExit se při svém zpracování propadne dolů až k poslednímu případu Failed_ChrdevRegion, čímž je zajištěno uvolnění všech prostředků ve správném pořadí. Na druhou stranu na řádku 25 vidíme, že když selže registrace major-minor páru, tak se žádný prostředek neuvolňuje, protože registrace major-minor páru je první prostředek, který se alokační funkce pokouší alokovat.
Uvolnění modulem registrovaného major-minor páru včetně celého rozsahu zabraných vedlejších čísel zajistí funkce unregister_chrdev_region(), které předáme major-minor pár v podobě proměnné typu dev_t a počet zabraných vedlejších čísel (řádek 22).
Funkce cdev_del() vyčlení modul z jádra, to znamená, že už nelze volat funkce modulu - modul se stane pro zbytek jádra a uživatelské aplikace nedostupný. Zajímavostí je, že funkce cdev_del() se nemá používat pro uvolnění paměti alokované funkcí cdev_alloc(). Tj. v situaci, kdy jsme sice úspěšně alokovali paměť pro instanci cdev, ale už se nám nepodařilo začlenit modul do jádra. Jinými slovy v jádru není k dispozici opak funkce cdev_alloc().
1static void cleanup(int error)
2{
3      switch ( error )
4      {
5              // kompletni uklid pri exitu
6              case ModuleExit:
7                      // vyjmeme modul z jadra                        
8                      // pozn.: cdev_del() se vola jen po uspesnem cdev_add()
9                      cdev_del(dskel.cdev_ptr);
1011             case Failed_CdevAdd:
12                     // pozn.: neexistuje opak fce cdev_alloc(), uvolneni pameti resi
13                     // jadro samo
14             case Failed_CdevAlloc:
15                     // uvolnime registrovany major-minor par
16                     unregister_chrdev_region(dskel.dev_num, DEV_COUNT);
1718             case Failed_ChrdevRegion:
19                     // jeste nic se nealokovalo, takze neni co vracet
20                     break;
2122             default:
23                     break;
24     }
25}
2627//——————————————————————-- 
28static void __exit dskel_exit(void)
29{
30     cleanup(ModuleExit);
3132     pr_alert("dskel_exit(): done\n");
33}
3435module_exit(dskel_exit);
36
Výpis zdrojového kódu ovladače - funkce exit()