等待队列实现代码浅析

等待队列是很多其他linux内核机制的基石,例如异步事件通知机制,信号量也是。


   Linux中等待队列的实现思想:当一个任务需要在某个wait_queue_head上睡眠时,将自己的进程控制块信息封装到wait_queue中,然后挂载到wait_queue的链表中,执行调度睡眠。当某些事件发生后,另一个任务(进程)会唤醒wait_queue_head上的某个或者所有任务,唤醒工作也就是将等待队列中的任务设置为可调度的状态,并且从队列中删除。

但是很少有文章深入分析该机制的内核代码,所以有必要分析一下等待队列的内核实现:

 

1.首先分析等待队列链表头的数据结构:wait_queue_head_t

struct __wait_queue_head {

       spinlock_t lock;

       struct list_head task_list;

};

typedef struct __wait_queue_head wait_queue_head_t;

从上面分析,等待队列头数据头其实就是内核链表的一个节点(这个结点叫做task_list)+一个自旋锁。

wait_queue_head_t myqueue;//实际上就是定义一个等待队列链表的头结点

 

其次还有一个很重要的数据结构,就是等待队列的数据结构:wait_queue_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指针,一个回调函数,一个链表的结点。

 

哥明白了,哥继续!

 

2.现在我们来初始化等待队列链表头,使用以下语句:

Init_waitqueue_head(&myqueue);

我们来分析一下这个语句做了哪些事:

static inline void init_waitqueue_head(wait_queue_head_t *q)

{

       spin_lock_init(&q->lock);

       INIT_LIST_HEAD(&q->task_list);

}

(1)初始化了头结点的自旋锁

(2)调用INIT_LIST_HEAD宏把链表结点的prev指针和next指针指向自己

这样就完成了初始化,创建了一个保存等待队列的链表并初始化了该链表的头结点。

 

3.有了等待队列的链表,下面我们要完成把当前进程加入到等待队列中,并把该等待队列加入到等待队列的链表中去,首先我们看看怎么把当前进程加入到等待队列中去。

用一个宏实现:DECLARE_WAITQUEUEwait, current让我们来分析这个宏:

#define DECLARE_WAITQUEUE(name, tsk)                                /

       wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)

 

代码定义了一个等待队列,名字叫name,然后调用__WAITQUEUE_INITIALIZER宏的值来赋值给它。柯南继续追踪

#define __WAITQUEUE_INITIALIZER(name, tsk) {                      /

       .private   = tsk,                                         /

       .func              = default_wake_function,                    /

       .task_list  = { NULL, NULL } }

 

int default_wake_function(wait_queue_t *curr, unsigned mode, int sync,

                       void *key)

{

       task_t *p = curr->private;

       return try_to_wake_up(p, mode, sync);

}

 

原来如此,__WAITQUEUE_INITIALIZER宏将等待队列name的private指针指向当前tsk,该等待队列的回调函数初始化为默认的唤醒函数,同时将等待队列name的链表的next和prev指针指向空。

总的来说DECLARE_WAITQUEUE宏意思就是定义一个叫做wait的等待队列项,该项指向当前进程,同时指定了当队列被唤醒时执行的默认唤醒函数,另外它的两个链表指针指向空。(该函数调用try_to_wake_up,这个就复杂了,有兴趣的童鞋可以关注一下,好像就是把任务加到run_list中去。

 

4.有了等待队列的链表,也有了等待队列项,现在我们来完成将等待队列项加入到等待队列链表中去。

add_wait_queue(&myqueue ,wait)

让我们来详细分析一下:

void fastcall add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)

{

       unsigned long flags;

 

       wait->flags &= ~WQ_FLAG_EXCLUSIVE;

       spin_lock_irqsave(&q->lock, flags);

       __add_wait_queue(q, wait);//核心函数,继续追踪

       spin_unlock_irqrestore(&q->lock, flags);

}

__add_wait_queue(q, wait);//核心函数,继续追踪………………..

static inline void __add_wait_queue_tail(wait_queue_head_t *head,

                                          wait_queue_t *new)

{

       list_add_tail(&new->task_list, &head->task_list);

}

原来如此,看到这大家都明白了,其实add_wait_queue就是把等待队列项的链表结点,加到等待队列头初始化的链表中去!哈哈!现在大家都看清楚等待队列的实质了把,其实就是一个链表,每个结点就是一个“等待的task”。

 

5.现在有了等待队列,接下来的任务就是让task睡眠,和在一定时机让task苏醒。你可以调用系统有的函数例如Wait_event()或者sleep on()也可以自己在驱动中写代码实现(实际就是设置进程状态位,然后调用schedule()让出cpu)

Wait_event(queue, condition)

 

#define wait_event(wq, condition)                                   /

do {                                                         /

       if (condition)                                          /

              break;                                               /

       __wait_event(wq, condition);                                   /

} while (0)

核心是__wait_event(wq, condition),继续追踪

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

又见死循环!太牛逼了!首先判断condition是否满足,如果满足的话wait_event()立即返回,否则就schedule()进行睡眠。如果被唤醒,那么就再次判断condition的条件,如果成立就跳出,如果不成立,再次睡眠。

 

6.Wake_up函数,唤醒每个在等待队列上的等待的任务。

#define wake_up(x)  __wake_up(x, TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE, 1, 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;

       }

 

其实就是遍历等待队列链表上的每个结点,调用默认的唤醒函数func(curr, mode, sync, key),该函数唤醒进程,并使进程进入内核的run_list(前面提到过程比较复杂)这样就完成了进程的唤醒啦!

 

具体的例程代码有很多,大家参考设备驱动开发详解中的相关例程代码就好了,快12点了,先去休息了

你可能感兴趣的:(等待队列实现代码浅析)