首页 > 代码库 > linux设备驱动归纳总结(三):5.阻塞型IO实现【转】

linux设备驱动归纳总结(三):5.阻塞型IO实现【转】

本文转载自:http://blog.chinaunix.net/uid-25014876-id-60025.html

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

2 #include

3 #include

4 #include

5

6 #include

7 #include

8

9 #include

10 #include

11

12 #define DEBUG_SWITCH 1

13 #if DEBUG_SWITCH

14 #define P_DEBUG(fmt, args...) printk("<1>" "[%s]"fmt, __FUNCT ION__, ##args)

15 #else

16 #define P_DEBUG(fmt, args...) printk("<7>" "[%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

2 #include

3 #include

4 #include

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("buf is [%s]\n", buf);

21

22 close(fd);

23 return 0;

24 }

 

/*app_write*/

1 #include

2 #include

3 #include

4 #include

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

[test_read]read data..... //因为没有数据,程序阻塞

[root: 1st]# ./app_write //再运行app_write

[test_write]write 10 bytes, cur_size:[10]

[test_write]kbuf is [xiao bai]

[test_read]read 10 bytes, cur_size:[0] //read继续执行

buf is [xiao bai] //打印读到的内容

[1] + Done ./app_read

[root: 1st]#

 

xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

 

三、非阻塞型操作的实现

 

上面的程序虽然不是很完善,但基本的功能已经实现了,但还有一个问题需要解决,当我们在应用层以非阻塞方式打开文件时,读写操作不满足条件时并不阻塞,而是直接返回。

 

实现非阻塞操作也很简单,判断filp->f_flags中的是否存在O_NONBLOCK标志(标志在定义,并被自动包含),如果有就返回-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

3 #include

4 #include

5 #include

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("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

linux设备驱动归纳总结(三):5.阻塞型IO实现【转】