参考:https://www.cnblogs.com/apprentice89/archive/2013/05/09/3068274.html
内核版本:4.4
源码地址:https://gitee.com/llcccc/qemu_arm/tree/master/SourceCode/Driver/2020-12/01_wait_queue_test
1 简单的例子
1.1 测试源码
先使用一个简单的例子来测试一下wait_queue的基本使用, 然后再进行结构体讲解和源码分析
代码基本的思路如下:
第1, 创建一个wait queue头
第2, 代码的一处地方调用wait_event进行等待
第3, 代码的另一处地方调用wake_up进行唤醒
代码实际创建了2个线程, 一个负责调用wait_event, 一个负责调用wake_up
#include <linux/device.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/platform_device.h>
#include <asm/uaccess.h>
#include <linux/sched.h> //wake_up_process()
#include <linux/kthread.h> //kthread_create()、kthread_run()
#include <linux/delay.h>
#include <linux/time.h>
#include "my_error.h"
/* 代码思路
1 定义等待队列头
2 创建线程1 用于执行wait_event
3 创建线程2 用于wake up
执行流程
1 线程 add_wait_func 执行wait_event, 等待唤醒
2 线程 add_wait_func 执行wake_up 唤醒,让执行wait_event 继续执行
*/
#define WQ_TEST_NAME "wait queue test"
// 定义等待队列头
static DECLARE_WAIT_QUEUE_HEAD(wq_head);
static struct task_struct *add_wait1 = NULL;
static struct task_struct *up_wait = NULL;
static int count = 0; // 当为0时,满足wait_event唤醒条件
static int break_flag = 0; // 线程退出标志位
static int add_wait_func(void *data)
{
while(1)
{
if (break_flag)
{
break;
}
// 代码卡在这里, 直到有人调用wake_up唤醒
wait_event(wq_head, count == 0);
printk("wait event done \n");
count = 1;
}
return 0;
}
static int up_wait_func(void *data)
{
while(1)
{
if (break_flag)
{
break;
}
msleep(1000);
printk("wait up done\n");
count = 0; // 让wait_event满足唤醒条件
// 唤醒wait event
wake_up(&wq_head);
}
return 0;
}
static int wq_test_init(void)
{
int ret = 0;
add_wait1 = kthread_create(add_wait_func, NULL, "add_wait1");
if(IS_ERR(add_wait1))
{
PRINT_ERR("crate thread fail \n");
ret = PTR_ERR(add_wait1);
add_wait1 = NULL;
return ret;
}
up_wait = kthread_create(up_wait_func, NULL, "up_wait");
if(IS_ERR(up_wait))
{
PRINT_ERR("crate thread fail \n");
ret = PTR_ERR(up_wait);
up_wait = NULL;
return ret;
}
count = 1;
break_flag = 0;
wake_up_process(add_wait1);
wake_up_process(up_wait);
PRINT_INFO("%s init \n", WQ_TEST_NAME);
return 0;
}
static void wq_test_exit(void)
{
break_flag = 1;
// 唤醒 add_wait_func线程, 避免卡住
count = 0;
wake_up(&wq_head);
kthread_stop(add_wait1);
kthread_stop(up_wait);
PRINT_INFO("%s exit \n", WQ_TEST_NAME);
}
module_init(wq_test_init);
module_exit(wq_test_exit);
MODULE_LICENSE("GPL");
1.2 测试log
通过log可以看到,只有当调用完wait up后, wait event才会被唤醒
2 源码分析
2.1 主要结构体介绍
涉及的结构体非常简单, 主要就2个, 一个是等待队列头, 一个是等待队列
等待队列头就如同链表头一样,后续会把等待队列挂载到等待队列头上面
等待队列头
// 等待队列头的结构体
struct __wait_queue_head {
spinlock_t lock; // 锁,操作链表肯定需要锁来进行保护
struct list_head task_list; //链表
};
typedef struct __wait_queue_head wait_queue_head_t;
等待队列
typedef struct __wait_queue wait_queue_t;
typedef int (*wait_queue_func_t)(wait_queue_t *wait, unsigned mode, int flags, void *key);
struct __wait_queue {
unsigned int flags; //标志位
void *private; // 私有变量, 一般指向current
wait_queue_func_t func; // 回调函数
struct list_head task_list; // 链表
};
2.2 wait_event 主要流程
wait_event实际是一个宏,有2个参数, 第1个参数是等待队列头,所以就需要先定义一个等待队列头。一般使用宏 DECLARE_WAIT_QUEUE_HEAD 来实现, 就是简单的给里面的变量赋初始值
#define __WAIT_QUEUE_HEAD_INITIALIZER(name) { \
.lock = __SPIN_LOCK_UNLOCKED(name.lock), \
.task_list = { &(name).task_list, &(name).task_list } }
#define DECLARE_WAIT_QUEUE_HEAD(name) \
wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
wait_event第2个参数是用来当做判断条件的,如果为真则跳出wait_event, 为假的话才会往后执行
#define wait_event(wq, condition) \
do { \
// might_sleep 可以忽略, 主要是起到调试作用
might_sleep(); \
// 条件为真的话就直接返回了
if (condition) \
break; \
__wait_event(wq, condition); \
} while (0)
__wait_event 只是一层封装,继续调用 ___wait_event
#define __wait_event(wq, condition) \
// 这里是3条下划线
(void)___wait_event(wq, condition, TASK_UNINTERRUPTIBLE, 0, 0, \
schedule())
___wait_event 有2个重要的函数, 一个是prepare_to_wait_event, 会把等待队列头和等待队列关联起来了
另一个是schedule(),由cmd = schedule()传入进来, 进行调度让出CPU
// 本来是一个宏,因为每行都有续行符导致看起来比较碍眼, 所以全部删除了,就当做函数来看吧
#define ___wait_event(wq, condition, state, exclusive, ret, cmd)
({
__label__ __out;
// 定义了一个等待队列
wait_queue_t __wait;
long __ret = ret; /* explicit shadow */
// 初始化链表
INIT_LIST_HEAD(&__wait.task_list);
// exclusive = 0,
if (exclusive)
__wait.flags = WQ_FLAG_EXCLUSIVE;
else
// yes
__wait.flags = 0;
for (;;) {
// state = TASK_UNINTERRUPTIBLE
// 调用了函数 prepare_to_wait_event,后面分析
long __int = prepare_to_wait_event(&wq, &__wait, state);
// 再次判断是否满足退出条件
if (condition)
break;
// __int 一般返回0, 所以这里条件不会成立
if (___wait_is_interruptible(state) && __int) {
__ret = __int;
// exclusive = 0, 不满足
if (exclusive) {
abort_exclusive_wait(&wq, &__wait,
state, NULL);
goto __out;
}
break;
}
// cmd = schedule()
// cmd 是schedule, 到这里当前进程就让出了CPU, 休眠在了这里, 以此这里不会消耗CPU了
cmd;
}
// 后面分析
finish_wait(&wq, &__wait);
__out: __ret;
})
再来分析 ___wait_event 里面的prepare_to_wait_event
long prepare_to_wait_event(wait_queue_head_t *q, wait_queue_t *wait, int state)
{
unsigned long flags;
if (signal_pending_state(state, current))
return -ERESTARTSYS;
// 设置私有变量
wait->private = current;
// 设置了回调函数, 这里划重点
wait->func = autoremove_wake_function;
spin_lock_irqsave(&q->lock, flags);
// 如果等待队列的链表是空的,则连接到等待队列头上
if (list_empty(&wait->task_list)) {
if (wait->flags & WQ_FLAG_EXCLUSIVE)
__add_wait_queue_tail(q, wait);
else
// wait->flags = 0, 添加到链表头
__add_wait_queue(q, wait);
}
// state = TASK_UNINTERRUPTIBLE
// 当前进程设置为不可打断状态
set_current_state(state);
spin_unlock_irqrestore(&q->lock, flags);
return 0;
}
到这里,等待队列头和等待队列才关联起来了
finish_wait 函数很简单,主要就是删除链表
void finish_wait(wait_queue_head_t *q, wait_queue_t *wait)
{
unsigned long flags;
// 当前进程设置为运行状态
__set_current_state(TASK_RUNNING);
// 等待队列从链表中删除
// 这里判断的时候比较小心, 因为当调用wake_up时也会有同样的操作
if (!list_empty_careful(&wait->task_list)) {
spin_lock_irqsave(&q->lock, flags);
list_del_init(&wait->task_list);
spin_unlock_irqrestore(&q->lock, flags);
}
}
大体的流程如下图
2.3 wake_up 主要流程
讲完了wait_event, 接下来就是讲如何唤醒,唤醒就调用wake_up
wake_up 只用传一个参数,那就是等待队列头, 不过wake_up是一个宏,里面还有其他一些默认的参数
#define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL)
__wake_up 主要就是调用 __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);
// mode = TASK_NORMAL, nr_exclusive = 1, key = NULL
// 调用 __wake_up_common
__wake_up_common(q, mode, nr_exclusive, 0, key);
spin_unlock_irqrestore(&q->lock, flags);
}
__wake_up_common 主要就是遍历等待队列头的链表, 将节点依次取出来, 然后调用回调函数
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;
// mode = TASK_NORMAL, nr_exclusive = 1, wake_flags = 0, key = NULL
list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
// 之前传入的curr->flags = 0
unsigned flags = curr->flags;
// 回调 autoremove_wake_function
if (curr->func(curr, mode, wake_flags, key) &&
(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
}
}
autoremove_wake_function 就做2件事, 1 调用 default_wake_function, 2 删除节点, 因为这里也有删除节点的操作,因此在finish_wait才会调用list_empty_careful 小心翼翼的删除节点
int autoremove_wake_function(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
// 执行成功返回1
int ret = default_wake_function(wait, mode, sync, key);
if (ret)
list_del_init(&wait->task_list);
return ret;
}
default_wake_function 会调用try_to_wake_up, 这个函数比较复杂, 但最后会把当前进程设置为TASK_RUNNING,加入到CPU队列中去运行从而达到唤醒功能, 执行成功的话会返回1
int default_wake_function(wait_queue_t *curr, unsigned mode, int wake_flags,
void *key)
{
return try_to_wake_up(curr->private, mode, wake_flags);
}
wake_up大致流程如下
3 总结
主要流程如下
1 定义等待队列头
2 调用wait_event进行等待
2.1) wait_event 首先把自己链接到等待队列头的链表中
2.2) 然后调用schedule()让出CPU
====>3 调用 wake_up 进行唤醒
====>3.1)最后调用try_to_wake_up,把进程添加到CPU队列中, 相当于唤醒当前进程
====>3.2)删除节点
2.3) 当被唤醒后, 调用finish_wait把自己从链表中删除
其实wait_event 被唤醒时还要判断第2个参数是否为真, 为真则终止等待, 如果只把第2个参数设置为真是否可以结束等待?
答案是不行, 因此进程已经调用了schedule(), 需要重新添加到CPU队列中才可以