参考博客:博客
0.前言
阻塞与非阻塞 I/0 是设备访问的俩种不同模式,驱动程序可以灵活地支持这 2 种用户对设备的访问。
其中阻塞方式就是我们说的休眠和唤醒。
而非阻塞方式就是我们说的轮询
1.阻塞与非阻塞
参考:Linux设备驱动开发详解第 8 章
阻塞
阻塞操作是指在执行设备操作时,托不能获得资源,则挂起进程直到满足操作所需的条件后再进行操作。被挂起的进程进入休眠状态(不占用cpu资源),从调度器的运行队列转移到等待队列,直到条件满足。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Iw4czSpD-1611297346687)(C:\Users\123\Pictures\Markdown\image-20201228213409948.png)]
非阻塞
非阻塞操作是指在进行设备操作是,若操作条件不满足并不会挂起,而是直接返回或重新查询(一直占用CPU资源)直到操作条件满足为止。
当用户空间的应用程序调用read(),write()等方法时,若设备的资源不能被获取,而用户又希望以阻塞的方式来访问设备,驱动程序应当在设备驱动层的对应 read(),write()操作中,将该进程阻塞直到资源可以获取为止;若用户是以非阻塞方式获取资源,当资源不能获取时设备驱动的read()、write()应当立即返回,用户空间的read()、write()也相应的立即返回。
阻塞从字面上听起来似乎意味着效率低,其实不是这样。如果以非阻塞方式,用户想获取某一资源只能不停地查询,这样会占用CPU大量资源。而阻塞访问,若不能获取资源就会进入休眠从而节省CPU资源给其他进程使用。
很显然阻塞的进程会进入到休眠状态,因此必须保证有一个地方能够唤醒休眠的进程。唤醒进程的地方一般都在中断里面,意味硬件资源的获得同时往往伴随着一个中断
在设备驱动中,阻塞的实现通常是通过等待队列。
2.阻塞IO实现(等待队列)
在Linux驱动程序中,可以使用等待队列(wait queue)来实现阻塞进程的唤醒。wait queue在很早就作为一个基本的功能出现在Linux内核里了,它是一种以队列为基础的数据结构,与进程调度机制紧密结合,能够用于实现内核中的异步事件通知机制。等待队列可以用来同步对系统资源的访问,信号量在内核中也依赖等待队列来实现。
Linux内核的等待队列是以双链表为基础的数据结构。
涉及到两个比较重要的数据结构:__wait_queue_head,该结构描述了等待队列的链头,其包含一个链表和一个原子锁,结构定义如下:
struct __wait_queue_head {
spinlock_t lock; /* 保护等待队列资源的一个自旋锁 */
struct list_head task_list; /* 等待队列 */
};
typedef struct __wait_queue_head wait_queue_head_t;
__wait_queue,该结构是对一个等待任务的抽象(等待队列项)。每个等待任务都会抽象成一个wait_queue,并且挂载到wait_queue_head上。该结构定义如下:
struct __wait_queue
{
unsigned int flags;
void *private; /* 通常指向当前任务控制块 */
/* 任务唤醒操作方法,该方法在内核中提供,通常为autoremove_wake_function */
wait_queue_func_t func;
struct list_head task_list; /* 挂入wait_queue_head的挂载点 */
};
Linux中等待队列的实现思想如下图所示,当一个任务需要在某个wait_queue_head上睡眠时,将自己的进程控制块信息封装到wait_queue中,然后挂载到wait_queue的链表中,执行调度睡眠。当某些事件发生后,另一个任务(进程)会唤醒wait_queue_head上的某个或者所有任务,唤醒工作也就是将等待队列中的任务设置为可调度的状态,并且从队列中删除。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kJJOYYnI-1611297346691)(C:\Users\123\Pictures\Markdown\image-20201228220737269.png)]
使用等待队列时首先需要定义一个wait_queue_head,这可以通过DECLARE_WAIT_QUEUE_HEAD宏来完成,这是静态定义的方法。该宏会定义一个wait_queue_head,并且初始化结构中的锁以及等待队列。当然,动态初始化的方法也很简单,初始化一下锁及队列就可以了。
一个任务需要等待某一事件的发生时,通常调用wait_event,该函数会定义一个wait_queue,描述等待任务,并且用当前的进程描述块初始化wait_queue,然后将wait_queue加入到wait_queue_head中。
函数实现流程说明如下:
a – 用当前的进程描述块(PCB)初始化一个wait_queue描述的等待任务。
b – 在等待队列锁资源的保护下,将等待任务加入等待队列。
c – 判断等待条件是否满足,如果满足,那么将等待任务从队列中移出,退出函数。
d – 如果条件不满足,那么任务调度,将CPU资源交与其它任务。
e – 当睡眠任务被唤醒之后,需要重复b、c 步骤,如果确认条件满足,退出等待事件函数。
在Linux设备驱动中等待队列实现阻塞一般方式是:
- 定义一个等待队列头 wait_queue_head_t wq_h;
- 初始化等待队列头
- 当有操作要以阻塞方式访问资源时,调用 wait_event()加入到等待队列中即可
- 在条件满足时调用wake_up()唤醒等待队列
3.等待队列的接口函数
1、定义和初始化“等待队列头”
wait_queue_head_t my_queue; /* 定义等待队列头 */
init_waitqueue_head(&my_queue); /* 初始化等待队列头 */
也可以使用 Linux 内核中的宏来同时完成 “等待队列头”的定义和初始化
DECLARE_WAIT_QUEUE_HEAD(my_queue);
2、定义等待队列:
DECLARE_WAITQUEUE(name,tsk); /* 定义并初始化一个名为name的等待队列。*/
name 是等待队列的名字,tsk表示这个等待队列属于哪个进程(app),一般设为current,在linux内核中 current相当于一个全局变量,表示当前进程,因此该宏的作用是给当前进程在运行的进程创建并初始化一个等待队列项。
3、(从等待队列头中)添加/移出等待队列:
/* add_wait_queue()函数,设置等待的进程为非互斥进程,并将其添加进等待队列头(q)的队头中*/
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
/* 该函数也和add_wait_queue()函数功能基本一样,只不过它是将等待的进程(wait)设置为互斥进程。*/
void add_wait_queue_exclusive(wait_queue_head_t *q, wait_queue_t *wait);
4、等待事件:
(1)wait_event(queue, condition)宏:
/**
* wait_event - sleep until a condition gets true
* @wq: the waitqueue to wait on
* @condition: a C expression for the event to wait for
*
* The process is put to sleep (TASK_UNINTERRUPTIBLE) until the
* @condition evaluates to true. The @condition is checked each time
* the waitqueue @wq is woken up.
*
* wake_up() has to be called after changing any variable that could
* change the result of the wait condition.
*/
#define wait_event(wq, condition) \
do { \
if (condition) \
break; \
__wait_event(wq, condition); \
} while (0)
在等待会列中睡眠直到condition为真。在等待的期间,进程会被置为TASK_UNINTERRUPTIBLE进入睡眠,直到condition变量变为真。每次进程被唤醒的时候都会检查condition的值.
(2)wait_event_interruptible(queue, condition)函数:
和wait_event()的区别是调用该宏在等待的过程中当前进程会被设置为TASK_INTERRUPTIBLE状态.在每次被唤醒的时候,首先检查condition是否为真,如果为真则返回,否则检查如果进程是被信号唤醒,会返回-ERESTARTSYS错误码.如果是condition为真,则返回0.
(3)wait_event_timeout(queue, condition, timeout)宏:
也与wait_event()类似.不过如果所给的睡眠时间为负数则立即返回.如果在睡眠期间被唤醒,且condition为真则返回剩余的睡眠时间,否则继续睡眠直到到达或超过给定的睡眠时间,然后返回0
(4)wait_event_interruptible_timeout(wq, condition, timeout)宏:
与wait_event_timeout()类似,不过如果在睡眠期间被信号打断则返回ERESTARTSYS错误码.
(5) wait_event_interruptible_exclusive(wq, condition)宏
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-22YwgkbF-1611297346693)(C:\Users\123\Pictures\Markdown\image-20201228223813059.png)]
比较重要的参数就是:
① wq: waitqueue,等待队列
休眠时除了把程序状态改为非 RUNNING 之外,还要把进程/进程放入 wq 中,以后中断服务程序
要从 wq 中把它取出来唤醒。
没有 wq 的话,茫茫人海中,中断服务程序去哪里找到你?
② condition
这可以是一个变量,也可以是任何表达式。表示“一直等待,直到 condition
5、唤醒队列
(1)wake_up(x)函数
#define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL)
/**
* __wake_up - wake up threads blocked on a waitqueue.
* @q: the waitqueue
* @mode: which threads
* @nr_exclusive: how many wake-one or wake-many threads to wake up
* @key: is directly passed to the wakeup function
*/
void __wake_up(wait_queue_head_t *q, unsigned int mode,
int nr_exclusive, void *key)
{
unsigned long flags;
spin_lock_irqsave(&q->lock, flags);
__wake_up_common(q, mode, nr_exclusive, 0, key);
spin_unlock_irqrestore(&q->lock, flags);
}
EXPORT_SYMBOL(__wake_up);
4.驱动框架
驱动框架如下:/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WLhfQQBR-1611297346696)(C:\Users\123\Pictures\Markdown\image-20201228231712674.png)]
要休眠的线程,放在 wq 队列里,中断处理函数从 wq 队列里把它取出来唤醒。
所以,我们要做这几件事:
-
初始化 wq 队列
-
在驱动的 read 函数中,调用 wait_event_interruptible:
它本身会判断 event 是否为 FALSE,如果为 FASLE 表示无数据,则休眠。
当从 wait_event_interruptible 返回后,把数据复制回用户空间。 -
在中断服务程序里:
设置 event 为 TRUE,并调用 wake_up_interruptible 唤醒线程。 -
初始化 wq 队列
-
在驱动的 read 函数中,调用 wait_event_interruptible:
它本身会判断 event 是否为 FALSE,如果为 FASLE 表示无数据,则休眠。
当从 wait_event_interruptible 返回后,把数据复制回用户空间。 -
在中断服务程序里:
设置 event 为 TRUE,并调用 wake_up_interruptible 唤醒线程。