Linux設備驅動程序學習(4)-高級字符驅動程序操作[(1)ioctl and llseek]

今天進入《Linux設備驅動程序(第3版)》第六章高級字符驅動程序操作的學習。

一、ioctl
大部分設備除了讀寫能力,還可進行超出簡單的數據傳輸之外的操作,所以設備驅動也必須具備進行各種硬件控制操作的能力. 這些操作常常通過 ioctl 方法來支持,它有和用戶空間版本不同的原型:

int (*ioctl) (struct inode *inode, struct file *filp,
              unsigned int cmd, unsigned long arg);

需要注意的是:不管可選的參數arg是否由用戶給定爲一個整數或一個指針,它都以一個unsigned long的形式傳遞。如果調用程序不傳遞arg參數, 被驅動收到的 arg 值是未定義的。因爲在arg參數上的類型檢查被關閉了,所以若一個非法參數傳遞給 ioctl,編譯器是無法報警的,且任何關聯的錯誤難以查找.

選擇ioctl命令

爲了防止向錯誤的設備使用正確的命令,命令號應該在系統範圍內唯一。爲方便程序員創建唯一的 ioctl 命令代號, 每個命令號被劃分爲多個位字段。要按 Linux 內核的約定方法爲驅動選擇 ioctl 的命令號, 應該首先看看 include/asm/ioctl.h 和 Documentation/ioctl-number.txt。 要使用的位字段符號定義在 <linux/ioctl.h> :

type(幻數):8 位寬(_IOC_TYPEBITS),參考ioctl-number.txt選擇一個數,並在整個驅動中使用它。

number(序數):順序編號,8 位寬(_IOC_NRBITS)。

direction(數據傳送的方向):可能的值是 _IOC_NONE(沒有數據傳輸)、_IOC_READ、 _IOC_WRITE和 _IOC_READ|_IOC_WRITE (雙向傳輸數據)。該字段是一個位掩碼(兩位), 因此可使用 AND 操作來抽取_IOC_READ 和 _IOC_WRITE。

size(數據的大小):寬度與體系結構有關,ARM爲14位.可在宏 _IOC_SIZEBITS 中找到特定體系的值. 

 <linux/ioctl.h> 中包含的 <asm/ioctl.h>定義了一些構造命令編號的宏:

_IO(type,nr)/*沒有參數的命令*/
_IOR(type, nr, datatype)/*從驅動中讀數據*/
_IOW(type,nr,datatype)/*寫數據*/
_IOWR(type,nr,datatype)/*雙向傳送*/
/*type 和 number 成員作爲參數被傳遞, 並且 size 成員通過應用 sizeof 到 datatype 參數而得到*/

這個頭文件還定義了用來解開這個字段的宏:

_IOC_DIR(nr)
_IOC_TYPE(nr)
_IOC_NR(nr)
_IOC_SIZE(nr)

具體的使用方法在實驗中展示。

返回值

POSIX 標準規定:如果使用了不合適的 ioctl 命令號,應當返回-ENOTTY 。這個錯誤碼被 C 庫解釋爲"不合適的設備 ioctl。然而,它返回-EINVAL仍是相當普遍的。

預定義命令

有一些ioctl命令是由內核識別的,當這些命令用於自己的設備時,他們會在我們自己的文件操作被調用之前被解碼. 因此, 如果你選擇一個ioctl命令編號和系統預定義的相同時,你永遠不會看到該命令的請求,而且因爲ioctl 號之間的衝突,應用程序的行爲將無法預測。預定義命令分爲 3 類:

(1)用於任何文件(常規, 設備, FIFO和socket) 的命令

(2)只用於常規文件的命令

(3)特定於文件系統類型的命令 

下列 ioctl 命令是預定義給任何文件,包括設備特定文件:

FIOCLEX :設置 close-on-exec 標誌(File IOctl Close on EXec)。
FIONCLEX :清除 close-no-exec 標誌(File IOctl Not CLose on EXec)。
FIOQSIZE :這個命令返回一個文件或者目錄的大小; 當用作一個設備文件, 但是, 它返回一個 ENOTTY 錯誤。
FIONBIO:"File IOctl Non-Blocking I/O"(在"阻塞和非阻塞操作"一節中描述)。 

使用ioctl參數

在使用ioctl的可選arg參數時,如果傳遞的是一個整數,它可以直接使用。如果是一個指針,,就必須小心。當用一個指針引用用戶空間, 我們必須確保用戶地址是有效的,其校驗(不傳送數據)由函數 access_ok 實現,定義在 <asm/uaccess.h> :

int access_ok(int type, const void *addr, unsigned long size);


第一個參數應當是 VERIFY_READ(讀)或VERIFY_WRITE(讀寫);addr 參數爲用戶空間地址,size 爲字節數,可使用sizeof()。access_ok 返回一個布爾值: 1 是成功(存取沒問題)和 0 是失敗(存取有問題)。如果它返回假,驅動應當返回 -EFAULT 給調用者。

注意:首先, access_ok不做校驗內存存取的完整工作; 它只檢查內存引用是否在這個進程有合理權限的內存範圍中,且確保這個地址不指向內核空間內存。其次,大部分驅動代碼不需要真正調用 access_ok,而直接使用put_user(datum, ptr)和get_user(local, ptr),它們帶有校驗的功能,確保進程能夠寫入給定的內存地址,成功時返回 0, 並且在錯誤時返回 -EFAULT.。

put_user(datum, ptr)
__put_user(datum, ptr)
get_user(local, ptr)
__get_user(local, ptr)

這些宏它們相對copy_to_user 和copy_from_user快, 並且這些宏已被編寫來允許傳遞任何類型的指針,只要它是一個用戶空間地址. 傳送的數據大小依賴 prt 參數的類型, 並且在編譯時使用 sizeof 和 typeof 等編譯器內建宏確定。他們只傳送1、2、4或8 個字節。如果使用以上函數來傳送一個大小不適合的值,結果常常是一個來自編譯器的奇怪消息,如"coversion to non-scalar type requested". 在這些情況中,必須使用 copy_to_user 或者 copy_from_user。

__put_user和__get_user 進行更少的檢查(不調用 access_ok), 但是仍然能夠失敗如果被指向的內存對用戶是不可寫的,所以他們應只用在內存區已經用 access_ok 檢查過的時候。作爲通用的規則:當實現一個 read 方法時,調用 __put_user 來節省幾個週期, 或者當你拷貝幾個項時,因此,在第一次數據傳送之前調用 access_ok 一次。

權能與受限操作

Linux 內核提供了一個更加靈活的系統, 稱爲權能(capability)。內核專爲許可管理上使用權能並導出了兩個系統調用 capget 和 capset,這樣可以從用戶空間管理權能,其定義在 <linux/capability.h> 中。對設備驅動編寫者有意義的權能如下:

CAP_DAC_OVERRIDE /*越過在文件和目錄上的訪問限制(數據訪問控制或 DAC)的能力。*/

CAP_NET_ADMIN /*進行網絡管理任務的能力, 包括那些能夠影響網絡接口的任務*/

CAP_SYS_MODULE /*加載或去除內核模塊的能力*/

CAP_SYS_RAWIO /*進行 "raw"(裸)I/O 操作的能力. 例子包括存取設備端口或者直接和 USB 設備通訊*/

CAP_SYS_ADMIN /*截獲的能力, 提供對許多系統管理操作的途徑*/

CAP_SYS_TTY_CONFIG /*執行 tty 配置任務的能力*/

在進行一個特權操作之前, 一個設備驅動應當檢查調用進程有合適的能力,檢查是通過 capable 函數來進行的(定義在 <linux/sched.h> )範例如下:

if (! capable (CAP_SYS_ADMIN))
 return -EPERM;


二、定位設備(llseek實現)

llseek是修改文件中的當前讀寫位置的系統調用。內核中的缺省的實現進行移位通過修改 filp->f_pos, 這是文件中的當前讀寫位置。對於 lseek 系統調用要正確工作,讀和寫方法必須通過更新它們收到的偏移量來配合。

如果設備是不允許移位的,你不能只制止聲明 llseek 操作,因爲缺省的方法允許移位。應當在你的 open 方法中,通過調用 nonseekable_open 通知內核你的設備不支持 llseek :

int nonseekable_open(struct inode *inode; struct file *filp);

完整起見, 你也應該在你的 file_operations 結構中設置 llseek 方法到一個特殊的幫助函數 no_llseek(定義在 <linux/fs.h> )。 具體的應用在試驗程序中學習.


三、ioctl和llseek實驗。

模塊程序鏈接:ioctl_and_llseek

模塊測試程序鏈接ioctl_and_llseek-test

ARM9實驗板的實驗現象是:

[Tekkaman2440@SBC2440V4]#cd /lib/modules/
[Tekkaman2440@SBC2440V4]#insmod scull.ko scull_nr_devs=1
[Tekkaman2440@SBC2440V4]#cd /tmp/
[Tekkaman2440@SBC2440V4]#./scull_test2
open scull !
SCULL_IOCSQUANTUM-SCULL_IOCQQUANTUM : scull_quantum=10
SCULL_IOCTQUANTUM-SCULL_IOCGQUANTUM : scull_quantum=6
SCULL_IOCXQUANTUM : scull_quantum=6 --> 10
SCULL_IOCHQUANTUM : scull_quantum=10 --> 6
SCULL_IOCSQSET-SCULL_IOCQQSET : scull_qset=2
SCULL_IOCTQSET-SCULL_IOCGQSET : scull_qset=4
SCULL_IOCXQSET : scull_qset=4 --> 2
SCULL_IOCHQSET : scull_qset=2 --> 4
before reset : scull_quantum=6 scull_qset=4
close scull !
reopen scull !
reopen : scull_quantum=6 scull_qset=4
write code=6 i=20
write code=6 i=14
write code=6 i=8
write code=2
lseek scull SEEK_SET-->0 !
read code=6 i=20
read code=6 i=14
read code=6 i=8
read code=2
[0]=0 [1]=1 [2]=2 [3]=3 [4]=4
[5]=5 [6]=6 [7]=7 [8]=8 [9]=9
[10]=10 [11]=11 [12]=12 [13]=13 [14]=14
[15]=15 [16]=16 [17]=17 [18]=18 [19]=19
SCULL_IOCRESET
after reset : scull_quantum=4000 scull_qset=1000
close scull !
reopen scull !
write code=20
lseek scull SEEK_CUR-10-->10 !
read code=10
[0]=10 [1]=11 [2]=12 [3]=13 [4]=14
[5]=15 [6]=16 [7]=17 [8]=18 [9]=19
lseek scull SEEK_END-20-->0 !
read code=20
[0]=0 [1]=1 [2]=2 [3]=3 [4]=4
[5]=5 [6]=6 [7]=7 [8]=8 [9]=9
[10]=10 [11]=11 [12]=12 [13]=13 [14]=14
[15]=15 [16]=16 [17]=17 [18]=18 [19]=19
close scull !

[Tekkaman2440@SBC2440V4]#cat /proc/scullseq

Device 0: qset 1000, q 4000, sz 20
  item at c3dd3d74, qset at c3f54000
       0: c3e71000
[Tekkaman2440@SBC2440V4]#  

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章