
Jak to chodí v jádře aneb napište si vlastní ovladač (3)
Dnes dodržíme slib z minulého článku a rozebereme si podrobněji některé již
naťuknuté pasáže z minulých dílů. Co se výkladu týká, nepůjdeme o moc dále,
ale o to hlouběji.
Kompilace
Makro __KERNEL__
Určitě jste si všimli trochu opomíjeného makra __KERNEL__
z minulého dílu. O co jde? Jádro nabízí několik API pro přístup ke svým
funkcím a zpřístupňuje je uživatelským aplikacím a utilitám pomocí
hlavičkových souborů. V těchto souborech jsou deklarovány jak věci
jádra samotného, tak věci určené pro uživatelské aplikace. Právě k tomuto
rozlišení slouží naše makro __KERNEL__.
Podívejme se například na hlavičkový soubor linux/errno.h.
#ifndef _LINUX_ERRNO_H
#define _LINUX_ERRNO_H
#include <asm/errno.h>
#ifdef __KERNEL__
/* Should never be seen by user programs */
#define ERESTARTSYS 512
#define ERESTARTNOINTR 513
...
V případě, že překládáme věci pro jádro (definujeme makro
__KERNEL__) můžeme pracovat s makry ERESTARTSYS,
atp. V případě, že tento hlavičkový soubor vkládáme do uživatelské aplikace,
toto makro neni definováno a náš program "vidí" definice pouze ze souboru
asm/errno.h.
V případě vývoje jakékoli části jádra je nezbytně nutné definovat makro
__KERNEL__ a nejlépe na úrovni argumentu překladače
(-D__KERNEL__). V opačném případě nebude zpřístupněna celá řada
funkcí, které je možne v jádru využít.
Makro __MODULE__
Ještě malá poznámka k nám již známému makru. Definice musí být provedena
dříve než budeme vkládát hlavičkový soubor linux/module.h do
našeho ovladače.
Následující ukázka je samozřejmě špatně!
#include <linux/module.h>
#define MODULE
Nejlelegantnější řešení je definovat makro na úrovni překladače a to pomocí
-DMODULE. V našem ovladači již pouze kontrolujeme existenci makra
a podle toho se zařizujeme. Viz ukázky v minulém díle.
Konfigurační soubor linux/config.h
Tento soubor slouží ke zjištění přesné konfigurace jádra při
kompilaci. Využívá k tomu automaticky generovaný hlavičkový soubor
linux/autoconf.h, který obsahuje námi vytvořenou konfiguraci
(pomocí make config, make menuconfig či make
xconfig).
Podíváme-li se blíže do souboru linux/autoconf.h uvidíme nám
notoricky známé konfigurační volby ...
/*
* Automatically generated C config: don't edit
*/
#define AUTOCONF_INCLUDED
#define CONFIG_X86 1
#define CONFIG_ISA 1
#undef CONFIG_SBUS
#define CONFIG_UID16 1
/*
* Code maturity level options
*/
#define CONFIG_EXPERIMENTAL 1
...
Informace z tohoto hlavičkového souboru (tedy přesnou konfiguraci jádra)
využijeme v případě, že se náš ovladač chová rozdílně při několika nastaveních
jádra. V tom případě musíme vložit hlavičkový soubor
linux/config.h do našeho ovladače a odkazovat se na
CONFIG_* makra. Chcete-li předejít problémům, raději nikdy přímo
nevkládejte hlavičkový soubor linux/autoconf.h.
Makro __SMP__
Většina lidí vlastní počítač s jedním procesorem a říká si, že tímto makrem
se nebude zabývat, protože ho stejně nikdy nevyužije. To je ale samozřejmě
špatně, protože ostatní mohou vlastnit více procesorový stroj a Váš ovladač
jim tam nebude správně pracovat. Proto je nutné se tímto makrem zabývat i na
jedno procesorových systémech. Samozřejmě pouze v případě, že se Váš ovladač
chová rozdílně na jedno a na více procesorových strojích.
V případě, že ovladač překládáme pro více procesorový stroj musíme
definovat makro __SMP__ před vkládáním jakýchkoli hlavičkových souborů jádra.
To nám zajistí krátký kód na začátku našeho modulu ...
#include <linux/config.h>
#ifdef CONFIG_SMP
#define __SMP__
#endif /* CONFIG_SMP */
Optimalizace, inline funkce
Co je inline funkce? Je to v podstatě "obdoba" maker kdy funkce není
volána, ale na místo volání je vloženo její tělo. Rozdíl mezi makrem a inline
funkcí je ten, že makra zpracovává preprocesor a inline funkce již samotný
překladač.
Velké množství funkcí je deklarováno jako tzv. inline funkce.
Vše se odehrává v námi používaných hlavičkových souborech a pro správnou
funkci je nutné zapnout optimalizaci -Ox překladače (mluvím
o překladači, který je v drtivé většině používán a tím je
gcc).
Verze a moduly
Každý modul si v sobě nese informaci o verzi jádra, pro které byl
přeložen. Tento údaj je uložen jako řetězec a je deklarován v hlavičkovém
souboru linux/module.h takto ...
static const char __module_kernel_version[]
__attribute__((section(".modinfo"))) = "kernel_version=" \
UTS_RELEASE;
Problém ale nastává v případě, že je náš ovladač rozdělen do více
zdrojových souborů a v každém z nich je nutné použít zmiňovaný hlavičkový
soubor. Proč? V tom případě, by pole __module_kernel_version bylo
definována několikrát, což je nežádoucí.
K tomuto účelu slouží další makro __NO_VERSION__. V případě,
že toto makro existuje před vložením hlavičkového souboru
linux/module.h bude upuštěno od definice symbolu
__module_kernel_version. Pro nás to znamená jediné. V hlavním
zdrojovém souboru ovladače vložíme hlavičkový soubor klasickou cestou
a v ostatních zdrojových souborech budeme definovat makro
__NO_VERSION__ a náš problém bude vyřešen.
Další makra spojená s verzemi
UTS_RELEASE
Jedná se o textovou reprezentaci verze jádra. Například "2.4.18".
KERNEL_VERSION(major, minor, release)
Toto makro vytváří číselnou reprezentaci jádra a je definováno
následovně.
#define KERNEL_VERSION(a,b,c) (((a) << 16) + ((b) << 8) + (c))
Například číselná reprezentace verze jádra "2.4.18" je 132114.
LINUX_VERSION_CODE
Toto makro nese číselnou reprezentaci jádra, kterou jsme si ukázali
v předchozím případě. Neboli pro jádro "2.4.18" to bude 132114.
Tyto makra se používají při porovnávání verzí jádra v kterých došlo ke
změnám, které je nutné ošetřit v ovladači. Praktickou ukázku nalezneme
například v hlavičkovém souboru inteligentního více portového ovladače
stallion.
#if (LINUX_VERSION_CODE < KERNEL_VERSION(2,3,0))
struct wait_queue *open_wait;
struct wait_queue *close_wait;
struct wait_queue *raw_wait;
#else
wait_queue_head_t open_wait;
wait_queue_head_t close_wait;
wait_queue_head_t raw_wait;
#endif
K ukázce není nutný další komentář. Téma verzí jsme prozkoumali zase o něco
blíže a jelikož se jedná o rozsáhlejší kapitolu, necháme si některé další
pasáže do dalších dílů.
Inicializace, ...
Vlastní názvy funkcí
V minulém článku jsem si ukázali jakým způsobem je modul inicializován
a jakým způsobem se ukončuje. V případě, že se nám nelíbí funkce
init_module() a cleanup_module() můžeme použít
vlastní a pomocí maker module_init() a module_exit()
označit naší inicializační a ukončovací funkci. Na ukázku si zmodifikujeme
příklad (hello.c) z minulého dílu.
#ifdef MODULE
#include <linux/module.h>
MODULE_DESCRIPTION("Hello wolrd example");
MODULE_AUTHOR("Robert V0jta ");
MODULE_LICENSE("GPL");
#endif
#include <linux/init.h>
static int __init
module_start(void)
{
printk("<1>Hello world!\n");
return 0;
}
static void __exit
module_end(void)
{
printk("<1>Hello world is going home ...\n");
}
module_init(module_start);
module_exit(module_end);
Obě uvedená makra jsou definována v hlavičkovém souboru
linux/init.h.
Kontrola chyb v rámci inicializace modulu
Důležitou součástí inicializační funkce je i správná kontrola všech
potencionálních chyb. Znamená to zjednodušeně to, že v případě neúspěšné
inicializace NENÍ volána funkce cleanup_module() a proto je nutné
uvolnit veškeré, již alokované, prostředky v rámci funkce
init_module(), případně zavolat vlastní silou funkci
cleanup_module().
Nyní si ukážeme kratičkou ukázku, které se zabývá kontrolou chyb při
inicializaci.
#ifdef MODULE
#include <linux/module.h>
MODULE_DESCRIPTION("Error check example");
MODULE_AUTHOR("Robert V0jta ");
MODULE_LICENSE("GPL");
#endif
#include <linux/init.h>
#include <linux/mm.h>
static void *mem1 = NULL;
static void *mem2 = NULL;
static void __exit
cleanup_module(void)
{
if (mem1 != NULL)
{
kfree(mem1);
mem1 = NULL;
}
if (mem2 != NULL)
{
kfree(mem2);
mem2 = NULL;
}
}
static int __init
init_module(void)
{
mem1 = (void *)kmalloc(4096, GFP_KERNEL);
mem2 = (void *)kmalloc(4096, GFP_KERNEL);
if (mem1 == NULL || mem2 == NULL)
{
printk("<1>Out of free memory");
cleanup_module();
return -ENOMEM;
}
return 0;
}
Existuje zde několik možností jak vzniklé chyby ošetřit a další ukázku
můžete nalézt v přiloženém souboru (errcheck2.c).
User space, Kernel space
V krátkosti se zmíníme o významu oněch magických pojmů. Většina dnešních
procesorů nabízí několik úrovní ochrany, resp. přístupů k systémovým
prostředkům (například paměť, ...). Většinou je tento počet roven dvoum, ale
některé produkty nabízejí i víceúrovňové rozdělení.
Jádro samotné pracuje v té nejvyšší úrovni, tzv. supervisor módu, kdy má
neomezený přístup ke všem funkcím, které procesor nabízí. Tomuto módu se
v Linuxovém světě říká tzv. Kernel space.
Uživatelské aplikace běží na opačné straně, v té nejnižší úrovni a přístup
k prostředkům je kontrolován jádrem, resp. procesorem které dostává pokyny od
jádra. Tomuto módu se v Linuxovém světě zase říká tzv. User space.
Zájemce o problematiku odkazuji na stranu 19 LDD či do zdrojových kódů
jádra.
Závěr
Když jsem minule avizoval, které téma budeme probírat v dnešním díle tak
jsem si neuvědomil jaký rozsah jsem si určil (nad rámec tohoto článku) a proto
se i v následujícím díle budeme věnovat dnešnímu tématu.
Soubory
Použité ukázky v dnešním díle naleznete zde.
Další části seriálu:
|