1. 引言
在 Linux 设备驱动开发中,我们经常需要将一些耗时操作从中断上下文或原子上下文"推迟"到进程上下文执行。内核提供了多种机制来实现这种推迟执行,例如软中断、tasklet 和工作队列。其中,工作队列(workqueue)允许驱动程序将工作推迟到内核线程的上下文中执行,从而可以睡眠、可以获取信号量,使用起来非常灵活。
本篇文章将聚焦于内核提供的全局工作队列 (即系统默认工作队列 system_wq),介绍其基本用法、常用 API、注意事项,并通过一个完整的字符设备驱动示例演示如何在驱动中使用全局工作队列。
2. 为什么需要工作队列
在编写设备驱动时,中断处理程序(top half)必须快速执行并返回,不能在其中执行可能睡眠的操作。然而,很多设备的处理流程中不可避免地存在需要睡眠的操作,例如:
- 等待硬件完成某个操作;
- 使用休眠锁(mutex)保护共享资源;
- 进行耗时的内存分配或 I/O 操作。
为了解决这个问题,内核提供了"下半部"(bottom half)机制。工作队列正是下半部机制的一种实现,它将工作推迟到进程上下文(内核线程)中执行,因此可以睡眠。
与 tasklet 相比,工作队列运行在进程上下文,可以睡眠;而 tasklet 运行在软中断上下文,不能睡眠。因此,当驱动需要在下半部中执行可能睡眠的操作时,工作队列是更合适的选择。
3. 全局工作队列概述
内核维护了一个默认的工作队列,称为全局工作队列 ,对应的内核线程是 kworker。驱动开发者可以直接使用这个全局工作队列,而无需自己创建和管理工作队列线程。
全局工作队列对应的导出符号为 system_wq,在 kernel/workqueue.c 中定义,并通过 kernel/workqueue.h 头文件对外暴露。
使用全局工作队列的好处是简单方便,无需额外创建工作队列线程,适合执行频率不高、执行时间较短的任务。但需要注意的是,全局工作队列被系统中的所有模块共享,如果某个任务执行时间过长,可能会影响其他模块的任务调度。
4. 核心 API 介绍
4.1 初始化工作项
使用全局工作队列前,需要先定义一个 struct work_struct 结构体变量,并对其进行初始化。
c
#include <linux/workqueue.h>
struct work_struct my_work;
INIT_WORK(&my_work, my_work_handler);
其中 my_work_handler 是工作处理函数,其原型为:
c
void my_work_handler(struct work_struct *work);
4.2 调度工作
将工作项提交到全局工作队列中执行,使用 schedule_work 函数:
c
int schedule_work(struct work_struct *work);
该函数将工作项加入到全局工作队列 system_wq 中。如果工作项当前未在队列中,则返回 1;如果已经在队列中,则返回 0。
4.3 延迟调度
如果需要延迟一段时间再执行工作,可以使用 schedule_delayed_work:
c
int schedule_delayed_work(struct delayed_work *dwork, unsigned long delay);
使用延迟工作队列时,需要将工作项定义为 struct delayed_work 类型,并使用 INIT_DELAYED_WORK 进行初始化:
c
struct delayed_work my_delayed_work;
INIT_DELAYED_WORK(&my_delayed_work, my_delayed_work_handler);
其中 delay 参数的单位是 jiffies。
4.4 取消工作
在驱动卸载或不再需要执行该工作时,应取消尚未执行的工作项:
c
int cancel_work_sync(struct work_struct *work);
int cancel_delayed_work_sync(struct delayed_work *dwork);
带 _sync 后缀的版本会等待工作处理函数执行完毕后才返回,确保在驱动卸载时不会出现工作项仍在执行而模块已被卸载的情况。
5. 完整驱动示例
下面通过一个完整的字符设备驱动示例,演示如何使用全局工作队列。该驱动在 open 时调度一个工作项,在 release 时取消工作项。
c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/workqueue.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "wq_demo"
#define CLASS_NAME "wq_demo_class"
static dev_t dev_num;
static struct cdev wq_cdev;
static struct class *wq_class;
static struct device *wq_device;
/* 工作项 */
static struct work_struct my_work;
/* 工作处理函数 */
static void my_work_handler(struct work_struct *work)
{
printk(KERN_INFO "wq_demo: work handler executed in process context\n");
/* 这里可以执行可能睡眠的操作,例如获取 mutex、进行 I/O 等 */
}
/* 打开设备 */
static int wq_open(struct inode *inode, struct file *filp)
{
printk(KERN_INFO "wq_demo: device opened, scheduling work\n");
/* 将工作项提交到全局工作队列 */
schedule_work(&my_work);
return 0;
}
/* 关闭设备 */
static int wq_release(struct inode *inode, struct file *filp)
{
printk(KERN_INFO "wq_demo: device released, cancelling work\n");
/* 取消尚未执行的工作项,并等待正在执行的工作完成 */
cancel_work_sync(&my_work);
return 0;
}
/* 文件操作结构体 */
static struct file_operations wq_fops = {
.owner = THIS_MODULE,
.open = wq_open,
.release = wq_release,
};
/* 模块加载 */
static int __init wq_demo_init(void)
{
int ret;
/* 初始化工作项 */
INIT_WORK(&my_work, my_work_handler);
/* 分配设备号 */
ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
if (ret < 0) {
printk(KERN_ERR "wq_demo: failed to allocate device number\n");
return ret;
}
/* 初始化 cdev 并添加到内核 */
cdev_init(&wq_cdev, &wq_fops);
wq_cdev.owner = THIS_MODULE;
ret = cdev_add(&wq_cdev, dev_num, 1);
if (ret < 0) {
printk(KERN_ERR "wq_demo: failed to add cdev\n");
unregister_chrdev_region(dev_num, 1);
return ret;
}
/* 创建设备类 */
wq_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(wq_class)) {
printk(KERN_ERR "wq_demo: failed to create class\n");
cdev_del(&wq_cdev);
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(wq_class);
}
/* 创建设备节点 */
wq_device = device_create(wq_class, NULL, dev_num, NULL, DEVICE_NAME);
if (IS_ERR(wq_device)) {
printk(KERN_ERR "wq_demo: failed to create device\n");
class_destroy(wq_class);
cdev_del(&wq_cdev);
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(wq_device);
}
printk(KERN_INFO "wq_demo: module loaded\n");
return 0;
}
/* 模块卸载 */
static void __exit wq_demo_exit(void)
{
/* 确保工作项被取消 */
cancel_work_sync(&my_work);
device_destroy(wq_class, dev_num);
class_destroy(wq_class);
cdev_del(&wq_cdev);
unregister_chrdev_region(dev_num, 1);
printk(KERN_INFO "wq_demo: module unloaded\n");
}
module_init(wq_demo_init);
module_exit(wq_demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple demo of using global workqueue in Linux driver");
5.1 编译与测试
编写 Makefile:
makefile
obj-m := wq_demo.o
KDIR := /lib/modules/$(shell uname -r)/build
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
编译并加载模块:
bash
make
sudo insmod wq_demo.ko
创建设备节点(如果 udev 没有自动创建):
bash
sudo mknod /dev/wq_demo c $(grep wq_demo /proc/devices | awk '{print $1}') 0
测试:
bash
cat /dev/wq_demo
查看内核日志:
bash
dmesg | tail
可以看到类似如下的输出:
wq_demo: device opened, scheduling work
wq_demo: work handler executed in process context
wq_demo: device released, cancelling work
6. 使用注意事项
6.1 工作处理函数可以睡眠
工作队列运行在进程上下文(内核线程),因此工作处理函数中可以调用可能睡眠的函数,例如 mutex_lock、wait_event、msleep 等。这是工作队列相比 tasklet 最大的优势。
6.2 避免长时间占用全局工作队列
全局工作队列被整个系统共享,如果某个驱动的工作处理函数执行时间过长,会阻塞其他模块的工作项调度。因此,对于耗时较长的任务,建议创建专用工作队列 (create_workqueue / alloc_workqueue),而不是使用全局工作队列。
6.3 卸载模块前必须取消工作
在模块卸载函数中,必须调用 cancel_work_sync 或 cancel_delayed_work_sync,确保工作项不会在模块卸载后仍然执行。否则可能导致内核崩溃或非法内存访问。
6.4 工作项不可重复初始化
同一个 work_struct 在未完成或未取消之前,不应再次调用 INIT_WORK 重新初始化,否则可能导致未定义行为。
6.5 注意并发与重入
同一个工作项可能被多次调度。如果工作处理函数尚未执行完毕,再次调用 schedule_work 不会再次加入队列,而是返回 0。如果需要保证每次调度都执行一次,可以考虑使用 system_unbound_wq 或自行管理状态。
7. 总结
本文介绍了 Linux 内核全局工作队列的基本概念和使用方法。通过 INIT_WORK 初始化工作项、schedule_work 调度工作、cancel_work_sync 取消工作,驱动开发者可以方便地将耗时操作推迟到进程上下文执行。
全局工作队列使用简单,适合轻量级、低频的任务;对于高频或耗时的任务,建议使用专用工作队列。在后续文章中,我们将进一步介绍专用工作队列的创建与使用,以及工作队列与 tasklet、软中断的对比与选型。