linux設備驅動歸納總結(三):5.阻塞型IO實現
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
一、休眠簡介:
進程休眠,簡單的說就是正在運行的進程讓出CPU。休眠的進程會被內核擱置在在一邊,只有當內核再次把休眠的進程喚醒,進程纔會會重新在CPU運行。這是內核中的進程調度,以後的章節會介紹。
現在應該先知道這樣的一個概念,一個CPU在同一時間只能有一個進程在運行,在宏觀上,我們覺得是所有進程同時進行的。實際上並不是這樣,內核給每個進程分配了4G的虛擬內存,並且讓每個進程傻乎乎的以爲自己霸佔着CPU運行。同時,內核暗中的將所有的進程按一定的算法將CPU輪流的給每個進程使用,而休眠就是進程沒有被運行時的一種形式。在休眠下,進程不佔用CPU,等待被喚醒。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
二、阻塞型IO的實現:
知道什麼是休眠,接下來就好辦了。接下來就是要實現阻塞型的read和write函數,函數將實現一下功能:
read:當沒數據可讀時,函數讓出CPU,進入休眠狀態,等待write寫入數據後喚醒read。
write:寫入數據,並喚醒read。
先上函數:我只上需要修改的函數,open和release就不貼了
/*3rd_char_5/1st/test.c*/
1 #include <linux/module.h>
2 #include <linux/init.h>
3 #include <linux/fs.h>
4 #include <linux/cdev.h>
5
6 #include <linux/wait.h>
7 #include <linux/sched.h>
8
9 #include <asm/uaccess.h>
10 #include <linux/errno.h>
11
12 #define DEBUG_SWITCH 1
13 #if DEBUG_SWITCH
14 #define P_DEBUG(fmt, args...) printk("<1>" "<kernel>[%s]"fmt, __FUNCT ION__, ##args)
15 #else
16 #define P_DEBUG(fmt, args...) printk("<7>" "<kernel>[%s]"fmt, __FUNCT ION__, ##args)
17 #endif
18
19 #define DEV_SIZE 100
20
21 struct _test_t{
22 char kbuf[DEV_SIZE];
23 unsigned int major;
24 unsigned int minor;
25 unsigned int cur_size;
26 dev_t devno;
27 struct cdev test_cdev;
28 wait_queue_head_t test_queue; //1、定義等待隊列頭
29 };
30
。。。。。。省略。。。。。。。
43
44 ssize_t test_read(struct file *filp, char __user *buf, size_t count, loff_t *offset)
45 {
46 int ret;
47 struct _test_t *dev = filp->private_data;
48
49 /*休眠*/
50 P_DEBUG("read data.....\n");
51 if(wait_event_interruptible(dev->test_queue, dev->cur_size > 0))
52 return - ERESTARTSYS;
53
54 if (copy_to_user(buf, dev->kbuf, count)){
55 ret = - EFAULT;
56 }else{
57 ret = count;
58 dev->cur_size -= count;
59 P_DEBUG("read %d bytes, cur_size:[%d]\n", count, dev->cur_size);
60 }
61
62 return ret; //返回實際寫入的字節數或錯誤號
63 }
64
65 ssize_t test_write(struct file *filp, const char __user *buf, size_t count, loff_t *offset)
66 {
67 int ret;
68 struct _test_t *dev = filp->private_data;
69
70 if(copy_from_user(dev->kbuf, buf, count)){
71 ret = - EFAULT;
72 }else{
73 ret = count;
74 dev->cur_size += count;
75 P_DEBUG("write %d bytes, cur_size:[%d]\n", count, dev->cur_size);
76 P_DEBUG("kbuf is [%s]\n", dev->kbuf);
77 /*喚醒*/
78 wake_up_interruptible(&dev->test_queue);
79 }
80
81 return ret; //返回實際寫入的字節數或錯誤號
82 }
83
84 struct file_operations test_fops = {
85 .open = test_open,
86 .release = test_close,
87 .write = test_write,
88 .read = test_read,
89 };
90
91 struct _test_t my_dev;
92
93 static int __init test_init(void) //模塊初始化函數
94 {
95 int result = 0;
96 my_dev.cur_size = 0;
97 my_dev.major = 0;
98 my_dev.minor = 0;
99
100 if(my_dev.major){
101 my_dev.devno = MKDEV(my_dev.major, my_dev.minor);
102 result = register_chrdev_region(my_dev.devno, 1, "test new driver") ;
103 }else{
104 result = alloc_chrdev_region(&my_dev.devno, my_dev.minor, 1, "test alloc diver");
105 my_dev.major = MAJOR(my_dev.devno);
106 my_dev.minor = MINOR(my_dev.devno);
107 }
108
109 if(result < 0){
110 P_DEBUG("register devno errno!\n");
111 goto err0;
112 }
113
114 printk("major[%d] minor[%d]\n", my_dev.major, my_dev.minor);
115
116 cdev_init(&my_dev.test_cdev, &test_fops);
117 my_dev.test_cdev.owner = THIS_MODULE;
118 /*初始化等待隊列頭,注意函數調用的位置*/
119 init_waitqueue_head(&my_dev.test_queue);
120
121 result = cdev_add(&my_dev.test_cdev, my_dev.devno, 1);
122 if(result < 0){
123 P_DEBUG("cdev_add errno!\n");
124 goto err1;
125 }
126
127 printk("hello kernel\n");
128 return 0;
129
130 err1:
131 unregister_chrdev_region(my_dev.devno, 1);
132 err0:
133 return result;
134 }
爲了方便講解,函數我精簡了很多,紅色好亮代碼是新加的知識點,其他都是之前已經講過的。
下面開始介紹上面使用的知識:
知識1)什麼是等待隊列。
前面說了進程休眠,而其他進程爲了能夠喚醒休眠的進程,它必須知道休眠的進程在哪裏,出於這樣的原因,需要有一個稱爲等待隊列的結構體。等待隊列是一個存放着等待某個特定事件進程鏈表。
在這裏的程序,用於存放等待喚醒的進程。
既然是隊列,當然要有個隊列頭,在使用等待隊列之前,必須先定義並初始化等待隊列頭。
先看一下隊列頭的樣子:
/*linux/wait.h*/
50 struct __wait_queue_head {
51 spinlock_t lock; //這個是自旋鎖,在這裏不需要理會。
52 struct list_head task_list; //這就是隊列頭中的核心,鏈表頭。
53 };
54 typedef struct __wait_queue_head wait_queue_head_t;
說白了就是定義並初始化一個鏈表。以後就能夠在這個鏈表添加需要等待的進程了。
定義並初始化隊列頭有兩種方法:
1)靜態定義並初始化,一個函數執行完兩個操作。省力又省心。
DECLARE_WAIT_QUEUE_HEAD(name)
使用:定義並初始化一個叫name的等待隊列。
2)分開兩步執行。
2.1)定義
wait_queue_head_t test_queue;
2.2)初始化
init_waitqueue_head(&test_queue);
我使用的是第二種方法,這些都是在加載模塊時應該完成的操作。其中,等待隊列頭的定義我放在”struct _test_t”結構體中,初始化放在模塊加載函數中。
這裏值得注意的是初始化函數的位置,它必須在cdev添加函數”cdev_add”前。因爲”cdev_add”執行成功就意味着設備可以被操作,設備被操作前當然需要把所有的事情都幹完,包括等待隊列的初始化。
知識2)進程休眠
在test_read函數中就實現了進程休眠,使用了函數”wait_evenr_interruptible”。
wait_event_interruptible(wq, condition)
使用:
如果condition爲真,函數將進程添加到等待隊列頭wq並等待喚醒。
返回值:
添加成功返回0。另外,interruptition的意思是休眠進程可以被某個信號中斷中斷,如果被中斷,驅動程序應該返回-ERESTARTSYS。
這有一類的函數,操作跟”wait_evevt_interruptition”類似
wait_event(queue, condition)
/*函數成功會進入不可中斷休眠,不推薦*/
wait_event_interruptible(queue, condition)
/*函數調用成功會進入可中斷休眠,推薦,返回非零值意味着休眠被中斷,且驅動應返回-ERESTARTSYS*/
wait_event_timeout(queue, condition, timeout)
wait_event_interruptible_timeout(queue, condition, timeout)
/*比上面兩個函數多了限時功能,若休眠超時,則不管條件爲何值返回0,*/
上面的四個函數大致都是完成一下的操作:
以wait_event_interruptible(dev->test_queue, dev->cur_size > 0)舉例:
1、定義並初始化一個wait_queue_t結構體,然後將它添加到等待隊列test_queue中。
2、更改進程的狀態,休眠的狀態有兩種:(可中斷休眠)TASK_INTERRUPTIBLE和(不可中斷休眠)TASK_UNINTERRUPTIBLE。上面的函數會切換到可中斷休眠。
3、判斷條件 dev->cur_size > 0是否成立,如果不成立,則調用函數schedule()讓出CPU。注意,一旦讓出CPU進入休眠後,進程再次被喚醒後就會從這一步開始,再次檢測條件是否成立,如果還是不成立,繼續讓出CPU,等待下一次的喚醒。如果成立,則進行下一步的操作。所以,這個函數的條件會被多次判斷,因此這個判斷語句並不能對這個進程帶來任何副作用。
4、條件成立後做一些相應的清理工作,並把進程狀態更改爲TASK_RUNNING。
我剛學的時候還在納悶,爲什麼定義了一個隊列頭後,就可以在test_read函數直接根據條件進入休眠?
現在我總算是明白了。進程休眠是需要在等待隊列添加一個wait_queue_t結構體,但是上面的休眠函數內部已經幫我們實現了這個操作。
既然上面的函數有四個操作,內核肯定會有拆分出來的操作。這就是《linux設備驅動程序》(第三版)P155上面講的高級休眠。有興趣可以自己看書。
知識3)喚醒休眠進程。
在test_write函數中使用wake_up_interruptible(&dev->test_queue)喚醒指定等待隊列中睡眠的進程。
這裏也有兩個類似的函數:
void wake_up(wait_queue_head_t *queue); //喚醒等待隊列中所有休眠的進程
void wake_up_interruptible(wait_queue_head_t *queue); //喚醒等待隊列中所有可中斷睡眠的進程
一般來說,用 wake_up 喚醒 wait_event ;用 wake_up_interruptible 喚醒wait_event_interruptible。
一旦上面的函數調用成功,等待隊列裏面所有符合休眠狀態的進程都會被喚醒,所有進程都會執行上面說的休眠函數的第三步——輪流佔用CPU來是判斷時候否符合條件。一旦有一個進程符合條件,那個進程就會運行下去,其他進程變回原來的休眠狀態等待下一次的被喚醒。如果全部都不符合,全部都會變回原來的休眠狀態。
《linux設備驅動程序》P160有介紹獨佔等待的概念,大概的意思就是不要讓所有符號休眠狀態的進程同時被喚醒,只喚醒其中的一個。
知識點已經介紹完,總結一下上面驅動函數的操作:
1)首先需要定義並初始化一個等待隊列。
2)test_read函數中,如果條件不符合,調用該函數的進程就會進入休眠。
3)每當另一個進程調用test_write函數喚醒等待隊列,test_read中的函數就會再一次判斷條件是否符合,如果不符合,就會繼續休眠,直到哪次的喚醒時條件符合。
寫兩個應用程序驗證驅動:
/*app_read.c*/
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <sys/stat.h>
4 #include <fcntl.h>
5
6 int main(void)
7 {
8 char buf[20];
9 int fd;
10 int ret;
11
12 fd = open("/dev/test", O_RDWR);
13 if(fd < 0)
14 {
15 perror("open");
16 return -1;
17 }
18
19 read(fd, buf, 10);
20 printf("<app>buf is [%s]\n", buf);
21
22 close(fd);
23 return 0;
24 }
/*app_write*/
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <sys/stat.h>
4 #include <fcntl.h>
5
6 int main(void)
7 {
8 char buf[20];
9 int fd;
10 int ret;
11
12 fd = open("/dev/test", O_RDWR);
13 if(fd < 0)
14 {
15 perror("open");
16 return -1;
17 }
18
19 write(fd, "xiao bai", 10);
20
21 close(fd);
22 return 0;
23 }
驗證一下:
[root: 1st]# insmod test.ko
major[253] minor[0]
hello kernel
[root: 1st]# mknod /dev/test c 253 0
[root: 1st]# ./app_read& //先後臺運行app_read
<kernel>[test_read]read data..... //因爲沒有數據,程序阻塞
[root: 1st]# ./app_write //再運行app_write
<kernel>[test_write]write 10 bytes, cur_size:[10]
<kernel>[test_write]kbuf is [xiao bai]
<kernel>[test_read]read 10 bytes, cur_size:[0] //read繼續執行
<app>buf is [xiao bai] //打印讀到的內容
[1] + Done ./app_read
[root: 1st]#
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
三、非阻塞型操作的實現
上面的程序雖然不是很完善,但基本的功能已經實現了,但還有一個問題需要解決,當我們在應用層以非阻塞方式打開文件時,讀寫操作不滿足條件時並不阻塞,而是直接返回。
實現非阻塞操作也很簡單,判斷filp->f_flags中的是否存在O_NONBLOCK標誌(標誌在<linux/fcntl.h>定義,並被<linux/fs.h>自動包含),如果有就返回-EAGAIN。
貼上修改後的程序,其實就加了兩行:
/*3rd_char_5/2nd/test.c*/
44 ssize_t test_read(struct file *filp, char __user *buf, size_t count, loff_t *offset)
45 {
46 int ret;
47 struct _test_t *dev = filp->private_data;
48
49 if(filp->f_flags & O_NONBLOCK)
50 return - EAGAIN;
51
52 /*休眠*/
53 P_DEBUG("read data.....\n");
54 if(wait_event_interruptible(dev->test_queue, dev->cur_size > 0))
55 return - ERESTARTSYS;
56
57 if (copy_to_user(buf, dev->kbuf, count)){
58 ret = - EFAULT;
59 }else{
60 ret = count;
61 dev->cur_size -= count;
62 P_DEBUG("read %d bytes, cur_size:[%d]\n", count, dev->cur_size);
63 }
64
65 return ret; //返回實際寫入的字節數或錯誤號
66 }
再來個應用程序:
2 #include <sys/types.h>
3 #include <sys/stat.h>
4 #include <fcntl.h>
5 #include <errno.h>
6
7 int main(void)
8 {
9 char buf[20];
10 int fd;
11 int ret;
12
13 fd = open("/dev/test", O_RDWR | O_NONBLOCK);
14 if(fd < 0)
15 {
16 perror("open");
17 return -1;
18 }
19
20 ret = read(fd, buf, 10);
21 if (ret = -1) 檢查錯誤的原因
22 {
23 perror("open");
24 printf("errno = %d\n", errno);
25 }
26 else
27 {
28 printf("<app>buf is [%s]\n", buf);
29 }
30
31 close(fd);
32 return 0;
33 }
驗證一下:
[root: 2nd]# ./app_read
open: Resource temporarily unavailable
errno = 29 //這就是-EAGAIN錯誤號返回給用戶態的errno
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
四、總結
上面講了四個內容:
1什麼是休眠.
2什麼是等待隊列
3怎麼通過等待隊列把進程休眠
4怎麼喚醒進程
其中有三處處擴展:
1我只是實現了read的阻塞性IO,在一般的驅動中,write也是有阻塞功能的,大家可以嘗試實現。
2我只介紹瞭如何使用最簡單的函數把進程休眠,在《linux設備驅動程序》有介紹高級休眠,其實就是細說wait_event的內部是用什麼函數實現——我上面講述的四個步驟。
3.喚醒進程時的高級操作——獨佔等待。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
源代碼: