linux設備驅動歸納總結(三):5.阻塞型IO實現

小白的博客——提升自已,分享別人

xiaobai.blog.chinaunix.net

好的女人可以不共享。好的技術絕對要共享!   
個人資料
  • 博客訪問:74410
  • 博文數量:42
  • 博客積分:687
  • 博客等級:中校
  • 關注人氣: 4
  • 註冊時間:2010-12-01 18:55:26
訂閱我的博客
  • 訂閱
  • 訂閱到鮮果
  • 訂閱到抓蝦
  • 訂閱到Google
最近來訪
字體大小:  博文

linux設備驅動歸納總結(三):5.阻塞型IO實現


xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx


一、休眠簡介:


進程休眠,簡單的說就是正在運行的進程讓出CPU。休眠的進程會被內核擱置在在一邊,只有當內核再次把休眠的進程喚醒,進程纔會會重新在CPU運行。這是內核中的進程調度,以後的章節會介紹。

現在應該先知道這樣的一個概念,一個CPU在同一時間只能有一個進程在運行,在宏觀上,我們覺得是所有進程同時進行的。實際上並不是這樣,內核給每個進程分配了4G的虛擬內存,並且讓每個進程傻乎乎的以爲自己霸佔着CPU運行。同時,內核暗中的將所有的進程按一定的算法將CPU輪流的給每個進程使用,而休眠就是進程沒有被運行時的一種形式。在休眠下,進程不佔用CPU,等待被喚醒。


xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx


二、阻塞型IO的實現:


知道什麼是休眠,接下來就好辦了。接下來就是要實現阻塞型的readwrite函數,函數將實現一下功能:

read:當沒數據可讀時,函數讓出CPU,進入休眠狀態,等待write寫入數據後喚醒read

write:寫入數據,並喚醒read


先上函數:我只上需要修改的函數,openrelease就不貼了

/*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)首先需要定義並初始化一個等待隊列。

2test_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


源代碼:

 3rd_char_5.rar   

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