[转载]Linux系统中休眠的理解

来源:互联网 发布:不可抗力网络剧01集 编辑:程序博客网 时间:2024/06/06 10:58

首先必须意识到:休眠是一种进程的特殊状态(即task->state= TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE)]

一、休眠的目的
简单的说,休眠是为在一个当前进程等待暂时无法获得的资源或者一个event的到来时(原因),避免当前进程浪费CPU时间(目的),将自己放入进程等待队列中,同时让出CPU给别的进程(工作)。休眠就是为了更好地利用CPU。
一旦资源可用或event到来,将由内核代码(可能是其他进程通过系统调用)唤醒某个等待队列上的部分或全部进程。从这点来说,休眠也是一种进程间的同步机制。

二、休眠的对象
休眠是针对进程,也就是拥有task_struct的独立个体。
当进程执行某个系统调用的时候,暂时无法获得的某种资源或必须等待某event的到来,在这个系统调用的底层实现代码就可以通过让系统调度的手段让出CPU,让当前进程处于休眠状态。

进程什么时候会被休眠?
进程进入休眠状态,必然是他自己的代码中调用了某个系统调用,而这个系统调用中存在休眠代码。这个休眠代码在某种条件下会被激活,从而让改变进程状态,说到底就是以各种方式包含了:
1、条件判断语句
2、进程状态改变语句
3、schedule();

三、休眠操作做了什么
进程被置为休眠,意味着它被标识为处于一个特殊的状态(TASK_UNINTERRUPTIBLE或 TASK_INTERRUPTIBLE),并且从调度器的运行队列中移走。这个进程将不在任何 CPU 上被调度,即不会被运行。 直到发生某些事情改变了那个状态(to TASK_WAKING)。这时处理器重新开始执行此进程,此时进程会再次检查是否需要继续休眠(资源是否真的可用?),如果不需要就做清理工作,并将自己的状态调整为TASK_RUNNING。过程如下图所示:

四、谁来唤醒休眠进程
进程在休眠后,就不再被调度器执行,就不可能由自己唤醒自己,也就是说进程不可能“睡觉睡到自然醒”。唤醒工作必然是由其他进程或者内核本身来完成的。唤醒需要改变进程的task_struct中的状态等,代码必然在内核中,所以唤醒必然是在系统调用的实现代码中(如你驱动中的read、write方法)以及各种形式的中断代码(包括软、硬中断)中。
如果在系统调用代码中唤醒,则说明是由其他的某个进程来调用了这个系统调用唤醒了休眠的进程。
如果是中断中唤醒,那么唤醒的任务可以说是内核完成了。

五、休眠和唤醒的代码简要分析
下面我们简单分析一下休眠与唤醒的内核原语。

1、休眠:wait_event/*** wait_event - 休眠,直到 condition 为真* @wq: 所休眠的等待队列* @condition: 所等待事件的一个C表达式** 进程被置为等待状态 (TASK_UNINTERRUPTIBLE) 直到* @condition 评估为真. @condition 在每次等待队列@wq 被唤醒时* 都被检查。** wake_up() 必须在改变任何可能影响等待条件结果* 的变量之后被调用。*/#define wait_event(wq, condition) \do { \if (condition) \break; \先测试条件,看看是否真的需要休眠__wait_event(wq, condition); \} while (0)#define __wait_event(wq, condition) \do { \DEFINE_WAIT(__wait); \定义一个插入到等待队列中的等待队列结构体,注意.private = current,(即当前进程)#define DEFINE_WAIT_FUNC(name, function) \wait_queue_t name = { \.private = current, \.func = function, \.task_list = LIST_HEAD_INIT((name).task_list), \}#define DEFINE_WAIT(name) DEFINE_WAIT_FUNC(name, autoremove_wake_function)\for (;;) { \prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE); \将上面定义的结构体__wait放入wq等待队列中,并设置当前进程状态为TASK_UNINTERRUPTIBLEif (condition) \break; \测试条件状态,看看是否真的需要休眠调度schedule(); \开始调度,程序停于此处,直到有其他进程唤醒本进程,就从此处继续......} \finish_wait(&wq, &__wait); \由于测试条件状态为假,跳出以上循环后执行休眠后的扫尾工作:设置当前进程状态为TASK_RUNNING将上面定义的__wait从等待队列链表中删除。} while (0)2、唤醒:wake_up#define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL)/*** __wake_up - 唤醒阻塞在等待队列上的线程.* @q: 等待队列* @mode: which threads* @nr_exclusive: how many wake-one or wake-many threads to wake up* @key: is directly passed to the wakeup function** It may be assumed that this function implies a write memory barrier before* changing the task state if and only if any tasks are woken up.*/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);}EXPORT_SYMBOL(__wake_up);kernel/sched.c/** 核心唤醒函数.非独占唤醒(nr_exclusive == 0) 只是* 唤醒所有进程. If it's an exclusive wakeup (nr_exclusive == small +ve* number) then we wake all the non-exclusive tasks and one exclusive task.** There are circumstances in which we can try to wake a task which has already* started to run but is not in state TASK_RUNNING. try_to_wake_up() returns* zero in this (rare) case, and we handle it by continuing to scan the queue.*/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) {遍历指定等待队列中的wait_queue_t.unsigned flags = curr->flags;if (curr->func(curr, mode, wake_flags, key) &&(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)break;调用唤醒函数,也就是创建wait_queue_t时的 autoremove_wake_function}}int autoremove_wake_function(wait_queue_t *wait, unsigned mode, int sync, void *key){int ret = default_wake_function(wait, mode, sync, key);if (ret)list_del_init(&wait->task_list);从等待队列中删除这个进程return ret;}EXPORT_SYMBOL(autoremove_wake_function);int default_wake_function(wait_queue_t *curr, unsigned mode, int wake_flags,void *key){return try_to_wake_up(curr->private, mode, wake_flags);主要是要改变进程状态为 TASK_WAKING,让调度器可以重新执行此进程。}EXPORT_SYMBOL(default_wake_function);

六、使用休眠的注意事项
(1) 永远不要在原子上下文中进入休眠,即当驱动在持有一个自旋锁、seqlock或者 RCU 锁时不能睡眠;关闭中断也不能睡眠,终端例程中也不可休眠。
持有一个信号量时休眠是合法的,如果代码在持有一个信号量时睡眠,任何其他的等待这个信号量的线程也会休眠。发生在持有信号量时的休眠必须短暂,而且决不能阻塞那个将最终唤醒你的进程。
(2)当进程被唤醒,它并不知道休眠了多长时间以及休眠时发生什么;也不知道是否另有进程也在休眠等待同一事件,且那个进程可能在它之前醒来并获取了所等待的资源。所以不能对唤醒后的系统状态做任何的假设,并必须重新检查等待条件来确保正确的响应。

(3)除非确信其他进程会在其他地方唤醒休眠的进程,否则也不能睡眠。使进程可被找到意味着:需要维护一个等待队列的数据结构。它是一个进程链表,其中包含了等待某个特定事件的所有进程的相关信息。

七、不可在中断例程中休眠的原因
如果在某个系统调用中把当前进程休眠,是有明确目标的,这个目标就是过来call这个系统调用的进程(注意这个进程正在running)。
但是中断和进程是异步的,在中断上下文中,当前进程大部分时候和中断代码可能一点关系都没有。要是在这里调用了休眠代码,把当前进程给休眠了,那就极有可能把无关的进程休眠了。再者,如果中断不断到来,会殃及许多无辜的进程。
在中断中休眠某个特定进程是可以实现的,通过内核的task_struct链表可以找到的,不论是根据PID还是name。但是只要这个进程不是当前进程,休眠它也可能没有必要。可能这个进程本来就在休眠;或者正在执行队列中但是还没执行到,如果执行到他了可能又无须休眠了。
还有一个原因是中断也是所谓的原子上下文,有的中断例程中会禁止所有中断,有的中断例程还会使用自旋锁等机制,在其中使用休眠也是非常危险的。 下面会介绍。

八、不可在持有自旋锁、seqlock、RCU 锁或关闭中断时休眠的原因
其实自旋锁、seqlock、RCU 锁或关闭中断期间的代码都称为原子上下文,比较有代表性的就是自旋锁spinlock。
对于UP系统,如果A进程在拥有spinlock时休眠,这个进程在拥有自旋锁后主动放弃了处理器。其他的进程就开始使用处理器,只要有一个进程B去获取同一个自旋锁,B必然无法获取,并做所谓的自旋等待。由于自旋锁禁止所有中断和抢占,B的自旋等待是不会被打断的,并且B也永远获得不了锁。因为B在CPU中运行,没有其他进程可以运行并唤醒A并释放锁。系统就此锁死,只能复位了。
对于SMP系统,如果A进程在拥有spinlock时休眠,这个进程在拥有自旋锁后主动放弃了处理器。如果所有处理器都为了获取这个锁而自旋等待,由于自旋锁禁止所有中断和抢占,,就不会有进程可能去唤醒A了,系统也就锁死了。
并不是所一旦系统获得自旋锁休眠就会死,而是有这个可能。但是注意了计算机的运行速度之快,只要有亿分之一的可能,也是很容易发生。
所有的原子上下文都有这样的共性:不可在其中休眠,否则系统极有可能锁死。

全文节选自http://blog.chinaunix.net/uid-20543672-id-3248664.html?page=2

0 0