Operace ioctl()
Operace ioctl() a odpovídající systémové volání, resp. knihovní funkce ioctl() slouží k rozšířenému ovládání zařízení. Většinou se s její pomocí nastavují parametry, režimy zařízení apod.
Začneme trochu netypicky a ukážeme si prototyp funkce ioctl() v uživatelském prostoru:
int ioctl(int fd, unsigned long cmd, ...);
kde fd je souborový deskriptor, cmd je kód příkazu a ... nereprezentuje libovolný počet parametrů - ten je vždy jen jeden - ale indikuje, že parametrem může být proměnná jakéhokoliv typu. Nicméně je doporučeno používat ukazatel na daný typ (int * apod.).
Adekvátní operace ioctl() jak ji implementuje modul ovladače má prototyp:
int (* ioctl)(struct inode * inode, struct file * filp, unsigned int cmd, unsigned long arg);
kde parametr cmd odpovídá stejnojmennému parametru v prototypy systémového volání a parametr arg je pak oním třetím parametrem systémového volání reprezentovaný jako ....
Jak vidíme, pomocí systémového volání ioctl() lze ovladači předat skutečně libovolný požadavek včetně případného parametru. Právě proto je nutná důkladná kontrola platnosti příkazu a jeho parametru, která se neobejde bez dobře definovaného rozhraní - seznamu definic ioctl příkazů.
Definice ioctl příkazů
Je pravidlem uvádět seznam ioctl příkazů, které modul podporuje, do zvláštního hlavičkového souboru, aby jej bylo možné zpřístupnit i uživatelské aplikaci.
Příkaz ioctl, resp jeho kód (unikátní číslo příkazu v rámci systému) se definuje pomocí speciálních maker, které na základě svých parametrů vygenerují 16bitové číslo. To představuje unikátní kód příkazu v rámci systému. Definice maker najdeme v hlavičkovém souboru <linux/ioctl.h> a vypadají takto:
- _IO(type, nr) - pro definování ioctl příkazu bez parametrů
- _IOR(type, nr, datatype) - definice příkazu, který čte data z modulu (parametr arg funkce ioctl() je v tomto případě výstupním parametrem).
- _IOW(type, nr, datatype) - definice příkazu, který zapisuje data do modulu (parametr arg funkce ioctl() je v tomto případě vstupním parametrem).
- _IOWR(type, nr, datatype) - definice příkazu, který data zapisuje i čte.
Parametry uvedených maker mají tento význam:
- type - tzv. magické číslo (magic number). Seznam obsazených čísel je dostupný v souboru Documentation/ioctl/ioctl-number.txt ve stromu jádraTento soubor je součástí pouze kompletního stromu zdrojového kódu jádra. Nicméně je snadno k nalezení na internetu, např. zde: http://www.mjmwired.net/kernel/Documentation/ioctl/ioctl-number.txt. Stačí si tedy vybrat nějaké, které není obsazené a to používat pro daný modul.
- nr - pořadové číslo příkazu v seznamu příkazů (to si přidělíme my). Začíná se obvykle od 0.
- datatype - typ parametru arg funkce ioctl(), např. int nebo int *.
O definici ioctl příkazů víme vše - nabyté znalosti můžeme aplikovat v implementaci našeho modulu dskel. Vytvoříme si tedy nový hlavičkový soubor dskel-ioctl.h. Výpis 24 ukazuje jeho obsah.
1#ifndef DSKEL_IOCTL_H_ 2#define DSKEL_IOCTL_H_ 3 4#include <linux/ioctl.h> 5/* 6 * zvolime si pokud mozno unikatni cislo, tzn. magic number ktere neni 7 * v Documentation/ioctl/ioctl-number.txt 8 */ 9 10#define DSKEL_MAGIC_NO 0xE0 11 12/* 13 * seznam prikazu: 14 * DSKEL_GET_PARAM - vycte parametr 15 * DSKEL_SET_PARAM - nastavi parametr 16 */ 17#define DSKEL_GET_PARAM _IOR(DSKEL_MAGIC_NO, 0, int *) 18#define DSKEL_SET_PARAM _IOW(DSKEL_MAGIC_NO, 1, int *) 19 20/* 21 * Nejvyssi poradove cislo prikazu 22 * (pouze ke kontrolnim ucelum v ovladaci.) 23 */ 24 25#define DSKEL_MAX_CMD_NR 1 26 27#endif /* DSKEL_IOCTL_H_ */
V hlavičkovém souboru dskel_ioctl.h si definujeme dva cvičné příkazy, kterými můžeme nastavit nebo vyčíst hodnotu nějakého parametru v modulu.
Zpracování ioctl příkazu
Když už máme nadefinovaný seznam příkazů, tak je načase se podívat, jak se vlastně ioctl příkazy zpracovávají uvnitř modulu.
Samotná implementace operace ioctl() je vlastně velmi jednoduchá. Základem je switch, kde se kód větví podle kódu příkazu. Pak následuje zpracování samotných příkazů. Větvení dle kódu příkazu ovšem musí předcházet důkladná kontrola vstupních parametrů, protože operace ioctl() umožňuje značnou volnost a riziko chyby v uživatelské aplikaci je velmi velké.
Pro účely kontroly kódu příkazu - parametr cmd operace ioctl() - se používají makra _IOC_TYPE(cmd), _IOC_NR(cmd) a _IOC_DIR(cmd), které umí z kódu příkazu vyextrahovat tu část, která odpovídá jejich názvu. Pokud kontrola pomocí těchto maker selže, tak vracíme chybu -ENOTTY (nesprávný ioctl příkaz pro zařízení).
Argument ioctl příkazu, pokud je ukazatelem, zkontrolujeme již popsaným makrem access_ok().
Kód funkce dskel_ioctl() našeho modulu je na výpisu 25. Vidíme zde použití maker pro kontrolu platnosti kódu ioctl příkazu (řádek 6 - 10). Dále ověřujeme, že typ přístupu (čtení/zápis) příkazu je v souladu s právy přístupu k paměti v uživatelském prostoru a že ukazatel na tuto paměť je platný (řádek 14 - 27). A pak konečně dle kódu příkazu buď přiřadíme obsah proměnné typu int v uživatelské paměti do pomocné proměnné tmpVar (řádek 34) nebo naopak obsah pomocné proměnné přeneseme do proměnné v uživatelském prostoru (řádek 39). Zde už můžeme použít funkce, resp. makra pro přístup k paměti v uživatelském prostoru, která nekontrolují platnost ukazatele, protože tuto kontrolu jsme už udělali dříve. Nicméně návratové hodnoty obou maker musíme kontrolovat, protože přenos mezi oběma prostory může selhat i z jiných důvodů.
1static int dskel_ioctl(struct inode * inode_ptr, struct file * file_ptr, 2 unsigned int cmd, unsigned long arg) 3{ 4 int result = 0, tmp_var = 0; 5 6 // kontrola kodu prikazu 7 if ( (_IOC_TYPE(cmd) != DSKEL_MAGIC_NO) || 8 (_IOC_NR(cmd) > DSKEL_MAX_CMD_NR) )) 9 { 10 result = -ENOTTY; 11 goto End; 12 } 13 14 // kontrola argumentu (platnost ukazatele a smeru pristupu) 15 // Pozn.: smery v access_ok() jsou presne opacne nez u ioctl 16 if ( _IOC_DIR(cmd) & _IOC_READ ) 17 { 18 result = access_ok(VERIFY_WRITE, (void __user *) arg, _IOC_SIZE(cmd)); 19 } 20 else if ( _IOC_DIR(cmd) & _IOC_WRITE ) 21 { 22 result = access_ok(VERIFY_READ, (void __user *) arg, _IOC_SIZE(cmd)); 23 } 24 else 25 { ; /*empty*/ } 26 27 if ( result < 0 ) 28 { 29 goto End; 30 } 31 32 // dekodujme a provedeme prikaz 33 switch( cmd ) 34 { 35 // vracime hodnotu parametru modulu 36 case DSKEL_GET_PARAM: 37 result = __put_user(tmp_var, (int *) arg); 38 break; 39 40 // nastavujeme novou hodnotu parametru do modulu 41 case DSKEL_SET_PARAM: 42 result = __get_user(tmp_var, (int *) arg); 43 break; 44 45 default: 46 result = -ENOTTY; 47 break; 48 } 49 50End: 51 pr_alert("dskel_ioctl(): %d\n", result); 52 53 return result; 54}
Kontrola přístupových práv
Samotnou kontrolu přístupu k souboru zařízení, kdy se ověřuje, který uživatel a která skupina uživatelů může zařízení používat, provádí operační systém podle nastavených práv přístupu na souboru zařízení.
Nicméně jsou situace, kdy je třeba omezit možnosti rozšířeného ovládání zařízení pomocí systémového volání ioctl(). Například uživatel, který může normálně přenášet data z/do zařízení, by neměl mít možnost změnit mód zařízení nebo nastavit jeho parametry.
Operační systém Linux k tomuto účelu nabízí řízení oprávnění (capabilities). V uživatelském prostoru lze oprávnění uživatele vyčíst a případně i změnit pomocí dvou systémových volání - capget() a capset(). V modulu se pak úroveň oprávnění před vykonáním privilegované operace ověřuje funkcí capable(), která je definována v hlavičkovém souboru <linux/sched.h> a má prototyp:
int capable(int capability)
Funkce vrací nenulovou hodnotu, pokud daný uživatel má potřebnou úroveň oprávnění předanou jako parametr capability. Jinak vrací nulu a v reakci na to by měl modul vrátit chybu -EPERM.
Výčet úrovní oprávnění je definován v souboru <linux/capability.h> a je pevně dán. Není možné jej rozšířit bez zásahu do zdrojového kódu jádra. Programátor si tak nemůže definovat svoji vlastní úroveň.