对 Linux 系统休眠的理解

今天看了一个关于中断例程为什么不能休眠的文章,引发了我的思考。其实这个问题在学习驱动的时候早就应该解决了,但是由于5年前学驱动的时候属于Linux初学者,能力有限,所以对这个问题就知其然,没有能力知其所以然。现在回头看这个问题的时候,感觉应该可以有一个较为清晰的认识了。

首先必须意识到:休眠是一种进程的特殊状态(即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中的状态等,代码必然在内核中,所以唤醒必然是在系统调用的实现代码中(如你驱动中的readwrite方法)以及各种形式的中断代码(包括软、硬中断)中。

如果在系统调用代码中唤醒,则说明是由其他的某个进程来调用了这个系统调用唤醒了休眠的进程。

如果是中断中唤醒,那么唤醒的任务可以说是内核完成了。

  • 如何找到需要唤醒的进程:等待队列

上面其实已经提到了:休眠代码的一个工作就是将当前进程信息放入一个等待队列中。它其实是一个包含等待某个特定事件的所有进程相关信息的链表。一个等待队列由一个wait_queue_head_t 结构体来管理,其定义在中。

wait_queue_head_t 类型的数据结构如下: 

它包含一个自旋锁和一个链表。这是一个等待队列链表头,链表中的元素被声明做wait_queue_t。自旋锁用于包含链表操作的原子性。 

wait_queue_t包含关于睡眠进程的信息和唤醒函数

他们在内存中的结构大致如下图所示:

等待队列头wait_queue_head_t一般是定义在模块或内核代码中的全局变量,而其中链接的元素 wait_queue_t的定义被包含在了休眠宏中。

休眠和唤醒的过程如下图所示:

五、休眠和唤醒的代码简要分析

下面我们简单分析一下休眠与唤醒的内核原语。

1、休眠:wait_event

2、唤醒:wake_up

上面分析的休眠函数是最简单的休眠唤醒函数,其他类似的函数,如后缀为_timeout_interruptible_interruptible_timeout的函数其实都是在唤醒后的条件判断上有些不同,多判断一些唤醒条件而已。这里就不再赘述了。

六、使用休眠的注意事项

1) 永远不要在原子上下文中进入休眠,即当驱动在持有一个自旋锁、seqlock或者 RCU 锁时不能睡眠;关闭中断也不能睡眠,终端例程中也不可休眠。

持有一个信号量时休眠是合法的,如果代码在持有一个信号量时睡眠,任何其他的等待这个信号量的线程也会休眠。发生在持有信号量时的休眠必须短暂,而且决不能阻塞那个将最终唤醒你的进程。

2)当进程被唤醒,它并不知道休眠了多长时间以及休眠时发生什么;也不知道是否另有进程也在休眠等待同一事件,且那个进程可能在它之前醒来并获取了所等待的资源。所以不能对唤醒后的系统状态做任何的假设,并必须重新检查等待条件来确保正确的响应。

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

七、不可在中断例程中休眠的原因

如果在某个系统调用中把当前进程休眠,是有明确目标的,这个目标就是过来call这个系统调用的进程(注意这个进程正在running)。

但是中断和进程是异步的,在中断上下文中,当前进程大部分时候和中断代码可能一点关系都没有。要是在这里调用了休眠代码,把当前进程给休眠了,那就极有可能把无关的进程休眠了。再者,如果中断不断到来,会殃及许多无辜的进程。

在中断中休眠某个特定进程是可以实现的,通过内核的task_struct链表可以找到的,不论是根据PID还是name。但是只要这个进程不是当前进程,休眠它也可能没有必要。可能这个进程本来就在休眠;或者正在执行队列中但是还没执行到,如果执行到他了可能又无须休眠了。

还有一个原因是中断也是所谓的原子上下文,有的中断例程中会禁止所有中断,有的中断例程还会使用自旋锁等机制,在其中使用休眠也是非常危险的。 下面会介绍。

八、不可在持有自旋锁、seqlockRCU 锁或关闭中断时休眠的原因 

其实自旋锁、seqlockRCU 锁或关闭中断期间的代码都称为原子上下文,比较有代表性的就是自旋锁spinlock

对于UP系统,如果A进程在拥有spinlock时休眠,这个进程在拥有自旋锁后主动放弃了处理器。其他的进程就开始使用处理器,只要有一个进程B去获取同一个自旋锁,B必然无法获取,并做所谓的自旋等待。由于自旋锁禁止所有中断和抢占,B的自旋等待是不会被打断的,并且B也永远获得不了锁。因为BCPU中运行,没有其他进程可以运行并唤醒A并释放锁。系统就此锁死,只能复位了。

对于SMP系统,如果A进程在拥有spinlock时休眠,这个进程在拥有自旋锁后主动放弃了处理器。如果所有处理器都为了获取这个锁而自旋等待,由于自旋锁禁止所有中断和抢占,,就不会有进程可能去唤醒A了,系统也就锁死了。

并不是所一旦系统获得自旋锁休眠就会死,而是有这个可能。但是注意了计算机的运行速度之快,只要有亿分之一的可能,也是很容易发生

所有的原子上下文都有这样的共性:不可在其中休眠,否则系统极有可能锁死。

只要对其设备节点做两次读写操作,系统必死。我在X86 SMP系统,ARMv5ARMv6ARMv7中都做了如下的实验(单核(UP)系统必须配置CONFIG_DEBUG_SPINLOCK,否则自旋锁是没有实际效果(起码不会有“自旋”), 系统可以多次获取自旋锁,没有实验效果。之后博文中有详细描述)。现象都和上面叙述的死法相同,看了源码就知道(关键在readwrite方法)。以下是实验记录:

1 5 收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部