等待队列
等待队列是Linux系统中重要的一个机制,从等待锁的释放、等待事件、complete机制到通知队列等都隐含着等待队列的身影。
等待队列思想
等待队列其时就是一个链表加一个回调函数,其并不包含等待队列等待的条件。
等待队列的思路大体如下:
- 为等待条件生成一个等待链表;
- 当一个任务等待该等待条件时:
- 为等待该任务生成一个等待节点;
- 将该节点挂入到这个等待链表中;
- 将该任务的运行状态设置为睡眠态,并将自己调度出去;
- 当等待条件满足时:
- 通过等待链表遍历其上的所有节点;
- 调用该节点上的回调函数来唤醒该节点上的任务并从等待链表中将成功唤醒的节点删除;
等待队列只是负责建立一块区域保存睡眠的任务和要唤醒时执行的回调函数;至于等待原因/条件,是在建立的等待队列的原因——即一个等待队列对应于一个等待原因/等待条件;不同等待原因/等待条件的等待队列之间没有关联,等待队列也不存在统一的根挂载点。
因此,系统中并不对等待队列进行统一的管理和操作,而是由等待的源头——产生等待原因/等待条件的生产者去做管理和调用回调函数进行处理。
等待队列上的节点顺序
在一个任务被挂载到等待队列上时,其在队列上的排列顺序是:
- 有等待队列优先级标志位的任务排在没有等待队列优先级标志位的任务;
- 同等级的任务按照头插的方式插入到等待队列中,即后插入的任务排在先插入任务的前面;
- 独占任务位于队列的末尾;
问题
惊群现象
惊群现象是指在多进程或者多线程场景下,多个进程或者多个线程在同一条件下睡眠;当唤醒条件发生的时候,会同时唤醒这些睡眠的进程或者线程;但是只有一个是可以成功执行的,而其他的进程或者线程被唤醒后存在着执行开销的浪费。
惊群的触发条件
- 多个线程在获取同一把锁的时;
- 多个线程同时进行 accept 时;
- 多个线程在同一个 epoll 等 I/O 复用的机制上获取事件时;
惊群带来的性能问题
- 会造成无效的调度;
- 可能会造成地址空间的无效切换;
- 可能会造成 **Cache ** 和 TLB 的频繁刷新;
解决方案
Linux 采用 WQ_FLAG_EXCLUSIVE 标志位来进行特殊处理。#
在核心唤醒函数
static int __wake_up_common(struct wait_queue_head *wq_head, unsigned int mode,
int nr_exclusive, int wake_flags, void *key,
wait_queue_entry_t *bookmark)
中,系统遍历工作队列,会寻找等待队列的节点 flags 中含有 WQ_FLAG_EXCLUSIVE 独占标志的任务。如果没有设置独占标志,则根据唤醒要求唤醒每个睡眠的进程。参数 nr_exclusiv 表示需要唤醒的设置了独占标志进程的数目。当找到 nr_exclusive 个设置了 WQ_FLAG_EXCLUSIVE 独占标志的任务的时候,就不再唤醒新的节点。
核心函数
static int __wake_up_common(struct wait_queue_head *wq_head, unsigned int mode,
int nr_exclusive, int wake_flags, void *key,
wait_queue_entry_t *bookmark)
{
wait_queue_entry_t *curr, *next;
int cnt = 0;
lockdep_assert_held(&wq_head->lock);
/*
* bookmark 指向要开始遍历的节点的位置
* 如果 bookmark 存在则将 bookmark 指向的下一个节点赋值给 curr 变量
* 否则将等待队列的第一个节点赋值给 curr
*/
if (bookmark && (bookmark->flags & WQ_FLAG_BOOKMARK)) {
curr = list_next_entry(bookmark, entry);
list_del(&bookmark->entry);
bookmark->flags = 0;
} else
curr = list_first_entry(&wq_head->head, wait_queue_entry_t, entry);
/*
* 如果开始遍历的节点是头节点,则表明运行队列为空,
* 返回 nr_exclusive —— 因为一个独占节点都没有被唤醒
*/
if (&curr->entry == &wq_head->head)
return nr_exclusive;
/*
* 从 curr 开始遍历等待队列
* 实际循环中使用的迭代变量为 next
*/
list_for_each_entry_safe_from(curr, next, &wq_head->head, entry) {
unsigned flags = curr->flags;
int ret;
/* 跳过所有的被标记为 bookmark 的节点 */
if (flags & WQ_FLAG_BOOKMARK)
continue;
/*
* 调用当前节点的回调函数,唤醒当前节点的任务
* 该回调函数默认为 default_wake_function()
*
* 如果唤醒失败则跳出遍历循环
*/
ret = curr->func(curr, mode, wake_flags, key);
if (ret < 0)
break;
/*
* 如果唤醒被标记为独占的节点的数量等于 nr_exclusive
* 则跳出遍历循环
*
* 这里采用判断唤醒独占节点的剩余数量是否为 0,来判断是否唤醒了足够的独占节点。
*/
if (ret && (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
/*
* 系统中设置了最大唤醒数量 WAITQUEUE_WALK_BREAK_CNT
* 当系统中存在 bookmark时,如果唤醒数量超出最大数量,则设置当前节点为 bookmark 节点并跳出遍历
* 即下一次唤醒从此节点的下一个节点开始遍历
*/
if (bookmark && (++cnt > WAITQUEUE_WALK_BREAK_CNT) &&
(&next->entry != &wq_head->head)) {
bookmark->flags = WQ_FLAG_BOOKMARK;
list_add_tail(&bookmark->entry, &next->entry);
break;
}
}
return nr_exclusive;
其中遍历等待队列使用了 list_for_each_entry_safe_from()
宏
#define list_for_each_entry_safe_from(pos, n, head, member) \
for (n = list_next_entry(pos, member); \
!list_entry_is_head(pos, head, member); \
pos = n, n = list_next_entry(n, member))
展开后为
for(next = list_next_entry(curr, entry);
!list_entry_is_head(curr, &wq_head->head, entry);
curr = next, next = list_next_entry(next, entry))
总结
等待队列其时采用的链表机制:
- 为每一个条件创建一个链表;
- 将等待该条件的任务挂载到该链表上;
- 等到条件满足时,唤醒对应链表上挂载的任务;