DMA? Nutnost!
Máme-li zařízení, se kterým si počítač vyměňuje větší objemy dat a toto zařízení podporuje přímý přístup do paměti (DMA), je využití tohoto přístupu faktickou nutností. Nejde ani tak o rychlost (i když také) jako hlavně o zátěž procesoru. Kdo nevěří tomu, o jak drastickou zátěž jde, může si to snadno vyzkoušet. Stačí pomocí programu hdparm vypnout u své CD/DVD mechaniky DMA přenos a pak zkusit číst nějaký velký soubor. Zátěž procesoru může být oproti DMA přístupu vyšší běžně o dva řády!
I když samozřejmě, pokud to jde, implementujeme v ovladači také procesorem řízený přenos, DMA musí zůstat hlavní metodou volby, která se použije ve všech případech, kdy to bude možné.
Přímý přístup do paměti je hardwarově řešen typicky pomocí specializovaného řadiče. Zpracovává požadavky zařízení na přenos, žádá řadič sběrnice o přístup ke sběrnici, řídí vlastní přenos, po dokončeném přenosu uvolňuje sběrnici a výsledek indikuje pomocí stavových informací nebo přerušením.
Konkrétní řešení jak řadiče, tak vlastního přenosu se může velmi lišit podle konkrétní architektury. Pro nás to ovšem není příliš důležité, protože jádro poskytuje abstraktní odstínění od těchto platformně odlišných věcí.
Příprava pro DMA komunikaci
Podobně jako u přerušení i v případě DMA pracujeme s tzv. kanály, které patří mezi prostředky, k nimž je nutné vyhrazovat přístup. Vyhrazení a uvolnění probíhá podobným způsobem jako u přerušení (DMA kanál ale nelze sdílet). Protože skoro vždy potřebujeme kromě tohoto kanálu alokovat také právě přerušení, je vhodné to sdružit (nejdřív přerušení, pak DMA; při uvolňování naopak). Je také dobré tuto alokaci provést až v okamžiku, kdy je ovladač otevřen prvním procesem (a uvolnění pak při zavírání posledním procesem), aby pouhé načtení modulu neznamenalo zbytečné zabírání vzácných prostředků. Zde je krátký příklad vyhrazení a uvolnění:
int res = request_irq(9, mydevice_int_handler, SA_INTERRUPT, "mydevice", NULL); if (res != 0) return res; res = request_dma(2, "mydevice"); if (res != 0) { free_irq(9, NULL); return res; } ... free_dma(2); free_irq(9, NULL);
Přehled aktuálně vyhrazených DMA kanálů je k dispozici přes procfs v souboru /proc/dma.
Alokace paměti
I když to zní jednoduše, někdy to může být docela věda. Jde totiž o to, jaké jsou požadavky daného zařízení, jak budeme zpracovávat přijatá data atd. Můžeme využívat třeba jen jediný jednoduchý buffer, pak ale nastane problém, když se mají přenášet další data a ta původní tam stále „straší“, protože nebyla dosud zpracována nebo odeslána do zařízení.
Obvykle se tedy používají různá řešení kruhového bufferu. Hlavní problémy, se kterými se potýkáme, je jednak fragmentace fyzické paměti (může působit potíže u větších bufferů) a dále pak omezení na adresní prostor přístupný ze zařízení (prostor sběrnicových adres paměti). K řešení prvního problému se ještě dostaneme, druhý se vyskytuje jen u některých zařízení (přes starou sběrnici ISA nebo u některých méně obvyklých zařízení) a také ho ještě připomenu.
Všimněte si, že se pro sběrnicovou adresu nepoužívá typ unsigned long (ani void*), nýbrž speciální typ dma_addr_t. Je to proto, aby šlo snadno zkontrolovat, že se tato adresa nepoužívá k jiným účelům, než ke kterým je určena.
Jednoduchá alokace
Není to vlastně nic složitého, pouze se alokuje potřebný kus paměti – typicky voláním __get_free_pages(), paměť se pak uvolňuje pomocí free_pages(). Pokud nemáme naprostou jistotu, že je příslušné zařízení schopno adresovat celou paměť (a ne třeba jen dolních 24 bitů), není nutno omezovat adresní prostor; v opačném případě musíme při volání použít příznak __GFP_DMA. Pro alokaci paměti pro přenos ze zařízení je dobré použít příznak __GFP_COLD, který označuje předností použití stránek, které nejsou aktuálně cachovány (zbytečně by se zhoršilo aktuální využití stránek z cache). Viz příklad (zde se stránka také nuluje – kvůli bezpečnosti):
void* mem = (void*) __get_free_pages(GFP_KERNEL | __GFP_DMA | __GFP_COLD | __GFP_ZERO, 4);
Pokud se jedná o problém s fragmentací paměti, řešení je několik. První je přidat při alokaci příznak __GFP_REPEAT. Ten zajistí vícenásobné opakování pokusu alokace, i když může také selhat. „Drsnější“ verzí téhož je pak __GFP_NOFAIL, kdy se pokus opakuje až do úspěšného výsledku. Použití se ovšem silně nedoporučuje, protože může vést případně až k zablokování systému.
Jinou volbou je při startu systému omezit využitelnou paměť a pak si ji (pomocí funkce ioremap()) přivlastnit. Zde je ale problém s již zmíněným adresním prostorem. Lepší je ovšem rozdělit hodně velký kruhový buffer na více menších (což není problém), a pokud to zařízení umožňuje, rozdělit i buffery pro jednotlivé přenosy (s tím, že se pak využije scatter/gather metoda přístupu).
Není snad nutno připomínat, že veškeré adresy vracené alokačními funkcemi jsou logické adresy jádra a pro použití v zařízení se musí převést na sběrnicové adresy funkcí virt_to_bus(). Až tak jednoduché to ale zase není, jak se hned ukáže.
Abstraktní vrstva
Z výše uvedených odstavců je patrné, že to s pamětí pro DMA není až tak úplně triviální a chtělo by to nějaký pohodlnější mechanismus. Jádro nabízí abstraktní generickou hladinu, která se postará o řešení řady problémů, na které zde narážíme.
Nejprve adresní prostor – podle toho, o jaké zařízení jde, může být nutné prostor omezit, jako jsme to dělali při přímé alokaci paměti. Slouží k tomu funkce dma_set_mask(), která přijímá jako parametry ukazatel na strukturu device a dále 64bitovou masku adresního prostoru. Volání může selhat (tehdy vrací záporný kód chyby). Prostor podporovaný platformou lze zjistit funkcí dma_supported(). Pro zařízení podporující celý adresní prostor se nastavování masky neprovádí.
Další problém, který jsem zatím nezmínil, se týká koherence hlavní paměti a cache procesoru. Při použití abstraktní hladiny se o to není třeba zvlášť starat. Následující příklad ukazuje, jak alokovat a uvolňovat paměť, která bude splňovat potřebné požadavky a kdy získáme ihned i sběrnicovou adresu paměti:
dma_addr_t daddr; void* kaddr = dma_alloc_coherent(dev, 8192, &daddr, GFP_KERNEL | __GFP_ZERO); if (kaddr == NULL) return -ENOMEM;
Bohužel tu není dost prostoru pro podrobnější popis, vezmeme to tedy dále jen heslovitě. Pro malé DMA buffery lze používat „zásobníky“ paměti (pools). Zásobník se vytvoří funkcí dma_pool_create(), jednotlivé buffery se alokují pomocí dma_pool_alloc(), uvolňují funkcí dma_pool_free() a celý zásobník se na závěr zruší zavoláním dma_pool_destroy(). Zatímco u předchozího způsobu je minimální velikostí bufferu jedna stránka, zde může být velikost i mnohem menší. Jinak se použití v podstatě neliší, při alokaci opět získáme jak logickou adresu jádra, tak i sběrnicovou adresu.
Zatímco klasické koherentní mapování paměti (viz výše) přináší poměrně značnou režii, existuje ještě možnost tzv. proudového mapování (streaming). Při použití této funkcionality musíme předem zajistit alokaci paměti, určit směr přenosu (lze volit i obousměrný přenos, ale snižuje to výkon) a dokud je mapování aktivní, na tuto paměť vůbec nesahat (resp. lze to, ale musíme si to vyžádat a pak paměť vrátit zpět zařízení). Pro proudové mapování slouží funkce dma_map_single() a dma_unmap_single(), případně dma_map_page() a dma_unmap_page(); druhý pár funkcí pracuje s ukazatelem na strukturu page namísto logické adresy jádra.
Na informace o scatter/gather technice, o 64bitovém DMA na sběrnici PCI a další podobné zajímavé věci tu už bohužel nezbývá prostor. Snad se k nim ještě někdy vrátíme. Ještě připomenu, že na některých platformách (včetně např. x86) je implementace funkcí abstraktní generické vrstvy DMA velice triviální, ale kvůli přenositelnosti vždy používáme příslušné funkce.
Ovládání řadiče DMA
Před jakoukoli manipulací musíme přístup k DMA řadiči zamknout, aby se o něj ovladače „neporvaly“, po použití zase odemknout. Zamyká se funkcí claim_dma_lock(), odemyká release_dma_lock(). Zamykací funkce vrací aktuální stav přerušení, ten se pak musí předat odemykací funkci.
Dalším nezbytným krokem, než se bude nastavovat řadič, je zakázat příslušný kanál. Použije se k tomu funkce disable_dma(). Po skončení konfigurace můžeme kanál opět povolit funkcí enable_dma(). Ještě zbývá jedna nutná operace, a sice vynulování bitu, který určuje, která z polovin 16bitových registrů se používá. Tento bit se po přenosu bajtu automaticky přehodí, proto je nutné explicitně zajistit, aby byl nulový. Je pro to určena funkce clear_dma_ff().
Samotné nastavování řadiče spočívá ve volbě směru přenosu, nastavení adresy bufferu a jeho délky. Slouží k tomu funkce set_dma_mode(), set_dma_addr() a set_dma_count(). Po nastavení potřebných hodnot můžeme řadič opět aktivovat zmíněnou funkcí enable_dma() a odemknout ho.
Pro zjištění, zda již skončil, lze použít funkci get_dma_residue(). Vrátí-li hodnotu 0, přenos byl dokončen, je-li hodnota jiná, přenos ještě probíhá. Funkce sice „jako“ vrací zbývající objem dat, jenže vzhledem k neatomičnosti 16bitových přenosů může vracet nesmysly. Celé to může vypadat například takto:
unsigned long ifl = claim_dma_lock(); disable_dma(chan); clear_dma_ff(chan); set_dma_mode(chan, DMA_MODE_READ); set_dma_addr(chan, daddr); set_dma_count(chan, 100); enable_dma(chan); release_dma_lock(ifl);
Přístup k paměti procesu
V řadě případů se hodí, když můžeme v jádře pracovat s pamětí procesu přímo, bez volání speciálních funkcí. Je to jednak pohodlnější, ale hlavně rychlejší. Hodí se to hlavně pro práci s většími objemy dat. Pozor ovšem, že musíme na uživatelské straně bezpodmínečně zajistit nějakou formu synchronizace přístupu k této paměti, aby byla zajištěna konzistence dat.
Při mapování uživatelské paměti do jádra je nutné, aby byla adresa této paměti zarovnána na začátek stránky. Pozor také na to, že ve volání get_user_pages() lze změnit úroveň ochrany stránek.
K namapování paměti procesu do jádra slouží funkce get_user_pages(). Použití této funkce je poněkud obtížnější, protože jednak vyžaduje dost parametrů, a navíc se musí před jejím zavoláním zavřít (pro čtení) semafor správy paměti a po zavolání ho otevřít.
Po použití se musí každá ze stránek uvolnit zavoláním page_cache_release(). Pokud byla změněna, musí se také zavoláním SetPageDirty() označit za „špinavou“ (aby byla při případném uvolňování zapsána na disk) – výjimku tvoří pouze rezervované stránky, které lze otestovat voláním PageReserved().
Paměť lze použít po namapování jednotlivých stránek do logického adresního prostoru jádra – funkcí kmap(). Paměť samozřejmě obecně není fyzicky souvislá, takže pro běžné použití „vcelku“ v rámci jádra se musí namapovat do virtuálního adresního prostoru funkcí vmap(). Pokud bychom ji chtěli použít pro DMA (i to lze a je to někdy velmi výhodné), pak se při přenosech objemů nad jednu stránku neobejdeme bez použití metody scatter/gather (zařízení ji musí podporovat). Veškerá mapování samozřejmě musíme po použití zrušit.
Bloková zařízení
Veškeré dosavadní informace, pokud byly závislé na druhu zařízení, se týkaly zařízení znakových. Tedy takových, které komunikují prostřednictvím proudu znaků. Máme ovšem také mnoho zařízení, především úložných, která pracují s bloky dat. Tato zařízení mají svá specifika, ke kterým se dostaneme příště.