内核等待队列以及用户态的类似机制
内核态-等待队列
参考链接:https://www.cnblogs.com/xinghuo123/p/13347964.html
参考链接:https://mp.weixin.qq.com/s/GZ8ekqzLK_zXEwR5p7bLeA
咱们今天来唠唠 Linux 内核里的 “等待队列”—— 这东西说白了,就是内核里的 “候诊室”,专门管那些暂时 “没活儿干” 的进程。你想啊,要是进程们都跟没头苍蝇似的,明明条件不满足还死盯着 CPU 要干活,那系统不得乱成一锅粥?等待队列就是那个穿白大褂的护士,柔声说:“您先坐着等,叫到号再进来。”
概括一下:“等待队列”用于解决进程等待某个条件的问题,是同步机制,涉及进程阻塞与唤醒。
一、啥是等待队列?先整个接地气的类比
想象你去奶茶店买喝的:
- 你(进程)想要一杯 “珍珠奶茶”(某个资源,比如锁、数据就绪)
- 店员说:“珍珠还在煮,您先在旁边等会儿,好了叫您”(条件不满足,进入等待队列)
- 你找个椅子坐下刷手机(进程睡眠,释放 CPU)
- 珍珠煮好了,店员喊:“等珍珠奶茶的来拿!”(条件满足,唤醒等待队列里的进程)
这就是等待队列的核心逻辑:让进程在条件不满足时乖乖睡觉,条件满足时再叫醒,避免无效占用 CPU。要是没这机制,进程就得像个愣头青似的,每隔 100ms 就跑过去问 “好了没”(忙轮询),纯属浪费感情(CPU 资源)。
二、核心 “工具”:数据结构 和 函数
内核里管理这个 “候诊室” 得有套规矩,咱们看看关键的 “道具”:
1. 等待队列头(wait_queue_head_t)
这玩意儿就是 “候诊室门口的签到台”,所有要等的进程都得来这儿排队并登记。它本质是个链表头。当多个进程等待同一个事件时,这些进程就会被组织成一个等待队列。等待队列头长这样:
/* include/linux/wait.h */
struct wait_queue_head {spinlock_t lock; // 保护队列的锁(防止多人同时改签到表)struct list_head head; // 进程链表(排队的人)
};
typedef struct wait_queue_head wait_queue_head_t;
初始化这个 “签到台” 得用init_waitqueue_head()
:
wait_queue_head_t wq;
init_waitqueue_head(&wq); // 相当于摆好签到台,准备迎接排队的人
或者你嫌弃上面两个步骤太繁琐,直接用下面这个宏:
#define DECLARE_WAIT_QUEUE_HEAD(name) \struct wait_queue_head name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
既定义了等待队列头同时也完成了初始化,你只需要给他命个名就完事儿了。内核总是熟练的让人省心。
2. 等待队列项(wait_queue_entry_t)
每个来排队的进程都得填写一张“挂号单”,记录自己是谁,等哪个医师。定义简化版长这样:
struct wait_queue_entry {unsigned int flags;void *private;wait_queue_func_t func; struct list_head entry; // 链表节点(把自己挂到签到台的链表上)
};/* include/linux/wait.h */
typedef struct wait_queue_entry wait_queue_entry_t;
动态初始化挂号单(等待队列项)用init_waitqueue_entry()
//例如:
wait_queue_entry_t wait;
init_waitqueue_entry(&wait, current); // current是当前进程,相当于"我叫张三,来排队"// init_waitqueue_entry函数实现如下
static inline void init_waitqueue_entry(struct wait_queue_entry *wq_entry, struct task_struct *p)
{wq_entry->flags = 0;wq_entry->private = p;wq_entry->func = default_wake_function;
}
,还有种静态初始化等待队列项目方法:DEFINE_WAIT(name)
#define DEFINE_WAIT_FUNC(name, function) \struct wait_queue_entry name = { \.private = current, \.func = function, \.entry = LIST_HEAD_INIT((name).entry), \}#define DEFINE_WAIT(name) DEFINE_WAIT_FUNC(name, autoremove_wake_function)
等待队列头(管理节点)与 等待队列项(个体节点)通过链表结构串联起来,关系如下图:
3. 核心"操作":“坐下等” 和 “叫号”
(1)进程 “坐下等”:wait_event 系列宏
进程主动说: “我要等,条件不满足别叫我” 的操作,最常用的是wait_event()
和wait_event_interruptible()
。
-
wait_event(wq_head, condition)
位于include/linux/wait.h
wq_head
:为等待队列头condition
:期望的条件翻译一下:“我在
wq_head
这个候诊室等候,条件condition
满足了再叫我。期间就算有人喊我(发信号),我也不搭理!"((不可中断睡眠,适合必须等到结果的场景)) -
wait_event_interruptible(wq_head, condition)
位于include/linux/wait.h
翻译一下:“我情况跟wait_event差不多,但是器件如果有紧急电话(信号)找我,我可能要先走。”(可中断睡眠,返回 0 表示正常唤醒,-ERESTARTSYS 表示被信号打断)
类比场景:wait_event
就像死等奶茶,老婆打电话说家里着火了也得等。而wait_event_interruptible
是能被信号劝走的等。
wait_event
的底层逻辑其实是个循环(防止 “虚假唤醒”,比如护士喊错人):
/* 伪代码理解 */
#define wait_event(wq, condition) \
{struct wait_queue_entry __wq_entry; //创建等待队列项 \init_wait_entry(&__wq_entry, exclusive ? WQ_FLAG_EXCLUSIVE : 0); //初始化等待队列项 \for (;;) { \if (condition) break; // 条件满足,直接走 \// 利用刚创建的等待队列项,把进程自身挂到等待队列 \add_wait_queue(&__wq_entry, &wq_entry); \// 睡过去,释放CPU \schedule(); \}
}
(2)唤醒进程:wake_up 系列函数
当条件满足时(比如珍珠煮好了),就得叫醒排队的人,常用的有:
-
wake_up(&wq)
:当前“签到台”的所有人,都起来看看条件是不是满足了,会唤醒
wait_event
和wait_event_interruptible
所在的进程。 -
wake_up_interruptible(&wq)
:只能唤醒当前“签到台”中这些
wait_event_interruptible
能被劝走的,wait_event
这种死等的继续睡。
类比场景:
三、注意事项:别在候诊室干蠢事
1. 条件判断必须用循环
内核里可能出现 “虚假唤醒”(比如别的进程误操作唤醒了你),所以wait_event
宏本身就是循环。
2. 锁和等待队列要配合好
当操作等待队列头挂的等待队列时,要用好等待队列头wait_queue_head_t
里的lock
自旋锁,避免竞态。
3. 中断里唤醒要小心
可以在中断里使用wake_up
,但绝不能使用wait_event
.
4. 别让队列变成 “死胡同”
要是唤醒后条件还是不满足,进程会重新睡过去 —— 但如果没人再唤醒它,就会 “永眠”(死锁)。比如奶茶店店员忘了喊人,你就永远坐在那儿等。所以一定要确保:唤醒操作和条件满足是绑定的。
5. wake_up函数本身创建并初始化等待队列项
查看内核代码,你会发现,wake_up
本身就创建一个 struct wait_queue_entry
类型的等待队列项。
四、总结(记住关键工具)
等待队列是内核中的智能候诊室,让该等待的进程好好睡,该干活的进程不耽误,CPU资源不浪费,记住几个关键词:
- 签到台:等待队列头(
wait_queue_head_t
),所有客人都得来这排队。 - 挂号单:等待队列项(
struct wait_queue_entry
),客人排队需要用到的挂号单(虽然这部分代码隐藏在wait_event
函数内)。 - 客人坐下等:
wait_event
系列宏,客人拿着挂号单去签到台排队。 - 服务员叫号:
wake_up
系列函数,服务员去特定的签到台队伍叫号。
五、拓展(类似功能函数)
其他与进程等待wait_event
类似的函数:
wait_event_interrupable()//使得进程处于可中断(TASK_INTERRUPTIBLE)状态,从而睡眠进程可以通过接收信号被唤醒;
wait_event_timeout()//等待满足指定的条件,但是如果等待时间超过指定的超时限制则停止睡眠,可以防止进程永远睡眠;
wait_event_interruptible_timeout() //使得进程睡眠,不但可以通过接收信号被唤醒,也具有超时限制。
与进程唤醒wake_up
类似的函数:
#define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL)
#define wake_up_nr(x, nr) __wake_up(x, TASK_NORMAL, nr, NULL)
#define wake_up_all(x) __wake_up(x, TASK_NORMAL, 0, NULL)
#define wake_up_locked(x) __wake_up_locked((x), TASK_NORMAL, 1)
#define wake_up_all_locked(x) __wake_up_locked((x), TASK_NORMAL, 0)
#define wake_up_sync(x) __wake_up_sync(x, TASK_NORMAL)#define wake_up_interruptible(x) __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
#define wake_up_interruptible_nr(x, nr) __wake_up(x, TASK_INTERRUPTIBLE, nr, NULL)
#define wake_up_interruptible_all(x) __wake_up(x, TASK_INTERRUPTIBLE, 0, NULL)
#define wake_up_interruptible_sync(x) __wake_up_sync((x), TASK_INTERRUPTIBLE)
六、代码举例
/*a simple wait_queue demo*task_1,task_2 added into the wait_queue, if condition is 0.*task_3 change condition to 1, and task_1 task_2 will be wake up*/#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_LICENSE("GPL");
MODULE_AUTHOR("cengku@gmail.com");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); //定义并初始化等待队列头,wqstatic int thread_func_1(void *data)
{int i = 0;while (i++ < 100) {wait_event(wq, condition == 1); //该函数内定义并初始化等待队列项,条件不满足,则将其挂到wqmsleep(1000);printk(">>>>>this task 1\n");}return 0;
}static int thread_func_2(void *data)
{int i = 0;while (i++ < 100) {wait_event(wq, condition == 1);msleep(1000);printk(">>>>>this task 2\n");}return 0;
}
static int thread_func_3(void *data)
{int i = 0;while (i++ < 10) {condition = 0;msleep(2000);printk(">>>>>this task 3\n");condition = 1;wake_up(&wq);msleep(2000);}return 0;
}static int __init mod_init(void)
{printk("=====mod set up===\n");condition = 0;task_1 = kthread_run(thread_func_1, NULL, "thread%d", 1);if (IS_ERR(task_1))printk("**********create thread 1 failed\n");elseprintk("======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");elseprintk("======success create thread 2\n");task_3 = kthread_run(thread_func_3, NULL, "thread%d", 3);if (IS_ERR(task_3))printk("**********create thread 3 failed\n");elseprintk("======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);if (ret > 0)printk("<<<<<<<<, ret);}if (!IS_ERR(task_2)) {ret = kthread_stop(task_2);if (ret > 0)printk("<<<<<<<<, ret);}if (!IS_ERR(task_3)) {ret = kthread_stop(task_3);if (ret > 0)printk("<<<<<<<<, ret);}
}
module_init(mod_init);
module_exit(mod_exit);
用户态-等待队列
用户态代码也有类似内核等待队列的机制,不就是让线程满足条件唤醒,不满足休眠嘛。使用pthread_cond_wait
与 pthread_cond_broadcast
这两个用户态代码实现就行了。如果理解了内核等待队列,这个也类似,且用起来更简单.涉及到的知识点 条件变量pthread_cond_t
,唤醒过程使用的信号并非系统信号,注意区分。
一、核心"工具"与用法
两个函数是条件变量机制的核心接口,用于实现线程间的”等待-通知“同步,举例子,当一些线程需要等待某个条件满足时,通过pthread_cond_wait
阻塞,当条件满足时,其他线程可以调用pthread_cond_broadcast
(或 pthread_cond_signal
)将这些等待的线程唤醒。
基本用法
- 前提:条件变量必须与互斥锁(
mutex
)配合,确保对"条件"的修改是原子操作。 - 流程:
- 线程加锁(
pthread_mutex_lock
)。 - 检查条件是否满足:
- 若不满足,调用
pthread_cond_wait
阻塞等待(此时会自动释放互斥锁,允许其他线程修改条件)。 - 若满足,执行后续操作。
- 若不满足,调用
- 其他线程修改条件后,通过
pthread_cond_broadcast
唤醒所有等待的线程(或pthread_cond_signal
唤醒一个)。 - 被唤醒的线程重新获取互斥锁,再次检查条件(防止虚假唤醒),执行后续操作并解锁。
- 线程加锁(
代码举例
#include <pthread.h>
#include <stdio.h>pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //定义互斥锁变量并静态初始化,比动态初始化更简洁
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //定义条件变量并静态初始化,用于线程间等待-通知机制
int condition = 0; // 共享条件// 等待条件的线程
void *waiter(void *arg) {pthread_mutex_lock(&mutex);// 循环检查条件(防止虚假唤醒)while (condition == 0) {printf("等待条件满足...\n");// 阻塞等待,自动释放mutex,被唤醒后重新获取mutexpthread_cond_wait(&cond, &mutex);}printf("条件已满足,执行操作...\n");pthread_mutex_unlock(&mutex);return NULL;
}// 唤醒等待的线程
void *notifier(void *arg) {pthread_mutex_lock(&mutex);condition = 1; // 修改条件printf("条件已修改,广播唤醒所有等待线程...\n");pthread_cond_broadcast(&cond); // 唤醒所有等待的线程pthread_mutex_unlock(&mutex);return NULL;
}int main() {pthread_t t1, t2, t3;pthread_create(&t1, NULL, waiter, NULL);pthread_create(&t2, NULL, waiter, NULL);pthread_create(&t3, NULL, notifier, NULL);pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_mutex_destroy(&mutex);pthread_cond_destroy(&cond);return 0;
}
运行结果:
等待条件满足...
等待条件满足...
条件已修改,广播唤醒所有等待线程...
条件已满足,执行操作...
条件已满足,执行操作...
二、功能类比
- 内核等待队列:内核态中,进程 / 线程通过
wait_event
等宏等待事件,通过wake_up
等函数唤醒,用于内核内部同步。 - pthread 条件变量:用户态中,线程通过
pthread_cond_wait
等待条件,通过pthread_cond_broadcast
唤醒,用于用户态线程同步。
两者本质都是 “等待 - 通知” 机制,只是处于不同的运行态(内核态 vs 用户态)。但是用户态的接口更简单,使用起来省心多了,但是本质上也依赖等待队列机制。
三、总结
pthread_cond_wait
和pthread_cond_broadcast
是用户态 pthread 库的函数,用于用户态线程同步。- 用法上需配合互斥锁,通过 “等待 - 广播” 机制实现线程间的条件同步。
- 它们的底层实现依赖内核的等待队列(或类似机制),是内核态同步原语在用户态的封装。