Linux设备驱动程序 之 休眠

休眠简介

当一个进程被置入休眠时,它会被标记为一种特殊状态,并从调度器的运行队列中移走;直到某些情况下修改了这个状态,进程才会在任意cpu上调度,即运行该进程;休眠中的进程会被搁置在一边,等待将来的某个时间发生;

为了将进程以一种安全方式进入休眠,需要牢记下面的规则:

第一条规则,永远不要在原子上下文中进入休眠;原子上下文是指下面这种状态:在执行多个步骤时,不能有任何的并发访问;这意味着,对休眠来讲,我们的驱动程序不能再任何拥有自旋锁,顺序锁或者RCU锁的时候休眠;如果我们已经禁止了中断,也不能休眠;在拥有信号量时休眠是合法的,但是必须仔细检查有用信号量时休眠的代码,如果代码在拥有信号量时休眠,任何其他等待该信号量的线程也会休眠,因此任何拥有该信号量而休眠的代码必须很短,并且还需要确保拥有信号量并不会阻塞最终唤醒我们自己的那个进程;

第二条规则:唤醒之后的状态不能做任何假定,必须检查以确保我们等待的条件为真;当从休眠中唤醒时,无法知道休眠了多长时间,以及休眠时发生了什么事情;通常也无法知道是否还有其他进程在同一事件上休眠,这个进程可能会在我们之前呗唤醒并将我们等待的资源拿走;

第三条规则:除非我们知道有其他人会在其他地方唤醒我们,否则进程不能进入休眠;唤醒任务的代码必须能够找到我们的进程,这样才能唤醒休眠的进程;为确保唤醒发生,需要整体理解代码,并清除的知道对每个休眠而言哪些事件序列会结束休眠;能够找到休眠的进程意味着,需要维护一个称谓等待队列的数据结构;等待队列就是一个进程链表,其中包含了要等待某个特定事件的所有进程;

Linux中,一个等待队列通过一个“等待队列头”来管理,等待队列头是一个类型为wait_queue_head_t的结构体,定义在<linux/wait.h>中;

静态定义和初始化一个等待队列头使用下面宏:

或者使用动态方法:

简单休眠

当进程休眠时,它将期待某个条件会在未来成真;而当一个进程被唤醒时,它必须再次检查它所等待的条件的确为真;

Linux内核中最简单的睡眠方法是wait_event宏,在实现休眠的同时,它也检查进程等待的条件;

对应的唤醒应该使用wake_up宏,带有interruptible的要配对使用;下面两个函数会唤醒队列上的所有非独占进程,以及单个独占进程;

除了上述列出的方法,内核还定义了一些其他wait_event和wake_up类似的方法,具体在<linux/wait.h>中;其中wake_up相关函数与独占等待有很多关联;后面独占等待部分详细说明;

高级休眠

简单休眠函数可以满足很多驱动程序的休眠要求,但是在某些情况下,我们需要对Linux等待队列的机制有更加深入的理解;复杂的锁定以及性能需求会强制驱动程序使用底层的函数来实现休眠;

进程如何休眠-wait_event的内部原理

在<linux/wait.h>中,我们看到wait_queue_head_t类型的数据结构为一个自旋锁和一个链表组成;链表中保存的是一个等待队列的入口,该入口声明为wait_queue_t类型,这结构中包含了休眠进程的信息以及其期望被唤醒的相关细节信息;

将进程置于休眠的步骤:

第一步,通常是分配并初始化一个wait_queue_t结构,然后将其加入到对应的等待队列中,完成这些工作之后,不管谁负责唤醒该进程,都能找到正确的进程;

第二步,设置进程的状态,将其标记为休眠;<linux/sched.h>定义了多个任务状态,TASK_RUNNING表示进程可运行,尽管进程并不一定在任何给定时间都运行在某个处理器上;有两个状态表明进程处于休眠状态:TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE;显然,它们分别对应于两种休眠;

通常不需要驱动程序之间操作进程状态,如果需要可以调用下面宏设置:

通过修改当前状态,我们只是改变了调度器处理该进程的方式,但尚使进程让出处理器;

第三步,让出处理器,放弃处理器是最后的步骤,但在此之前还需要做另外一件事,必须首先检查休眠等待的条件;如果不做这个检查,可能引入竞态;试想,如果在上述过程中条件变成了真,而其他线程正在试图唤醒我们,这时会发生什么呢?我们会丢掉被唤醒的机会,从而可能休眠更长时间;因此,深入休眠代码,我们可以看到下面语句:

如果在等待的条件在设置进程状态之前发生,我们会在这个检查中注意到且不会真正的进入休眠;如果唤醒在其后发生,不管我们是否真正进入休眠,进程都会被置于可运行状态;

对schedule()的调用将调用调度器,并让出cpu;无论在什么时候调用这个函数,都将告诉内核重新选择其他进程运行,并在必要时将控制切换到那个进程;这样,我们无法知道,在调度返回到我们的代码之前需要多少时间;

在上述if条件测试以及可能的schedule调用之后,需要完成一些清理工作;因为代码不在期望休眠,因此必须确保任务状态被重置为TASK_RUNNING;如果代码从schedule中返回,则不需要这一步,但是如果因为不需要休眠而跳过了对schedule的调用,那么进程状态是不正确的;并且需要将进程从等待队列中移走,佛足额可能会被多次唤醒;

手工休眠

为了设置一些特殊的操作(比如设置独占),也可以使用手工休眠的方式完成上述所有步骤;

第一步,初始化一个等待队列入口,使用下面的方式静态定义:

或者动态定义:

第二步,将等待队列入口添加到队列中,并且设置进程的状态:

其中,q和wait为等待队列头和进程入口,state是进程的新状态,它应该是TASK_INERRUPTIBLE或者TASK_UNINTERRUPTIBLE;

第三步,调用schedule,当然在这之前,需要确保有必要等待;

第四步,一旦schedule返回,就到了清理时间,这个工作可以通过下面的函数进行;

第五步,代码可测试其状态,并且判断是否需要重新等待;

独占等待

当某个进程在等待队列上调用wake_up时,所有等待在该队列上的进程都将被置为可运行状态;假如,我们知道只会有一个被唤醒的进程可以获得期望的资源,而其他呗唤醒的进程只会再次休眠,这些被唤醒的进程中每一个都要获得处理器,为资源竞争,然后再次进入休眠;如果等待队列中的进程数非常庞大,则这种行为将严重影响性能;

为了解决这个问题,内核中增加了“独占等待”选项,一个独占等待的行为和通常的休眠类似,但有如下两个重要区别:

1. 等待队列入口设置了WQ_FLAG_EXCLUSIVE标志,则会被添加到等待队列的尾部,而没有这个标志的入口则会被添加到头部;

2. 在某个等待队列上调用了wake_up时,它会在唤醒第一个具有WQ_FLAG_EXCLUSIVE标志的进程只会停止唤醒其他进程;(注意,因为非独占的进程都在队列前面,所以都会被唤醒)

使用独占标记需要考虑两个条件:

1. 对某个资源存在严重的竞争;

2. 唤醒单个进程就能完整消耗该资源;

将进程设置成独占等待状态可以调用prepare_wait_exclusive来设置入口的独占标记,并将进程添加到等待队列的尾部;

而wait_event以及其变种的方法无法执行独占等待;

本文链接:Linux设备驱动程序 之 休眠

转载声明:转载请注明来源:Linux TCP/IP Stack,谢谢!


发表评论

电子邮件地址不会被公开。 必填项已用*标注