等待队列是内核中实现进程调度的一个十分重要的数据结构,其任务是维护一个链表,链表中每一个节点都是一个PCB(进程控制块),内核会将PCB挂在等待队列中的所有进程都调度为睡眠状态,直到某个唤醒的条件发生。应用层的阻塞IO与非阻塞IO的使用我已经在Linux I/O多路复用一文中讨论过了,本文主要讨论驱动中怎么实现对设备IO的阻塞与非阻塞读写。显然,实现这种与阻塞相关的机制要用到等待队列机制。本文的内核源码使用的是3.14.0版本
设备阻塞IO的实现
当我们读写设备文件的IO时,最终会回调驱动中相应的接口,而这些接口也会出现在读写设备进程的进程(内核)空间中,如果条件不满足,接口函数使进程进入睡眠状态,即使读写设备的用户进程进入了睡眠,也就是我们常说的发生了阻塞。In a word,读写设备文件阻塞的本质是驱动在驱动中实现对设备文件的阻塞,其读写的流程可概括如下:
1. 定义-初始化等待队列头
//定义等待队列头
wait_queue_head_t waitq_h;//初始化,等待队列头
init_waitqueue_head(wait_queue_head_t *q); //或//定义并初始化等待队列头
DECLARE_WAIT_QUEUE_HEAD(waitq_name);
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
上面的几条选择中,***一种会直接定义并初始化一个等待头,但是如果在模块内使用全局变量传参,用着并不方便,具体用哪种看需求。
我们可以追一下源码,看一下上面这几行都干了什么:
//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.
- 2.
- 3.
- 4.
- 5.
- 6.
wait_queue_head_t
--36-->这个队列用的自旋锁
--27-->将整个队列"串"在一起的纽带
然后我们看一下初始化的宏:
#define __WAIT_QUEUE_HEAD_INITIALIZER(name) {
.lock = __SPIN_LOCK_UNLOCKED(name.lock),
.task_list = { &(name).task_list, &(name).task_list } }
define DECLARE_WAIT_QUEUE_HEAD(name) \
wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
DECLARE_WAIT_QUEUE_HEAD()
--60-->根据传入的字符串name,创建一个名为name的等待队列头
--57-->初始化上述task_list域,竟然没有用内核标准的初始化宏,无语。。。
2. 将本进程添加到等待队列
为等待队列添加事件,即进程进入睡眠状态直到condition为真才返回。**_interruptible的版本版本表示睡眠可中断,_timeout**版本表示超时版本,超时就会返回,这种命名规范在内核API中随处可见。
void wait_event(wait_queue_head_t *waitq_h,int condition);
void wait_event_interruptible(wait_queue_head_t *waitq_h,int condition);
void wait_event_timeout(wait_queue_head_t *waitq_h,int condition);
void wait_event_interruptible_timeout(wait_queue_head_t *waitq_h,int condition);
- 1.
- 2.
- 3.
- 4.
这可是等待队列的核心,我们来看一下
wait_event
└── wait_event
└── _wait_event
├── abort_exclusive_wait
├── finish_wait
├── prepare_to_wait_event
└── ___wait_is_interruptible
#define wait_event(wq, condition)
do {
if (condition)
break;
__wait_event(wq, condition);
} while (0)
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
wait_event
--246-->如果condition为真,立即返回
--248-->否则调用__wait_event
#define ___wait_event(wq,condition,state, exclusive, ret, cmd) \
({ \
for (;;) { \
long __int = prepare_to_wait_event(&wq, &__wait, state);\
\
if (condition) \
break; \
if (___wait_is_interruptible(state) && __int) { \
__ret = __int; \
if (exclusive) { \
abort_exclusive_wait(&wq, &__wait, \
state, NULL); \
goto __out; \
} \
break; \
} \
cmd; \
} \
finish_wait(&wq, &__wait); \
__out: __ret; \
})
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
--206-->死循环的轮询
--209-->如果条件为真,跳出循环,执行finish_wait();进程被唤醒
--212-->如果进程睡眠的方式是interruptible的,那么当中断来的时候也会abort_exclusive_wait被唤醒
--222-->如果上面两条都不满足,就会回调传入的schedule(),即继续睡眠
模板
struct wait_queue_head_t xj_waitq_h;
static ssize_t demo_read(struct file *filp, char __user *buf, size_t size, loff_t *offset)
{ if(!condition) //条件可以在中断处理函数中置位
wait_event_interruptible(&xj_waitq_h,condition);
}
static file_operations fops = {
.read = demo_read,
};
static __init demo_init(void){
init_waitqueue_head(&xj_waitq_h);
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
IO多路复用的实现
对于普通的非阻塞IO,我们只需要在驱动中注册的read/write接口时不使用阻塞机制即可,这里我要讨论的是IO多路复用,即当驱动中的read/write并没有实现阻塞机制的时候,我们如何利用内核机制来在驱动中实现对IO多路复用的支持。下面这个就是我们要用的API
int poll(struct file *filep, poll_table *wait);void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
- 1.
当应用层调用select/poll/epoll机制的时候,内核其实会遍历回调相关文件的驱动中的poll接口,通过每一个驱动的poll接口的返回值,来判断该文件IO是否有相应的事件发生,我们知道,这三种IO多路复用的机制的核心区别在于内核中管理监视文件的方式,分别是位,数组,链表,但对于每一个驱动,回调的接口都是poll。
模板
struct wait_queue_head_t waitq_h;static unsigned int demo_poll(struct file *filp, struct poll_table_struct *pts){ unsigned int mask = 0;
poll_wait(filp, &wwaitq_h, pts); if(counter){
mask = (POLLIN | POLLRDNORM);
} return mask;
}static struct file_operations fops = {
.owner = THIS_MODULE,
.poll = demo_poll,
};static __init demo_init(void){
init_waitqueue_head(&xj_waitq_h);
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
其他API
刚才我们讨论了如何使用等待队列实现阻塞IO,非阻塞IO,其实关于等待队列,内核还提供了很多其他API用以完成相关的操作,这里我们来认识一下
//在等待队列上睡眠
sleep_on(wait_queue_head_t *wqueue_h);
sleep_on_interruptible(wait_queue_head_t *wqueue_h);//唤醒等待的进程
void wake_up(wait_queue_t *wqueue);
void wake_up_interruptible(wait_queue_t *wqueue);
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.