přejít na obsah přejít na navigaci

Linux E X P R E S, Vývoj jádra VI. - další paměťové operace

Vývoj jádra VI. - další paměťové operace

Minule jsme se věnovali základům práce s pamětí. Protože se jedná o velice rozsáhlou oblast, přichází nyní pokračování. Stále jsme totiž nepřekopírovali ani jediný bajt paměti, což je potřeba velice rychle napravit. Přijdou na řadu též oblasti virtuální paměti a také namapování paměti v obsluhovaném zařízení.


Kopírování paměti v jádře

Začneme tím nejjednodušším – kopírováním úseků paměti v rámci jádra. To se prakticky neliší od téhož v normálním programu, snad kromě toho, že je potřeba zvlášť pečlivě kontrolovat, co se kam kopíruje. Asi bude nejlepší ukázat to na příkladu (kontrola není jeho součástí, je to záležitost konkrétního použití):

memcpy(dest1, src, 100);
memmove(dest2, src, 50);
strcpy(s2, s1);
strncpy(s3, s1, 20);

Funkce readb(), readw(), readl() a jejich „zápisové“ protějšky jsou platformě x86 ve skutečnosti implementovány jako přímá dereference ukazatele a přiřazení hodnoty. Lze je tedy používat bez obav o snížení rychlosti. Totéž platí i o funkcích pro práci s větším množstvím paměti (memset_io(), memcpy_fromio() a memcpy_toio()).

Použitá volání vypadají úplně stejně, jako kdyby se jednalo o běžný program. Lze používat jak „paměťově“ orientované funkce (memcpy() apod.), tak i funkce řetězcové. Vnitřní implementace je silně platformně závislá, jde také o jednu z mála věcí v jádře, které jsou napsány v assembleru.

Potřebujeme-li vymazat kus paměti v uživatelském prostoru, i na to máme k dispozici jednoduchý nástroj. Je to funkce clear_user(). Existuje i ve verzi __clear_user() bez implicitní kontroly přístupu.

Nastavování hodnot bajtů a další operace

Ani zde se to nebude moc lišit od práce v uživatelském prostoru. Zde je ukázka:

struct mystruct ms;
memset(&ms, 0, sizeof(ms));

if (strcmp(s1, s2) != 0) {
  size_t len = strlen(s1);
  strncpy(s1, s2, len);
  ...
}

Velmi povědomé, že? Opravdu, paměťové i řetězcové operace lze bez obav používat i v rámci jádra a navíc jsou ve většině případů implementovány optimálním způsobem (pro danou platformu) – proto jsou pro příslušné účely vždy první volbou. Ještě doplním, že jsou k nalezení v souboru linux/string.h.

Kopírování mezi jádrem a programy

Zatímco kopírování uvnitř jádra bylo velice jednoduché, s přenosem mezi jádrem a uživatelským programem je to poněkud složitější. Jednak musíme vždy používat výhradně funkce k tomu určené (přenositelnost!), a to i pro přenos jediné hodnoty proměnné. A současně je nutno vždy počítat s tím, že přenos nemusí být úspěšný (např. pokud chceme zapsat do místa určeného jen ke čtení nebo když se pokusíme přistupovat k adresám, které procesu nepatří). Nemusím snad také zdůrazňovat (neboť to vyplývá z principu využití paměti v procesech), že všechny tyto operace mohou blokovat a nesmí se proto používat tam, kde by to vadilo.

Kontrola přístupu

Před samotným přenosem dat se musí vždy zkontrolovat, zda je adresa v paměťovém procesu vůbec platná a použitelná pro přenos. Může se totiž snadno stát, že z uživatelského prostoru (např. přes ioctl() volání) získáme nesmyslnou nebo záměrně chybnou adresu.

Protože by bylo poměrně pracné vždy explicitně kontrolovat adresy i v případech, kdy nám jde třeba jen o jednu proměnnou, existují také verze přenosových operací, které tuto kontrolu provedou samy. Pokud na stejném úseku paměti provádíme přenosů více, je efektivnější nejprve úsek zkontrolovat a pak již volat operace bez této kontroly.

Přenos jedné hodnoty

Mezi uživatelským prostorem a jádrem nemůžeme provádět přímá přiřazení hodnot mezi proměnnými. Nejzrádnější na tom je, že na některých platformách fungovat bude, na jiných nikoli a konečně na některých jen za určitých okolností. Proto je jedinou bezpečnou, spolehlivou a přenositelnou cestou používat funkce pro přenos hodnot.

K přenesení hodnoty stačí mít adresu v uživatelském prostoru, proměnnou v jádře a velikost proměnné. Vlastní přenos probíhá následovně: int mymodule_do_transfer( u8* __user user_ptr, u32* __user user_data_ptr)
{
  u8 cmd = 0;
  u8 res = 0;
  u32 data = 0;
  if (!access_ok(VERIFY_WRITE, user_ptr, sizeof(u8)))
    return -EFAULT;
  if (__get_user(cmd, user_ptr) != 0)
    return -EFAULT;
  ...
  if (__put_user(cmd, user_ptr) != 0)
    return -EFAULT;
  return put_user(data, user_data_ptr); 
}

Příklad záměrně uvádím jako definici funkce, aby bylo vidět, jak se označuje user-space adresa. Kompilátor sice správné použití nekontroluje, ale mohou tak činit jiné nástroje. Ve funkci se provádějí tři přenosy – nejdřív z programu do jádra, pak obráceně a pak ještě jeden přenos „ven“. První dvě volání jsou bez kontroly („podtržítkové“ verze funkcí), ta se provádí ještě před nimi, poslední je naopak s kontrolou.

Všimněte si, že i když se před voláním přenosové funkce provede kontrola, stejně může volání funkce selhat. Nestává se to často (ovšem hlavně v preemptivním jádře to není zdaleka vyloučeno), nicméně test návratové hodnoty nelze nikdy vynechat. Druhá poznámka směřuje k určení velikosti testované oblasti – i když se zde jedná o hodnotu předem známé velikosti, kvůli systematičnosti je vhodné stejně použít operátor sizeof.

Přenos úseku paměti

Celý úsek paměti se přenáší obdobně, není na tom nic komplikovaného. Opět lze kontrolovat jak explicitně, tak implicitně. Takto to vypadá, když alokujeme úsek paměti a přeneseme do něj nějaká data z uživatelského prostoru (s implicitní kontrolou):

void* ptr = kmalloc(size, GFP_KERNEL);
if (ptr == NULL)
  return -ENOMEM;

if (copy_from_user(ptr, user_ptr, size) != 0)
  return -EFAULT;
...

Snad je ještě vhodné dodat, že je zde ještě pár funkcí řetězcového typu. Zájemci je, stejně jako výše uvedené funkce, najdou v souboru asm/uaccess.h (takto se to uvádí v direktivě #include, k prohlížení se samozřejmě podívejte do adresáře konkrétní platformy, např. asm-i386).

Oblasti virtuální paměti

S virtuální pamětí a její adresací jsme se setkali už minule. Pro některé manipulace s ní si však nevystačíme s adresami a stránkovými strukturami. Každá oblast virtuální paměti procesu (virtual memory area, VMA) představuje souvislý úsek virtuálních adres a má určité společné vlastnosti, které se uvádějí v příslušné datové struktuře (struct vm_area_struct).

Mezi tyto vlastnosti patří oprávnění (čtení/zápis/spouštění, sdílená/soukromá oblast) a informace o souvisejícím objektu (soubor, swap). Běžící proces má těchto oblastí obecně celou řadu, např. pro spustitelný soubor, knihovny, zásobník, datovou oblast a otevřené soubory mapované do paměti. Všechny tyto informace o běžících procesech jsou dostupné přes procfs, a to v souborech /proc//maps.

Každá oblast může mít přiřazeny čtyři funkce: pro „otevření“ oblasti (vytvoření reference), „uzavření“ oblasti, výpadek stránky a předpřipravení stránek (tato poslední se používá málokdy).

VMA budeme potřebovat pro mapování paměti zařízení do procesu a k dalším podobným účelům. K tomu se ale vrátíme později – nejdřív se totiž podíváme na samotný přístup k paměti zařízení, příště pak přijde na řadu zamykání, bez něhož se neobejdeme.

Přístup do paměti zařízení

Slíbil jsem přístup k paměti zařízení, ale již předem říkám, že úplně jednoduché to není. Na některých platformách si můžeme bez problémů namapovat paměť zařízení a používat ji, jinde to takto nelze. Proto musíme postupovat poněkud konzervativně.

V tuto chvíli tedy vyřešíme pouze jediný problém – namapování fyzických adres zařízení do virtuálního adresního prostoru jádra. Použijeme funkce ioremap() pro vytvoření mapování, a iounmap() pro jeho zrušení. Bude se to chovat podobně jako minule představená funkce vmalloc(), resp. vfree(), bude se tedy vytvářet a rušit tabulka stránek. Zde je příklad:

u8* ptr = ioremap(0xb4000000, PAGE_SIZE << 2);
if (ptr == NULL)
  return -EFAULT;
val = ptr[4];          // chyba – nepřenositelné!
val = readb(ptr + 4);  // správný postup
...
iounmap(ptr);

Jak je z příkladu zřetelné, nejprve se namapuje paměť zařízení do virtuálního prostoru jádra. Pokud to dopadlo dobře, lze z této paměti číst atd. a na závěr se paměť odmapuje. U přístupu do paměti je zde uveden jak správný, tak i špatný postup. Pokud vytváříme modul určený pro jednu konkrétní platformu, typicky x86, lze nepřenositelný způsob použít, ale je lépe se mu vyhýbat. Ještě se k tomu vrátíme v kapitole o komunikaci se zařízením.

Synchronizujeme. Již jsem naznačil, že se v mnohých situacích v jádře neobejdeme bez zamykání či jiných synchronizačních metod. Jindy sice ano, ale přesto potřebujeme mít nějak zajištěno, aby se různé kontexty nepraly o sdílené prostředky – a to tím spíš, že máme preemptivní jádro a také že může být v systému více procesorů. Nároky kladené na synchronizaci nejsou malé, proto bych vám rád v příštím dílu přiblížil vlastnosti jednotlivých mechanismů a jejich použití.

Nahoru

Odkazy

Přidat názor

Nejsou podporovány žádné značky, komentáře jsou jen čistě textové. Více o diskuzích a pravidlech najdete v nápovědě.
Diskuzi můžete sledovat pomocí RSS kanálu rss



 
 

Top články z OpenOffice.cz

Lukáš Jelínek

Lukáš Jelínek

Dlouholetý člen autorského týmu LinuxEXPRESu a OpenOffice.cz. Vystudoval FEL ČVUT v oboru Výpočetní technika. Žije v Kutné Hoře, podniká v oblasti IT a zároveň pracuje v týmu projektu Turris. Ve volném čase rád fotografuje, natáčí a stříhá video, občas se věnuje powerkitingu a na prahu čtyřicítky začal hrát tenis.


  • Distribuce: Debian, Kubuntu, Linux Mint
  • Grafické prostředí: KDE

| proč linux | blog