
Jak to chodí v jádře aneb napište si vlastní ovladač (2)
V dnešním díle si ukážeme možnosti které nám jádro nabízí při sestavování
výsledného ovladače a nedočkavý člověk si taktéž přijde na své při klasické
ukázce "Hello world!" se stručným popisem.
Ovladač jako modul či nikoli?
Dnes máme na výběr dvě možnosti jak daný ovaladač sestavit. První možnost
je přímá kompilace do jádra. Druhá možnost je sestavení ovladače jako modulu.
Obě mají své výhody a nevýhody, které ve stručnosti shrneme níže.
Ovladač jako modul
Jako nesporná výhoda tohoto řešení se jeví snadná výměna chybného či
staršího ovladače za novější. V případě, že je daný ovladač nevyužit nám
jádro nabízí snadnou cestu jak právě "běžící" ovladač vyjmout a to pomocí
příkazu rmmod (více viz. man rmmod). Vyměnit
stávající modul za nový a následně ho opětovně zavést do jádra pomocí příkazu
insmod či modprobe (více viz. man
insmod, resp. man modprobe).
Tento systém modulárních ovladačů se nehodí například pro embedded
systémy. Proč? Embedded systémy většinou disponují velmi malou kapacitou
a utility na zavádění a vyjímání ovladače z jádra zbytečně zabírají místo
včetně přílušného kódu v samotném jádru. Pro představu tento kód
zabírá v současné stabilní řadě zhruba 1200 řádků jazyka C a uznejte, že
v embedded systémech je uspořených 1200 řádků celkem dost.
Další z výhod je možnost zadávání různých parametrů při opětovném zavádění
modulu a to pomocí souboru /etc/modules.conf či přímo na
příkazové řádce insmod, resp. modprobe.
V případě, že se rozhodnete zkompilovat ovladač jako modul, je nutné
definovat makro MODULE ať už na úrovni argumentu překladače
(-DMODULE) či přímo ve zdrojovém souboru #define
MODULE. Další nutností je zahrnutí hlavičkového souboru
linux/module.h. Více informací získate v níže uvedeném
příkladu.
Ovladač přímo v jádře
Nevýhoda tohoto řešení je "krkolomný" update ovladače s nutností sestavit
celé jádro znovu a poté restartovat počítač využívající tohoto jádra. Na
druhou stranu se tato možnost perfektně hodí pro embedded systému.
Pamatuji se, jak zde panoval mýtus o tom, že ovladač přímo zakompilovaný
v jádru je rychlejší než ovladač zavedený jako modul. Nikdy jsem to nezkoušel,
nikdy jsem to neměřil, ale mohu zde prohlásit, že tomu tak není.
Modul či přímo do jádra?
Jak se rozhodnout? Záleží na spoustě okolností a rozhodnutí na tom, jak
daný ovladač sestavit nechám na čtenáři. Pokud Vás netrápí místo (jako jsou
v embedded systémech) doporučuji zůstat u ovladačů v modulech.
Ať už se rozhodnete jakkoli, měl by Váš ovladač nabízet obě možnosti
a v případě rozdílného chování ošetřit patřičné části kódu pomocí
#ifdef, atd.
Dodatečné informace ohledně modulu
Modul má ještě jednu "výhodu", lze v něm nadefinovat několik doplňujících
informací jakými jsou například popis, autor modulu, licence modulu či seznam
parametrů modulu.
MODULE_DESCRIPTION(string)
Pomocí tohoto makra můžeme nadefinovat jednoduchý popis modulu. Text
by měl být stručný, maximálně jednořádkový a měl by informovat o čem
daný modul pojednává.
MODULE_AUTHOR(string)
Pomocí tohoto makra můžeme nadefinovat autora či případné autory
modulu. Platí zde "omezení" stejně jako v popisu, co nejkratší a co
nejvýstižnější.
MODULE_LICENSE(string)
Toto makro definuje pod jakou licencí je daný modul napsán. Pokud
zavádíte modul a licence není uvedena nebo se "neshoduje" s licencí
jádra, bude vypsáno varování, že Váš modul nakazil jádro a bude
zaveden.
Seznam akceptovaných licencí (pro free software) je tento - "GPL",
"GPL and additional rights", Dual BSD/GPL" and "Dual MPL/GPL". Pokud
vytváříte komerční produkt Vaše licence by měla být nastavena na
"Proprietary".
MODULE_PARM(var,type-string)
Tímto makrem definujeme parametr jádra a jeho typ. První argument je
var což značí název parametru a druhý argument je typ,
který značí typ námi definovaného parametru.
Jako typ můžete uvést b (pro byte), h (pro short),
i (pro int), l (pro long) a s (pro string). Dále
můžete uvést minimální a maximální počet prvků pole, které zastupuje
daný parametr a to ve formátu [MIN[-MAX]]{b,h,i,l,s}. V případě, že
neuvedete ani MIN ani MAX, považuje se MIN za 1. V případě, že uvedete
pouze MIN, tak MAX se nastaví na MIN a tudíž udáváte pevný počet prvků
pole. Pokud uvedete i MIN i MAX, definujete minimální a maximální počet
zadávaných prvků.
Například MODULE_PARM(irq, "1-2i") uvádí, že parametr
irq je typu int a může obsahovat jedno až dvě čísla, například
irq=7,11.
Ukázku použití parametrů modulu najdete na konci tohoto článku
a v přiloženém souboru, který obsahuje všechny dnes použité
ukázky.
MODULE_PARM_DESC(var,desc-string)
Tímto makrem definujeme popis parametru jádra. Například
MODULE_PARM_DESC(irq, "IRQ lines") definuje popis parametru
irq.
- A další ...
... které zatím nebudeme zmiňovat.
K čemu mi ale takové dodatečné informace jsou? Takové informace jsou
důležité pro uživatele, který chce Váš modul použít a nezná seznam dostupných
parametrů. K získání těchto informací slouží utilita modinfo se
kterou se seznámíme níže v rámci ukázkového příkladu.
Hello world!
Pro nedočkavé jedince zde máme ukázkový modul ...
Zdrojový kód
#ifdef MODULE
#include <linux/module.h>
MODULE_DESCRIPTION("Hello wolrd example");
MODULE_AUTHOR("Robert V0jta ");
MODULE_LICENSE("GPL");
#endif /* MODULE */
#include <linux/init.h>
int __init
init_module(void)
{
printk("<1>Hello world!\n");
return 0;
}
void __exit
cleanup_module(void)
{
printk("<1>Hello world is going home ...\n");
}
MODULE
Jak sami vidíte, v našem jednoduchém ovladači kontrolujeme definici makra
MODULE a podle toho se v našem zdrojovém souboru
zařizujeme. V případě, že náš ovladač kompilujeme jako modul tak musíme
zahrnout hlavičkový soubor linux/module.h a také definujeme
dodatečné informace ohledně našeho modulu.
modinfo
Nyní se dostáváme k ukázce použití utility modinfo aplikovanou
na náš ukázkový modul.
[root@echelon 2]# modinfo ./hello.o
filename: ./hello.o
description: "Hello wolrd example"
author: "Robert V0jta "
license: "GPL"
[root@echelon 2]#
Inicializace a deinicializace ovladače
Inicializace našeho modulu provádí jedna z hlavních funkcí a tou je
int init_module(void), která je volána jádrem při zavádění
modulu. V této funkci se provádí inicializace modulu, HW a registrují se
veškeré funkce, které modul nabízí. Rozdíl proti klasické aplikaci a modulem
je ten, že aplikace se spustí a její hlavní funkce běží dál a obstarává
komunikaci s uživatelem, atd. Kdežto modul se spustí, init_module()
zaregistruje veškeré nabízené funkce a ukončí se. Návratová hodnota, která
značí úspěšnou inicializaci je 0 a jakákoli jiná hodnota
signalizuje chybu při zavádění modulu.
Druhou funkcí tohoto modulu je void cleanup_module(void)
která se volá před odstraněním modulu z jádra a obstarává odregistrování
veškerých funkcí, které modul nabízí. Jedná se o tzv. úklid kdy je potřeba
uvolnit všechny alokované a používané prostředky.
Pozorný čtenář si jistě všiml maker __init či
__exit, která jsou definována v hlavičkovém souboru
linux/init.h. Co znamenají? Takto označujeme funkce, které jsou
inicializační a deinicializační. Vysvětlím blíže, při zavádění modulu se
zavolá funkce init_module která po úspěšném provedení nebude
již nikdy použita a tudíž ji označíme pomocí __init a jádro je
schopno tuto funkci po použití odstranit z paměti a nově vzniklé místo využít
jinak. Analogicky funguje makro __exit.
Poslední použitou funkcí je printk(). Jedná se o obdobu
notoricky známé printf() přizpůsobenou potřebám jádra. Již na
první pohled Vás zaujme ona "ozávorkovaná" jednička. Jedná se úroveň logování
použitou pro danou zprávu. Úrovní máme osm a může je vyžívat pomocí
"ozávorkovaných" čísel (0-7) či pomocí definovaných maker:
#define KERN_EMERG "<0>" /* system is unusable */
#define KERN_ALERT "<1>" /* action must be taken immediately */
#define KERN_CRIT "<2>" /* critical conditions */
#define KERN_ERR "<3>" /* error conditions */
#define KERN_WARNING "<4>" /* warning conditions */
#define KERN_NOTICE "<5>" /* normal but significant condition */
#define KERN_INFO "<6>" /* informational */
#define KERN_DEBUG "<7>" /* debug-level messages */
Zápis printk(KERN_ALERT "Hello world!\n"); je ekvivalentní
volání printk() z funkce init_module(). Pro použití
těchto maker je nutné vložit hlavičkový soubor
linux/kernel.h.
Prvně definovanou úroveň (KERN_EMERG) doporučuji používat
pouze v naléhavých případech, kdy hrozí, že se systém zhroutí, protože tato
zpráva je vypsána na všechny aktivní terminály počítače.
Kompilace modulu, zavádění modulů
Stále tu mluvíme o kompilaci modulu, zavedení a odstranění modulu. Kde nic
tu nic o praktické ukázce kompilace. Zde je ukázkový Makefile ...
MODULES = hello.o hello-fail.o
CC = gcc
all: $(MODULES)
clean:
rm -f $(MODULES)
hello.o: hello.c
gcc -o hello.o -c hello.c -DMODULE -D__KERNEL__
hello-fail.o: hello.c
gcc -o hello-fail.o -c hello.c -D__KERNEL__
... který zkompiluje z našeho zdrojového souboru dva moduly. Jeden z nich
je hello.o a druhý z nich je hello-fail.o. V prvním
definujeme makro MODULE které je nutné pro modul a v druhém tuto
definici vypustíme. Nyní si tedy moduly zkompilujeme ...
[root@echelon 2]# make
gcc -o hello.o -c hello.c -DMODULE
gcc -o hello-fail.o -c hello.c
[root@echelon 2]#
... zavedeme první z nich ...
[root@echelon 2]# insmod hello.o
... podíváme se, zda je modul zaveden ...
[root@echelon 2]# lsmod | grep hello
hello 796 0 (unused)
[root@echelon 2]#
... a náš modul odstraníme z jádra.
[root@echelon 2]# rmmod hello
Pokud jste na konzoli neviděli kýžené výpisy zpráv pomocí
printk() nezoufejte a prohlédněte si je pomocí dmesg.
[root@echelon 2]# dmesg | tail -2
Hello world!
Hello world is going home ...
[root@echelon 2]#
Nyní si ukážeme co se stane pokud zadáme příkaz na zavedení našeho druhého
modulu v kterém nebylo při kompilaci definováno makro MODULE.
[root@echelon 2]# insmod hello-fail.o
hello-fail.o: couldn't find the kernel version the module was \
compiled for
[root@echelon 2]#
Na závěr si ukážeme co nám poví jádro v případě, že není uvedena licence
u modulu.
[root@echelon 2]# insmod hello.o
Warning: loading hello.o will taint the kernel: no license
See http://www.tux.org/lkml/#export-tainted for information about\
tainted modules
Module hello loaded, with warnings
[root@echelon 2]#
Ukázka ovladače s parametry
Zde je malá ukázka ovladače, který využívá možnosti paremtrů.
#ifdef MODULE
#include <linux/module.h>
#define MAX_IRQ 9
static int irq [ MAX_IRQ + 1 ] = { [ 0 ... MAX_IRQ ] = -1 };
static int verbose = 0;
MODULE_DESCRIPTION("Hello wolrd example");
MODULE_AUTHOR("Robert V0jta ");
MODULE_LICENSE("GPL");
MODULE_PARM(irq, "1-10i");
MODULE_PARM_DESC(irq, "IRQ lines");
MODULE_PARM(verbose, "i");
MODULE_PARM_DESC(verbose, "Be verbose about IRQs?");
#endif
#include <linux/init.h>
int __init
init_module(void)
{
printk("<1>Loading modargs module ...\n");
if (verbose)
{
int i;
printk("<1>modargs verbose on ...\n");
for (i = 0 ; i <= MAX_IRQ ; i++)
if (irq[i] != -1)
printk("<1>modargs IRQ: %i\n", irq[i]);
else
break;
}
return 0;
}
void __exit
cleanup_module(void)
{
printk("<1>Unloading modargs module ...\n");
}
Použití parametrů při zavádění modulu ...
[root@echelon v0jta-ldd-2]# insmod ./modargs.o irq=10,5,6
Loading modargs module ...
[root@echelon v0jta-ldd-2]# rmmod modargs
Unloading modargs module ...
[root@echelon v0jta-ldd-2]# insmod ./modargs.o irq=10,5,6 verbose=1
Loading modargs module ...
modargs verbose on ...
modargs IRQ: 10
modargs IRQ: 5
modargs IRQ: 6
[root@echelon v0jta-ldd-2]# rmmod modargs
Unloading modargs module ...
[root@echelon v0jta-ldd-2]#
Závěr
Nyní jsme schopni napsat jednoduchý modul a zkompilovat ho. Dále umíme
zjistit informace o modulu pomocí modinfo a také umíme modul
zavést a odstranit z jádra.
V příštím díle se podíváme na problematiku kompilace, instalace a zavádění
modulu více podrobněji.
Soubory
Použité ukázky v dnešním díle naleznete zde.
Další části seriálu:
|