
Jak to chodí v jádře aneb napište si vlastní ovladač (5)
Dnešní díl bude trochu delší a podíváme se na to jakým způsobem jádro pozná
jaký ovladač použít. Co to jsou major, minor čísla a jak se s nimi pracuje
a také se podíváme na modul znakového zařízení a v krátkosti si představíme
nějaké souborové operace nad tímto zařízením. Samozřejmostí jsou ukázky
připojené k článku.
Malá změna
Rozhodl jsem se opustit téma verzí u exportovaných symbolů a přesunout je
na pozdější dobu. Budeme se věnovat zajímavějším věcem a k nim se vrátíme hned
jak bude potřeba.
Napsal jsem tento díl a snažil jsem se do něj umístit co nejvíce potřebných
informací. Bohužel se mi to nepovedlo a tak jsem je musel (z důvodu úspory
místa) přesunout do dalšího dílu. Potom mi to nedalo a říkal jsem si, že přece
nemohu popisovat strukturu file, kterou používám v tomto díle až
v díle dalším a tak jsem ty články zase spojil. Tím vznikl o malinko delší
a informativně obsažnější díl.
Rozlišování zařízení
V některém z prvních dílů jsem vám psal, že takřka vše je v Linuxu
soubor. Není tomu jinak v rámci přístupu na zařízení, který je řešen pomocí
speciálních souborů umístěných v adresáři /dev. Chceme-li
například přehrát audio data na našem DSP procesoru (zvukové kartě), použijeme
k tomu zařízení /dev/dsp. Tady se ale naskýtá otázka jakým
způsobem jádro pozná, že tento speciální soubor obsluhuje zrovna náš ovladač?
K tomu slouží tzv. hlavní a vedlejší čísla. Mě se tento překlad nelíbí a tak
v dalším textu zůstanu u originálu a budeme tomu říkat major a minor
čísla.
Major, minor čísla
Zvuková karta má několik zařízení (a speciálních souborů) kde jedním z nich
je již zmiňovaný /dev/dsp. Podívejme se na něj blíže (ls -l
/dev/dsp*).
crw------- 1 vojta root 14, 3 čec 23 20:50 /dev/dsp
crw------- 1 vojta root 14, 19 čec 23 20:50 /dev/dsp1
crw------- 1 vojta root 55, 0 čec 23 20:50 /dev/dsp56k
Z nabitých znalostí poznáme, že se jedná o znakově orientované
zařízení. Jeho vlastníkem jsem já a dále tu vidíme časovou informaci udávající
datum vytvoření. Ona dvě čárkou oddělená čísla jsou major a minor čísla.
Pomocí prvního z nich, neboli major čísla, jádro pozná, který modul
(ovladač) dané zařízení obsluhuje. V případě, že máme více zařízení stejného
druhu (zvukové karty), tak je rozlišujeme pomocí minor čísla. Toto minor
číslo je předáváno přímo ovladači (jádru samotnému na nic neslouží).
Obě čísla jsou osmibitová a jejich rozsah je "pouze" 0 až 255. Z toho jsou
rozsahy major čísla 60-63, 120-127 a 240-254 rezervovány pro
experimentování. Je to docela málo a tak vznikla možnost dynamického
přidělování major čísel.
Již alokovaná major čísla získáme ze souboru
/usr/src/linux/Documentation/devices.txt.
Ještě malá poznámka: dvě typově různá zařízení (blokové, znakové, ...)
mohou mít stejné major číslo.
Dynamická major čísla
Tato možnost je o něco složitější než výše uvedená a to z toho důvodu, že
je nutné vytvořit speciální soubory pro přístup k zařízením až po
zavedení modulu do jádra. Máme podporu dynamických major čísel v modulu,
zavedeme ho a jakým způsobem zjistíme, které major číslo nám bylo přiděleno?
Tyto informace dostaneme necháme-li si vypsat obsah souboru
/proc/devices. Ukázkový výstup následuje:
Character devices:
1 mem
2 pty
3 ttyp
4 ttyS
5 cua
6 lp
7 vcs
10 misc
13 input
Vzniká nám zde potřeba vlastního skriptu na zavádění modulu, který provede
samotné zavedení, vyparsuje major číslo z výstupu cat
/proc/devices a vytvoří příslušné speciální soubory v adresáři
/dev. Ukázkový skript je připojen k tomuto článku, ale teď
nepředbíhejme.
Souborový systém zařízení
Další možností je tzv. "Device filesystem". Ve zkratce jde o to, že při
inicializaci modulu bude vytvořen speciální soubor a po odstranění modulu bude
tento soubor také odstraněn. My se teď touto možností nebudeme zabývat (kvůli
rozsahu článku) a necháme si ji na začátek článku dalšího.
Major, minor čísla a jádro
V rámci jádra jsou tato dvě čísla zkombinována do jednoho a jsou uloženy
v jedné proměnné. Můžeme se setkat se dvěma typy:
kdev_t (linux/kdev_t.h)
Typ kdev_t se používá v rámci Linuxu a kombinuje major,
minor číslo do jednoho. Žádná část jádra nezná jeho přesnou strukturu
a přistupuje s k němu pomocí několika maker a funkcí. Proč tomu tak je
se dozvíme u dalšího typu.
dev_t (sys/types.h)
Typ dev_t byl definován pro UNIXy jako 16 bitové číslo,
to znamená osm
bitů pro major a osm bitů pro minor číslo. S postupem času vznikla
potřeba rozšířit alespoň rozsah minor čísel, ale nešlo to. Spousta
ovladačů přistupuje k tomuto typu přímo (zná jeho strukturu) a tak by se
změnou tohoto typu musela přijít i změna všech ovladačů, který tento
typ takto využívají. Což je nemyslitelné a tak se to stalo tomuto typu
osudným. Proto se u kdev_t používá sada maker a funkcí pro
případ, že by někdo chtěl kdev_t nadále modifikovat bez
nutnosti zasahovat do věcí, které ho používají.
Teď se podíváme na sadu maker a funkcí, které je nutné znát pro práci
s kdev_t.
MAJOR(kdev_t dev)
Toto makro vrátí major číslo.
MINOR(kdev_t dev)
Toto makro vrátí minor číslo.
MKDEV(int ma, int mi)
Toto makro vytvoří kdev_t typ z major a minor
čísel.
kdev_t_to_nr(kdev_t dev)
Převede typ kdev_t na typ
dev_t.
to_kdev_t(int dev)
Převede číslo dev_t na typ
kdev_t.
Vytváření speciálních souborů aneb mknod
Zde si dovolím vložit část manuálové stránky příkazu mknod.
JMÉNO
mknod - vytváří speciální soubory
POUŽITÍ
mknod [volby] soubor {bcu} major minor
mknod [volby] soubor p
Volby:
[-m práva] [--mode=práva] [--help] [--version]
Písmena {bcu} či p znamenají typ vytvářeného souboru.
- b - blokově orientované zařízení
- c - znakově orientované zařízení
- u - znakově orientované zařízení (nebufferované)
- p - pojmenovaná roura (FIFO) bez major a minor čísla
V následující ukázce si vytvoříme znakové zařízení lzdev s major
číslem 62 a minor číslem 0.
[root@echelon root]# mknod /dev/lzdev c 62 0
[root@echelon root]# ls -l /dev/lzdev
crw-r--r-- 1 root root 62, 0 lis 16 19:54 /dev/lzdev
[root@echelon root]#
Takto vytvořený soubor bude na disku do té doby než ho smažeme a stačí nám
na to "obyčejný" příkaz rm.
Ukázka zavedení modulu s dynamicky přiděleným major číslem
Nyní známe všechny informace, které potřebujeme pro vytvoření ukázkového
skriptu na zavedení modulu s dynamicky přidělovaným major číslem. Tady je:
#!/bin/sh
#
# Pri hrani si dejte pozor na prikaz ve skriptu rm -rf a
# promennou DESCRIPTION. Mozna dopadnete jako ja, kdyz
# jsem si pri psani toho smazal /dev/* adresar :)
#
MODULE=lzdev
DEVICE=lzdev
DESCRIPTION=LZ_oc_example
MODE=644
#
# 0 - dynamicky pridelene, > 0 staticky urcene Vami
#
MAJOR=0
# Nahrajeme nas modul
insmod $MODULE.o major=${MAJOR} || exit 1
# Smazeme vsechny nase zarizeni v /dev/
rm -rf /dev/${DEVICE}*
# Zjistime major cislo naseho modulu
MAJOR=`awk "\\$2==\"${DESCRIPTION}\" {print \\$1}" /proc/devices`
for MINOR in `seq 0 255`; do
mknod -m ${MODE} /dev/${DEVICE}${MINOR} c ${MAJOR} ${MINOR}
done
echo "Module ${MODULE} loaded with major device number ${MAJOR}"
Jak jednoduché, že? Nyní se podíváme na to, jak naše zařízení
implementovat.
Implementace ovladače - znakové zařízení
Myslím, že již máme dost teoretických informací a tak je na čase podívat se
na implementaci nějakého ovladače zařízení. Nejjednodušší zařízení je znakové
a tak stejně jako Alessandro začneme u něj.
Registrace zařízení
Řekli jsme si, že při nahrávání modulu, resp. jeho inicializaci se musí
zaregistrovat všechny nabízené možnosti našeho ovladače. Zabýváme-li se
ovladačem znakového zařízení musíme toto zařízení (ať už třeba fiktivní)
zaregistrovat. K tomu nám slouží následující funkce:
extern int register_chrdev(unsigned int, const char *, \
struct file_operations *);
Prvním argumentem je námi požadované major číslo pro zařízení. Číslo musí
být v rozsahu 0 až 255. Pokud chceme dynamicky přidělené major číslo tak
použijeme hodnotu 0.
Druhým argumentem je popis našeho zařízení, který se objevuje například ve
výstupu /proc/devices.
A na konec zde máme ukazatel na strukturu file_operations. Co
to je? Jelikož se jedná o znakové zařízení, přistupuje se k němu jako
k souboru, je nutné implementovan souborové operace jakými jsou například
open, read, write, close, ... K tomu slouží uvedená struktura, kterou si
popíšeme o pár řádků níže.
U návratových hodnot této funkce mohou nastat tři možnosti:
- < 0
V případě, že návratová hodnota je menší jako nula - nastala
chyba.
- == 0
V tomto případě funkce proběhla bez problémů a naše zařízení je
zaregistrováno.
- > 0
V tomto případě funkce také proběhla bez problémů, naše zařízení je
zaregistrováno a dynamicky přidělené major číslo je právě tato návratová
hodnota. Již víme, že o dynamické major číslo žádáme použitím 0 jako
prvního argumentu registrační funkce.
Souborové operace
Dnes se zaměříme pouze na základní souborové operace jakými jsou open a
close. Je to z toho důvodu, že článek by neměl přesahovat určité množství
řádků a tak se na zbývající operace podíváme v rámci pokračování série.
Struktura file_operations je deklarována v hlavičkovém souboru
linux/fs.h. (Doporučuji se předběžně podívat na další typy
funkcí z této struktury)
int (*open) (struct inode *, struct file *)
Otevře-li někdo naše zařízení (speciální soubor) zavolá se tato
funkce. Prvním parametrem je struktura inode ke které se
dostaneme v dalších dílech a dnes si jenom povíme, že jedna její část
i_rdev je typu kdev_t a obsahuje major a minor
číslo našeho otevřeného zařízení (inode->i_rdev).
Druhým důležitým parametrem je struktura file
deklarována taktéž v hlavičkovém souboru
linux/fs.h. Tuto strukturu si popíšeme níže a zatím ji
přeskočíme.
Návratová hodnota funkce by měla být 0 v případě úspěchu. V opačném
případě je hodnota záporná a obsahuje jeden z předem definovaných
chybových kódů.
Funkce nemusí být ve struktuře definovaná a stejně se nám podaří
zařízení úspěšně otevřít. Jediný problém je, že o tom nebude náš ovladač
nijak informován.
int (*release) (struct inode *, struct file *)
Tato funkce má parametry stejné jako funkce
open. V případě úspěchu vrací 0, jinak vrací záporný již
předem definovaný chybový kód.
Teď víme jak vypadají typy našich základních funkcí a my se v krátkosti
podíváme na to co by zhruba měli dělat.
open
- zvýšit počítadlo použití modulu
- zkontrolovat HW zařízení
- inicializovat HW zařízení v případě, že jej otevíráme poprvé
- pomocí minor čísla se rozhodnout o jaké zařízení jde, případně
modifikovat ukazatel na souborové operace (viz. dále
file->f_op)
- alokovat datové struktury a uložit je do
file->private_data
close
- uvolnit všechny použité prostředky uložené
v
private_data
- ukončit činnost zařízení pokud jde o poslední zavření
- snížit počítadlo použití modulu
Jedna z dalších věcí, která se týká struktury file_operations
je nastavení vlastníka. Slouží k automatickému určování toho, zda je modul
využíván či nikoli. K tomuto účelu je určeno makro
SET_MODULE_OWNER() definováno v hlavičkovém souboru
linux/module.h.
SET_MODULE_OWNER(&fop);
Struktura file
Každý otevřený soubor je v Linuxu reprezentován touto strukturou. Pokud
otevřeme jakýkoli soubor, jádro alokuje tuto strukturu, naplní ji a pracuje
s ní. Struktura je uvolněna jakmile je soubor všemi procesy, které ho
používají uzavřen.
Upozorňuji, že to nemá nic společného se známým typem FILE
a také to nemá nic společného se souborem na disku, který je reprezentován
strukturou inode.
My se teď podíváme na důležité části struktury:
mode_t f_mode
Tato část reprezentuje informaci, která nám říka jestli je soubor
otevřen pouze pro čtení, pro zápis či pro oba dva režimy. Zjistíte
to podle nastavených bitů FMODE_READ
a FMODE_WRITE. Zda je soubor otevřen pro čtení, zápis
kontrolujeme pouze v naší ioctl funkci. Jádro totiž naši
metodu pro zápis nepovolí zavolat v případě, že je soubor otevřen pouze
pro čtení, ale to malinko předbíháme.
loff_t f_pos
V této proměnné je uložena aktuální pozice pro čtení či
zápis. Ovladač tuto proměnnou může používat chce-li zjistit aktuální
pozici, ale neměl by tuto hodnotu nikdy měnit (kromě funkcí read, write,
ale o tom až se k tomu dostaneme).
unsigned int f_flags
Zde jsou nastaveny známé flagy O_RDONLY,
O_NONBLOCK či O_SYNC. Všechny jsou definovány
v hlavičkovém souboru linux/fcntl.h. Tato proměnná je
nejpoužívanější pro kontrolu zda-li naše operace jsou blokující či
nikoli. Zbytek se moc často nepoužívá, protože například mód
čtení/zápisu je kontrolován vůči f_mode.
struct file_operations f_op
Tato část ukazuje na námi implementované souborové operace pro dané
zařízení. Hodnotu ukazatele může dle libosti měnit a ihned po změně se
promítne a budou používány naše nové funkce. Slouží to například k tomu,
že pro jedno major číslo můžeme implementovan několik různých zařízení,
které rozlišujeme podle minor čísla. Tuto možnost jsem zmiňoval výše,
když jsem psal o tom co by měl ovladač udělat při otevření
souboru. Ukázka použití této vlastnosti je v příkladu na konci
článku.
void *private_data
Také jsem zmiňoval, že si ovladač při otevření může alokovat svá
vlastní data a ukazatel na ně uložit do této proměnné. Tak tady ji máte,
je obsažena ve struktuře file.
Struktura file obsahuje několik dalších informací, ale ty pro
nás nejsou důležité. Náš modul bude tyto informace pouze číst a nebude je
modifikovat, proto nás zbylé tolik nezajímají.
Odregistrování zařízení
K tomu nám slouží podobná funkce jako k zaregistrování:
extern int unregister_chrdev(unsigned int, const char *);
Kde prvním argumentem je major číslo (ať už námi pevně zvolené či dynamicky
přidělené). Druhým argumentem musí být stejný popis zařízení, který jsem
použil v případě registrace zařízení.
Ukázky
lzdev modul
Tento modul implementuje jak statické tak dynamické přidělování major čísla
(ovlivnitelné pomocí parametru major). Dále nám při otevření
zařízení vypíše v jakém módu jsme ho otevřeli a podle minor čísla přiřadí
správné souborové operace. Po uzavření zařízení nám bude vypsáno, která funkce
tento soubor zavřela. Ukázka je velmi jednoduchá a proto nepotřebuje dalšího
komentáře.
#ifdef MODULE
#include <linux/module.h>
MODULE_DESCRIPTION("LZ open/close example");
MODULE_AUTHOR("Robert V0jta ");
MODULE_LICENSE("GPL");
#endif /* MODULE */
#include <linux/init.h>
#include <linux/fs.h>
// Popis major cisla pro /proc/devices
static char *major_description = "LZ_oc_example";
// Nas modul bude automaticky zadat o dynamicke major cislo
static int major = 0;
// Pokud mame nejake pridele major cislo, muzeme ho zmenit
// pomoci major parametru tohoto modulu
MODULE_PARM(major, "i");
MODULE_PARM_DESC(major, "0 = dynamic, < 0 = static");
// Nasledujici dve funkce lz_release_* jsou zde na ukazku toho,
// jakym zpusobem muzeme zmenit ukazatel na souborove operace
// a tak ovlivnit chovani modulu pro ruzna minor cisla, resp.
// ruzna zarizeni
// Tato funkce je volana po close() na zarizeni s minor
// cislem 0-127
static int
lz_release_minor_0_127(struct inode *i, struct file *f)
{
printk("<1>lz_release_minor_0_127: Closing device \
with minor number %d\n",
MINOR(i->i_rdev));
MOD_DEC_USE_COUNT;
return 0;
}
// Tato funkce je volana po close() na zarizeni s major
// cislem 128-255
static int
lz_release_minor_128_255(struct inode *i, struct file *f)
{
printk("<1>lz_release_minor_128_255: Closing device \
with minor number %d\n",
MINOR(i->i_rdev));
MOD_DEC_USE_COUNT;
return 0;
}
// Nyni si nadefinujeme dve struktury souborovych operaci pro
// ruzna zarizeni, ktera se lisi podle vyse uvedenych minor cisel
static struct file_operations minor_0_127_fop =
{
release: lz_release_minor_0_127
};
static struct file_operations minor_128_255_fop =
{
release: lz_release_minor_128_255
};
// 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 with minor number \
%d\n", MINOR(i->i_rdev));
// 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");
// A podle minor cisla priradime spravne souborove operace
if (MINOR(i->i_rdev) < 128)
f->f_op = &minor_0_127_fop;
else
f->f_op = &minor_128_255_fop;
return 0;
}
// Definice zakladni operace open na nase zarizeni
static struct file_operations basic_fop =
{
open: lz_open
};
// Funkce zaregistruje nase zarizeni, eventuelne zaroven pozada
// o dynamicke prideleni major cisla a sdeli nam to pomoci
// printk()
static int __init
init_module(void)
{
int res;
SET_MODULE_OWNER(&basic_fop);
SET_MODULE_OWNER(&minor_0_127_fop);
SET_MODULE_OWNER(&minor_128_255_fop);
res = register_chrdev(major, major_description, &basic_fop);
if (res < 0)
return res;
if (res > 0)
major = res;
printk("<1>lz_sym device registered with major\
number %d\n", major);
return 0;
}
// Funkce odregistruje nase znakove zarizeni a da nam
// o tom zpravu
static void __exit
cleanup_module(void)
{
unregister_chrdev(major, major_description);
printk("<1>lz_sym device unregistered with major number\
%d\n", major);
}
lzdevuse program
Tento ukázkový program otevře naše fiktivní zařízení v několika různých
módech. Výstup z modulu, které nám říká v jakých módech bylo zařízení otevřeno
si můžeme přečíst pomocí dmesg.
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
void
use_dev(char *dev, int flags)
{
int fd = open(dev, flags);
if (fd == -1)
{
fprintf(stderr, "Failed to open '%s' device\n", dev);
return;
}
close(fd);
}
int
main(int argc, char *argv[])
{
int i;
if (argc == 1)
{
printf("Usage: lzdevuse dev1 [dev2 [dev3] [...]]]\n");
return -1;
}
for (i = 1 ; i < argc ; i++)
{
use_dev(argv[i], O_RDONLY);
use_dev(argv[i], O_WRONLY);
use_dev(argv[i], O_RDWR);
use_dev(argv[i], O_RDWR | O_NONBLOCK);
}
}
Makefile
MODULES = lzdev.o
CC = gcc
CFLAGS = -O2 -DMODULE -D__KERNEL__
USERPROG = lzdevuse
all: $(MODULES) $(USERPROG)
clean:
rm -f $(MODULES) $(USERPROG)
lzdev.o: lzdev.c
$(CC) $(CFLAGS) -o $@ -c $<
lzdevuse: lzdevuse.c
$(CC) -o $@ $<
Použití
Pomocí Makefile si sestavte modul včetně ukázkové aplikace. Modul zaveďte
pomocí lzload.sh a dále spusťe ukázkovou aplikaci
lzdevuse. V případě, že neuvidíte žádný výstup, podívejte se na
něj pomocí dmesg. Ukázkové příklady jsou jednoduché a nepotřebují
dalšího komentáře.
Ukázky není nutné kopírovat neb na konci článku najdete odkaz na archiv
obsahující všechny výše uvedené zdrojové kódy.
Závěr
Jsme zase o kus dále a umíme vytvořit modul pro znakově orientované
zařízení, umíme mu nadefinovat základní souborové operace a také zvládáme
zavádět a používat modul s dynamicky přidělovaným major číslem.
Příště se podíváme na zmiňovaný Device filesystem a další
souborové operace.
Soubory
Použité ukázky v dnešním díle naleznete zde.
Další části seriálu:
|