Druhy adres
Předpokladem správné práce s pamětí je pochopení její adresace. Ta se liší podle jednotlivých "druhů" paměti, s nimiž v jádře pracujeme. Druhy adres nesmíme vzájemně zaměňovat, byť jsou adresy různých druhů (zejména na některých architekturách) často totožné. O segmentaci paměti hovořit nebudu, nebudeme to zde potřebovat.
Fyzické adresy
Fyzická adresa označuje místo v operační paměti (RAM) tak, jak ho vidí procesor(y). Většinou se s ní pracuje jako s číslem, celá operační paměť se pak chápe jako jedno velké pole bajtů, kde adresa je indexem konkrétního bajtu.
Sběrnicové adresy
Jde opět o adresaci fyzické operační paměti, ale tentokrát z pohledu zařízení připojeného na sběrnici. Tato adresace je významná např. při DMA přenosech. Často (např. na platformě x86) se jedná o číselně shodné adresy s adresami fyzickými. Proto pozor na záměnu, kvůli přenositelnosti kódu nelze adresy vzájemně zaměňovat!Logické adresy
Zde se již pohybujeme v oblasti "normální" adresace, pracujeme tedy většinou již s ukazateli. Také se často jedná přímo o fyzické adresy (příp. s pevným offsetem), s výjimkou případu, kdy máme v systému více paměti, než je velikost logického adresního prostoru (viz dále). Pak se vždy část fyzické paměti namapuje do prostoru logických adres, abychom mohli i tyto části využívat.
Virtuální adresy jádra
Tato paměť je obecně stránkovaná, úseky nemusejí být fyzicky souvislé. Využívá se i zbývající část adresního prostoru (mimo oblast logických adres). Každá logická adresa je automaticky virtuální adresou, obráceně to neplatí.
Virtuální adresy procesu
Opět stránkovaná paměť, ovšem z pohledu procesu. Každý proces má vlastní adresní prostor, který je nezávislý i na adresním prostoru jádra. V mnoha případech se úseky adresního prostoru více procesů mapují na tutéž fyzickou oblast paměti, další mapování na stejnou oblast může mít i jádro.
Adresace paměti
Relativní "omezenost" 32bitového adresního prostoru přináší kompromisy. Máme-li více paměti, nelze ji adresovat najednou celou. Navíc na některých platformách (včetně x86) musíme virtuální adresový prostor dělit - jádro zvlášť, procesy zvlášť. Na 64bitových platformách nás toto nemusí trápit, ale kvůli kompatibilitě s oběma délkami virtuálních adres se musíme stejně přizpůsobit. Na x86 je typické dělení prostoru 1 GB pro jádro, 3 GB pro procesy. V případě potřeby lze dělení změnit nastavením při kompilaci jádra; existují i cesty, jak ho úplně zrušit, avšak za cenu určitého zpomalení systému. Paměť přímo dostupnou přes logické adresy označujeme jako dolní (low), ostatní jako horní (high).
Pro zjišťování informací o stavu paměti v systému lze využít např. soubory /proc/meminfo (základní informace o aktuálním využití paměti), /proc/vmstat (statistika správy paměti) a /proc/slabinfo (informace o využití paměti jádrem pro konkrétní účely). Podobně poslouží také programy free, vmstat a slabtop (zvláště ten velice doporučuji - umožňuje sledování paměti v reálném čase).
Základní pravidla
Než začneme převádět adresy mezi prostory, ujasněme si několik věcí. Bohužel, některé funkce v jádře mají (z historických důvodů) zavádějící názvy a ve skutečnosti pracují s jiným druhem adres, než by z názvu vyplývalo. Ještě na to upozorním. Dále tu máme problémy s horní pamětí a neuniformním přístupem (NUMA) na některých platformách, proto některé věci nejsou tak jednoduché, jak by se mohlo zdát.
Dále tu máme pár důležitých (a asi leckomu dobře známých) pojmů: stránka a její velikost (konstanta PAGE_SIZE), číslo stránky (page frame number) a bitový posun stránky (PAGE_SHIFT). Je to prosté - vezmeme-li adresu (jakoukoliv), pak bitovým posunem vpravo o PAGE_SHIFT získáme číslo stránky. Bitový posun samozřejmě odpovídá velikosti stránky. Při jakýchkoli převodech apod. pracujeme vždy s těmito konstantami, žádné spoléhání na konkrétní velikost stránky (třeba 4 KB) nepřipadá v úvahu.
Důležitým elementem je struktura stránky (struct page). Obsahuje mapování virtuální paměti na fyzickou (bez rozlišení druhu, může to být i paměť v zařízení), počitadlo referencí a označení stavu stránky. Do struktury stránky nezasahujeme přímo, ale vždy jen pomocí dostupných funkcí.
Převodní funkce
Funkce (resp. makra) __pa() a __va() slouží k převodu z logické adresy na fyzickou a naopak (pozor, písmeno "v" neznamená virtuální adresu!). Lze je používat pouze pro dolní paměť.
Dále tu máme funkce virt_to_page() a pfn_to_page() - první z nich vrací ukazatel na strukturu stránky pro logickou (pozor na to!) adresu, druhá pro totéž použije číslo stránky. Opačným směrem pracuje funkce page_address(), vracející virtuální adresu stránky - pro horní paměť funguje jen tehdy, je-li stránka namapovaná (jinak vrací NULL).
Kvůli použití horní paměti se (namísto page_address()) lépe hodí používat funkce kmap() a kmap_atomic(). Obě vracejí virtuální adresu. Pro dolní paměť ji vracejí přímo, pro horní provedou namapování do virtuálního adresního prostoru. Liší se tím, že druhá pracuje atomicky - pozor ovšem na to, že pokud takto namapujeme stránku, až do jejího uvolnění nesmí dojít k uspání běhu!Jak je v jádře zvykem, co si přivlastníme, musíme uvolnit. Zde k tomu slouží funkce kunmap(), resp. kunmap_atomic(), které vytvořené mapování zruší.
Pro virtuální adresy získané voláním funkce vmalloc() - viz dále - se používá funkce vmalloc_to_page(). Vrací ukazatel na strukturu stránky a používá se úplně stejně jako funkce virt_to_page().
Ještě pro úplnost přidám dvě funkce pro převod z a na sběrnicové adresy. Jsou to virt_to_bus() a bus_to_virt(), ale při implementaci modulů se běžně nepoužívají. Máme totiž k dispozici lepší mechanismy, které řeší převody samy a současně zajišťují lepší přenositelnost kódu. Bude o tom řeč v jednom z pozdějších dílů seriálu.
Alokace a uvolňování paměti
Je to do jisté míry podobné jako v běžných programech, ale i odlišností je mnoho. Nejprve se zmíním o automatické alokaci na zásobníku. Vždy je nutné počítat s tím, že zásobník je velice malý (1-2 stránky). Větší datové struktury a pole proto musíme buď deklarovat jako statické (se všemi důsledky), nebo je alokovat explicitně.
Alokace bloku logické paměti
Nejjednodušší je alokace úseku logické paměti. Obvykle používáme funkci kmalloc(), která je podobná známé funkci malloc(), ale má jeden argument navíc. Podívejme se na příklad:
void* ptr = kmalloc(100, GFP_KERNEL);
Alokujeme 100 bajtů, a to způsobem implicitním pro jádro. Ve skutečnosti se alokuje víc - až dvojnásobek požadovaného množství, v tomto případě konkrétně 128 B. Nepoužívá se totiž klasická hromada (heap), místo toho si jádro předem připravuje zásobu bloků paměti různé velikosti (od 32 B do 128 KB), a z nich pak podle požadavků přiděluje.
Druhou věcí, která nás zajímá, je zbývající parametr. V tomto případě jsme alokovali tím nejobvyklejším způsobem, ale možností je mnohem víc. Např. GFP_ATOMIC se používá v případech, kdy je nutná atomická alokace - za normálních okolností totiž kmalloc() může blokovat, což třeba při obsluze přerušení nesmíme připustit. Zajímat nás může také třeba __GFP_DMA pro omezení logických adres na rozsah použitelný pro DMA na sběrnici ISA. Další konstanty najdete v dokumentaci (s novými verzemi jádra občas přibývají).
Proč kmalloc() může blokovat? Jednoduše proto, že paměť nemusí být k dispozici. Pak je nutné požadované množství uvolnit (např. odložením na disk), což zabere nějakou dobu. Atomicky volaná funkce se o nic takového nepokouší - buď se to povede hned, nebo vůbec.
Maximální alokovatelný blok je 128 KB, což se někomu může zdát málo, ale není tomu tak. Obvykle totiž tak velký kus logické paměti nepotřebujeme.
Paměť musíme po použití bezpodmínečně uvolnit zavoláním kfree(). Pokud by se tak nestalo, byla by ztracena až do restartu systému.
Alokace bloku virtuální paměti
Při větších požadavcích na množství paměti můžeme alokovat jako virtuální paměť jádra. Získáme stránkovanou paměť (nemusí být fyzicky souvislá) o prakticky libovolné velikosti. Vzhledem k alokaci po stránkách bude samozřejmě přidělený úsek zarovnán na celé stránky, navíc se pro každou stránku vytváří režijní datová struktura (položka v tabulce stránek) - proto nemá smysl používat tento postup pro alokaci malého množství paměti (lépe se hodí kmalloc()).
Dalším omezením (zcela logickým) je použití takto získané oblasti paměti - nelze ji použít mimo jádro, zejména ne v žádném zařízení připojeném přes sběrnici. Týká se to jak pohledu ze zařízení do paměti (DMA apod.), tak obráceně (paměťová oblast ležící v zařízení). Pokud bychom přesto potřebovali, aby se podobným způsobem pracovalo s pamětí v zařízení, použili bychom funkci ioremap() - ale tím se teď zabývat nebudeme.
Připomenu ještě, že alokace virtuální paměti není atomická a může blokovat. To ale není na závadu, správně napsaný modul by nikdy neměl vyžadovat alokaci většího množství paměti v kritických místech.
Pro alokaci použijeme funci vmalloc(), pro uvolnění vfree(). Pracuje se s nimi stejně jako s funkcemi malloc() a free():
void* ptr = vmalloc(10000);if (ptr == NULL) return -NOMEM; ...vfree(ptr);
Opět připomínám nutnost paměť po použití uvolnit.
Alokace celých stránek
Předchozí způsoby alokace se hodí hlavně tam, kde předem neznáme potřebné množství paměti. Někdy ho ovšem známe již při v době implementace, čehož můžeme s výhodou využít ke snížení režie jádra při alokaci a uvolňování - zejména tehdy, jedná-li se o větší množství paměti.
Máme dvě skupiny funkcí - první vrací adresu, druhé ukazatel na stránkovou strukturu. Podívejme se nejprve na ty první.
Můžeme alokovat třeba jen jedinou stránku. K tomu slouží funkce __get_free_page() a get_zeroed_page(). Jejich jediným parametrem jsou příznaky v podobě známé od kmalloc(). Funkce se liší tím, že první vrací stránku "tak, jak je", kdežto druhá ji předem vynuluje. Po použití paměť uvolníme funkcí free_page(). Pozor - nepracuje se zde s ukazateli, nýbrž s čísly typu unsigned long!
Nulování paměti by se někomu mohlo zdát zbytečné, ale pokud předáváme nějaká data uživatelským procesům (anebo do sdílené paměti), je to zcela nezbytné. Alokovaná paměť může totiž obsahovat prakticky libovolná data (třeba i velice citlivá), a pokud je později nepřepíšeme, proces je může získat a zneužít. Kromě get_zeroed_page() máme také modifikátor __GFP_ZERO a funkci kzalloc() - bohužel jsou ale až v poměrně pozdních verzích jádra. V ostatních případech lze po alokaci použít funkci memset().
Pro alokaci více stránek se používá funkce __get_free_pages(). Má o jeden argument víc, tím je dvojkový logaritmus počtu požadovaných stránek:
unsigned long mem = __get_free_pages(GFP_KERNEL | __GFP_DMA, 4);...free_pages(mem, 4);
Uvedený příklad představuje alokaci a uvolnění 16 stránek (tj. 64 KB při velikosti stránky 4 KB) v oblasti vhodné pro DMA. Paměť se uvolní voláním free_pages() - ovšem pozor na to, že se musí správně zadat počet uvolňovaných stránek.
Pokud potřebujeme pracovat se stránkovou strukturou (struct page), můžeme použít funkce alloc_page() a alloc_pages(). Hodí se to hlavně při práci s horní pamětí. Paměť se pak uvolní funkcemi __free_page(), resp. __free_pages(). Pozor na ten zmatek s podtržítky, je to velice nešťastně navrženo!Vyšší úroveň práce s pamětí
Tímto bych úvod do paměťové magie uzavřel. Příště budeme pracovat s oblastmi virtuální paměti, přenášet data z programu do jádra a naopak a také se trochu podíváme na používání paměti v zařízení, kterému by měl modul sloužit.
Modely adresace paměti
Operační paměť
Nesegmentovaný model adresace operační paměti - existuje jeden lineární paměťový prostor adresovatelný v rozsahu <0,N> (tzv. offset).
Segmentovaný model adresace operační paměti - existuje více lineárních paměťových prostorů, které se mohou překrývat. Adresa je poté určena ze dvou komponent: lineární komponenty (offset) v rozsahu <0,N> a určení segmentu. Segment je definován svým počátkem v paměti a délkou.
Vnější paměť
CHS - adresace vnějších (diskových, ...) pamětí pomocí čísel válce (cylinder), hlavy (head) a sektoru. Jen pro menší kapacity disků.
LBA - adresace vnějších (diskových, flash, ...) pamětí pomocí lineárního čísla sektoru.
Poznámka redakce, zdroj Wikipedia.