Linux Kernel 中 Workqueue 使用系统默认队列和创建队列的方法

本文深入探讨了 Linux 内核中的 workqueue 机制,包括默认队列 system_wq 的使用,自定义 workqueue 的创建方法,以及 work 和 queue 的绑定方式。特别介绍了如何避免系统 default 队列中 work 过多导致的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

关于workqueue,网上资料爆翻天。当然即便是这样,对此我们还是有很多话要说安静
想必大家对workqueue相关的函数(schedule_work 、queue_work、INIT_WORK、create_singlethread_workqueue 等)都不陌生。但说起差异,可能还有许多话需要坐下来慢慢讲。
对于workqueue,与之最为相关的两个东西便是work和queue。
work是用来绑定实际执行函数使用的结构体
queue是用来链接work使用的队列。
具体的结构体,可以自己到/kernel/include/linux/workqueue.h中自行查看,这里不再赘述。
我们想关注的重点在于:
1:系统中是否有default的workqueue供我们使用
2:我们能否创建自己的workqueue?如何创建?
Yes!你说对了。linux系统所提供的workqueue机制中,已经帮忙提供了一个默认的workqueue队列“system_wq”,并提供了一套函数来方便大家直接使用。
例子来了:
          

static struct work_struct work;

          
INIT_WORK(&workrun);
schedule_work(&work);

static void run(struct work_struct *work)
{
     Do something here!!
}
    
就这么简单的,当然,你也可以用DECLARE_WORK来完成和INIT_WORK同样初始化work的工作。区别是DECLARE_WORK是预编译命令,而INIT_WORK可以在code中动态初始化。
那么除了调用schedule_work直接把work放到系统defaultworkqueue中外,我们还有什么办法可以初始化自己的workqueue,并且放入work呢?

我们看看函数schedule_work的定义,一切就真相大白了!
static inline bool schedule_work(struct work_struct *work)
{
     return queue_work(system_wq, work);
}
哈哈,原来schedule_work是把传入的work直接放入了系统的default workqueue “system_wq”中而已。
自然,我们只需要调用queue_work函数来绑定workqueue和work就ok啦!
初始化work的方法和前面一样,只要调用DECLARE_WORKINIT_WORK就好了。
那么我们如何去创建自己的workqueue呢?
答案是:
#define alloc_ordered_workqueue(fmt, flags, args…)               \
     alloc_workqueue(fmt, WQ_UNBOUND | __WQ_ORDERED | (flags), 1, ##args)
#define create_workqueue(name)                              \
     alloc_workqueue((name), WQ_MEM_RECLAIM, 1)

#define create_freezable_workqueue(name)                    \
     alloc_workqueue((name), WQ_FREEZABLE | WQ_UNBOUND | WQ_MEM_RECLAIM, 1)

#define create_singlethread_workqueue(name)                    \
     alloc_workqueue((name), WQ_UNBOUND | WQ_MEM_RECLAIM, 1)
我们只要调用如上几种方法的某一种,即可创建属于自己的workqueue了。

kernel中最常见到的函数是create_singlethread_workqueue。 

我们只要拿这个函数举个例子相信世界就美好了,栗子来喽!

static struct workqueue_struct *time_sync_wq;
time_sync_wq = create_singlethread_workqueue(“timesync“); //timesync就是workqueue的名字
static DECLARE_WORK(etr_worketr_work_fn);
queue_work(time_sync_wq, &etr_work);

这样一来,我们的work和自己的workqueue就绑在一起了。
个人认为,自己新建wq最大的好处:可以避免system_wq中被挂的work过多,或者由于某个被挂上去的work处理函数质量不高导致死锁,而导致挂在同一个queue上的我们自己work的handler因为无法被调度到而完蛋了。
当然,建太多自己的workqueue,必然会导致系统调度开销的变大,所以需要取舍。
至于DELAYED_WORK

#define INIT_DELAYED_WORK(_work, _func)                         \

     __INIT_DELAYED_WORK(_work, _func, 0)

queue_delayed_work

schedule_delayed_work

都和前面类似,就不再赘述了。

随后,咱们可以调用cancel_delayed_work来把还未执行的work给取消掉。
基本上每次 cancel_delayed_work 之后您都得调用flush_scheduled_work() 这个函数 , 特别是对于内核模块 , 如果一个模块使用了工作队列机制 , 并且利用了系统的default队列 , 那么在卸载这个模块之前 , 您必须得调用这个函数 , 这叫做刷新一个工作队列 , 也就是说 , 该函数会一直等待 , 直到队列中所有对象都被执行以后才返回 ,从而避免队列调度错误。
函数cancel_delayed_work_sync的出现,让新的流程变得更加简单了,大家参照kernel中的代码,很容易知道应该怎么用。
最后别忘了调用destroy_workqueue等清尾的函数哦~~

问题来了:

如果我们在handler的执行过程中,同时再次调用调度函数queue_work,那么我们的handler会被执行多少次呢?(究竟是执行被调度的次数,还是就只执行一次呢?)


解答:

这个问题比较有意思,写了这个例子来验证答案(例子跑在android 4.4 code base中)

sample test code:

static struct workqueue_struct *test_wq;


static void try_to_test(struct work_struct *work)
{
          printk(“[bevis] :wq into \n”);
          msleep(5*1000);  //5s
          printk(“[bevis] :wq out \n”);         
}
static DECLARE_WORK(mytest_work, try_to_test);
gsensor probe function end add :
    test_wq = alloc_ordered_workqueue(“test_wq”, 0); //初始化一个单独的工作队列
     int a = 0;
     for(a=0 ; a<3 ; a++){
     printk(“[bevis] : read func (%d) before \n”,a);
     queue_work(test_wq, &mytest_work); //让这个队列开始被调度
     printk(“[bevis] : read func (%d) msleep 2s \n”,a);
     msleep(2*1000);
     printk(“[bevis] : read func (%d) after \n”,a);
     }

log如下:


10-16 14:10:41.940 I/KERNEL  (  109): [    6.954658] [bevis] : read func (0) before
10-16 14:10:41.940 I/KERNEL  (  109): [    6.954727] [bevis] : read func (0) msleep 2s
10-16 14:10:41.940 I/KERNEL  (  109): [    6.954742] [bevis] :wq into
10-16 14:10:43.950 I/KERNEL  (  109): [    8.960997] [bevis] : read func (0) after
10-16 14:10:43.950 I/KERNEL  (  109): [    8.961085] [bevis] : read func (1) before
10-16 14:10:43.950 I/KERNEL  (  109): [    8.961155] [bevis] : read func (1) msleep 2s
10-16 14:10:45.960 I/KERNEL  (  109): [   10.971954] [bevis] : read func (1) after
10-16 14:10:45.960 I/KERNEL  (  109): [   10.972076] [bevis] : read func (2) before
10-16 14:10:45.960 I/KERNEL  (  109): [   10.972132] [bevis] : read func (2) msleep 2s
10-16 14:10:46.950 I/KERNEL  (    6):  [   11.961884] [bevis] :wq out
10-16 14:10:46.950 I/KERNEL  (    6):  [   11.961953] [bevis] :wq into
10-16 14:10:47.970 I/KERNEL  (  109): [   12.982276] [bevis] : read func (2) after
10-16 14:10:51.960 I/KERNEL  (    6):  [   16.973719] [bevis] :wq out 


看到了吧,虽然我们使用queue_work函数调度了三次handler,但实际上wq的handler只被执行了两次。
如果把probe函数的delay直接拿掉,你更加会发现,即使wq被调度三次,handler却实际上只跑了一次。

结论

如果wq被调度的时候,wq中的这个handler正在执行过程中,则这次调度会被遗弃。只有handler执行完成并返回后,下次调度才会真正的生效。

kernel这么做的原因,我猜想应该是为了防止,当某个wq的handler在执行过程中因为资源无法获取而暂时阻塞时,
不会因为其他进程再次调度了该wq而导致出现线程实例的不断累加。
实际上,在绝大多数情况下,我们只需要一个handler实例来帮忙做事就够了,例如earlysuspend的处理函数中,只要userspace进行想睡眠,那就直接调度suspend wq的handler,而不必管再关心上次的suspend过程是否有阻塞。
这样一来,逻辑就清爽多了。对吧!


### Linux Kernel Workqueue Implementation and Usage #### 工作队列概述 工作队列Workqueue)是一种用于推迟执行任务的机制,允许开发者将某些任务提交给内核线程来处理。这种设计的主要目的是为了简化异步任务调度并提高系统的可扩展性性能。 在Linux内核中,`work_struct` 是表示工作的核心数据结构[^1]。当一个 `work_struct` 被加入某个工作队列时,它会被安排在一个特定的上下文中运行。如果该工作被标记为不可中断,则会在进程上下文中运行;如果是可中断的任务,则可能由专门的内核线程负责完成。 #### 创建管理不同类型的工作队列 - **通用绑定型工作队列 (Bound Workqueues)** 默认情况下,大多数工作会分配到全局共享的工作队列上,这些队列中的工作者线程通常绑定了具体的 CPU 核心。这意味着它们倾向于在同一颗处理器上运行以减少缓存失效带来的开销。然而,在多核环境中,这可能导致负载不均衡的情况发生。 - **无约束工作队列 (Unbound Workqueues)** 对于那些希望能够在任意可用的核心之上灵活迁移其作业场景的应用程序来说,“未受限制”的工作队列提供了更大的灵活性。通过调用函数 `alloc_workqueue()` 并指定参数标志位 WQ_UNBOUND 可创建此类实例。 - **有序单一线程工作队列 (Ordered Single-threaded Workqueues)** 使用宏定义 `create_singlethread_workqueue()` 或者直接调用底层实现 `alloc_ordered_workqueue()` 来建立仅含单一工作者线程且按序执行所有请求项的目标对象。这类特殊形式特别适用于串行化操作需求较高的场合下使用。 #### 提交与销毁工作任务流程解析 要向已存在的某条路径里追加新的待办事项清单项目,需先初始化对应的实体变量并通过标准 API 方法将其挂载至目标容器内部: ```c INIT_WORK(&example_work, example_func); schedule_work(&example_work); ``` 上述代码片段展示了如何准备以及规划一次简单的后台计算活动。其中 `INIT_WORK` 宏用来配置初始状态信息而无需额外分配内存资源;随后借助 `schedule_work` 函数即可正式触发整个链条运转起来。 对于不再需要继续保留下来的旧有记录而言,则应当及时清理释放关联存储单元以免造成泄漏隐患: ```c cancel_work_sync(&example_work); destroy_workqueue(example_wq); ``` 这里需要注意的是,在彻底摧毁之前必须确认没有任何残留动作正在进行当中,因此推荐采用同步取消策略确保安全退出条件达成后再执行最终拆除步骤。 #### 实际案例分析 - 冻结进程功能模块剖析 下面摘录了一段来自电源管理系统部分源码的内容作为例子进一步阐释实际应用层面的知识要点: ```c int freeze_processes(void){ pm_freezing = true; try_to_freeze_tasks(true); } static int try_to_freeze_tasks(bool user_only){ for_each_process_thread(g, p) { freeze_task(p); } } ``` 此段逻辑主要实现了暂停全部活跃线程的功能以便后续能够顺利进入低功耗模式阶段。具体做法是遍历当前系统中存在的每一个独立个体,并逐一对他们施加重启保护措施直至满足预期效果为止[^2]。 --- ### 示例代码展示 以下是关于自定义构建专属版本延迟响应型工作队列的一个简单演示样例: ```c #include <linux/workqueue.h> #include <linux/delay.h> struct delayed_example_data { struct delayed_work dwork; }; void my_delayed_handler(struct work_struct *work) { printk(KERN_INFO "Delayed task executed.\n"); } static void setup_my_dwork(struct delayed_example_data *data) { INIT_DELAYED_WORK(&data->dwork, my_delayed_handler); } // Schedule the delayable work to run after 5 seconds. setup_my_dwork(data_ptr); schedule_delayed_work(&data_ptr->dwork, msecs_to_jiffies(5000)); ``` 以上脚本清楚地描述了怎样设立一套具备时间间隔特性的自动化运维框架体系架构图谱。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值