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; //用于链表管理 };
这里,睡眠进程被分成两种,最低位为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 都指向自己 }
等待队列头和等待队列的关系图
等待队列的操作
将等待队列项插入等待队列
通过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) }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通