设备驱动程序学习笔记(5)-休眠与唤醒

来源:互联网 发布:张怡宁 知乎 编辑:程序博客网 时间:2024/05/22 14:56
By:             潘云登

Date:         2009-6-3

Email:        intrepyd@gmail.com

Homepage:http://blog.csdn.net/intrepyd

Copyright:该文章版权由潘云登所有。可在非商业目的下任意传播和复制。

对于商业目的下对本文的任何行为需经作者同意。


写在前面

1.         本文内容对应《linux设备驱动程序》第六章。

2.         参考wait.hwait.c两个文件。

3.         希望本文对您有所帮助,也欢迎您给我提意见和建议。

4.         本文包含以下内容:

²        手工休眠步骤

²        休眠的背后

²        独占等待

²        唤醒细节

²        简单睡眠

²        需要牢记的几条规则


手工休眠步骤

1.       首先,建立并初始化一个等待队列入口,即wait_queue_t结构。

/*静态声明*/

DEFINE_WAIT(my_wait);

 

/*运行时初始化*/

wait_queue_t my_wait;

init_wait(&my_wait);

2.       然后,将等待队列入口添加到等待队列中,即wait_queue_head_t结构,并设置进程的状态。

void prepare_to_wait(wait_queue_head_t *queue,

                    wait_queue_t *wait,

                    int state);

其中, queue为等待队列,state是进程的新状态,为TASK_INTERRUPTIBLETASK_UNINTERRUPTIBLE

3.       检查休眠等待的条件,确定有必要休眠后,调用schedule,让出CPU

if (!condition)

   schedule( );

4.       schedule返回后,修改进程状态为TASK_RUNNING,将等待队列入口从等待队列中移出,这些都通过finish_wait完成。

void finish_wait(wait_queue_head_t *queue, wait_queue_t *wait);

这时候,不能保证休眠等待的条件已经成立,必须再次判断是否需要重新等待。因此,一个典型的手工休眠模型如下所示:

wait_queue_head_t queue;

init_waitqueue_head(&queue);

 

while (!condition) {

       /* 步骤1 */

       DEFINE_WAIT(wait);

       

       /* 如果是非阻塞操作,无需休眠 */

       if (filp->f_flags & O_NONBLOCK)

           return -EAGAIN;

       

       /* 步骤2 */

       prepare_to_wait(&queue, &wait, TASK_INTERRUPTIBLE);

 

       /* 步骤3 */

       if (!condition)

           schedule( );

 

       /* 步骤4 */

       finish_wait(&dev->outq, &wait);

}


休眠的背后

休眠机制与信号量机制有些类似:当一个人走到房间前,发现房间被其他人占用(休眠等待条件不成立);于是,他在访客名单上留下个人信息(加入等待队列);然后去做其它事情,而不是在门外守着(让出CPU);当房里的人出来后,便呼唤名单上的访客(唤醒)。不同之处在于:访客在名单上留下个人信息后,会敲一下门,以确认房间内的人没有离开;另外,当访客被呼唤回来后,会再次敲门,确认是否有其他访客在他之前进入了房间。

在这个机制中,等待队列和等待队列入口分别充当了名单和个人信息的角色。先来看一下它们的结构:

struct __wait_queue_head {

        spinlock_t lock;

        struct list_head task_list;

};

typedef struct __wait_queue_head wait_queue_head_t;

 

typedef struct __wait_queue wait_queue_t;

struct __wait_queue {

        unsigned int flags;

#define WQ_FLAG_EXCLUSIVE         0x01

        void *private;

        wait_queue_func_t func;

        struct list_head task_list;

};

其中,除了保证互斥访问的自旋锁和连接结构的链表头外,最重要的信息就是访客的姓名private和联系方式funcflags就像一张VIP卡,用来标识独占等待。有趣的是,每个访客的个人信息看起来都一样,即当前进程current和回调函数autoremove_wake_function,它们在初始化时设置。

#define DEFINE_WAIT(name)                                                   /

        wait_queue_t name = {                                                       /

                  .private      = current,                              /

                  .func           = autoremove_wake_function,                /

                  .task_list    = LIST_HEAD_INIT((name).task_list),/

        }

这样,prepare_to_wait只需要修改进程状态,并利用list_add方法把等待队列入口加入到等待队列的头部。然后,由finish_wait进行相反的操作。可以在<kernel/wait.c>中找到它们。


独占等待

通常,所有等待队列上的进程都会在唤醒时被置为可运行状态。有时候,只有一个被唤醒的进程可以获得期望的资源,而其它的进程会再次休眠。因此,当对某个资源存在严重竞争,并且唤醒单个进程就能完整消耗该资源的时候,可以使用独占等待提高系统性能。

要使用独占等待,可以使用prepare_to_wait_exclusive函数替换prepare_to_wait。它设置等待队列入口的独占标志flags,就是前面的VIPJ,并将进程添加到等待队列的尾部。为什么不像prepare_to_wait那样加到队列头部呢?等你看完下面的唤醒细节,也许就明白了。

void fastcall

prepare_to_wait_exclusive(wait_queue_head_t *q, wait_queue_t *wait, int state)

{

……

        wait->flags |= WQ_FLAG_EXCLUSIVE;

        ……

                  __add_wait_queue_tail(q, wait);

    ……

}


唤醒细节

当房间里的人走出来后,他会查看名单上的访客信息,然后通过他们留下的联系方式挨个联系。这通过wake_up宏或其变种完成,而真正调用的都是__wake_up方法。

#define wake_up(x)                       __wake_up(x, TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE, 1, NULL)

#define wake_up_nr(x, nr)             __wake_up(x, TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE, nr, NULL)

#define wake_up_all(x)                           __wake_up(x, TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE, 0, NULL)

#define wake_up_interruptible(x) __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)

#define wake_up_interruptible_nr(x, nr)__wake_up(x, TASK_INTERRUPTIBLE, nr, NULL)

#define wake_up_interruptible_all(x)     __wake_up(x, TASK_INTERRUPTIBLE, 0, NULL)

 

void fastcall __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);

}

 

static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,

                                int nr_exclusive, int sync, void *key)

{

        struct list_head *tmp, *next;

 

        list_for_each_safe(tmp, next, &q->task_list) {

                  wait_queue_t *curr;

                  unsigned flags;

                  curr = list_entry(tmp, wait_queue_t, task_list);

                  flags = curr->flags;

                  if (curr->func(curr, mode, sync, key) &&

                      (flags & WQ_FLAG_EXCLUSIVE) &&

                      !--nr_exclusive)

                           break;

        }

}

__wake_up中,确切地说,应该是在__wake_up_common中,从头部开始遍历等待队列,并调用等待队列入口项初始化时指定的func回调函数,将进程设置为可运行状态,并且如果该进程具有更高的优先级,则会执行一次上下文切换以便切换到该进程。

这时候,如果有VIP访客在,则遍历会在唤醒nr_exclusive个独占等待的进程后停止。nr_exclusive根据不同的宏调用进行设置。常用的wake_upwake_up_interruptible宏,会在唤醒队列中第一个具有WQ_FLAG_EXCLUSIVE标志的进程后停止遍历。由于独占等待入口项总是被加入到队列的尾部,因此最先加入等待队列的VIP进程会先被唤醒。此外,处于等待队列头部的非独占等待进程,在每一次wake_up的时候,都将被唤醒。


简单睡眠

随着社会的不断进步,生产工具也得到了不断的改进。现在你不再需要手工休眠,只要调用wait_event系列宏之一,便可轻松入眠。唯一的缺憾是,无法设置独占等待。

wait_event(queue, condition)

wait_event_interruptible(queue, condition)

wait_event_timeout(queue, condition, timeout)

wait_event_interruptible_timeout(queue, condition, timeout)

 

#define wait_event(wq, condition)                                              /

do {                                                                            /

        if (condition)                                                            /

                  break;                                                                /

        __wait_event(wq, condition);                                             /

} while (0)

 

#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)

如果使用wait_event,进程被置于非中断休眠。最好的选择是使用wait_event_interruptible,它可以被信号中断。这个版本可返回一个整数值,非零值表示休眠被某个信号中断,这时驱动程序要返回-ERESTARTSYSwait_event_timeoutwait_event_interruptible_timeout会等待限定的时间,当给定的时间到期时,它们都会返回0值,而不管condition如何求值。

在实践中,约定的做法是在使用wait_event时使用wake_up,它会唤醒所有非独占等待进程;而在使用wait_event_interruptible时使用wake_up_interruptible,它只会唤醒那些执行可中断休眠的进程。


需要牢记的几条规则

Ø        永远不要在原子上下文中进入休眠,即不能在拥有自旋锁、seqlock或者RCU锁时休眠。

Ø        当进程被唤醒时,永远无法知道休眠了多长时间,或者休眠期间发生了什么事情。因此,对休眠后的状态不能做任何假定,必须检查以确保等待的条件真正为真。

Ø        除非知道有其他人会在其他地方唤醒我们,否则进程不能休眠。