4.5.4 CFS —— 睡眠与唤醒
1 进程休眠机制
进程把自己放入等待队列中,设置为不可执行状态,与等待队列相关的事件发生时,唤醒队列上的进程
1.1 以前的模式
if (!condition) {
set_current_state(TASK_INTERRUPTIBLE);
add_wait_queue(&wait_queue, &wait_entry);
schedule(); // 直接调度休眠
}
- 检查唤醒条件,如果不成立
- 放入等待队列中
- 设置进程为可唤醒状态
- 进入调度,切换进程
1.2 存在的问题
- 在设置状态之后,加入队列之前,如果发生了唤醒,则唤醒会丢失。
- 没有循环重新检查条件,无法应对虚假唤醒
- 第一个问题具体指的是:
- 进程A检查条件,不满足,然后执行步骤A:设置状态为TASK_INTERRUPTIBLE。
- 在加入等待队列之前(即在执行步骤B之前),发生了中断,进程B运行,它使条件满足并调用wake_up()。由于进程A还没有加入队列,所以唤醒操作找不到进程A。
- 然后进程B执行完毕,进程A继续运行,它将自己加入等待队列(步骤B),然后调用schedule()休眠。但条件已经满足,而唤醒已经发生,所以进程A将一直休眠。
- 第二个问题具体指的是:
- 进程在设置状态并且加入等待队列后,被虚假唤醒(在条件变量或等待队列上的等待操作意外返回,尽管程序期望的条件尚未真正满足,状态被设为RUNNING),此时正确操作应该是继续休眠,但是直接调用了schedule,而由于状态是RUNNING,schedule不会使进程再次进入休眠
2 改进
DEFINE_WAIT(wait);
while (1) {
prepare_to_wait(&group->notification_waitq, &wait, TASK_INTERRUPTIBLE);
if (signal_pending(current))
break;
schedule();
}
finish_wait(&group->notification_waitq, &wait);
- prepare_to_wait() 的原子性(解决核心问题)
- 在同一把锁中完成:插入队列 + 设置进程状态
- 确保唤醒操作wake_up()必须等待此临界区结束
- 解决了第一个问题:设置状态与加入等待队列之间的间隔
- 唤醒后的二次检查
- 使用循环,每次从schedule()唤醒后先重新将进程放入等待队列中并设置状态,然后再检查条件是否真的满足了(if (signal_pending(current))):真的满足了则调用finish_wait恢复进程的RUNNING状态并从等待队列中移除;如果不是真的满足了,说明是虚假唤醒,则使用schedule重新进入休眠
add_wait_queue 函数调用链
kernel\wait.c
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
{
unsigned long flags;
wait->flags &= ~WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&q->lock, flags);
__add_wait_queue(q, wait);
spin_unlock_irqrestore(&q->lock, flags); } EXPORT_SYMBOL(add_wait_queue);
include\linux\wait.h
static inline void __add_wait_queue(wait_queue_head_t *head, wait_queue_t *new)
{
list_add(&new->task_list, &head->task_list);
}
include\linux\list.h
/**
- list_add - add a new entry
- @new: new entry to be added
- @head: list head to add it after *
- Insert a new entry after the specified head.
- This is good for implementing stacks. */ static inline void list_add(struct list_head *new, struct list_head *head) { __list_add(new, head, head->next); }
include\linux\list.h
/*
- Insert a new entry between two known consecutive entries. *
- This is only for internal list manipulation where we know
- the prev/next entries already! */ #ifndef CONFIG_DEBUG_LIST static inline void __list_add(struct list_head *new, struct list_head *prev, struct list_head *next) { next->prev = new; new->next = next; new->prev = prev; prev->next = new; } #else extern void __list_add(struct list_head *new, struct list_head *prev, struct list_head *next); #endif
prepare_to_wait 函数调用链
kernel\wait.c
void
prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state)
{
unsigned long flags;
wait->flags &= ~WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&q->lock, flags);
if (list_empty(&wait->task_list))
__add_wait_queue(q, wait);
set_current_state(state);
spin_unlock_irqrestore(&q->lock, flags); }
signal_pending 函数调用链
include\linux\sched.h
static inline int signal_pending(struct task_struct *p)
{
return unlikely(test_tsk_thread_flag(p,TIF_SIGPENDING));
}
finish_wait 调用链
kernel\wait.c
/*
* finish_wait - clean up after waiting in a queue
* @q: waitqueue waited on
* @wait: wait descriptor
*
* Sets current thread back to running state and removes
* the wait descriptor from the given waitqueue if still
* queued.
*/
void finish_wait(wait_queue_head_t *q, wait_queue_t *wait)
{
unsigned long flags;
__set_current_state(TASK_RUNNING);
/*
* We can check for list emptiness outside the lock
* IFF:
* - we use the "careful" check that verifies both
* the next and prev pointers, so that there cannot
* be any half-pending updates in progress on other
* CPU's that we haven't seen yet (and that might
* still change the stack area.
* and
* - all other users take the lock (ie we can only
* have _one_ other CPU that looks at or modifies
* the list).
*/
if (!list_empty_careful(&wait->task_list)) {
spin_lock_irqsave(&q->lock, flags);
list_del_init(&wait->task_list);
spin_unlock_irqrestore(&q->lock, flags);
}
}
留下评论