Linux設備驅動程序 之 ioctl


ioctl

除了讀取和寫入設備之外,大部分驅動程序還需要另外一種能力,即通過設備驅動程序執行各種類型的硬件控制,通常這種需求使用ioctl方法支持,該方法實現了同名的系統調用;

在用戶空間,ioctl系統調用的原型如下:

1 int ioctl(int d, int request, ...);

原型中的可變參數不是數目不定的一串參數,而只是一個可選參數;可選參數的具體格式依賴於控制命令,也就是第二個參數;某些控制命令不需要參數,某些需要一個整數參數,某些需要一個指針參數;使用指針參數可以向ioctl傳遞任意數據,這樣設備可以與用戶空間交換任意數量的數據;

ioctl系統調用內核中的定義如下:

1 SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)

ioctl在file_operations中的函數原型如下:

1 long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
2 long (*compat_ioctl) (struct file *, unsigned int, unsigned long);

大多數ioctl的實現都包含了一個switch語句來根據cmd參數選擇對應的操作;不同的命令被賦予不同的數值,為了簡化代碼,通常會在代碼中使用符號名代替數值,這些符號名由c語言的預處理語句定義,訂制設備驅動程序通常會在它們的頭文件中聲明這些符號;

選擇ioctl命令

ioctl命令號碼定義使用了4個為字段,其含義如下:

type:幻數;選擇一個號碼,並在整個驅動程序中使用這個號碼;這個字段是8位寬(_IOC_TYPEBITS);通常使用一個英文字母;比如#define MY_IOC_MAGIC ‘k’;需要注意避免命令號沖突;

number:序數(順序編號);是8位寬(_IOC_NRBITS);通常從0開始順序編號;

direction:如果命令涉及到數據的傳輸,則該位字段定義數據傳輸的方向;可以使用的值包括_IOC_NONE(沒有數據傳輸)、_IOC_READ、_IOC_WRITE、_IOC_READ|_IOC_WRITE(雙向傳輸);數據傳輸是從應用程序的角度來看的,也就是說,IOC_READ意味着從設備中讀取數據,所以驅動程序必須向用戶空間寫入數據;注意,該字段是一個位掩碼,因此可以使用邏輯AND操作從中分解出_IOC_READ和_IOC_WRITE;

size:所涉及的用戶數據大小;這個字段的寬度與體系結構有關,通常是13或者14位,具體可以通過宏_IOC_SIZEBIT找到針對特定體系結構的具體數值;系統並不強制只用這個字段,也就是說,內核不會檢查這個字段;對該字段的正確使用可以幫助我們檢測用戶程序的錯誤,並且如果我們從不改變相關的數據項大小的話,這個位字段哈可以幫我們實現向后的兼容性;但是,如果需要很大的數據傳輸,則可以忽略這個位字段;

 

用於構造命令編號的宏如下,其中type和number位字段通過參數傳入,而size位字段通過對datatype參數取sizeof獲取的;

_IO(type,nr)用於構造無參數的命令編號;

_IOR(type,nr,size)用於構造從驅動程序讀取數據的命令編號;

_IOW(type,nr,size)用於構造用用戶空間寫入數據的命令;

_IOWR(type,nr,size)用於雙向傳輸;

 

用於解開位字段的宏如下:

_IOC_DIR(nr)、_IOC_TYPE(nr)、_IOC_NR(nr)、_IOC_SIZE(nr);

返回值

ioctl實現通常就是一個基於命令號的switch語句;當命令號不合法時,有些內核函數返回-ENVAL(非法參數);POSIX標准規定,應該返回-ENOTTY,C庫將這個錯誤碼解釋為不合適的設備ioctl;但是普遍做法是返回-EINVAL;

使用ioctl參數

ioctl的附加參數,如果是個整數,直接使用就可以了,如果是個指針,就需要注意一些問題;

當用一個指針指向用戶空間時,必須確保指向的用戶空間是合法的;對未驗證的用戶空間指針的訪問,可能導致內核oops,系統崩潰或者安全問題;驅動程序應該負責對每個用到的用戶空間地址做適當的檢查,如果是非法地址則應該返回一個錯誤;

copy_from_user和copy_to_user可以安全的與用戶空間交換數據,這兩個函數也可以在ioctl中使用,但是因為ioctl通常涉及較小的數據項,因此可以通過其他方法更有效的操作;為此,我們首先要通過access_ok函數驗證地址,而不傳輸數據,函數聲明如下:<asm-generic/uaccess.h>

1 #define access_ok(type, addr, size) __access_ok((unsigned long)(addr),(size))

第一個參數type應是VERIFY_READ或者VERIFY_RIWTE,取決於要執行的動作是讀取還是寫入用戶空間內存區;addr參數是一個用戶空間地址,size是字節數,如ioctl要從用戶空間讀取一個整數,則size是sizeof(int);如果在指定地址處既要讀取又要寫入,則應該用VERIFY_WRITE,因為它是VERIFY_READ的超集;

與大多數函數不同,access_ok返回1標識成功,0標識失敗;如果返回失敗,則通常需要返回-EFAULT給調用者;

關於該函數,需要注意兩點:第一,它並沒有完成驗證內存的全部工作,而只是檢查了所引用的內存是否位於進程有對應訪問權限的區域中,特別是要確保訪問的地址沒有指向內核空間的內存區;第二,大多數驅動程序代碼中都不需要真正調用access_ok,因為內存管理程序會處理它;

數據傳送

除了copy_from_user和copy_to_user函數之外,內核還提供了常用的數據大小為1,2,4,8字節優化過的一組函數;

1 #define put_user(x, ptr)
2 #define __put_user(x, ptr)
3 
4 #define get_user(x, ptr)
5 #define __get_user(x, ptr)

其中put_user函數把數據x寫到用戶空間;它們相對比較快,當需要傳遞單個數據時,使用這些宏而不是用copy_to_user;由於這些宏在展開時不做類型檢查,所以可以傳遞給put_user任意類型的指針,只要是個用戶空間地址就行;傳遞數據大小依賴於ptr參數的類型,在編譯時由編譯器的內建指令sizeof和typeof確定;總之,若ptr是一個字符指針,就傳遞1個字節,2,4,8字節的情況類似;

put_user已經進行了檢查確保進程可以寫入指定的內存地址,並在成功時返回0,出錯是返回-EFAULT;__put_user則做的檢查少些,它不調用access_ok,但是如果地址指向的用戶不能寫入內存,會出現操作失敗,因為__put_user要在已經使用過access_ok檢查后使用;

get_user從用戶空間接收一個數據,接收的數值保存在局部變量x中,返回值指明了操作是否成功;通用,__get_user應該在操作地址已經被access_ok檢查通過后使用;

如果是不滿足上述的傳遞大小的數值,則必須使用copy_to_user和copy_from_user;

權能與受限操作

權限相關的定義在<uapi/linux/capability.h>中,其中包含了系統能夠理解的所有權能;不修改內核源碼,驅動程序無法定義新的權能;對驅動程序開發來講有意義的權能如下:

CAP_DAC_OVERRIDE

越過文件或者目錄的訪問限制的能力;

CAP_NET_ADMIN

執行網絡管理任務的能力,包括哪些能影響網絡接口的任務;

CAP_SYS_MODULE

載入或者卸載內核模塊的能力

CAP_SYS_RAWIO

執行裸IO操作的能力,例如,訪問設備端口或者直接與USB設備通信;

CAP_SYS_ADMIN

截獲的能力,它提供了訪問許多系統管理操作的途徑;

在執行某項特權操作之前,需要檢查調用進程是否有合適的權能;如果不進行這類檢查,將導致用戶進程執行非授權操作,從而影響系統穩定性和安全性;權能檢查通過capable實現;在<linux/capability.h>中;

1 bool capable(int cap);

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM