Produkty Novinky Články Návody Kontakty

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:
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_
34#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 */
910#define DSKEL_MAGIC_NO                       0xE0
1112/*
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 *)
1920/*
21 * Nejvyssi poradove cislo prikazu
22 * (pouze ke kontrolnim ucelum v ovladaci.)
23 */
2425#define DSKEL_MAX_CMD_NR     1
2627#endif /* DSKEL_IOCTL_H_ */
Výpis souboru 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;
56      // 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     }
1314     // 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*/ }
2627     if ( result < 0 )
28     {
29             goto End;
30     }
3132     // 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;
3940             // nastavujeme novou hodnotu parametru do modulu
41             case DSKEL_SET_PARAM:
42                     result = __get_user(tmp_var, (int *) arg);
43                     break;
4445             default:
46                     result = -ENOTTY;
47                     break;
48     }
4950End:
51     pr_alert("dskel_ioctl(): %d\n", result);
52
                                                53     return result;
54}
Výpis zdrojového kódu ovladače - funkce ioctl()

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ň.