一个进程的睡眠意味着它的进程状态标识符被置为睡眠,而且从调度器的运行队列中去除,直到某些事件的发生将它们从睡眠态中唤醒,在睡眠态,该进程将不被CPU调度,而且,若是不被唤醒,它将永远不被运行。html
在驱动中很容易经过调度等方式使当前进程睡眠,可是进程并非在任什么时候候都是能够进入睡眠状态的。 linux
第一条规则是:当运行在原子上下文时不能睡眠:好比持有自旋锁,顺序锁或者RCU锁。 数据结构
在关中断中也不能睡眠。函数
持有信号量时睡眠是合法的,但它所持有的信号量不该该影响唤醒它的进程的执行。另外任何等待该信号量的线程也将睡眠,所以发生在持有信号量时的任何睡眠都应当短暂。ui
进程醒来后应该进行等待事件的检查,以确保它确实发生了。spa
等待队列能够完成进程的睡眠并在事件发生时唤醒它,它由一个进程列表组成。在 Linux 中, 一个等待队列由一个"等待队列头"来管理: .net
linux/wait.h struct __wait_queue_head { spinlock_t lock; struct list_head task_list; }; typedef struct __wait_queue_head wait_queue_head_t;
因为睡眠的进程颇有可能在等待一个中断来改变某些状态,或通告某些事件的发生,那么中断上下文颇有可能修改该等待队列,因此该结构中的自旋锁lock必须考虑禁中断,也即便用spin_lock_irqsave。线程
队列中的成员是以下数据结构的实例,它们组成了一个双向链表: unix
typedef struct __wait_queue wait_queue_t; typedef int (*wait_queue_func_t)(wait_queue_t *wait, unsigned mode, int flags, void *key); int default_wake_function(wait_queue_t *wait, unsigned mode, int flags, void *key); struct __wait_queue { unsigned int flags; #define WQ_FLAG_EXCLUSIVE 0x01 void *private; wait_queue_func_t func; struct list_head task_list; };
flags的值或者为0,或者为WQ_FLAG_EXCLUSIVE。后者表示等待进程想要被独占地唤醒。
private指针指向等待进程的task_struct实例。该变量本质上能够指向任何私有数据,单内核只有不多状况下才这么用。
调用func,唤醒等待进程。
task_list用做一个链表元素,将wait_queue_t实例放置到等待队列中。
为了使用等待队列,一般须要以下步骤:首先应该创建一个等待队列头:指针
DECLARE_WAIT_QUEUE_HEAD(name);
另一种方法是静态声明,并显式初始化它:
wait_queue_head_t wait_queue;
init_waitqueue_head(&wait_queue);
接着为使得当前进程进入睡眠,并等待某一事件的发生,须要将它加入到等待队列中,内核提供了如下函数完成此功能:
wait_event(queue, condition);
wait_event_interruptible(queue, condition);
wait_event_timeout(queue, condition, timeout);
wait_event_interruptible_timeout(queue, condition, timeout);
在全部的形式中,参数queue是要等待的队列头,因为这几个函数都是经过宏实现的,这里的队列头不是指针类型,而是对它的直接使用。条件condition是一个被这些宏在睡眠先后所要求值的任意的布尔表达式。直到条件求值为真,进程持续睡眠。
经过wait_event进入睡眠的进程是不可中断的,此时进程的state成员置TASK_UNINTERRUPTIBLE位。可是它应该被wait_event_interruptible所替代,它能够被信号中断,这意味着用户程序在等待的过程当中能够经过信号中断程序的执行。一个不能被信号中断的程序很容易激怒使用它的用户。wait_event函数没有返回值,而wait_event_interruptible有一个能够识别睡眠被某些信号打断的返回值-ERESTARTSYS。
wait_event_timeout和wait_event_interruptible_timeout意味着等待一段时间,它以滴答数表示,在这个时间期间超时后,该宏返回一个0值,而无论事件是否发生。
最后,咱们须要在其余进程或者线程(也多是中断)中经过相对应的函数,唤醒这些队列上沉睡的进程。内核提供了以下函数:
void wake_up(wait_queue_head_t *queue); void wake_up_interruptible(wait_queue_head_t *queue);
wake_up唤醒全部的在给定队列上等待的进程。
wake_up_interruptible唤醒全部的在给定队列上等待的可中断的睡眠的进程。
尽管wake_up能够替代wake_up_interruptible的功能,可是它们应该使用与wait_event对应的函数。经过等待队列实现一个管道的读写是可行的,内核中fs/pipe.c对管道的实现就是基于等待队列实现的,尽管它有些复杂。另外对于设备驱动来讲,一个温度采集器在收到读数据请求后,该进程被放入等待队列,而后唤醒它的布尔变量在该设备对应的中断处理程序中被置为真。
注意 wake_up_interruptible的调用可能使多个个睡眠进程醒来,而它们又是独占访问某一资源,如何使仅一个进程看到这个真值,这就是WQ_FLAG_EXCLUSIVE的做用,其余进程将继续睡眠。
等待队列实现原理
wait_event函数的核心实现以下:
#define __wait_event(wq, condition) \ do { \ DEFINE_WAIT(__wait); \ \ for (;;) { \ prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE); \ if (condition) \ break; \ schedule(); \ } \ finish_wait(&wq, &__wait); \ } while (0)
DEFINE_WAIT注册了一个名为__wait的队列元素,其中包含一个名为autoremove_wake_function的钩子函数,它用来唤醒的进程并将该元素从等待队列中删除。
prepare_to_wait用来将队列元素计入等待队列,并指定进程的state状态标识为TASK_UNINTERRUPTIBLE,固然对应wait_event_interruptible,则是TASK_INTERRUPTIBLE。
for无限循环决定了当前进程在不知足condition时老是被调度,其余进程将替换该进程执行。而且这个循环实际上永远只执行一次,而且只在唤醒时直接
在知足条件时,finish_wait将进程状态设置为TASK_RUNNING,并从等待队列中将其移除。
须要仔细考虑的是for循环的执行,显然它可能执行一次,也多是屡次,当condition不知足时,将会产生调度,而在此被调度时,将执行for的下一次循环,那么prepare_to_wait不是每次都添加一次__wait元素吗?查看prepare_to_wait代码能够发现,只有wait->task_list指向的链表为空时,也即__wait元素没有加入任何其余等待队列时才会把它加入到当前等待队列中,这也代表一个等待队列元素只能加入一个等待队列。
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); }
唤醒一个等待队列是经过wake_up系列函数完成的,一些列的唤醒函数都有对应的可中断形式:
#define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL) #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) #define wake_up_locked(x) __wake_up_locked((x), TASK_NORMAL)
这里分析它们的核心实现:
kernel/sched.c 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); }
__wake_up首先获取了自旋锁,而后调用__wake_up_common。该函数经过list_for_each_entry_safe遍历等待队列,若是没有设置独占标志,则根据mode唤醒每一个睡眠的进程。nr_exclusiv表示须要唤醒的设置了独占标志进程的数目,它在wake_up中设置为1,代表当处理了一个含有WQ_FLAG_EXCLUSIVE标志进程后,将再也不处理,独占标志的意义也在于此。另外看到这里经过func指针执行了真正的唤醒函数。
kernel/sched.c static void __wake_up_common(wait_queue_head_t *q, unsigned int mode, int nr_exclusive, int wake_flags, void *key) { wait_queue_t *curr, *next; list_for_each_entry_safe(curr, next, &q->task_list, task_list) { unsigned flags = curr->flags; if (curr->func(curr, mode, wake_flags, key) && (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive) break; } }
若是含有独占标志的进程并不位于队列尾部,将致使其后的不含有该标志的进程没法执行,prepare_to_wait_exclusive解决了该问题,它老是将含有独占标志的进程插入到队列尾部,该函数被wait_event_interruptible_exclusive宏调用。
转自:http://blog.chinaunix.net/uid-20608849-id-3126863.html