Linux E X P R E S

Facebook

Vývoj jádra XIV. - bloková zařízení

Dosud jsme se zabývali pouze zařízeními znakovými, nyní přicházejí na řadu ta bloková. Přestože je většina postupů stejných, některé věci jsou specifické a vyplatí se je dobře poznat.


Proč bloková zařízení

Zatímco znaková zařízení komunikují prostřednictvím proudů znaků, u blokových jsou to bloky dat o pevné velikosti. Pro znaková zařízení je také typické, že obecně nemusí být možné (a často není) přistupovat někam jinam než na konec proudu, kdežto bloková zařízení naopak umožňují přístup kamkoliv – pomocí adresace jednotlivých bloků.

Charakteristickými představiteli této kategorie jsou různá úložná zařízení – pevné disky, výměnné optické a magnetické disky (CD, DVD, ZIP, LS-120, diskety apod.), paměti typu flash (paměťové karty, USB disky) apod. Je ale samozřejmě možné používat takový způsob práce i u dalších zařízení, pro která to má smysl. Lze si například představit stroj pro hardwarové blokové šifrování, tam by to bylo vcelku logické. Významným členem rodiny blokových zařízení je loopback, což je virtuální zařízení poskytující blokový přístup do souboru uloženého na jiném zařízení – hodí se například pro přípravu instalačních obrazů (viz článek Michala Grni, pozn. šéfred.).

Logickým důsledkem vlastností blokových zařízení je, že se na nich dají vytvářet souborové systémy. U úložných zařízení je to vcelku samozřejmý požadavek. Nicméně mnohdy používáme bloková zařízení i bez souborových systémů, nejčastěji asi pro odkládací oddíl (swap).

Příprava ovladače

Úplně první věcí, která nás při psaní ovladače zajímá, je deklarace navenek. Podobně jako znaková, i bloková zařízení mají svá major a minor čísla. Číslovací prostor je oddělený, můžeme tedy mít znakové i blokové zařízení se stejným číslem, aniž by to jakkoli vadilo. Na základě těchto čísel se pak normálně vytváří soubory zařízení, typicky v /dev.

Nejprve si vyžádáme major číslo a zaregistrujeme název zařízení. Dále nás budou zajímat dvě datové struktury. První z nich je struktura souborových operací (podobná té u znakových zařízení), druhou pak struktura logického zařízení.

Ovladače zařízení (blokových i znakových) mohou nyní implementovat tři různé verze volání ioctl(). Pro nové ovladače by se měla používat verze unlocked_ioctl(), při které se neužívá Big Kernel Lock (BKL; zamyká celé jádro). Není-li tato verze implementována, použije se „původní“ ioctl() se zamykáním, která jinak existuje jen kvůli kompatibilitě s ovladači, které ještě mohou BKL potřebovat. Konečně poslední verzí je compat_ioctl(), určená pro 32bitové procesy s 64bitovým jádrem. Umožňuje definovat si vlastní převodní mechanismy pro přenášené hodnoty.

Struktura souborových operace obsahuje především funkce open() a release() a tři verze ioctl(). Dále jsou tu operace direct_access(), media_changed(), revalidate_disk() a getgeo() – ještě se o nich zmíním. Pak je tu struktura logického zařízení nazvaná gendisk. Alokuje se dynamicky a při tom se určí, kolik minor čísel potřebujeme. Major číslo se může přidělit automaticky. Pro vytvoření struktury se volá funkce alloc_disk(), důležitá je ještě funkce add_disk() – tou se vytvořená struktura přidá do systému a zařízení se stane viditelné navenek. Při odstraňování zavoláme del_gendisk() a put_disk(). Jak by to mohlo vypadat, vidíte v rámečku (deklaraci potřebných globálních proměnných vynechávám).

static int __init mymodule_init(void)
{
int res = 0;
res = register_blkdev(major, "myblk");
if (res <= 0)
goto reg_failed;
major = res;
gd = alloc_disk(1);
if (gd == NULL) {
res = -ENOMEM;
goto gd_failed;
}
rq = blk_init_queue(mymodule_request, &mylock);
if (rq == NULL) {
res = -ENOMEM;
goto rq_failed;
}    
gd->queue = rq;
gd->major = major;
gd->first_minor = 0;
gd->fops = &my_fops;
strcpy(gd->disk_name, "myblk");
set_capacity(gd, 1048576);
add_disk(gd);    
return 0;
rq_failed:
put_disk(gd);
gd_failed:
unregister_blkdev(major, "myblk");
reg_failed:
return res;
}
static void __exit mymodule_cleanup(void)
{
del_gendisk(gd);
blk_cleanup_queue(rq);
put_disk(gd);
unregister_blkdev(major, "myblk");
}

Příklad ukazuje kompletní inicializaci a uvolnění, včetně operací s frontou, o které teprve bude řeč. Funkce pro otevření a uvolnění zařízení, jakož i ioctl(), se v podstatě neliší od znakového zařízení. Proto je nyní můžeme přeskočit a přejít k něčemu mnohem důležitějšímu: zpracování požadavků.

Jistě si již každý všiml, že ve struktuře operací chybějí funkce pro čtení a zápis (byť lze tyto operace na souboru zařízení provádět). Bloková zařízení totiž tyto funkce nemají, namísto toho se pracuje s požadavky na přenos bloků. Požadavky se mohou (ale také nemusí) řadit do front – nad frontami pak pracují I/O plánovače blokové vrstvy, které umožňují optimalizovat přenosové operace.

Zpracování požadavků

Než se dostaneme k samotné realizaci, musím připomenout, že se jedná o část, která může běžet mimo kontext procesu. Proto je třeba dávat pozor na to, aby se zpracovávání nikde nezdržovalo (o uspání nemluvě). Pro synchronizaci přístupu se používají výhradně spinlocky. Bohužel ani ovladače přímo v jádře toto nedodržují úplně stoprocentně – sice to zatím funguje, ale v budoucnu se může něco změnit a rázem z toho bude problém.

Hlavním místem pro zpracování požadavků je funkce, která dostává jako parametr ukazatel na frontu. Před vlastním zpracování se požadavek vyžádá z fronty, vyhodnotí se, zda se týká souborového systému (existují i jiné požadavky, hardwarově specifické) a podle směru operace (čtení/zápis) se provede odpovídající přenos dat. Každý požadavek může v sobě ukrývat více elementárních požadavků, které se mají zpracovat společně (například pro delší souvislou oblast na disku). Po zpracování požadavku se musí oznámit výsledek zavoláním end_request() – která zajistí také odebrání z fronty – nebo některou ze složitějších metod.

Funkce end_request() je dost často nevhodná. Obsahuje totiž využití času obsluhy požadavku jako zdroje entropie pro generátor pseudonáhodných čísel (podobně, jako je to u obsluhy přerušení). U zařízení, která nevyhovují požadavkům na dostatečně náhodné chování, bychom funkci end_request() neměli používat. Raději dáme přednost řešení pomocí funkcí end_that_request_first() a end_that_request_last(), které se hodí též v případech, kdy potřebujeme mít odebírání z fronty a dokončování procesu lépe pod kontrolou.

Aby to fungovalo, tedy aby se funkce pro zpracování volala, musí se zaregistrovat. To se provede (obvykle při inicializaci modulu) zavoláním funkce blk_init_queue(), která současně inicializuje frontu požadavků. Frontu je potřeba při uvolňování ovladače zase zrušit (blk_cleanup_queue()). To vše už bylo součástí předchozího příkladu. Následující příklad ukazuje, jak popisovaná funkce vypadá:

static void mymodule_request(request_queue_t* q)
{
struct request *req;
while ((req = elv_next_request(q)) != NULL) {
if (!blk_fs_request(req)) {
end_request(req, 0);
}
else {
int res = rq_data_dir(req)
? mymodule_write(req->sector,
req->current_nr_sectors, req->buffer)
: mymodule_read(req->sector,
req->current_nr_sectors, req->buffer);
end_request(req, res);
}
}
}

Pro „nefilesystémové“ požadavky (zde nevítané) budeme výsledek nastavovat jako neúspěch, pro ostatní bude záležet na tom, jak dopadne přenosová operace. Nastavuje se 1 v případě úspěchu, 0 pro obecnou chybu (odpovídá -EIO) a záporné chybové kódy pro specifické chyby (dříve to bývalo pouze 1/0).

Požadavky lze do fronty vracet (elv_requeue_request()), lze zastavovat/povolovat příjem požadavků (blk_stop_queue()/blk_start_queue()) a provádět s frontou různé další operace.

Je jasné, že v řadě případů nelze přenos dokončit hned. Lze to udělat (jako je tomu i v uvedeném příkladu), jsou-li data v paměti, ale ne v případě, že se třeba čeká na pomalou hardwarovou operaci. V takovém případě se pouze pošle požadavek do zařízení (lze to třeba i jen naplánovat do pracovní fronty) a vlastní dokončení (end_request() apod.) provést až v okamžiku, kdy jsou data skutečně přenesena – tedy například v rámci obsluhy přerušení. Někdy je dobré ohlídat si časovačem, aby nějaký požadavek „nezkameněl“. Tedy aby nezůstal v zařízení věčně, což by se při nějaké hardwarové chybě mohlo stát. Trvá-li to moc dlouho, můžeme pak takový požadavek stornovat a případně vyvolat nějakou kontrolu stavu zařízení a/nebo ho resetovat apod., je-li to účelné.

Uvnitř požadavku

Jak jsem již řekl, každý požadavek může ve skutečnosti obsahovat požadavků více. Bloková vrstva se snaží optimalizovat přenosové operace, a proto spojuje dohromady takové požadavky, které je vhodné zpracovat společně (vždy ale pouze v jednom směru, buď čtení nebo zápis). Jsou uloženy v jednosměrně zřetězeném seznamu struktur bio. Tato struktura kromě jiného obsahuje pole vektorů pro segmenty fyzické paměti.

K čemu je to dobré? Pro běžné operace v ovladači to nepotřebujeme – mnohdy si vystačíme s virtuální adresou paměti uloženou v požadavku. Jenže pokud například používáme DMA, potřebujeme fyzické adresy, a právě v takovém případě využijeme toto. Na data ve strukturách bio ani na vektory se nepracuje přímo, používáme funkce a makra rq_for_each_bio(), bio_offset(), bio_kmap_irq() a další.

Další operace

Nyní bych se ještě krátce vrátil k dalším operacím, které se dají nastavit v příslušné struktuře. Funkce direct_access() je relativně nová a souvisí s mechanismem XIP (eXecute-In-Place). Umožňuje efektivně přistupovat k datům na rychlých paměťových médiích, která nepotřebují meziukládání do fyzické paměti. Funkci media_changed() jádro volá, když potřebuje zjistit, zda nebylo vyměněno médium. S tím souvisí i funkce revalidate_disk(), která se zavolá k aktualizaci stavu ovladače po výměně média. Obě funkce mají samozřejmě smysl jen pro výměnná média. A konečně poslední funkce, getgeo(), slouží ke zjištění geometrie disku – v zastaralé podobě (válec-hlava-sektor), se kterou ovšem některé programy (fdisk apod.) stále pracují.

Bohužel tu nezbývá místo pro mnoho dalších věcí, které se u ovladačů blokových zařízení používají. Mám na mysli třeba neopakovatelné požadavky, požadavky neřazené do fronty, vytváření požadavků v ovladači, předzpracování požadavků nebo bariéry pro zpracování. Také I/O plánovače by si zasloužily určitou pozornost. Zájemce odkazuji na informační zdroje včetně zdrojových kódů jádra, možná se ještě k některým zajímavým částem vrátíme v pozdějších dílech seriálu.

Zatímco ovladače pro bloková zařízení jsou ještě částečně podobné těm znakovým, pro síťová zařízení už to neplatí. Nemáme žádné soubory zařízení ani major a minor čísla, mezi aplikačním objektem (socketem) a zařízením je hodně dlouhá cesta – ale to bych předbíhal, příště se na to vrhneme naplno.

Diskuze (0) Nahoru