waitqueue
首先结构体的名字中确实带了queue,但是本质上是一个双向链表。
struct wait_queue_head { //等待队列头
struct list_head head;
};
struct wait_queue_entry { //等待队列实体,描述一个个任务
struct list_head entry;
};
等待队列是成对使用,比如可中断的wq和不可中断的wq
wait_event/wake_up
wait_event_interruptible/wake_up_interruptible
加入队列用不可中断API,唤醒用可中断API,行吗?不行
废话到此结束,下面讲下硬核知识点。首先思考几个问题:
- 为什么要成对使用,也就是两个API同步是可中断或者不可中断
- 可中断和不可中断的区别,代码是如何体现的
- 如何体现将当前task挂到wq的
- 一次性唤醒几个task
wait_event/wake_up
wait_event的本质是schedule,wake_up的本质是 try_to_wake_up
wait_event(_interruptible)
不加括号的任务state为TASK_UNINTERRUPTIBLE,含括号的任务state为TASK_INTERRUPTIBLE。
实现上呢是一个循环,使用condition来退出,而condition需要手动设置。下面函数拆解
使用该函数会自动建立一个等待任务,体现在init_wait_entry
wait_event
init_wait_entry:
wq_entry->flags = flags; //记录该任务是否是互斥的
wq_entry->private = current; //调用wait_event的对象
wq_entry->func = autoremove_wake_function; //唤醒该等待者的方法
1 标记是否是互斥,2 一个个等待任务是以current标识的。
建立完对象后将其挂入目标等待队列头的wq中,体现在prepare_to_wait_event函数中有一个链表add操作,该子函数在for(;;)循环中,第一次循环如果是exclusive,尾插,如果是非exclusive,头插,然后设置state。后面每进一次循环只做设置state的动作。
prepare_to_wait_event:
if (list_empty(&wq_entry->entry)) {//该等待队列未加入队列头
if (wq_entry->flags & WQ_FLAG_EXCLUSIVE)
__add_wait_queue_entry_tail(wq_head, wq_entry);
else
__add_wait_queue(wq_head, wq_entry);
}
set_current_state(state); //可中断或不可中断
然后理论上是个无止境的循环。
___wait_event:
for (;;) {
long __int = prepare_to_wait_event(&wq_head, &__wq_entry, state); //返回值记录了是否有信号干预它
if (condition) 条件满足退出循环
break;
//如果设置了interruptible,并且有信号打断,直接无视condition,退出等待
if (___wait_is_interruptible(state) && __int) {
__ret = __int;
goto __out;
}
cmd;
}
finish_wait(&wq_head, &__wq_entry);
__out: __ret;
设置interruptible的好处就是,多了一个唤醒的方法,如果条件一直不成立至少还能运行,比如杀死这个任务,kill信号也可以唤醒它。
如果条件不满足,且没有中断信号,那就一直执行cmd,默认是schedule(),当然可以使用wait_event_cmd,任意指定命令。
条件满足退出循环执行finish_wait,将任务从TASK_INTERRUPTIBLE或TASK_UNINTERRUPT设置为TASK_RUNNING。同时从当前等待队列退出。
finish_wait:
__set_current_state(TASK_RUNNING);
if (!list_empty_careful(&wq_entry->entry))
list_del_init(&wq_entry->entry);
使用wait_event_interruptible可以根据返回值来判断是条件唤醒还是信号唤醒。
wake_up(_interruptible)
__wake_up_common:
curr = list_first_entry(&wq_head->head, wait_queue_entry_t, entry);
if(&curr->entry == &wq_head->head) //链表为空
return nr_exclusive;
list_for_each_entry_safe_from(curr, next, &wq_head->head, entry) {
unsigned flags = curr->flags;
ret = curr->func(curr, mode, wake_flags, key);
if (ret < 0) //唤醒失败退出
break;
//退出条件,唤醒成功+exclusive属性+唤醒了足够的exclusive属性对象
if (ret && (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
}
在wait_event函数中会将任务加入某个等待队列头的等待队列中,exclusive属性使用尾插,非exclusive属性使用头插。wake_up中对非空链表进行从头开始的遍历,对每次遍历到的对象执行唤醒操作,所以优先唤醒的是非exclusive的对象。
执行了wake_up(_interruptible)会将整个等待队列头里的非exclusive对象都会唤醒,而唤醒exclusive对象的个数可由nr_exclusive决定。
唤醒函数:autoremove_wake_function
int autoremove_wake_function(struct wait_queue_entry *wq_entry, unsigned mode, int sync, void *key)
{
//成功唤醒返回1,失败返回0
int ret = default_wake_function(wq_entry, mode, sync, key);
if (ret)
list_del_init(&wq_entry->entry);
return ret;
}
default_wake_function会调用try_to_wakeup,将这个任务加入就绪队列等待调度。
if (!(p->state & state))
goto unlock;
try_to_wakeup中注意看如上这个判断,决定了这对API必须要么同时是可中断类型,要么是不可中断类型,否则无法执行唤醒操作。
唤醒成功后会将这个对象从等待队列中删除
exclusive等待队列
前面分析过,对于wait_event(_interruptible),执行了wake_up(_interruptible)会将整个等待队列头里的对象都会唤醒。如果只想要唤醒一个怎么整,很简单新建一个队列头,把新任务挂上去,如果有很多个呢那就创建更多的队列头,这不失为一种方法。但还有一种办法,设置任务之间是互斥的。
在__wake_up_common中有这样一个判断:
if (ret && (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
如果设置了互斥标志,互斥个数为1,则跳出队列遍历继而退出剩余成员的唤醒。
可以使用下面的API来设置等待任务为exclusive
wait_event_interruptible_exclusive(wq, condition)
init_wait_entry(&__wq_entry, exclusive ? WQ_FLAG_EXCLUSIVE : 0);
在init_wait_entry中设置成员的标志为WQ_FLAG_EXCLUSIVE
wake_up函数默认互斥数nr_exclusive为1,使用wake_up(_interruptible)即可一次只唤醒一个互斥任务
#define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL)
如果唤醒一个不够,还有其他变种,wake_up_nr可以唤醒nr-1个互斥任务,wake_up_all可以全部唤醒。注意这两个唤醒函数必须和exclusive类型的wait_event使用。
#define wake_up_nr(x, nr) __wake_up(x, TASK_NORMAL, nr, NULL)
#define wake_up_all(x) __wake_up(x, TASK_NORMAL, 0, NULL)
建议用法
假设只有一个等待队列头
- 如有两个任务挂在同一个头上,逐个唤醒
DECLARE_WAIT_QUEUE_HEAD(wq_head);
condition = 0;
A: wait_event_interruptible_exclusive(wq_head, condition);
B: wait_event_interruptible_exclusive(wq_head, condition);
C: wake_up_interruptible(&wq_head);
condition = 1;
wake_up_interruptible(&wq_head);
condition = 1;
- 如有两个任务挂在同一个头上,同时唤醒
DECLARE_WAIT_QUEUE_HEAD(wq_head);
condition = 0;
A: wait_event_interruptible(wq_head, condition);
B: wait_event_interruptible(wq_head, condition);
C: wake_up_interruptible(&wq_head);
condition = 1;
上述都可通过信号来额外唤醒。
那么如果认真看到至此,并捋一遍代码,开篇提到的问题,全部迎刃而解了。