Produkty Novinky Články Návody Kontakty

Operace read() a write()

Operace read() a write() si jsou velmi podobné. Liší se v podstatě jen směrem přenosu dat. Operace read() přenáší data ze zařízení, zatímco operace write() do zařízení.
Jejich prototypy vypadají takto:
ssize_t read(struct file * filp, char __user * buff, size_t count, loff_t * ppos);
ssize_t read(struct file * filp, const char __user * buff, size_t count, loff_t * ppos);
kde:
  • filp - je ukazatel na již dříve popsanou strukturu file.
  • buff - je ukazatel na data v uživatelském prostoru.
  • count - určuje množství dat, která se mají přenášet (v bytech).
  • ppos - je ukazatel na na pozici v souboru.

Přenos dat z/do uživatelského prostoru

Parametr buff si zaslouží více pozornosti a to z toho důvodu, že jde o ukazatel na data v uživatelském prostoru. Jak víme, adresní prostor se rozděluje na uživatelský prostor, kde každý proces běží ve vlastním adresním prostoru, a prostor jádra, což je adresní prostor sdílený celým kódem jádra, tedy i naším modulem. Protože při volání souborových operací read() a write() dochází k přenosu dat mezi adresním prostorem uživatelského programu a adresním prostorem jádra, tak nelze k zpřístupnění dat uživatelského programu použít pouhou dereferenci ukazatele buff! Skutečnost, že jde o ”jiný” ukazatel je zdůrazněna tokenem __user.
Je třeba využít služeb speciálních funkcí deklarovaných v hlavičkovém souboru <asm/uaccess.h>. V případě přenosu bloku dat jde o funkce copy_to_user() využívanou v implementaci operace read() a funkci copy_from_user(), kterou použijeme v implementaci operace write(). Jejich prototypy jsou tyto:
long copy_to_user(void __user * to, const void * from, unsigned long count);
long copy_from_user(void * to, const void __user * from, unsigned long count);
Funkce copy_to_user() a copy_from_user() vrací 0, pokud je vše v pořádku, jinak zápornou hodnotu indikující chybu. Pokud přenos dat selhal, ale nějaká data byla přece jen přenesena, tak vrátí kladnou hodnotu - počet bytů, které nebyly přeneseny. Odpovídající reakcí v obou případech selhání je vrácení chybového kódu -EFAULT.
Kromě funkcí pro přenos bloku data jsou k dispozici i funkce, resp. makra pro přenos jedné hodnoty libovolného primitivního typu (char, int, long apod.) mezi prostorem jádra a uživatelským prostorem. Je to funkce get_user() pro přenos ve směru z uživatelského prostoru do prostoru jádra a put_user() pro přenos v opačném směru. Jejich prototypy jsou následující:
int get_user(var, ptr); 
int put_user(var, ptr);
kde var je proměnná v prostoru jádra a ptr je ukazatel na paměť v uživatelském prostoru, odkud se hodnota čte nebo kam se zapisuje. Návratová hodnota je 0, když jde vše dobře, nebo -EFAULT, pokud je ukazatel ptr neplatný.
Funkce či makra pro přenos dat za nás provedou ještě jeden důležitý krok - zkontrolují, že ukazatel na data v uživatelském prostoru je platný, dále že uživatelská aplikace daný blok paměti skutečně vlastní a že je možné k němu bezpečně přistupovat.
Existují i ekvivalenty zmíněných funkcí, které kontrolu neprovádějí - jejich název je uvozen dvojpodtržítkem __. To se hodí v případě, kdy provádíme více přenosů a platnost ukazatele chceme ověřit jen na začátku - pak použijeme makro access_ok().
Použití makra access_ok() je jednoduché a vyplývá z jeho prototypu:
int access_ok(type, addr, size);
kde type je typ přístupu - čtení (použije se symbolická konstanta VERIFY_READ) nebo zápis (VERIFY_WRITE). Platnost přístupu pro zápis zároveň znamená, že je možné i bezpečně číst. Parametr addr je ukazatel na blok paměti v uživatelském prostoru, která je předmětem kontroly, a poslední parametr size udává velikost paměťového bloku. Nenulová návratová hodnota znamená, že ukazatel na blok paměti v uživatelském prostoru je platný, nulová hodnota, že je neplatný (pak vracíme -EFAULT).
K tomu patří jedna důležitá poznámka - to, že ukazatel na paměť v uživatelském prostoru projde kontrolou, ještě nutně neznamená, že funkce pro přenos dat nemohou selhat. Musíme proto vždy kontrolovat jejich návratové hodnoty.
S ohledem na kopírování z/do uživatelského prostoru je potřeba si pamatovat ještě jednu důležitou věc - všechny funkce pro přesnost dat mezi uživatelským prostorem a prostorem jádra mohou volající proces uspávat (kód souborových operací běží na pozadí uživatelského programu). Tuto skutečnost musíme mít na zřeteli při používání synchronizačních nástrojů (spinlocky, semafory apod.).

Pozice v souboru zařízení

Parametr ppos, ukazatel na pozici v souboru v operacích read() a write(), nám umožňuje aktualizovat pozici v souboru zařízení. Jde o to, že naše zařízení je znakové, takže by se mělo chovat jako proud bytů. Proto tento údaj aktualizujeme vždy, když se podaří pomocí operací read() a write() přenést nějaké množství dat. Jádro aktualizovanou hodnotu přenese do struktury file.

Návratová hodnota

Zajímavá je také návratová hodnota operací read() a write(). Vracet bychom měli:
  • 0 - pokud nedošlo k přenosu dat.
  • > 0 (kladnou hodnotu) - pokud se podařilo přenést data - ta pak určuje počet přenesených bytů.
  • < 0 (zápornou hodnotu) - např. -EFAULT, která udává kód chyby (EFAULT) záporně. Tento kód chyby jádro uloží do proměnné errno přístupné v uživatelském prostoru a zajistí, že návratová hodnota systémového volání bude -1. Uživatelská aplikace se tak jen dozví, že systémové volání selhalo. Samotný kód chyby si musí vyčíst z proměnné errno.

Obecné zásady implementace operace read() a write()

  • Pokud dojde během přenosu k chybě, tak operace read() či write() musí vrátit počet úspěšně vyčtených/zapsaných dat. Chybu (obvykle -EFAULT), která nastala si musí zapamatovat a vrátit ji při dalším volání, které ihned selže.
  • Operace read() a write() mohou vyčíst/zapsat méně než bylo požadováno v parametru count. Uživatelský program pak může opakovat volání těchto operací.
  • Jestliže operace read() vrátí 0, pak nebyli vyčtena žádná data a buffer v uživatelském prostoru musí zůstat nedotčen. Jinými slovy bylo dosaženo konce souboru.
  • Pokud je hodnota parametru count = 0, pak operace vrátí také 0 - nic nebylo vyčteno/zapsáno.
  • Po přenosu dat je nutné aktualizovat pozici v souboru přes ukazatel ppos.
  • Jestliže dojde k chybě, tak se obvykle vrací -EFAULT (špatná adresa, např. neplatný ukazatel na buffer) nebo -EINTR (přerušení systémového volání).

Operace read() a write() v modulu dskel

Po hutném teoretickém úvodu následuje velmi jednoduchá implementace obou operací. Do hlavičky zdrojového souboru modulu dskel musíme přidat soubor <asm/uaccess.h>.
Funkce dskel_read():
1static ssize_t dskel_read(struct file * file_ptr,
2                                                      char __user * data_ptr, size_t data_size,
3                                                      loff_t * pos_ptr)
4{
5      pr_alert("dskel_read(): done\n");
67      return 0;
8}
A funkce dskel_write():
1static ssize_t dskel_write(struct file * file_ptr,
2                                                      const char __user * data_ptr, size_t data_size,
3                                                      loff_t * pos_ptr)
4{
5      pr_alert("dskel_write(): %d\n", data_size);
67      return data_size;
8} 
Jen krátký komentář - funkce dskel_write() musí vracet počet bytů, které se uživatelský program pokusil zapsat, jinak by se její volání opakovalo. Což by v případě návratové hodnoty 0 znamenalo nekonečný cyklus a tedy i zatuhnutí počítače.
Nyní můžeme zkusit modul znova zkompilovat a zavést. Funkčnost nových funkcí našeho modulu ověříme snadno - zadáme příkaz:
$ echo nic > /dev/dskel
Tento příkaz se pokusí otevřít soubor zařízení reprezentující náš modul a zapsat do něj řetězec ”nic”. Pak jej uzavře. Jestliže jsme tento příkaz vyvolali ze systémové konzole, uvidíme hlášení z jednotlivých funkcí. Jinak si tyto hlášení můžeme prohlédnout příkazem:
$ dmesg | tail
který vypíše deset posledních hlášení jádra. Mezi nimi by měly být i výpisy z funkcí našeho modulu.