linux設備驅動歸納總結(三):4.ioctl的實現
一、ioctl的簡介:
雖然在文件操作結構體"struct file_operations"中有很多對應的設備操作函數,但是有些命令是實在找不到對應的操作函數。如CD-ROM的驅動,想要一個彈出光驅的操作,這種操作並不是所有的字符設備都需要的,所以文件操作結構體也不會有對應的函數操作。
出於這樣的原因,ioctl就有它的用處了————一些沒辦法歸類的函數就統一放在ioctl這個函數操作中,通過指定的命令來實現對應的操作。所以,ioctl函數裏面都實現了多個的對硬件的操作,通過應用層傳入的命令來調用相應的操作。
來個圖來說一下應用層與驅動函數的ioctl之間的聯繫:
上面的圖可以看出,fd通過內核後找到對應的inode和file結構體指針並傳給驅動函數,而另外兩個參數卻沒有修改(類型改了沒什麼關係)。
簡單介紹一下函數:
int (*ioctl) (struct inode * node, struct file *filp, unsigned int cmd, unsigned long arg);
參數:
1)inode和file:ioctl的操作有可能是要修改文件的屬性,或者訪問硬件。要修改
文件屬性的話,就要用到這兩個結構體了,所以這裏傳來了它們的指針。
2)cmd:命令,接下來要長篇大論地說。
3)arg:參數,接下來也要長篇大論。
返回值:
1)如果傳入的非法命令,ioctl返回錯誤號-EINVAL。
2)內核中的驅動函數返回值都有一個默認的方法,只要是正數,內核就會傻乎乎的認爲這是正確的返回,並把它傳給應用層,如果是負值,內核就會認爲它是錯誤號了。
Ioctl裏面多個不同的命令,那就要看它函數的實現來決定返回值了。打個比方,如果ioctl裏面有一個類似read的函數,那返回值也就可以像read一樣返回。
當然,不返回也是可以的。
二、ioctl的cmd
說白了,cmd就是一個數,如果應用層傳來的數值在驅動中有對應的操作,這樣就就可以了。
來個最簡單的ioctl實現:3rd_char_4/1st
1)要先定義個命令,就用一個簡單的0,來個命令的頭文件,驅動和應用函數都要包含這個頭文件:
/*test_cmd.h*/
1 #ifndef _TEST_CMD_H
2 #define _TEST_CMD_H
3
4 #define TEST_CLEAR 0
5
6 #endif /*_TEST_CMD_H*/
2)驅動實現ioctl:
命令TEST_CLEAR的操作就是清空驅動中的kbuf。
122 int test_ioctl (struct inode *node, struct file *filp, unsigned int cmd, uns igned long arg)
123 {
124 int ret = 0;
125 struct _test_t *dev = filp->private_data;
126
127 switch(cmd){
128 case TEST_CLEAR:
129 memset(dev->kbuf, 0, DEV_SIZE);
130 dev->cur_size = 0;
131 filp->f_pos = 0;
132 ret = 0;
133 break;
134 default: /*命令錯誤時的處理*/
135 P_DEBUG("error cmd!\n");
136 ret = - EINVAL;
137 break;
138 }
139
140 return ret;
141 }
3)再來個應用程序:
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <sys/stat.h>
4 #include <fcntl.h>
5 #include <sys/ioctl.h>
6 #include "test_cmd.h"
7
8 int main(void)
9 {
10 char buf[20];
11 int fd;
12 int ret;
13
14 fd = open("/dev/test", O_RDWR);
15 if(fd < 0)
16 {
17 perror("open");
18 return -1;
19 }
20
21 write(fd, "xiao bai", 10); //1先寫入
22
23 ioctl(fd, TEST_CLEAR); //2再清空
24
25 ret = read(fd, buf, 10); //3再驗證
26 if(ret < 0)
27 {
28 perror("read");
29 }
30
31 close(fd);
32 return 0;
33 }
注:這裏爲了read返回出錯,我修改了驅動的read、write函數的開始時的第一個
判斷,一看就知道了。
4)驗證一下:
[root: 1st]# insmod test.ko
major[253] minor[0]
hello kernel
[root: 1st]# mknod /dev/test c 253 0
[root: 1st]# ./app
<kernel>[test_write]write 10 bytes, cur_size:[10]
<kernel>[test_write]kbuf is [xiao bai]
read: No such device or address //哈哈!出錯了!因爲沒數據讀取。
按照上面的方法來定義一個命令是完全可以的,但內核開發人員發現這樣有點不對勁。
如果有兩個不同的設備,但它們的ioctl的cmd卻一樣的,哪天有誰不小心打開錯了,並且調用ioctl,這樣就完蛋了。因爲這個文件裏面同樣有cmd對應實現。
爲了防止這樣的事情發生,內核對cmd又有了新的定義,規定了cmd都應該不一樣。
三、ioctl中的cmd
一個cmd被分爲了4個段,每一段都有各自的意義,cmd的定義在<linux/ioctl.h>。注:但實際上<linux/ioctl.h>中只是包含了<asm/ioctl.h>,這說明了這是跟平臺相關的,ARM的定義在<arch/arm/include/asm/ioctl.h>,但這文件也是包含別的文件<asm-generic/ioctl.h>,千找萬找,終於找到了。
在<asm-generic/ioctl.h>中,cmd拆分如下:
解釋一下四部分,全部都在<asm-generic/ioctl.h>和ioctl-number.txt這兩個文檔有說明。
1)幻數:說得再好聽的名字也只不過是個0~0xff的數,佔8bit(_IOC_TYPEBITS)。這個數是用來區分不同的驅動的,像設備號申請的時候一樣,內核有一個文檔給出一些推薦的或者已經被使用的幻數。
/*Documentation/ioctl/ioctl-number.txt*/
164 'w' all CERN SCI driver
165 'y' 00-1F packet based user level communications
166 <mailto:[email protected]>
167 'z' 00-3F CAN bus card
168 <mailto:[email protected]>
169 'z' 40-7F CAN bus card
170 <mailto:[email protected]>
可以看到'x'是還沒有人用的,我就拿這個當幻數!
2)序數:用這個數來給自己的命令編號,佔8bit(_IOC_NRBITS),我的程序從1開始排序。
3)數據傳輸方向:佔2bit(_IOC_DIRBITS)。如果涉及到要傳參,內核要求描述一下傳輸的方向,傳輸的方向是以應用層的角度來描述的。
1)_IOC_NONE:值爲0,無數據傳輸。
2)_IOC_READ:值爲1,從設備驅動讀取數據。
3)_IOC_WRITE:值爲2,往設備驅動寫入數據。
4)_IOC_READ|_IOC_WRITE:雙向數據傳輸。
4)數據大小:與體系結構相關,ARM下佔14bit(_IOC_SIZEBITS),如果數據是int,內核給這個賦的值就是sizeof(int)。
強調一下,內核是要求按這樣的方法把cmd分類,當然你也可以不這樣幹,這只是爲了迎合內核的要求,讓自己的程序看上去很正宗。上面我的程序沒按要求照樣運行。
既然內核這樣定義cmd,就肯定有方法讓用戶方便定義:
_IO(type,nr) //沒有參數的命令
_IOR(type,nr,size) //該命令是從驅動讀取數據
_IOW(type,nr,size) //該命令是從驅動寫入數據
_IOWR(type,nr,size) //雙向數據傳輸
上面的命令已經定義了方向,我們要傳的是幻數(type)、序號(nr)和大小(size)。在這裏szie的參數只需要填參數的類型,如int,上面的命令就會幫你檢測類型的正確然後賦值sizeof(int)。
有生成cmd的命令就必有拆分cmd的命令:
_IOC_DIR(cmd) //從命令中提取方向
_IOC_TYPE(cmd) //從命令中提取幻數
_IOC_NR(cmd) //從命令中提取序數
_IOC_SIZE(cmd) //從命令中提取數據大小
越講就越複雜了,既然講到這,隨便就講一下預定義命令。
預定義命令是由內核來識別並且實現相應的操作,換句話說,一旦你使用了這些命令,你壓根也不要指望你的驅動程序能夠收到,因爲內核拿掉就把它處理掉了。
分爲三類:
1)可用於任何文件的命令
2)只用於普通文件的命令
3)特定文件系統類型的命令
其實上面的我三類我也沒搞懂,反正我自己隨便編了幾個數當命令都沒出錯,如果真的怕出錯,那就不要用別人已經使用的幻數就行了。
講了這麼多,終於要上程序了,修改一下上一個程序,讓它看起來比較有內涵。
/3rd_char/3rd_char_4/2nd
1)先改一下命令:
/*test_cmd.h*/
1 #ifndef _TEST_CMD_H
2 #define _TEST_CMD_H
3
4 #define TEST_MAGIC 'x' //定義幻數
5 #define TEST_MAX_NR 1 //定義命令的最大序數,只有一個命令當然是1
6
7 #define TEST_CLEAR _IO(TEST_MAGIC, 0)
8
9 #endif /*_TEST_CMD_H*/
2)既然這麼辛苦改了cmd,在驅動函數當然要做一些參數檢驗:
/*test.c*/
122 int test_ioctl (struct inode *node, struct file *filp, unsigned int cmd, unsigned long arg)
123 {
124 int ret = 0;
125 struct _test_t *dev = filp->private_data;
126
127 /*既然這麼費勁定義了命令,當然要檢驗命令是否有效*/
128 if(_IOC_TYPE(cmd) != TEST_MAGIC) return - EINVAL;
129 if(_IOC_NR(cmd) > TEST_MAX_NR) return - EINVAL;
130
131 switch(cmd){
132 case TEST_CLEAR:
133 memset(dev->kbuf, 0, DEV_SIZE);
134 dev->cur_size = 0;
135 filp->f_pos = 0;
136 ret = 0;
137 break;
138 default: /*命令錯誤時的處理*/
139 P_DEBUG("error cmd!\n");
140 ret = - EINVAL;
141 break;
142 }
143
144 return ret;
145 }
每個參數的傳入都會先檢驗一下幻數還有序數是否正確。
3)應用程序的驗證:
結果跟上一個完全一樣,因爲命令的操作沒有修改
[root: 2nd]# insmod test.ko
major[253] minor[0]
hello kernel
[root: 2nd]# mknod /dev/test c 253 0
[root: 2nd]# ./app
<kernel>[test_write]write 10 bytes, cur_size:[10]
<kernel>[test_write]kbuf is [xiao bai]
read: No such device or address
五、ioctl中的arg之整數傳參。
上面講的例子都沒有使用ioctl的傳參。這裏先要說一下ioctl傳參的方式。
應用層的ioctl的第三個參數是"...",這個跟printf的"..."可不一樣,printf中是意味這你可以傳任意個數的參數,而ioctl最多也只能傳一個,"..."的意思是讓內核不要檢查這個參數的類型。也就是說,從用戶層可以傳入任何參數,只要你傳入的個數是1.
一般會有兩種的傳參方法:
1)整數,那可是省力又省心,直接使用就可以了。
2)指針,通過指針的就傳什麼類型都可以了,當然用起來就比較煩。
先說簡單的,使用整數作爲參數:
例子,實現個命令,通過傳入參數更改偏移量,雖然llseek已經實現,這裏只是想驗證一下正數傳參的方法。
1)先加個命令:
1 #ifndef _TEST_CMD_H
2 #define _TEST_CMD_H
3
4 #define TEST_MAGIC 'x' //定義幻數
5 #define TEST_MAX_NR 2 //定義命令的最大序數
6
7 #define TEST_CLEAR _IO(TEST_MAGIC, 1)
8 #define TEST_OFFSET _IO(TEST_MAGIC, 2)
9
10 #endif /*_TEST_CMD_H*/
這裏有人會問了,明明你是要傳入參數,爲什麼不用_IOW而用_IO定義命令呢?
原因有二:
1)因爲定義數據的傳輸方向是爲了好讓驅動的函數驗證數據的安全性,而一般指針才需要檢驗安全性,因爲有人會惡意傳參(回想一下copy_to_user)。
2)個人喜好,方便我寫程序介紹另一種傳參方法,說白了命令也只是一個數,只要不要跟預定義命令衝突就可以了。
2)更新test_ioctl
122 int test_ioctl (struct inode *node, struct file *filp, unsigned int cmd, uns igned long arg)
123 {
124 int ret = 0;
125 struct _test_t *dev = filp->private_data;
126
127 /*既然這麼費勁定義了命令,當然要檢驗命令是否有效*/
128 if(_IOC_TYPE(cmd) != TEST_MAGIC) return - EINVAL;
129 if(_IOC_NR(cmd) > TEST_MAX_NR) return - EINVAL;
130
131 switch(cmd){
132 case TEST_CLEAR:
133 memset(dev->kbuf, 0, DEV_SIZE);
134 dev->cur_size = 0;
135 filp->f_pos = 0;
136 ret = 0;
137 break;
138 case TEST_OFFSET: //根據傳入的參數更改偏移量
139 filp->f_pos += (int)arg;
140 P_DEBUG("change offset!\n");
141 ret = 0;
142 break;
143 default: /*命令錯誤時的處理*/
144 P_DEBUG("error cmd!\n");
145 ret = - EINVAL;
146 break;
147 }
148
149 return ret;
150 }
TSET_OFFSET命令就是根據傳參更改偏移量,不過這裏要注意一個問題,那就是參數的類型,驅動函數必須要知道從應用傳來的參數是什麼類型,不然就沒法使用。在這個函數裏,從應用層傳來的參數是int,因此在驅動中也得用int。
3)再改一下應用程序:
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <sys/stat.h>
4 #include <fcntl.h>
5 #include <sys/ioctl.h>
6
7 #include "test_cmd.h"
8
9 int main(void)
10 {
11 char buf[20];
12 int fd;
13 int ret;
14
15 fd = open("/dev/test", O_RDWR);
16 if(fd < 0)
17 {
18 perror("open");
19 return -1;
20 }
21
22 write(fd, "xiao bai", 10); //先寫入
23
24 ioctl(fd, TEST_OFFSET, -10); //再改偏移量
25
26 ret = read(fd, buf, 10); //再讀數據
27 printf("<app> buf is [%s]\n", buf);
28 if(ret < 0)
29 {
30 perror("read");
31 }
32
33 close(fd);
34 return 0;
35 }
4)驗證一下
[root: 3rd]# insmod test.ko
major[253] minor[0]
hello kernel
[root: 3rd]# mknod /dev/test c 253 0
[root: 3rd]# ./app
<kernel>[test_write]write 10 bytes, cur_size:[10]
<kernel>[test_write]kbuf is [xiao bai]
<kernel>[test_ioctl]change offset! //更改偏移量
<kernel>[test_read]read 10 bytes, cur_size:[0] //沒錯誤,成功讀取!
<app> buf is [xiao bai]
上面的傳參很簡單把,接下來說一下以指針傳參。
考慮到參數不可能永遠只是一個正數這麼簡單,如果要傳多一點的東西,譬如是結構體,那就得用上指針了。
六、ioctl中的arg之指針傳參。
一講到從應用程序傳來的指針,就得想起我邪惡的傳入了非法指針的例子。所以,驅動程序中任何與應用層打交道的指針,都得先檢驗指針的安全性。
說到這檢驗又有兩種方法:
1)用的時候才檢驗。
2)一進來ioctl就檢驗。
先說用的時候檢驗,說白了就是用copy_xx_user系列函數,下面實現一下:
1)先定義個命令
1 #ifndef _TEST_CMD_H
2 #define _TEST_CMD_H
3
4 struct ioctl_data{
5 unsigned int size;
6 char buf[100];
7 };
8
9 #define DEV_SIZE 100
10
11 #define TEST_MAGIC 'x' //定義幻數
12 #define TEST_MAX_NR 3 //定義命令的最大序數
13
14 #define TEST_CLEAR _IO(TEST_MAGIC, 1)
15 #define TEST_OFFSET _IO(TEST_MAGIC, 2)
16 #define TEST_KBUF _IO(TEST_MAGIC, 3)
17
18 #endif /*_TEST_CMD_H*/
這裏有定義多了一個函數,雖然這個命令是涉及到了指針的傳參,但我還是_IOW,還是那一句,現在還不需要用上。
該命令的操作是傳進一個結構體指針,驅動根據結構體的內容修改kbuf和cur_size和偏移量。