Linux内核之——等待队列wait queue

前言

等待队列1
等待队列2
调度器

CPU调度如下图所示:
在这里插入图片描述
等待队列其原理是:

  1. cpu会调度就绪队列,或者打断执行线程,运行就绪队列
  2. 创建等待队列头和队列,使用wait event,当condition不满足时,当前线程进入等待队列
  3. 通过将当前线程加入等待队列中,同时schedule调度走cpu执行别的线程,下次cpu便不会再调度当前线程了
  4. 当wakeup后,将wait线程加入run queue或者就绪队列中,同时condition满足,下次被阻塞的线程会被调度,如果condition不满足,则继续cpu被调度走,不会执行当前进程,继续阻塞

等待队列是一种基于资源状态的线程管理的机制,它可以使线程在资源不满足的情况下处于休眠状态,让出CPU资源,而资源状态满足时唤醒线程,使其继续进行业务的处理。等待队列(wait queue)用于使线程等待某一特定的事件发生而无需频繁的轮询,进程在等待期间睡眠,在某件事发生时由内核自动唤醒。它是以双循环链表为基础数据结构,与进程的休眠唤醒机制紧密相联,是实现异步事件通知、跨进程通信、同步资源访问等技术的底层技术支撑。

1. 创建一个等待队列

在Linux内核中,wait_queue_head_t代表一个等待队列,只需要定义一个wait_queue_head_t类型的变量,就表示创建一个等待队列,还需要调用如下接口来初始化此队列:

staitc wait_queue_head_t prod_wq;
init_waitqueue_head(&prod_wq);

具体看一下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;

就是一个链表和一把自旋锁,链表是用于保存等待该队列的wait_queue_t类型waiter对象(此类型对象内部的private成员保存了当前的任务对象task_struct *),自旋锁是为了保证对链表操作的原子性。这里简单的看一下wait_queue_t数据类型:

typedef struct __wait_queue wait_queue_t;
struct __wait_queue {
    unsigned int flags;
#define WQ_FLAG_EXCLUSIVE   0x01
    void *private; // 保存当前任务的task_struct对象地址,为pcb
    wait_queue_func_t func; // 用于唤醒被挂起任务的回调函数,该回调函数是将该进程加入run queue,等待cpu调度
    struct list_head task_list; // 连接到wait_queue_head_t中的task_list链表                                                              
};

在这里插入图片描述
图中,task是PCB

2. 让当前进程开始等待

内核提供了如下的接口来让当前进程在条件不满足的情况下,阻塞等待:

wait_event(wq, condition)
wait_event_timeout(wq, condition, timeout)
wait_event_interruptible(wq, condition)
wait_event_interruptible_timeout(wq, condition, timeout)

返回值如下:

1)    -ERESTARTSYS: 表示被信号激活唤醒

2)    > 0: 表示condition满足,返回值表示距离设定超时还有多久

3)    = 0: 表示超时发生

其内部实现源码都很类似,只是有些细节不太一样,这里以wait_event_interruptible()为例子,看看其源码:

#define __wait_event_interruptible(wq, condition, ret)          \
do {                                    \
    // 定义一个waiter对象
    DEFINE_WAIT(__wait);                        \
                                    \
for (;;) {                          \
    // 将waiter对象加入到等待链表中,并设置当前task的状态为TASK_INTERRUPTIBLE
        prepare_to_wait(&wq, &__wait, TASK_INTERRUPTIBLE);  \                                                                            
        if (condition)                      \
            break;                      \
        if (!signal_pending(current)) {             \
            // 进行任务调度,
            schedule();                 \
            continue;                   \
        }                           \
        ret = -ERESTARTSYS;                 \
        break;                          \
}                               \
    // 将waiter对象从等待链表中删除
    finish_wait(&wq, &__wait);                  \
} while (0)

当我们调用wait_event_interruptible()接口时,会先判断condition是否满足,如果不满足,则会suspend当前task。

这里再看一下DEFINE_WAIT宏的源码,可以发现其private成员总是保存这当前task对象的地址current,还有一个成员func也是非常重要的,保存着task被唤醒前的操作方法,这里暂不说明,待下面的wait_up唤醒等待队列时再进行分析:

#define DEFINE_WAIT(name)                       \
    wait_queue_t name = {                       \
        .private    = current,              \
        .func       = autoremove_wake_function,     \
        .task_list  = LIST_HEAD_INIT((name).task_list), \
    }

3. 唤醒此等待队列上的进程:

内核提供了如下的接口:

void wake_up(wait_queue_head_t *q);
void wake_up_interruptible(wait_queue_head_t *q);
void wake_up_interruptible_all(wait_queue_head_t *q);

这里以分析wake_up_interruptible()函数的源码进行说明唤醒task的原理,因为其他的唤醒过程都是类似的。最后都会调用到__wake_up_common()这个函数:

void __wake_up_common(wait_queue_head_t *q, unsigned int mode,                                                                           
            int nr_exclusive, int sync, void *key)
{
    wait_queue_t *curr, *next;
 
    list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
        unsigned flags = curr->flags;
 
        if (curr->func(curr, mode, sync, key) &&
                (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
            break;
    }
}

从上面的源码可以看出最终就是调用了等待队列q上的task_list链表上的waiter对象的func方法,在前面又提到过这个方法就是autoremove_wake_function():

int autoremove_wake_function(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
   // 将wait对象private成员保存的task添加到run queue中,便于系统的调度
    int ret = default_wake_function(wait, mode, sync, key);
 
    // 将此wait对象从链表中删除
    if (ret)                                                                                                                             
        list_del_init(&wait->task_list);
    return ret;
}

defailt_wake_function()的源码如下,又看到我们熟悉的private成员

int default_wake_function(wait_queue_t *curr, unsigned mode, int sync,
              void *key)
{
    return try_to_wake_up(curr->private, mode, sync);                                                                                     
}

4. 实例探索

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/sched.h>
#include <linux/kthread.h>
#include <linux/delay.h>
 
MODULE_AUTHOR("Jimmy");
MODULE_DESCRIPTION("wait queue example");
MODULE_LICENSE("GPL");
 
static int condition;
static struct task_struct *task_1;
static struct task_struct *task_2;
static struct task_struct *task_3;
 
DECLARE_WAIT_QUEUE_HEAD(wq);
 
 
static int thread_func_1(void *data)
{
    msleep(100);//延时100ms,使得这个进程的等待队列插入在整个链表的头部,最先被唤醒。所以先打印这个进程!
    
    wait_event_interruptible(wq, condition);
    condition = 0;
    printk(">>>>>this task 1\n");
    
    do {
        msleep(1000);
    }while(!kthread_should_stop());
    
    return 1;
}
 
static int thread_func_2(void *data)
{
        
    wait_event_interruptible(wq, condition);
    condition = 0;
    printk(">>>>>this task 2\n");
    
    do {
        msleep(1000);
    }while(!kthread_should_stop());
    
    return 2;
}
 
static int thread_func_3(void *data)
{    
    msleep(2000);
    printk(">>>>>this task 3\n");
    condition = 1;
    wake_up_interruptible(&wq);
 
    msleep(2000);
    condition = 1;
    wake_up_interruptible(&wq);
    
    do {
        msleep(1000);
    }while(!kthread_should_stop());
        
    return 3;
}
 
static int __init mod_init(void)
{
    condition = 0;
 
    task_1 = kthread_run(thread_func_1, NULL, "thread%d", 1);
    if (IS_ERR(task_1)) {
        printk("******create thread 1 failed\n");
    } else {
        printk("======success create thread 1\n");
    }
    
    task_2 = kthread_run(thread_func_2, NULL, "thread%d", 2);
    if (IS_ERR(task_2)) {
        printk("******create thread 2 failed\n");
    } else {
        printk("======success create thread 2\n");
    }
 
    task_3 = kthread_run(thread_func_3, NULL, "thread%d", 3);
    if (IS_ERR(task_2)) {
        printk("******create thread 3 failed\n");
    } else {
        printk("======success create thread 3\n");
    }
        
    return 0;
}
 
static void __exit mod_exit(void)
{
    int ret;
    
    if (!IS_ERR(task_1)) {
        ret = kthread_stop(task_1);
        printk("<<<<<<<<task 1 exit, ret = %d\n", ret);
    }
        
    if (!IS_ERR(task_2)) {
        ret = kthread_stop(task_2);
        printk("<<<<<<<<task 2 exit, ret = %d\n", ret);
    }
 
    if (!IS_ERR(task_3)) {
        ret = kthread_stop(task_3);
        printk("<<<<<<<<task 3 exit, ret = %d\n", ret);
    }
    
    return;
}
 
module_init(mod_init);
module_exit(mod_exit);
   

注意:
1、要从wait_event()函数跳出,需要两个条件。1、condition = 1; 2、在内核的另一个地方调用了wake_up()函数。
2、wake_up()每次只能唤醒一个进程,而且是从队列头开始唤醒的,而wait_event()函数每次会将新建的等待队列插到队列头,因此最后调用wait_event()函数的进程先被唤醒!如果要唤醒某个特定的进程,没有现成的函数,只能使用wake_up_all()函数唤醒所有进程,然后在通过条件condition来控制(每个进程使用不同的变量来控制,在wake_up_all()函数后只将要唤醒的进程的变量置成真)。
3、当wake_up_all后,运行continue,结束本次循环,再次上锁,如果condition不满足,则继续挂起,只有当condition满足时,才break,跳出for循环,finish_wait将_wait从wq中删除!

猜你喜欢

转载自blog.csdn.net/weixin_44537992/article/details/105990158