阻塞和非阻塞 IO 是 Linux 驱动开发里面很常见的两种设备访问模式, 在编写驱动的时候一定要考虑到阻塞和非阻塞。
阻塞与非阻塞简介
阻塞操作是指在执行设备操作时, 若不能获得资源, 则挂起进程直到满足可操作的条件后再进行操作。被挂起的进程进入睡眠状态, 被从调度器的运行队列移走, 直到等待的条件被满足。 而非阻塞操作的进程在不能进行设备操作时, 并不挂起, 它要么放弃, 要么不停地查询, 直至可以进行操作为止。
在阻塞访问时, 不能获取资源的进程将进入休眠, 它将 CPU 资源“礼让” 给其他进程。 因为阻塞的进程会进入休眠状态, 所以必须确保有一个地方能够唤醒休眠的进程, 否则, 进程就真的“寿终正寝” 了。唤醒进程的地方最大可能发生在中断里面, 因为在硬件资源获得的同时往往伴随着一个中断。 而非阻塞的进程则不断尝试, 直到可以进行 I/O。 阻塞访问如图所示:
若用户以非阻塞的方式访问设备文件, 则当设备资源不可获取时, 设备驱动的 xxx_read() 、 xxx_write( ) 等操作应立即返回, read( ) 、 write( ) 等系统调用也随即被返回, 应用程序收到-EAGAIN 返回值。
应用程序可以使用如下所示示例代码来实现阻塞访问:
int fd;
int data = 0;
fd = open("/dev/xxx_dev", O_RDWR); /* 阻塞方式打开 */
ret = read(fd, &data, sizeof(data)); /* 读取数据 */
可以看出对于设备驱动文件的默认读取方式就是阻塞式的, 所以我们前面所有的例程测试 APP 都是采用阻塞 IO。
如果应用程序要采用非阻塞的方式来访问驱动设备文件, 可以使用如下所示代码:
int fd;
int data = 0;
fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); /* 非阻塞方式打开 */
ret = read(fd, &data, sizeof(data)); /* 读取数据 */
使用 open 函数打开“ /dev/xxx_dev” 设备文件的时候添加了参数“ O_NONBLOCK” , 表示以非阻塞方式打开设备, 这样从设备中读取数据的时候就是非阻塞方式的了。
等待队列
当我们进程去访问设备的时候, 经常需要等待有特定事件发生以后再继续往下运行, 这个时候就需要在驱动里面实现当条件不满足的时候进行休眠, 当条件满足的时候在由内核唤醒进程。 在 Linux 驱动程序中, 可以使用等待队列( Wait Queue) 来实现阻塞进程的唤醒。 等待队列很早就作为一个基本的功能单位出现在 Linux 内核里了, 它以队列为基础数据结构, 与进程调度机制紧密结合, 可以用来同步对系统资源的访问。 队列是一种特殊的线性表, 特殊之处在于它只允许在表的前端( front) 进行删除操作, 而在表的后端( rear) 进行插入操作, 和栈一样, 队列是一种操作受限制的线性表。 进行插入操作的端称为队尾, 进行删除操作的端称为队头。 即满足先进先出的形式 FIFO。
举个例子, 比如说我现在去食堂打饭, 阿姨和我说现在没有饭, 你需要等一会, 等我做好了我再叫你,那么我当前不能获得资源, 我被阻塞在这儿了, 那么等待队列就是让我们阻塞在这儿, 然后等特定的事件发生以后, 再继续运行。 那么等待队列阻塞在这儿的这件事情就相当于阿姨和我们说现在没有饭, 你需要等一会。 为什么我们要先讲完中断以后再讲等待队列呢? 举个例子来说, 比如说阿姨和你说现在没饭, 你需要在旁边等一会, 等我做好了我再叫你, 如果说阿姨做完了不叫你, 你又睡着了, 那么你今天是不是吃不上饭了, 所以说在我们阻塞访问的时候不能获得资源的进程, 将进入休眠状态, 他将 cpu 的资源全部让给别的进程, 必须保证有一个地方可以唤醒休眠进程, 否则的话将会长睡不醒。 进程唤醒最大可能的地方发生在中断里面, 伴随着一个中断的发生我们可以唤醒该进程, 对应的事件是阿姨说饭好了, 小王你过来打吧。 所以说, 我们学习等待队列在中断之后, 这样用等待队列可以极大的降低 cpu 的占用率。
Linux 内核的等待队列是以双循环链表为基础数据结构, 与进程调度机制紧密结合, 能够用于实现核心的异步事件通知机制。 它有两种数据结构: 等待队列头(wait_queue_head_t) 和等待队列项(wait_queue_t)。等待队列头和等待队列项中都包含一个 list_head 类型的域作为”连接件”。 它通过一个双链表和把等待 task的头, 和等待的进程列表链接起来。
等待队列头
等待队列头就是一个等待队列的头部, 每个访问设备的进程都是一个队列项, 当设备不可用的时候就要将这些进程对应的等待队列项添加到等待队列里面。
等待队列头使用结构体 wait_queue_head_t 来表示, 这个结构体定义在文件 include/linux/wait 里面, 结构体内容如下:
struct __wait_queue_head {
spinlock_t lock; //自旋锁
struct list_head task_list; //链表头
};
typedef struct __wait_queue_head wait_queue_head_t;
类型名是 wait_queue_head_t, 只需要记住这个即可。
定义一个等待队列头:
wait_queue_head_t test_wq; //定义一个等待队列的头
定义等待队列头以后需要初始化, 可以使用 init_waitqueue_head 函数初始化等待队列头, 函数原型如下:
函数 | void init_waitqueue_head(wait_queue_head_t *q) |
q | wait_queue_head_t 指针 |
功能 | 动态初始化等待队列头结构 |
也可以使用宏 DECLARE_WAIT_QUEUE_HEAD 来一次性完成等待队列头的定义和初始化。
DECLARE_WAIT_QUEUE_HEAD (wait_queue_head_t *q);
等待队列项
等待队列头就是一个等待队列的头部, 每个访问设备的进程都是一个队列项, 当设备不可用的时候就要将这些进程对应的等待队列项添加到等待队列里面。 结构体 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就是给当前正在运行的进程创建并初始化了一个等待队列项。
添加/删除队列
当设备不可访问的时候就需要将进程对应的等待队列项添加到前面创建的等待队列头中, 只有添加到等待队列头中以后进程才能进入休眠态。 当设备可以访问以后再将进程对应的等待队列项从等待队列头中移除即可, 等待队列项添加队列函数如下所示:
函数 | void add_wait_queue(wait_queue_head_t *q,wait_queue_t *wait) |
q | 等待队列项要加入的等待队列头。 |
wait | 要加入的等待队列项 |
返回值 | 无 |
功能 | 从等待队列头中添加队列 |
等待队列项移除队列函数如下:
函数 | void remove_wait_queue(wait_queue_head_t *q,wait_queue_t *wait) |
q | 要删除的等待队列项所处的等待队列头 |
wait | 要删除的等待队列项 |
返回值 | 无 |
等待唤醒
当设备可以使用的时候就要唤醒进入休眠态的进程, 唤醒可以使用如下两个函数。
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 状态的进程。
等待事件
除了主动唤醒以外, 也可以设置等待队列等待某个事件, 当这个事件满足以后就自动唤醒等待队列中的进程, 相关函数:
#define wait_event(wq, condition)
do {
if (condition)
break;
__wait_event(wq, condition);
} while (0)
wait_event(queue,condition);等待以 queue 为等待队列头等待队列被唤醒,condition 必须满足,否则阻塞
wait_event_interruptible(queue,condition);可被信号打断
wait_event_timeout(queue,condition,timeout);阻塞等待的超时时间, 时间到了, 不论 condition 是否满足,都要返回
wait_event_interruptible_timeout(queue,condition,timeout)
wait_event()宏
功能: 不可中断的阻塞等待, 让调用进程进入不可中断的睡眠状态, 在等待队列里面睡眠直到 condition 变成真, 被内核唤醒。
wait_event_interruptible() 函数
功能: 可中断的阻塞等待, 让调用进程进入可中断的睡眠状态, 直到 condition 变成真被内核唤醒或被信号打断唤醒。
wait_event_timeout() 宏:
也与 wait_event()类似.不过如果所给的睡眠时间为负数则立即返回.如果在睡眠期间被唤醒,且
condition 为真则返回剩余的睡眠时间,否则继续睡眠直到到达或超过给定的睡眠时间,然后返回 0。
wait_event_interruptible_timeout() 宏:
与 wait_event_timeout()类似,不过如果在睡眠期间被信号打断则返回 ERESTARTSYS 错误码。
wait_event_interruptible_exclusive() 宏:
同样和 wait_event_interruptible()一样,不过该睡眠的进程是一个互斥进程
注意: 调用的时要确认 condition 值是真还是假, 如果调用 condition 为真, 则不会休眠。