linux 内核 --- 等待队列

简介

在实际编程中,我们会经常碰到这种场景:进程P需要等待条件C的成立,才能继续执行某个动作。例如,当串口没有数据可以读取时,我们可以通过轮询的方式,等到有数据来的时候,串口程序再去读取。但是这种方式显得比较笨拙,影响CPU的性能。因此,内核中提供了等待队列的方式,即可以将进程P先挂到等待队列Q(wait_queue)上去,并将进程的状态由RUNNING切换为睡眠状态,主动让出CPU,直到条件满足时,由内核调用wake_up()接口,自动唤醒Q上的所有进程,这样进程P就继续执行。

因此,wait_queue的实现,能够提高整个系统以及CPU运行的效率。

相关API

wq_head为等待队列头,condition是一个bool表达式,

wait_event(wq_head, condition)  //非中断休眠
wait_event_timeout(wq_head, condition, timeout)  //同上,另外进程等待限定时间返回不论condition是否成立                                                   
wait_event_interruptible(wq_head, condition) //进程可以被信号打断
wait_event_interruptible_timeout(wq_head, condition, timeout)   //类似上面


wake_up(&wq_head)  //唤醒等待队列上的所有进程
wake_up_interruptible(&wq_head)  //只唤醒哪些执行可中断睡眠的进程
wake_up_nr(&wq_head, nr) //唤醒给定数目的独占等待进程
wake_up_interruptible_nr(&wq_head, nr)
wake_up_interruptible_all(&wq_head)

代码原理

数据结构分析

wait_queue使用到的数据结构很简单,就是使用内核链表,将等待同一事件的所有进程串联起来

第一个数据结构是等待队列头

struct wait_queue_head {
    spinlock_t      lock;        \\用于同步的自旋锁
    struct list_head    head;    \\等待队列头
};
typedef struct wait_queue_head wait_queue_head_t;

等待队列头可以用以下API进行初始化:

wait_queue_head_t wait_queue_head;
init_waitqueue_head(&wait_queue_head);

第二个数据结构是等待队列项

struct wait_queue_entry {                                                                                 
    unsigned int        flags; // 最低位为1代表互斥进程,其他代表非互斥进程
    void            *private;  //一般用于指向当前进程,即设置为current指针
    wait_queue_func_t   func;  //唤醒方式
    struct list_head    entry; //用于链表管理
};

/* wait_queue_entry::flags */
#define WQ_FLAG_EXCLUSIVE   0x01
#define WQ_FLAG_WOKEN       0x02
#define WQ_FLAG_BOOKMARK    0x04
#define WQ_FLAG_CUSTOM      0x08
#define WQ_FLAG_DONE        0x10
#define WQ_FLAG_PRIORITY    0x20

这里,睡眠进程被分成两种,最低位为1代表的是互斥进程,这些进程在唤醒时会被有选择地唤醒;其他代表的是非互斥进程,唤醒时,内核将唤醒所有的非互斥进程。

当多个进程等待互斥资源时,同时唤醒所有进程将会导致又一次的竞争,而只有一个进程会获得互斥资源,因此其他进程又必须重新睡眠。为了避免以上的情况,才定义了互斥进程的概念,并使用flags来区分互斥进程和非互斥进程。

func字段是进程被唤醒的方式,默认会被初始化为default_wake_function()。

可以使用以下API对等待队列的队列元素进行动态的初始化:

struct wait_queue_entry wait_entry;
init_wait(&wait_entry);

或者使用静态的初始化方法:

DEFINE_WAIT(wait_entry);

以上两种方式的结果相同,以 DEFINE_WAIT 为例:

#define DEFINE_WAIT(name) DEFINE_WAIT_FUNC(name, autoremove_wake_function)

#define DEFINE_WAIT_FUNC(name, function)                    \
    struct wait_queue_entry name ={     \ 
        .private    = current,                  \
        .func       = function,                 \
        .entry      = LIST_HEAD_INIT((name).entry),         \    // 初始化链表项,next 和 prev 都指向自己
    }

struct list_head {
    struct list_head *next, *prev;
};
#define LIST_HEAD_INIT(name) { &(name), &(name) }

 

等待队列头和等待队列的关系图

等待队列的操作

将等待队列项插入等待队列

通过prepare_to_wait() 或者 prepare_to_wait_exclusive ()函数,将进程的状态改变,并将其加入对应的等待队列中:

void prepare_to_wait(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry, int state)
{
    unsigned long flags;
 
    wq_entry->flags &= ~WQ_FLAG_EXCLUSIVE;
    spin_lock_irqsave(&wq_head->lock, flags);
    if (list_empty(&wq_entry->entry))
        __add_wait_queue(wq_head, wq_entry);
    set_current_state(state);
    spin_unlock_irqrestore(&wq_head->lock, flags);
}
EXPORT_SYMBOL(prepare_to_wait);
 
void prepare_to_wait_exclusive(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry, int state)
{
    unsigned long flags;
 
    wq_entry->flags |= WQ_FLAG_EXCLUSIVE;
    spin_lock_irqsave(&wq_head->lock, flags);
    if (list_empty(&wq_entry->entry))
        __add_wait_queue_entry_tail(wq_head, wq_entry);
    set_current_state(state);
    spin_unlock_irqrestore(&wq_head->lock, flags);
}
EXPORT_SYMBOL(prepare_to_wait_exclusive);

从以上源码可以看出,prepare_to_wait() 或者 prepare_to_wait_exclusive ()函数有以下区别:

1. prepare_to_wait_exclusive ()函数会将等待队列项对应的flags的WQ_FLAG_EXCLUSIVE使能,也就是说,通过prepare_to_wait_exclusive ()函数添加的等待队列项,对应的进程被设置为互斥进程;而通过prepare_to_wait()添加的等待队列项,对应的进程被设置为非互斥进程。

2. 通过prepare_to_wait()添加的非互斥等待队列项,会被添加至等待队列的队头,而通过 prepare_to_wait_exclusive ()函数添加的互斥项,会被添加至等待队列的队尾。

二者都会改变当前进程的状态,由传入的参数state 决定。

将等待队列项从等待队列中删除

当进程被唤醒后,一般会直接调用finish_wait函数,将进程的状态重新设置为running,并将该进程对应的等待队列项从等待队列中删除:

void finish_wait(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
    unsigned long flags;
 
    __set_current_state(TASK_RUNNING);
    /*
    ¦* We can check for list emptiness outside the lock
    ¦* IFF:
    ¦*  - we use the "careful" check that verifies both
    ¦*    the next and prev pointers, so that there cannot
    ¦*    be any half-pending updates in progress on other
    ¦*    CPU's that we haven't seen yet (and that might
    ¦*    still change the stack area.
    ¦* and
    ¦*  - all other users take the lock (ie we can only
    ¦*    have _one_ other CPU that looks at or modifies
    ¦*    the list).
    ¦*/
    if (!list_empty_careful(&wq_entry->entry)) {
        spin_lock_irqsave(&wq_head->lock, flags);
        list_del_init(&wq_entry->entry);
        spin_unlock_irqrestore(&wq_head->lock, flags);
    }
}

将等待队列项唤醒

唤醒等待队列上的进程:

一般,可以通过wake_up, wake_up_all等API对唤醒队列上的进程进行唤醒。wake_up将会唤醒所有的非互斥进程,而只唤醒一个互斥进程; wake_up_all将会唤醒所有的非互斥进程,和所有的互斥进程。其他API可以参看内核源码。

 

等待队列使用实例

wait_event() 等函数里面包含了等待队列项的定义初始化、等待队列项加入等待队列、启动任务切换、唤醒后从等待队列删除,所以使用起来会方便很多

// 声明
struct xxxx_touch {
    ...
    wait_queue_head_t   wait_queue;
    ...
}
// 初始化等待队列头
static struct xxxx_touch xxxx_touch_dev = {
    ...
    .wait_queue = __WAIT_QUEUE_HEAD_INITIALIZER(xxxx_touch_dev.wait_queue),
    ...
}

// 等待
static ssize_t p_sensor_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
    struct xxxx_touch_pdata *pdata = dev_get_drvdata(dev);
    struct xxxx_touch *touch_dev = pdata->device;

    wait_event_interruptible(touch_dev->wait_queue, pdata->psensor_changed);
    pdata->psensor_changed = false;

    return snprintf(buf, PAGE_SIZE, "%d\n", pdata->psensor_value);
}

// 唤醒
int update_p_sensor_value(int value)
{
    struct xxxx_touch *dev = NULL;

    mutex_lock(&xxxx_touch_dev.psensor_mutex);

    if (!touch_pdata) {
        mutex_unlock(&xxxx_touch_dev.psensor_mutex);
        return -ENODEV;
    }

    dev = touch_pdata->device;

    if (value != touch_pdata->psensor_value) {
        MI_TOUCH_LOGI(1, "%s %s: value:%d\n", MI_TAG, __func__, value);
        touch_pdata->psensor_value = value;
        touch_pdata->psensor_changed = true;
        wake_up(&dev->wait_queue);
    }

    mutex_unlock(&xxxx_touch_dev.psensor_mutex);
    return 0;
}

其他方式使用等待队列稍微麻烦一点

DECLARE_WAIT_QUEUE_HEAD(queue);      //创建等待队列queue
DECLARE_WAITQUEUE(wait, current);    //创建等待队列元素,tsk为当前进程,wakefunc为default_wake_function


for (;;) {
    add_wait_queue(&queue, &wait);            //等待队列元素入队列
    set_current_state(TASK_INTERRUPTIBLE);    //设定当前进程可被外部信号唤醒
    if (condition)                            //如果条件成立出循环
        break;
    schedule();                               //调用调度器,让出CPU,进程休眠
    remove_wait_queue(&queue, &wait);         //被唤醒,等待队列结束,将等待队列元素出队列
    if (signal_pending(current))              //检查当前进程是否有信号处理,返回不为0表示有信号需要处理
        return -ERESTARTSYS;                  //如果有信号需要处理,则触发系统调用
}
set_current_state(TASK_RUNNING);              //设定当前进程运行态
remove_wait_queue(&queue, &wait);             //等待队列元素出队列

或者

DELARE_WAIT_QUEUE_HEAD(queue);    //创建等待队列queue
DEFINE_WAIT(wait);                //创建等待队列元素,tsk为当前进程,wakefunc为autoremove_wake_function


while (! condition) {
    prepare_to_wait(&queue, &wait, TASK_INTERRUPTIBLE);
    if (! condition)
        schedule();
    finish_wait(&queue, &wait)                    
}

 

posted @ 2023-04-22 20:33  流水灯  阅读(277)  评论(0编辑  收藏  举报