第8章 Linux设备驱动中的阻塞与非阻塞I/O

本章导读

阻塞和非阻塞I/O是设备访问的两种不同模式,驱动程序可以灵活地支持这两种用户空间对设备的访问方式。

1、 阻塞和非阻塞I/O的区别,实现阻塞I/O的等待队列机制。

2、 设备驱动轮询(Poll)操作的概念和编程方法,轮询可以帮助用户了解是否能对设备进行无阻塞访问。

8.1 阻塞与非阻塞I/O

阻塞操作是指在执行设备操作时,若不能获得资源,则挂起进程直到满足可操作的条件后再进行操作。被挂起的进程进入睡眠状态,被从调度器的运行队列移走,直到等待的条件被满足。而非阻塞操作的进程在不能进行设备操作时,并不挂起,要么放弃,要么不停地查询,直至可以进行操作为止。

驱动程序通常需要提供这样的能力:当应用程序进行read()、write()等系统调用时,若设备的资源不能获取,用户又希望以阻塞的方式访问设备,驱动程序应在设备驱动的xxx_read()、xxx_write()等操作中将进程阻塞直到资源可以获取,此后,应用程序的read()、write()等调用才返回,整个过程仍然进行了正确的设备访问,用户并没有感知到;若用户以非阻塞的方式访问设备文件,则当设备资源不可获取时,设备驱动的xxx_read()、xxx_write()等操作应立即返回,read()、write()等系统调用也随即被返回,应用程序收到-EAGAIN返回值。

如图8.1所示,在阻塞访问时,不能获取资源的进程将进入休眠,它将CPU资源“礼让”给其他进程。因为阻塞的进程会进入休眠状态,所以必须确保有一个地方能够唤醒休眠的进程,否则,进程就真的“寿终正寝”了。唤醒进程的地方最大可能发生在中断里面,因为在硬件资源获得的同时往往伴随着一个中断。而非阻塞的进程则不断尝试,直到可以进行I/O。



图8.1 阻塞与非阻塞I/O

代码清单8.1和8.2分别演示了以阻塞和非阻塞方式读取串口一个字符的代码。8.1在打开文件的时候没有O_NONBLOCK标记,8.2使用O_NONBLOCK标记打开文件。

代码清单8.1 阻塞地读串口一个字符

char buf;
fd = open("/dev/ttyS1", O_RDWR);
...
res = read(fd,&buf,1);      /* 串口上有输入时才返回 */
if(res==1)
printf("%c\n", buf);

代码清单8.2 非阻塞地读串口一个字符

char buf;
fd = open("/dev/ttyS1", O_RDWR | O_NONBLOCK);
...
while(read(fd,&buf,1)!=1)
      continue;       /* 串口上无输入也返回,循环尝试读取串口 */

printf("%c\n", buf);

除了在打开文件时可以指定阻塞还是非阻塞方式以外,在文件打开后,也可以通过ioctl()和fcntl()改变读写的方式,如从阻塞变更为非阻塞或者从非阻塞变更为阻塞。例如,调用fcntl(fd,F_SETFL,O_NONBLOCK)可以设置fd对应的I/O为非阻塞。

8.1.1 等待队列

在Linux驱动程序中,可使用等待队列来实现唤醒阻塞进程。等待队列以队列为基础数据结构,与进程调度机制紧密结合,可以用来同步对系统资源的访问,信号量在内核中也依赖等待队列来实现。

Linux内核提供了如下关于等待队列的操作。

#include <linux/wait.h>

struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;

1.定义“等待队列头部”

wait_queue_head_t my_queue;

wait_queue_head_t是__wait_queue_head结构体的一个typedef。

2.初始化“等待队列头部”

init_waitqueue_head(&my_queue);

DECLARE_WAIT_QUEUE_HEAD()宏可以作为定义并初始化等待队列头部的“快捷方式”。

DECLARE_WAIT_QUEUE_HEAD (name)

3.定义等待队列元素

DECLARE_WAITQUEUE(name, tsk)

该宏用于定义并初始化一个名为name的等待队列元素。

4.添加/移除等待队列

void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);

add_wait_queue()用于将等待队列元素wait添加到等待队列头部q指向的双向链表中,而remove_wait_queue()用于将等待队列元素wait从由q头部指向的链表中移除。

5.等待事件

wait_event(queue, condition)
wait_event_interruptible(queue, condition)
wait_event_timeout(queue, condition, timeout)
wait_event_interruptible_timeout(queue, condition, timeout)

等待第1个参数queue作为等待队列头部的队列被唤醒,而且第2个参数condition必须满足,否则继续阻塞。wait_event_interruptible()可以被信号打断,wait_event()不可以被信号打断。

wait_event_timeout(queue, condition, timeout)、wait_event_interruptible_timeout(queue, condition, timeout)意味着阻塞等待的超时时间,以jiffy为单位,在第3个参数的timeout到达时,不论condition是否满足,均返回。

6.唤醒队列

void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);

上述操作会唤醒以queue作为等待队列头部的队列中所有的进程。

wake_up()应该与wait_event()或wait_event_timeout()成对使用,而wake_up_interruptible()则应与wait_event_interruptible()或wait_event_interruptible_timeout()成对使用。wake_up()可唤醒处于TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE的进程,而wake_up_interruptible()只能唤醒处于TASK_INTERRUPTIBLE的进程。

7.在等待队列上睡眠

sleep_on(wait_queue_head_t *q );
interruptible_sleep_on(wait_queue_head_t *q );

sleep_on()就是将目前进程的状态置成TASK_UNINTERRUPTIBLE,并定义一个等待队列元素,之后把它挂到等待队列头部q指向的双向链表,直到资源可获得,q队列指向链接的进程被唤醒。

interruptible_sleep_on()是将目前进程的状态置成TASK_INTERRUPTIBLE,并定义一个等待队列元素,之后把它附属到q指向的队列,直到资源可获得或者进程收到信号。

sleep_on()与wake_up()成对使用,interruptible_sleep_on()与wake_up_interruptible()成对使用。



猜你喜欢

转载自blog.csdn.net/xiezhi123456/article/details/80392426