Základní vlastnosti
Jednoduché ovladače (moduly) se obvykle skládají jen z jednoho zdrojového souboru. S jedním zdrojovým souborem si vystačíme i my. Tento soubor budeme postupně rozšiřovat, tak jak budou nám přibývat potřebné znalosti.
Začněme základními informacemi o modulu. Jméno modulu a počet minor čísel, které modul bude používat definujeme jako symbolické konstanty DEV_NAME a DEV_COUNT. Tyto konstanty budeme hojně používat v inicalizační a ukončovací části kódu.
Dále se na konec zdrojového kódu (nenechte se zmást umístěním v ukázce kódu) umistují informace se jménem autora, licencí a verzí modulu a krátkého popisu jeho funkce, které si můžeme později po zavedení modulu zobrazit z příkazové řádky příkazem modinfo. K tomuto účelu je v hlavičkovém souboru <linux/module.h> definována skupina maker - viz výpis 1. Vedle jména autora (řádek 8) je dobrým zvykem přidat i email kvůli možnosti ohlásit chybu. Makro s typem licence (řádek 9) určuje pod jakou licencí je modul šířen a má vliv i na přístup k exportovaným symbolům (zveřejněným proměnným a funkcím) ostatních modulů. Krátký popis modulu (řádek 10) by měl být v angličtině. Verze modulu (řádek 11) je uvedena v tom nejjednodušším formátu.
1#include <linux/module.h> 2#include <linux/fs.h> 3 4// ——————————————————————- 5#define DEV_NAME "dskel" 6#define DEV_COUNT 1 7 8//——————————————————————-- 9MODULE_AUTHOR("mBoss <marek.laznicka@elvoris.cz>"); 10MODULE_LICENSE("GPL"); 11MODULE_DESCRIPTION("Simple but useful device drive skeleton"); 12MODULE_VERSION("0.0.1"); 13 14//——————————————————————-- 15// virtualni tabulka - tabulka operaci podporovanych ovladacem 16static struct file_operations dskel_file_ops = 17{ 18 .owner = THIS_MODULE, 19 .open = NULL, //&dskel_open, 20 .release = NULL, //&dskel_release, 21 .read = NULL, //&dskel_read, 22 .write = NULL, //&dskel_write, 23 // modul nepodporuje metodu llseek 24 .llseek = &no_llseek, 25};
Na řádcích 15 - 24 je uvedena definice proměnné dskel_file_ops typu struktura file_operations. Je to vlastně definice seznamu souborových operací, tedy systémových volání, které modul podporuje. Vzpomeňme si na obrázek 1.1, kde je toto rozhraní označeno jako rozhraní A. Když například aplikace z uživatelského prostoru zavolá knihovní funkci read(), knihovna libc převede toto volání na stejnojmenné systémové volání. V reakci na toto systémové volání jádro nejprve zjistí, který modul má volání obsloužit. Pak prohledá strukturu nalezeného modulu file_operations a zavolá funkci registrovanou pro obsluhu systémového volání read() (v našem případě tedy funkci dskel_read()). Podobným způsobem funguje obsluha všech systémových volání směřovaných na modul.
Poznámka: Na výpise 1 jsou všechny ukazatele podporovaných funkcí nastaveny dočasně na NULL. Modul totiž nemá zatím žádné funkce implementovány, takže žádná systémová volání dosud nepodporuje.
Seznam všech souborových operací ve struktuře file_operations, které lze směřovat na modul je poměrně obsáhlý. Naštěstí není třeba nastavovat funkční ukazatele pro všechny operace. Ty, které modul nepodporuje, necháme nastaveny na výchozí hodnotu (NULL). Jádro pak buď nevykoná nic (jen vrátí návratovou hodnotu) nebo provede nějakou výchozí operaci. Druhá možnost - volání výchozí operace - platí právě pro operaci llseek() - posun v souboru. Jelikož naše moduly nebudou vůbec podporovat posun v souboru, musíme zajistit, že se místo výchozí funkce bude volat speciální funkce jádra no_llseek (řádek 19). Ta zajistí, že pozice v souboru (našem znakovém zařízení) se při volání operace llseek() nezmění. Deklarace této speciální funkce je uvedena v hlavičkovém souboru <linux/fs.h>, který je třeba vložit do zdrojového souboru našeho modulu (řádek 2).
Jistě jste si všimli, že kromě funkčních ukazatelů má struktura file_operations i člen s názvem owner (řádek 13). Ten slouží k nastavení vlastníka a takřka vždy se nastavuje na hodnotu THIS_MODULE.
Naše úvodní povídání zakončíme ”desaterem”, které je vhodné mít při vývoji ovladačů na paměti:
- Kód ovladače může být vykonáván více procesy současně. Vedle souběhu více procesů z uživatelského prostoru, může být kód ovladače vykonáván zároveň i z kontextu obsluhy přerušení, při vypršení časovačů apod. Říkáme, že kód ovladače musí být tzv. reentrantní, tedy odolný vůči souběhu (race conditions). To znamená, že vývojář musí používat správné synchronizační mechanismy (atomické proměnné, semafory, apod.).
- Jádro má k dispozici velmi malý zásobník (většinou 4kB), který je sdílen kompletně celým kódem běžícím v prostoru jádra. Musíme se proto vyvarovat velkého počtu zřetězených volání a hlídat si velikost a počet lokálních proměnných. Pokud potřebujeme pro své proměnné více paměti, musíme je vytvářet jako dynamicky alokované, abychom nezpůsobili přetečení zásobníku..
- V prostoru jádra nelze používat operace s plovoucí desetinnou čárkou.
- Kód ovladače musí být co nejkratší ve smyslu vykonávání. Strojový čas je v případě jádra velmi drahý. To platí zvláště o rutinách obsluhy přerušení. Proto je třeba pečlivě volit i funkce implementující prodlevy, používat adekvátní synchronizační prostředky (atomické proměnné, semafory, mutexy apod.) atd.
- Je třeba velice pečlivě kontrolovat data z uživatelského prostoru (user space).
- Jakoukoliv paměť získanou od jádra je nutné vynulovat nebo nastavit na nějaké výchozí hodnoty (předejde se tak možnému úniku informací - hesla apod.).
- Jakoukoliv paměť předávanou do uživatelského prostoru je nutné inicializovat na žádané hodnoty.
- Pokud ovladač interpretuje nějaká data (protokol), tak musí být tato část napsána tak, aby nedošlo vlivem nevhodných dat k narušení bezpečnosti systému.
- Je třeba pečlivě kontrolovat a ošetřovat návratové hodnoty používaných funkcí.
- Vývojáři modulu jsou k dispozici jen funkce dostupné ve stromě jádra včetně všech funkcí a proměnných zveřejněných ostatními moduly. Žádné knihovní funkce až na vyjímky nelze použít.
- Modul jádra může být úspěšně zaveden pouze do jádra, které má stejnou verzi a které je určeno pro stejnou platformu (procesor), jako bylo jádro vůči kterému je modul jádra sestaven.