
Jak to chodí v jádře aneb napište si vlastní ovladač (6)
Dnes se budeme věnovat posledně zmiňovaném souborovému systému pro zařízení
neboli The Device filesystemu. Ke konci budete uvedeni do problematiky race
conditions a nemine vás ani ukázkový příklad.
The Device filesystem
Jedná se o souborový systém, který nám nabízí dynamickou tvorbu zařízení
v adresáři /dev. Můžete namítnout, že toto již zvládáte pomocí
mknod. Máte pravdu, ale co již nezvládáte je tvorba (a rušení)
této hierarchie v rámci vašeho modulu. Pokud jádro podporuje The Device
filesystem (dále jen devfs) můžete všechny tyto výhody využívat.
Máte možnost tvorby adresářů, zařízení, symbolických linků, atd. Problém
ale nastává s kompatibilitou. Starší jádra tuto možnost oficiálně nepodporují
(pro jádra 2.2.x existuje patch) a tak byste se setkali s mnoha
problémy. Pokud se rozhodnete devfs použít, doporučuji zaimplementovat
i starší metodu pro práci s major/minor čísly, kterou jsme si ukázali
v minulém díle.
Devfs není samo o sobě moc rozšířené a to i navzdory tomu, že nabízí velké
výhody. Problém je v tom, že vyvolává velkou nekompatibilitu proti již hotovým
uživatelským aplikacím. Proč? Většina těchto aplikací využívá zařízení
z adresáře /dev a když jim je změníme tak bohužel nefungují (ne
všechny). Proto je standardně tato volba vypnutá a kdo chce experimentovat či
upravovat stávající programy má samozřejmě možnosti si ji zapnout. Například
mnou používaná distribuce (RedHat) toto ve svém jádru nepoužívá.
Podívejme se v krátkosti na výhody tohoto systému.
- možnost tvorby/rušení zařízení za běhu (včetně adresářů, linků, ...)
- možnost specifikacu názvu zařízení, vlastníka a přístupových práv
z modulu
- odpadá práce s major/minor čísly
Samozřejmě systém nabízí i svoje nevýhody a tou je například zmíněná
nekompatibilita s uživatelskými aplikacemi. My se na něj stejně
v krátkosti podíváme ať máme základní přehled.
Použití devfs
Devfs můžeme použít pouze v případě, že jej podporuje jádro. V kladném
případě je v hlavičkovém souboru linux/config.h,
resp. linux/autoconf.h definováno makro
CONFIG_DEVFS_FS.
Všechny deklarace pro devfs jsou v hlavičkovém souboru
linux/devfs_fs_kernel.h.
devfs_handle_t
Vytvoříme-li jakýkoli objekt v devfs (adresář, link, zařízení, ...) vždy
nám příslušná funkce vrátí handle (také nebudu překládat) tohoto
objektu. Chceme-li s tímto objektem dále pracovat musíme tento handle
použít.
Tvorba adresáře
K tvorbě adresáře nám slouží jednoduchá funkce a tou je
devfs_mk_dir s následující syntaxí:
extern devfs_handle_t devfs_mk_dir (devfs_handle_t dir, \
const char *name, void *info);
Prvním argumentem je rodičovský adresář v kterém bude náš adresář
umístěn. Chceme-li jej umístit přímo do adresáře /dev použijeme
hodnotu NULL. Dalším parametrem je name který určuje
název námi vytvářeného adresáře. Poslední argument info není
u této funkce využíván. Podívejme se jak tedy vytvořit hierarchickou strukturu
adresářů /dev/lzdev/sound:
static devfs_handle_t lzdev, sound;
lzdev = devfs_mk_dir(NULL, "lzdev", NULL);
if (!lzdev) return -EBUSY;
sound = devfs_mk_dir(lzdev, "sound", NULL);
if (!sound)
{
devfs_unregister(lzdev);
return -EBUSY;
}
Trochu jsme předběhli a ukázali si jak uvolnit námi již vytvořený objekt
v devfs.
Tvorba zařízení
K tomu nám slouží obdobná funkce se jménem devfs_register
deklarována takto:
extern devfs_handle_t devfs_register (devfs_handle_t dir, \
const char *name, unsigned int flags, unsigned int major,\
unsigned int minor, umode_t mode, void *ops, void *info);
Jak sami vidíte, zase vrací devfs_handle_t, který si sebou
nese informaci o vytvořeném objektu. Teď se podíváme podrobněji na všechny
parametry.
devfs_handle_t dir
Specifikujeme rodičovský adresář (v případě NULL to je
/dev).
const char *name
Jméno vytvářeného zařízení.
unsigned int flags
Bitová maska několika různých nastavení. Jejich seznam je uveden
dále.
unsigned int major, unsigned int minor
Zde uvádíme major a minor čísla pro zařízení. V případě, že použijeme
flag DEVFS_FL_AUTO_DEVNUM jsou tyto čísla
nevyužita.
umode_t mode
Přístupová práva k zařízení a jeho typ. Definovány v hlavičkovém
souboru linux/stat.h a patří mezi ně vám určitě známé věci
jako S_IFCHR, S_IRUSR či
S_IWOTH. Prostudujte si tento hlavičkový soubor a zjistíte,
že při definici makra __KERNEL__ nám budou zpřístupněny
další věci, které normální uživatelské aplikace nevidí.
void *ops
Tento parametr ukazuje na nám již známé
file_operations. Z tohoto důvodu nepotřebujeme pracovat
s minor čísly, protože správné souborové operace lze přiřadit již při
vytváření tohoto zařízení.
void *info
Tento taktéž nepovinný parametr bude přiřazen do struktury
file na pozici private_data při otevření
vytvářeného zařízení.
Vraťme se ještě k parametru unsigned int flags a jeho
možnostem.
DEVFS_FL_NONE
Hodnota 0, neboli žádné flagy nejsou nastaveny.
DEVFS_FL_DEFAULT
Toto makro je nastaveno na makro
DEVFS_FL_NONE.
DEVFS_FL_AUTO_OWNER
V případě, že naše zařízení nikdo nepoužívá má nastavená práva
čtení/zápis pro kohokoli. Jakmile nějaký proces toto zařízení otevře
změní se vlastník na vlastníka procesu a následná změna přístupových
práv dovoluje práci se zařízením pouze onomu procesu. Jakmile proces
toto zařízení zavře, resp. jej přestane používat, je okamžitě
zpřístupněno širému okolí.
DEVFS_FL_SHOW_UNREG
Tímto flagem zamezíme odstranění naše souboru z adresáře
/dev při odregistrování zařízení.
DEV_FL_HIDE
V případě úspěšné registrace nebude toto zařízení vidět v adresáři
/dev.
DEV_FL_AUTO_DEVNUM
Nastavíme-li tento flag bude nám automaticky přiděleno major/minor
číslo. Toto číslo bude pro naše zařízení alokováno do dalšího restartu
systému, tj. budeme-li opětovně registrovat a odregistrovávat naše
zařízení tak vždy obdží stejné major/minor číslo.
DEV_FL_NO_PERSISTENCE
Nastavíme-li tento flag nebudou zachovány výše zmíněné informace při
opětovném registrování a odregistrování modulu. Jedná se o přístupová
práva, vlastníka, major a minor čísla.
Malá ukázka toho jak vytvořit nám již známé znakové zařízení
lzdev.
devfs_h = devfs_register(
NULL, // Umístění v adresáři /dev
"lzdev", // Název našeho zařízení
DEVFS_FL_AUTO_DEVNUM, // Automaticke přiřazení major/minor
0, 0, // Nepoužíté major/minor číslo
S_IFCHR | S_IRUGO | S_IWUGO, // Znakové zařízení s právy rw-rw-rw-
&basic_fops, // Základní souborové operace
NULL); // Možnost nastavení filp->private data
Jak sami vidíte je to vcelku jednoduchá záležitost.
Symbolický link
Dále máme možnost vytváření linků. Někdo má například svoji CD-ROM
mechaniku v počítači jako zařízení /dev/hdb a chce k ní
přistupovat pomocí /dev/cdrom. Není nic jednoduššího než
vytvořit symbolický link. Tuto možnost nám nabízí i devfs pomocí funkce
devfs_mk_symlink s deklarací:
extern int devfs_mk_symlink (devfs_handle_t dir, const char *name,
unsigned int flags, const char *link,
devfs_handle_t *handle, void *info);
Parametry jsou analogicky odvoditelné od funkce devfs_register
s tím, že parametr handle ukazuje na objekt na který má náš
symbolický link odkazovat.
Rušení objektů
K tomu nám slouží funkce devfs_unregister s následující
deklarací:
extern void devfs_unregister (devfs_handle_t de);
Jako parametr uvedeme handle vytvořeného objektu a rázem bude
zrušen. Následující ukázka zruší výše vytvořené lzdev
zařízení.
devfs_unregister(devfs_h);
Závěr devfs
Devfs nabízí mnoho dalších funkcí, ale není předmětem dalšího výkladu a tak
případné zájemce odkazuji do patřičných hlavičkových souborů
linux/devfs_fs_kernel.h a zdrojového kódu jádra.
K článku je připojena ukázka tvorby zařízení a nalézá se v souboru
lzdevfs.c. Můžete ji prostudovat a vznést případné dotazy. Tímto
končíme naše pojednání o Devfs a vrátíme se zpět ke slibovaným souborovým
operacím.
Race conditions
Než si ukážeme další souborové operace je nutné se zastavit ještě u jedné
věci. Tou jsou tzv. race conditions. Kdo již někdy programoval aplikaci
složenou z vláken tak určitě ví o čem mluvím.
Co to je race conditions? Představme si, že naše zařízení bude otevřeno
několika procesy (což je samozřejmě možné) a oba budou chtít přečíst určité
kvantum dat. My máme naše data uložená v bufferu a v další proměnné je uložena
aktuální pozice. Jeden proces začne data číst, posouvat ukazatel a najednou se
do toho vloží proces druhý co začne s tímto ukazatelem také hýbat a data
číst. Nikdy nepřečtou požadovaný konzistentní úsek data a naše zařízení nebude
pracovat správně. Stejný problém nastává v případě zápisu a existuje zde
spousta jiných možností.
Semafory
Jak tomu ale předejít. Jádro nabízí několik možností a my se zatím podíváme
na tzv. semafory. Taková malá analogie ze všedního života. Je-li semafor (či
závora) dole tak nemůžeme s autem přes přejezd a zvedne-li se závora, můžeme
v klidu pokračovat. Přesně k tomuto účelu slouží naše semafory.
Deklarace všech potřebných věcí naleznete v hlavičkovém souboru
asm/semaphore.h. Náš semafor je deklarován jako struktura
semaphore a používá se následovně.
Semafor obsahuje číselnou hodnotu, která se při zavírání závory (semaforu)
snižuje a při otevírání závory (semaforu) zvyšuje. Dostane-li se hodnota
semaforu na nulu, žádný jiný proces nemůže tuto závoru dát dolů a tak mu
nezbyde nic jiného než čekat tak dlouho dokud závoru (semafor) někdo
nezvedne. Poté ji on sám může dát dolů a pokračovat v jízdě (běhu
programu).
Asi jste zmatení a tak Vám ukážu základní funkce na obsluhu semaforů
a podíváme se na ukázkový příklad.
První z nich je funkce inicializační a tou je sema_init()
deklarována takto:
static inline void sema_init (struct semaphore *sem, int val)
První argument je ukazatel na náš semafor a druhý argument je počáteční
hodnota semaforu. Ve velké většině se používá hodnota 1, ale existují případy
kdy se dá použít i jiná (na to se podíváme jindy).
Semafor se dá dát dolů pomocí dvou funkcí down()
a down_interruptible(). Jejich deklarace je následující:
static inline void down(struct semaphore * sem)
static inline int down_interruptible(struct semaphore * sem)
V prvním případě proces čeká tak dlouho, dokud se neukončí funkce down,
resp. dokud nedá dolů semafor. Může se stát, že někde v kódu zapomenete
zvednout semafor nahoru a tak vytvoříte nezničitelný proces. Proč? Protože
proces ignoruje veškeré jemu zasílané signály.
Jak již z názvu plyne tak druhá funkce je přerušitelná. Vrací navíc
hodnotu, která v případě 0 znamená úspěšné přesunutí semaforu směrem
dolů. V opačném případě to znamená, že funkce byla přerušena signálem a my to
musíme poslat dále. Obvyklé chování je vrácení chybového kódu
-ERESTARTSYS.
Jak se semaforem nahoru? Na to existuje jednoduchá funkce up()
deklarována takto:
static inline void up(struct semaphore * sem)
Ukázka
Představme si jednoduchou věc. Máme buffer, ukazatel na aktuální pozici
a funkci pro zápis dat. Jak ošetřit, že se námi zapisovaná data nebudou
přepisovat?
Deklarujeme si proměnnou typu struct semaphore
a inicializujeme ji na hodnotu 1.
#include <asm/semaphore.h>
static struct semaphore sem;
static int __init
module_init(void)
{
...
sema_init(&sem, 1);
...
}
Dále si představme, že máme neomezené pole buffer s aktuální
pozicí uloženou v proměnné pos a píšeme funkci na zápis dat
write_data().
static int
write_data(void *data, int len)
{
if (down_interruptible(&sem))
return -ERESTARTSYS;
memcpy(buffer + pos, data, len);
pos += len;
up(&sem);
return 0;
}
Toto je úplně smyšlená funkce, kterou nikdy na nic nepoužijete. Nám
k výkladu stačí a teď si popíšeme co dělá. Na začátku se bude snažit dát
semafor dolů, pokud se jí to povede zapíše data a semafor zase zvedne. Pokud
se jí to nepovede, z důvodu přerušení, vrátí zápornou hodnotu a my ji pošleme
dále. V opačném případě čeká tak dlouho dokud tu závoru jiný proces
nezvedne. Poté ji sám dá znovu dolů a pokračuje dál.
Začnou-li tedy zapisovat dva procesy pomocí stejné funkce do stejného
bufferu tak se nic nestane, protože jejich přístupy jsou oštřeny pomocí
semaforu a tak jedna z nich musí počkat dokud ta první nedokončí svou
práci.
Race conditions
Proč takto hrubý úvod do race conditions? K tomu je jeden prostý důvod -
velmi brzy se podíváme na implementaci funkcí read/write pro naše zařízení
a budeme nutně potřebovat ošetřit výše popsané situace.
K samotné problematice se v budoucnu trochu podrobněji určitě vrátíme.
Ukázka devfs
Nyní se podíváme na ukázku, která nám ukazuje jak v ovladači vytvořit námi
požadovaná zařízení v adresáři /dev pomocí devfs. Ukázka je
jednoduchá a komentovaná, takže nepotřebuje dalšího výkladu.
#ifdef MODULE
#include <linux/module.h>
MODULE_DESCRIPTION("LZ The device filesystem example");
MODULE_AUTHOR("Robert V0jta <robert@v0jta.net>");
MODULE_LICENSE("GPL");
#endif /* MODULE */
#ifndef CONFIG_DEVFS_FS
#error This module works only with The Device filesystem support
#endif
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/devfs_fs_kernel.h>
#include <linux/config.h>
// Maximalni pocet zarizeni
#define DEVICES 256
// Definuj typ lzdev_t ...
typedef struct lzdev
{
devfs_handle_t devfs_handle;
int devnum;
} lzdev_t;
// Vytvor promennou pro adresar /dev/lzdev
static devfs_handle_t lzdevfs_dir_handle;
// Vytvor pole pro informace o zarizenich v /dev/lzdev/
static lzdev_t lzdevs[DEVICES];
// Tato funkce je zavolana po zavreni souboru (ne vzdy), vetsinou
// po poslednim zavreni souboru
static int
lz_release(struct inode *i, struct file *f)
{
printk("<1>lz_release: Closing device number %d\n",
((lzdev_t *)f->private_data)->devnum);
MOD_DEC_USE_COUNT;
return 0;
}
// Tato funkce bude pouzivat pro vsechny zarizeni s major cislem.
// V ni se rozhoduje jake operace priradime ruznym zarizenim
// (ruznym minor cislum)
static int
lz_open(struct inode *i, struct file *f)
{
MOD_INC_USE_COUNT;
printk("<1>lz_open: Opening device number %d\n",
((lzdev_t *)f->private_data)->devnum);
// Tady se podivame v jakem modu je nase zarizeni otevreno
if ((f->f_mode & FMODE_READ) == FMODE_READ)
printk("<1>lz_open: Opening device for reading%s\n",
(f->f_mode & FMODE_WRITE) != FMODE_WRITE ? " only" : "");
if ((f->f_mode & FMODE_WRITE) == FMODE_WRITE)
printk("<1>lz_open: Opening device for writing%s\n",
(f->f_mode & FMODE_READ) != FMODE_READ ? " only" : "");
if ((f->f_flags & O_NONBLOCK) == O_NONBLOCK)
printk("<1>lz_open: Opening device in non-blocking mode\n");
return 0;
}
// Definice zakladni operace open na nase zarizeni
static struct file_operations basic_fops =
{
open: lz_open,
release: lz_release
};
// V pripade devfs musime zaregistrovat nase zarizeni
static int
lzdev_devfs_register(void)
{
int i;
char dev[16];
// Vytvorime adresar /dev/lzdev
lzdevfs_dir_handle = devfs_mk_dir(NULL, "lzdev", NULL);
if (!lzdevfs_dir_handle)
return -EBUSY;
// A v nem 256 zarizeni s automatickym MAJOR, MINOR cislem
for (i = 0 ; i < DEVICES ; i++)
{
lzdevs[i].devnum = i;
snprintf(dev, sizeof(dev), "%i", i);
lzdevs[i].devfs_handle = devfs_register(lzdevfs_dir_handle,
dev, DEVFS_FL_AUTO_DEVNUM,
0, 0, S_IFCHR | S_IRUGO | S_IWUGO,
&basic_fops,
&lzdevs[i]);
}
}
// Pri odstranovani modulu musime nami vytvorene polozky
// v /dev/ odstranit
static void
lzdev_devfs_unregister(void)
{
int i;
// Mame je vubec vytvorene?
if (!lzdevfs_dir_handle)
return;
// Ano, odstran vsechny zarizeni v /dev/lzdev/
for (i = 0; i < DEVICES ; i++)
devfs_unregister(lzdevs[i].devfs_handle);
// Odstran adresar /dev/lzdev
devfs_unregister(lzdevfs_dir_handle);
}
// Funkce zaregistruje nase zarizeni, eventuelne zaroven pozada
// o dynamicke prideleni major cisla a sdeli nam to pomoci printk()
static int __init
init_module(void)
{
SET_MODULE_OWNER(&basic_fops);
return lzdev_devfs_register();
}
// Funkce odregistruje nase znakove zarizeni a da nam o tom zpravu
static void __exit
cleanup_module(void)
{
lzdev_devfs_unregister();
}
Závěr
Víme co je devfs, tušíme co to jsou race conditions a jak obsluhovat
semafory. Nyní máme vědomosti na to, abychom v příštím díle začali
s implementací funkcí read, write pro naše fiktivní zařízení.
Soubory
Použité ukázky v dnešním díle naleznete zde.
Další části seriálu:
|