52.1 阻塞和非阻塞 IO
52.1.1 阻塞和非阻塞简介
这里的 IO 指的是 Input/Output,也就是输入/输出,是应用程序对驱动设备的输入/输出操作。
当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式 IO 就会将应用程
序对应的线程挂起,直到设备资源可以获取为止。
对于非阻塞 IO,应用程序对应的线程不会挂起,它要么一直轮询等待,直到设备资源可以使用,要么就直接放弃。
图 52.1.1.1 中应用程序调用 read 函数从设备中读取数据,
当设备不可用或数据未准备好的时候就会进入到休眠态。
等设备可用的时候就会从休眠态唤醒,然后从设备中读取数据返回给应用程序。
从图 52.1.1.2 可以看出,应用程序使用非阻塞访问方式从设备读取数据,
当设备不可用或数据未准备好的时候会立即向内核返回一个错误码,表示数据读取失败。
应用程序会再次重新读取数据,这样一直往复循环,直到数据读取成功。
应用程序可以使用如下所示示例代码来实现阻塞访问:
示例代码 52.1.1.1 应用程序阻塞读取数据
1 int fd;
2 int data = 0;
3
4fd = open("/dev/xxx_dev", O_RDWR); /* 阻塞方式打开 */
5 ret = read(fd, &data, sizeof(data)); /* 读取数据 */
从示例代码 52.1.1.1 可以看出,对于设备驱动文件的默认读取方式就是阻塞式的,所以我
们前面所有的例程测试 APP 都是采用阻塞 IO。
如果应用程序要采用非阻塞的方式来访问驱动设备文件,可以使用如下所示代码:
示例代码 52.1.1.2 应用程序非阻塞读取数据
1 int fd;
2 int data = 0;
3
4fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); /* 非阻塞方式打开 */
5 ret = read(fd, &data, sizeof(data)); /* 读取数据 */
第 4 行使用 open 函数打开“/dev/xxx_dev”
设备文件的时候添加了参数“O_NONBLOCK”
,
表示以非阻塞方式打开设备,这样从设备中读取数据的时候就是非阻塞方式的了。
52.1.2 等待队列
1、等待队列头
阻塞访问最大的好处就是当设备文件不可操作的时候进程可以进入休眠态,这样可以将
CPU
资源让出来。
但是,当设备文件可以操作的时候就必须唤醒进程,一般在中断函数里面完成唤醒工作。
Linux
内核提供了等待队列(wait queue)
来实现阻塞进程的唤醒工作,
如果我们要在驱动中使用等待队列,必须创建并初始化一个等待队列头,
等待队列头使用结构体wait_queue_head_t
表示,wait_queue_head_t
结构体定义在文件include/linux/wait.h
中,结构体内容如下所示:
示例代码 52.1.2.1 wait_queue_head_t 结构体
39 struct __wait_queue_head {
40 spinlock_t lock;
41 struct list_head task_list;
42 };
43 typedef struct __wait_queue_head wait_queue_head_t;
定义好等待队列头以后需要初始化, 使用init_waitqueue_head
函数初始化等待队列头,函
数原型如下:
void init_waitqueue_head(wait_queue_head_t *q)
参数 q
就是要初始化的等待队列头。
也可以使用宏DECLARE_WAIT_QUEUE_HEAD
来一次性完成等待队列头的定义的初始
化。
2、等待队列项
等待队列头就是一个等待队列的头部,每个访问设备的进程都是一个队列项,
当设备不可用的时候就要将这些进程对应的等待队列项添加到等待队列里面。
结构体wait_queue_t
表示等待队列项,结构体内容如下:
示例代码 52.1.2.2 wait_queue_t 结构体
struct __wait_queue {
unsigned int flags;
void *private;
wait_queue_func_t func;
struct list_head task_list;
};
typedef struct __wait_queue wait_queue_t;
使用宏 DECLARE_WAITQUEUE
定义并初始化一个等待队列项,宏的内容如下:
DECLARE_WAITQUEUE(name, tsk)
name
就是等待队列项的名字,tsk
表示这个等待队列项属于哪个任务(进程),一般设置为
current
, 在 Linux
内 核 中current
相 当 于 一 个 全 局 变 量 , 表 示 当 前 进 程 。
因 此 宏DECLARE_WAITQUEUE
就是给当前正在运行的进程创建并初始化了一个等待队列项。
3、将队列项添加/移除等待队列头
当设备不可访问的时候就需要将进程对应的等待队列项添加到前面创建的等待队列头中,
只有添加到等待队列头中以后进程才能进入休眠态。
当设备可以访问以后再将进程对应的等待队列项从等待队列头中移除即可,等待队列项添加 API
函数如下:
void add_wait_queue(wait_queue_head_t *q,
wait_queue_t *wait)
函数参数和返回值含义如下:
q: 等待队列项要加入的等待队列头。
wait:要加入的等待队列项。
返回值:无。
等待队列项移除 API
函数如下:
void remove_wait_queue(wait_queue_head_t *q,
wait_queue_t *wait)
函数参数和返回值含义如下:
q: 要删除的等待队列项所处的等待队列头。
wait:要删除的等待队列项
返回值:无
4、等待唤醒
当设备可以使用的时候就要唤醒进入休眠态的进程,唤醒可以使用如下两个函数:
void wake_up(wait_queue_head_t *q)
void wake_up_interruptible(wait_queue_head_t *q)
参数 q 就是要唤醒的等待队列头,这两个函数会将这个等待队列头中的所有进程都唤醒。
wake_up
函数可以唤醒处于TASK_INTERRUPTIBLE
和 TASK_UNINTERRUPTIBLE
状态的进
程,
而 wake_up_interruptible
函数只能唤醒处于 TASK_INTERRUPTIBLE
状态的进程