Linux内核:通过wait_event和wake_up内在机制分析等待队列

等待队列在linux内核中,等待队列是一个非常重要的概念,也是一个非常重要的机制。我们会在很多函数当中用到等待队列的知识,例如completion机制、wait_event机制等等。在解释这些机制之前,我们首先要弄清楚什么是等待队列。

在linux内核里面,我们将进程分为以下几种状态:

可运行状态(TASK_RUNNING)
处于这种状态的进程,要么正在运行,要么正准备被CPU调度运行。正在运行的进程就是当前进程(由current所指向的进程),而准备运行的进程只要得到CPU就可以立即投入运行,CPU是这些进程唯一等待的系统资源。系统中有一个运行队列(run_queue),用来容纳所有处于可运行状态的进程,调度程序执行时,从中选择一个进程投入运行。在后面我们讨论进程调度的时候,可以看到运行队列的作用。当前运行进程一直处于该队列中,也就是说,current总是指向运行队列中的某个元素,只是具体指向谁由调度程序(schedule)决定。

等待状态(TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE)
处于该状态的进程正在等待某个事件(event)或某个资源,它肯定位于系统中的某个等待队列(wait_queue)中。Linux中处于等待状态的进程分为两种:可中断的等待状态(TASK_INTERRUPTIBLE)和不可中断的等待状态(TASK_UNINTERRUPTIBLE))。处于可中断等待态的进程可以被信号唤醒,如果收到信号,该进程就从等待状态进入可运行状态,并且加入到运行队列中,等待被调度;而处于不可中断等待态的进程是因为硬件环境不能满足而等待,例如等待特定的系统资源,它任何情况下都不能被打断,只能用特定的方式来唤醒它,例如唤醒函数wake_up()等。

暂停状态
此时的进程暂时停止运行来接受某种特殊处理。通常当进程接收到SIGSTOP、SIGTSTP、SIGTTIN或 SIGTTOU信号后就处于这种状态。例如,正接受调试的进程就处于这种状态。

僵死状态
进程虽然已经终止,但由于某种原因,父进程还没有执行wait()系统调用,终止进程的信息也还没有回收。顾名思义,处于该状态的进程就是死进程,这种进程实际上是系统中的垃圾,必须进行相应处理以释放其占用的资源。

实际使用
了解了Linux内核的几个状态之后,现在我们开始着重讲解其中的一个状态,等待状态,由上面的等待状态的描述,我们可以知道处于等待状态的进程正在等待某一个事件或者某一个资源,它肯定位于系统中的某一个等待队列中。这里我们就以wait_event和wake_up机制来讲解。
在这一机制当中,wait_event用于将当前进程加入某一等待队列中,同时将该进程的状态修改为等待状态。而wake_up则用于将某一个等待队列上面所有的等待进程唤醒,也就是将其从等待队列上面删掉,同时将其的进程状态置为可运行状态。
等待队列由等待队列头等待队列项构成,所以当我们定义了一个等待队列头,也就是定义了一个等待队列了,等待队列的结构如下图所示:
等待队列

等待队列头(wait_queue_head)在内核文件中的定义(include\Linux\Wait.h)

typedef struct __wait_queue_head wait_queue_head_t;
struct __wait_queue_head {
    spinlock_t lock;
    struct list_head task_list;
};

从上面的等待队列头的结构体可以看出,其由两部分组成,第一部分是一个自旋锁,用于保护自身资源不被多个进程同时访问。第二部分是有一个list_head结构体构成的双向链表,当然等待队列头里面只有next存放下一个等待队列项(wait_queue_t)的地址。
等待队列项(wait_queue_t)在内核文件中的定义(include\Linux\Wait.h)

typedef struct __wait_queue wait_queue_t;
struct __wait_queue {
    unsigned int flags;//
#define WQ_FLAG_EXCLUSIVE   0x01 //表示等待进程想要被独占的唤醒
    void *private; //私有指针变量,使用过程中会用来存放task_struct结构体
    wait_queue_func_t func; //用于唤醒等待进程
    struct list_head task_list; //链表,用于将等待队列头、等待队列连接起来
};

等待队列项一共由五个部分构成,其中我们要引起注意的是func,这个func在wake_up函数中会用到,用于唤醒这个等待队列里面的这个等待线程。

使用方法:

第一步、定义一个等待队列头

wait_queue_head_t rd_waitq;

作用:其实只要定义一个等待队列头,并且初始化,就相当于在Linux内核中重新开辟了一条等待队列,后面的等待队列项只要往后加等待队列项就可以了。该等待队列是独一无二的。
第二步、初始化等待队列头

init_waitqueue_head(rd_waitq);

这里我们使用的是init_waitqueue_head函数,这个函数主要做两件事情,
第一件初始化等待队列头的自旋锁,即使自旋锁设置为未锁状态;
第二件事情初始化等待队列头里面的task_list结构体,使之不指向任何一个等待队列头。所以在这里我们也可以通过此来判断等待队列是否有等待队列项,如果没有等待队列项,task_list链表的nest指针应该是指向自己的,而不会指向其他等待队列项。

第三步、添加/移除等待队列项
顾名思义,我们在上两步中已经初始化了等待队列头了,那么现在就应该是在这个等待队列头后面增加等待队列项了,增加等待队列项的函数

//增加等待队列项
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait); 
//删除等待队列项
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)

这里我们以等待事件函数为例,来说明如何使用这两个函数。wait_event函数用于使当前线程进入休眠等待状态。

#define wait_event(wq, condition)                   
do {                                    
    if (condition) //判断条件是否满足,如果满足则退出等待         
        break;                          
    __wait_event(wq, condition);//如果不满足,则进入__wait_event宏
} while (0)

#define __wait_event(wq, condition)                     
do {    
DEFINE_WAIT(__wait);
/*定义并且初始化等待队列项,后面我们会将这个等待队列项加入我们的等待队列当中,同时在初始化的过程中,会定义func函数的调用函数autoremove_wake_function函数,该函数会调用default_wake_function函数。*/                    

    for (;;) {                          
        prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE);    
/*调用prepare_to_wait函数,将等待项加入等待队列当中,并将进程状态置为不可中断TASK_UNINTERRUPTIBLE;*/
        if (condition)  //继续判断条件是否满足                    
            break;                      
        schedule(); //如果不满足,则交出CPU的控制权,使当前进程进入休眠状态                      
    }
    /**如果condition满足,即没有进入休眠状态,跳出了上面的for循环,便会将该等待队列进程设置为可运行状态,并从其所在的等待队列头中删除    */                          
    finish_wait(&wq, &__wait);              
} while (0)                             
void prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state)
{
    unsigned long flags;
    wait->flags &= ~WQ_FLAG_EXCLUSIVE;//标记该等待队列
    spin_lock_irqsave(&q->lock, flags); //禁止本地中断,并且获取自旋锁
    if (list_empty(&wait->task_list))//判断等待队列是否为空,即只要检查等待队列头的task_list是否指向本身就可以了。
        __add_wait_queue(q, wait); //如果为空,则将该等待队列项加入等待队列
    set_current_state(state);//设置当前进程的状态
    spin_unlock_irqrestore(&q->lock, flags);//释放自旋锁
}

下面我来分析一下等待队列的唤醒机制。
与wait_event函数对应的就是wake_up函数了,wake_up函数用于唤醒处于该等待队列的进程。首先我们来看一下位于include\linux\wait.h文件中wake_up函数的定义

/**定义wake_up函数宏,同时其需要一个wait_queue_head_t的结构体指针,在该宏中调用__wake_up方法。*/
#define wake_up(x)          __wake_up(x, TASK_NORMAL, 1, NULL)

//该方法中主要是在自旋锁的状态下调用__wake_up_common方法
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);
}
/*其中:q是等待队列,mode指定进程的状态,用于控制唤醒进程的条件,nr_exclusive表示将要唤醒的设置了WQ_FLAG_EXCLUSIVE标志的进程的数目,这里其值是1,表示只有一个这样白标识的等待进程。 然后扫描链表,调用func(注册的进程唤醒函数,默认为default_wake_function)唤醒每一个进程,直至队列为空,或者没有更多的进程被唤醒,或者被唤醒的的独占进程数目已经达到规定数目。*/
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;
    //这个宏的作用是遍历整个等待队列,其实就相当于一个for函数。
    list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
        //将当前进程的标志位赋给flag,再调用func函数,以及其他判断机制唤醒等待队列上的进程
        unsigned flags = curr->flags;
        if (curr->func(curr, mode, wake_flags, key) &&(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
            break;
    }
}
//list_for_each_entry_safe的定义
#define list_for_each_entry_safe(pos, n, head, member)          \
    for (pos = list_entry((head)->next, typeof(*pos), member),  \
        n = list_entry(pos->member.next, typeof(*pos), member); \
        //只要pose的task_list不是该等待队列头的task_list就继续下去。
         &pos->member != (head);                    \
         pos = n, n = list_entry(n->member.next, typeof(*n), member))

通过上面的源代码的分析,应该可以基本了解等待队列以及整个wait_event宏和wake_up宏的工作流程了。

  • 6
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值