news 2026/9/30 5:49:09

Linux内核延迟工作队列schedule_delayed_work实战与避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux内核延迟工作队列schedule_delayed_work实战与避坑

1. 从真实场景看延迟工作队列的价值

写内核模块的人迟早会碰到一个需求:某个动作不能立刻做,得等一会儿再执行。比如按键驱动要去抖,硬件中断里不能睡,但20毫秒后需要读一次寄存器确认状态;又比如传感器轮询,每500毫秒采集一次数据;再比如某个错误发生后,等1秒再尝试恢复。这些场景如果直接用定时器,就得自己管理timer_list,还要在工作队列里跑可能睡眠的操作,两套机制拼起来容易出竞态。schedule_delayed_work恰好把这两件事揉在一起:它让一个工作项在指定的延迟时间之后被工作队列执行,既有定时器的延迟能力,又有工作队列可以睡眠、可以阻塞的上下文优势。

我最早接触这个API是在一个GPIO按键驱动里。中断处理函数只做标记,然后调用schedule_delayed_work安排20ms后去抖,时间到了工作函数在进程上下文运行,可以安全地msleep、可以拿互斥锁、可以调用可能睡眠的I2C接口。相比之前用tasklet加jiffies轮询的方案,代码量少了一半,稳定性还更好。后来做嵌入式Linux项目,电源管理、热插拔检测、网络链路状态恢复,几乎每个模块都能见到它的身影。

这篇文章面向的是已经写过简单字符设备、知道module_init和module_exit怎么写的开发者,也适合刚接触内核工作队列、想搞明白schedule_delayed_work和普通schedule_work区别的人。我会从数据结构讲起,然后给一个能编译加载的完整模块,再把实际调试中遇到的重入、取消、时间不准等问题逐个拆开。文章不会停留在API手册的层面,而是把参数计算、调用时机、卸载顺序这些容易翻车的地方讲透。如果你正在写驱动或者维护内核模块,这些内容可以直接拿去对照修改。

1.1 延迟工作队列到底解决了哪些实际问题

内核里延迟执行的需求大致分三类。第一类是硬件相关的去抖和稳定等待,比如机械按键、继电器、电源开关,信号跳变后需要等几毫秒到几十毫秒再采样。第二类是周期性任务,比如温度传感器每200ms读一次,网络PHY每1秒查一次链路状态。第三类是错误恢复和超时处理,比如USB设备枚举失败后延迟重试,DMA传输超时后延迟清理。这三类场景的共同点是:不能在中软中断或原子上下文里完成,必须推到进程上下文;同时又不能立刻执行,必须等一个确定的时间。

schedule_delayed_work的工作模型很直接:调用者给出一个struct delayed_work和一个延迟时间(单位是jiffies),内核把工作项挂到对应工作队列的延迟链表上,同时启动一个内核定时器。定时器到期后,回调函数把工作项从延迟链表移到普通工作链表,唤醒工作队列的内核线程去执行work_func_t。整个过程对调用者透明,你只需要关心两件事:延迟多久、工作函数里做什么。

注意:schedule_delayed_work本身可以在中断上下文调用,因为它内部只操作自旋锁和定时器,不会睡眠。但工作函数是在进程上下文执行的,里面可以睡眠。这个区别是理解整个机制的关键。

相比直接用timer_list加schedule_work,schedule_delayed_work省去了自己维护定时器结构、自己处理定时器和工作项之间竞态的麻烦。尤其是取消操作,cancel_delayed_work_sync会同时取消定时器并等待正在执行的工作函数结束,这个同步语义如果用裸定时器实现,需要写不少代码才能保证正确。

1.2 普通工作队列和延迟工作队列的关系

普通工作队列用struct work_struct,延迟工作队列用struct delayed_work。后者内部包含一个struct work_struct和一个struct timer_list。你可以把delayed_work看作一个带闹钟的工作项:闹钟没响之前,工作项不会被执行;闹钟响了,工作项才进入普通工作队列的待执行队列。

系统默认的工作队列是system_wq,还有system_highpri_wq、system_long_wq、system_unbound_wq等。schedule_delayed_work默认把工作项挂到system_wq上。如果你的工作函数执行时间很长,或者需要并发执行多个实例,可能需要考虑自定义工作队列,这个后面会细说。

很多人会混淆schedule_delayed_work和queue_delayed_work。前者使用系统默认工作队列,后者需要显式指定工作队列。schedule_delayed_work本质上就是queue_delayed_work(system_wq, dwork, delay)的封装。如果你对工作队列的并发度、优先级有要求,就应该用queue_delayed_work指定自己的队列。

1.3 适用边界:什么时候不该用它

schedule_delayed_work不是万能的。如果延迟时间非常短,比如几微秒,用高精度定时器或者hrtimer更合适,因为工作队列的调度本身有延迟,jiffies的精度也有限。如果工作函数需要极低的延迟抖动,工作队列受内核线程调度影响,抖动可能在毫秒级,这种情况下直接用中断下半部或者高精度定时器回调更稳。

另外,如果工作项需要频繁重复调度,比如每1毫秒一次,用schedule_delayed_work自唤醒会带来较大的调度开销。此时应该考虑用hrtimer配合工作队列,或者用专门的周期性任务机制。还有一个容易忽略的点:system_wq是共享的,如果大量模块都往上面挂长时间运行的工作,会互相影响。生产环境里,长时间运行的任务应该放到自定义的unbound工作队列里。

2. 核心API与数据结构拆解

理解schedule_delayed_work的用法,得先看清它背后的几个结构体和函数调用关系。这些结构体在include/linux/workqueue.h里定义,虽然内核版本之间有差异,但核心字段和语义是稳定的。我以较新的5.x内核为例,把关键部分拆开讲。

2.1 delayed_work、work_struct与timer_list的嵌套关系

struct delayed_work的定义大致如下:

struct delayed_work { struct work_struct work; struct timer_list timer; struct workqueue_struct *wq; int cpu; };

work_struct里保存了工作函数指针、待执行链表节点、以及一些状态标志。timer_list是内核定时器,用来实现延迟。wq记录这个工作项属于哪个工作队列,cpu记录绑定的CPU。当你调用INIT_DELAYED_WORK时,内核会把work的func设为你提供的工作函数,把timer的回调设为delayed_work_timer_fn,并初始化相关链表和锁。

这里有个细节值得注意:timer的回调是内核内部函数,不是你的工作函数。定时器到期后,内核在软中断上下文执行delayed_work_timer_fn,这个函数把work挂到工作队列的待执行链表上,然后唤醒worker线程。你的工作函数是在worker线程里执行的,所以可以睡眠。这个两阶段设计是延迟工作队列能够兼顾延迟和进程上下文的原因。

实操心得:不要在INIT_DELAYED_WORK之后手动去修改timer.function,也不要去操作work.entry。这些字段由内核管理,手动干预会导致链表损坏或者定时器异常。

2.2 schedule_delayed_work与queue_delayed_work怎么选

两个函数的声明如下:

bool schedule_delayed_work(struct delayed_work *dwork, unsigned long delay); bool queue_delayed_work(struct workqueue_struct *wq, struct delayed_work *dwork, unsigned long delay);

返回值表示工作项是否成功加入队列。如果返回false,通常是因为这个工作项已经在队列里了,内核不会重复添加同一个work_struct。这一点非常重要:同一个delayed_work在已经被调度但还没执行完之前,再次调用schedule_delayed_work不会产生第二个实例,而是返回false。如果你需要每次调用都保证执行,要么等上一次执行完再调度,要么使用多个delayed_work实例。

选择哪个函数,取决于你对工作队列的控制需求。schedule_delayed_work用起来最省事,适合大多数简单场景。queue_delayed_work允许你指定工作队列,比如创建自己的alloc_workqueue,控制最大并发数、是否绑定CPU、是否可重入。驱动里如果有多个设备实例,每个实例的工作项需要并发执行,就应该用自定义工作队列,避免所有实例在system_wq上串行等待。

2.3 取消操作的三个层次

取消一个延迟工作项有好几个API,语义差别很大:

函数作用是否等待工作函数结束适用场景
cancel_delayed_work尝试取消定时器,如果工作已经进入待执行队列则返回false否中断上下文,不关心是否正在执行
cancel_delayed_work_sync取消定时器并等待正在执行的工作函数完成是模块卸载、设备移除,必须确保没有并发
flush_delayed_work不取消,等待已经调度的工作执行完是只想同步,不想取消

cancel_delayed_work在中断上下文里可以用,因为它不会睡眠。但它不能保证工作函数没有在运行,也不能保证工作函数不会马上开始运行。如果工作函数正在执行,cancel_delayed_work返回false,你需要自己处理这种情况。cancel_delayed_work_sync会睡眠等待,所以只能在进程上下文调用,不能在中断上下文或者持有自旋锁时调用。

常见坑:在模块退出函数里只调用了cancel_delayed_work,没有调_sync,结果模块卸载后工作函数还在跑,访问了已经释放的内存,直接内核崩溃。这个错误在开发阶段不一定每次都复现,但压力测试或者卸载时刚好有工作在执行,就会稳定触发。

2.4 时间参数:jiffies与msecs_to_jiffies的换算

schedule_delayed_work的第二个参数是unsigned long delay,单位是jiffies,不是毫秒。很多新手直接传100,以为是100毫秒,实际上可能是100个jiffies。如果内核配置的HZ是250,100个jiffies就是400毫秒;如果HZ是1000,就是100毫秒。这个差异会导致延迟时间完全不符合预期。

正确的做法是用msecs_to_jiffies转换:

schedule_delayed_work(&my_dwork, msecs_to_jiffies(20));

msecs_to_jiffies会根据当前内核的HZ把毫秒转换成jiffies,并且做了向上取整,保证延迟不会短于指定毫秒。类似的还有usecs_to_jiffies和nsecs_to_jiffies。如果延迟时间以秒为单位,可以用msecs_to_jiffies(seconds * 1000),或者直接用HZ乘以秒数,但更推荐用转换函数,避免手动计算错误。

/* 延迟500毫秒 */ schedule_delayed_work(&sensor_work, msecs_to_jiffies(500)); /* 延迟2秒 */ schedule_delayed_work(&retry_work, 2 * HZ);

需要明确的是,实际延迟时间不会精确等于设定值。定时器精度受jiffies粒度限制,延迟时间会被向上取整到下一个jiffy边界。另外,工作队列的worker线程调度也有延迟。所以在实时性要求高的场景,不能依赖schedule_delayed_work做精确计时。

3. 从零写一个可加载的延迟工作队列模块

下面给一个完整的字符设备模块示例。它注册一个字符设备,用户写入数据时,模块安排一个延迟工作,在500毫秒后把数据长度和当前jiffies打印到内核日志。这个例子虽然简单,但包含了初始化、调度、取消、卸载清理的完整流程,可以直接编译加载测试。

3.1 完整模块代码

#include <linux/module.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/workqueue.h> #include <linux/jiffies.h> #include <linux/slab.h> #include <linux/uaccess.h> #define DEV_NAME "delayed_demo" #define BUF_SIZE 128 static dev_t dev_num; static struct cdev demo_cdev; static struct class *demo_class; static struct device *demo_device; static struct delayed_work demo_dwork; static char demo_buf[BUF_SIZE]; static int demo_len; static void demo_work_func(struct work_struct *work) { struct delayed_work *dwork = to_delayed_work(work); pr_info("delayed_demo: work running at jiffies=%lu, len=%d\n", jiffies, demo_len); pr_info("delayed_demo: data=%s\n", demo_buf); /* 这里可以安全睡眠、拿互斥锁、调用I2C/SPI等 */ } static ssize_t demo_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { if (count >= BUF_SIZE) return -EINVAL; if (copy_from_user(demo_buf, buf, count)) return -EFAULT; demo_buf[count] = '\0'; demo_len = count; /* 每次写入都重新调度,延迟500ms */ if (!schedule_delayed_work(&demo_dwork, msecs_to_jiffies(500))) pr_info("delayed_demo: work already queued, skip new schedule\n"); return count; } static const struct file_operations demo_fops = { .owner = THIS_MODULE, .write = demo_write, }; static int __init demo_init(void) { int ret; ret = alloc_chrdev_region(&dev_num, 0, 1, DEV_NAME); if (ret) return ret; cdev_init(&demo_cdev, &demo_fops); demo_cdev.owner = THIS_MODULE; ret = cdev_add(&demo_cdev, dev_num, 1); if (ret) goto err_cdev; demo_class = class_create(THIS_MODULE, DEV_NAME); if (IS_ERR(demo_class)) { ret = PTR_ERR(demo_class); goto err_class; } demo_device = device_create(demo_class, NULL, dev_num, NULL, DEV_NAME); if (IS_ERR(demo_device)) { ret = PTR_ERR(demo_device); goto err_device; } INIT_DELAYED_WORK(&demo_dwork, demo_work_func); pr_info("delayed_demo: module loaded\n"); return 0; err_device: class_destroy(demo_class); err_class: cdev_del(&demo_cdev); err_cdev: unregister_chrdev_region(dev_num, 1); return ret; } static void __exit demo_exit(void) { cancel_delayed_work_sync(&demo_dwork); device_destroy(demo_class, dev_num); class_destroy(demo_class); cdev_del(&demo_cdev); unregister_chrdev_region(dev_num, 1); pr_info("delayed_demo: module unloaded\n"); } module_init(demo_init); module_exit(demo_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("demo"); MODULE_DESCRIPTION("schedule_delayed_work demo");

配套的Makefile如下,假设内核源码路径已经准备好:

obj-m += delayed_demo.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) all: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean

3.2 代码逐段解析:哪些地方最容易写错

第一处是INIT_DELAYED_WORK的调用时机。它必须在任何schedule_delayed_work之前执行,通常放在模块初始化函数的末尾。如果在字符设备注册完成之前就调度工作,而用户态已经能打开设备并触发写入,就可能出现工作项未初始化就被调度的情况。虽然概率低,但在自动加载和用户态工具配合的场景下确实发生过。

第二处是to_delayed_work的用法。work_func_t收到的参数是struct work_struct *,而我们需要的是包含它的struct delayed_work。to_delayed_work宏通过container_of计算出外层结构体的地址。如果你的工作函数里需要访问自定义数据,通常会把delayed_work嵌入到一个更大的结构体里,再用container_of拿到外层结构体。这个技巧在多个设备实例的驱动里非常常见。

struct my_device { struct delayed_work dwork; void __iomem *base; int irq; /* 其他字段 */ }; static void my_work_func(struct work_struct *work) { struct delayed_work *dwork = to_delayed_work(work); struct my_device *dev = container_of(dwork, struct my_device, dwork); /* 使用dev->base等 */ }

第三处是写入函数里的重复调度处理。schedule_delayed_work返回false表示工作项已经在队列里,新的调度被忽略。如果业务要求“每次写入都重置延迟时间”,那么当前实现并不满足:第二次写入时如果工作项还在等待,schedule_delayed_work不会更新延迟时间,工作仍然按第一次的时间执行。要重置延迟,应该先cancel_delayed_work再重新调度,或者用mod_delayed_work。mod_delayed_work会修改已经排队的延迟工作项的到期时间,是专门为这种“刷新超时”场景设计的。

/* 刷新延迟,让工作从当前时刻重新计时500ms */ mod_delayed_work(system_wq, &demo_dwork, msecs_to_jiffies(500));

第四处是模块退出时的清理顺序。代码里先cancel_delayed_work_sync,再销毁设备节点和字符设备。这个顺序不能反。如果先销毁设备,用户态可能还在写入,写入函数里又会调度工作;同时工作函数可能在访问已经被销毁的数据。cancel_delayed_work_sync会等待正在执行的工作函数结束,确保后续销毁资源时没有并发访问。

3.3 编译、加载与验证延迟效果

编译前确认内核头文件已安装。在常见发行版上,可以安装对应内核版本的开发包,或者直接使用内核源码树。执行make后得到delayed_demo.ko。加载模块:

sudo insmod delayed_demo.ko dmesg | tail -n 5

你会看到模块加载日志。然后创建设备节点并写入数据:

sudo mknod /dev/delayed_demo c $(grep delayed_demo /proc/devices | awk '{print $1}') 0 echo "hello delayed work" | sudo tee /dev/delayed_demo

写入后约500毫秒,dmesg里会出现工作函数打印的日志。如果使用udev,设备节点可能会自动创建,直接用/dev/delayed_demo即可。要观察延迟是否准确,可以在写入后立刻记录时间,然后在工作函数里打印jiffies,对比差值。实际测下来,在空闲系统上延迟通常在设定值附近,但受HZ和调度影响,可能有几毫秒到十几毫秒的偏差。如果系统负载很高,偏差会更大。

卸载模块:

sudo rmmod delayed_demo dmesg | tail -n 3

如果卸载时刚好有工作项在等待,cancel_delayed_work_sync会取消它并等待。如果工作函数正在运行,卸载会阻塞到工作函数返回。这正是我们想要的安全行为。

4. 常见问题与排查技巧实录

延迟工作队列的API不多,但实际使用中遇到的问题五花八门。下面这几类是我在调试和代码审查里反复见到的,整理成速查表,方便对照排查。

4.1 工作函数不执行或只执行一次

最常见的原因是delayed_work没有被正确初始化,或者初始化之后被重复初始化。INIT_DELAYED_WORK会重置工作项的状态,如果在一个已经调度但还没执行的工作项上再次调用INIT_DELAYED_WORK,会把链表节点清掉,导致工作项丢失,永远不会执行。所以初始化只做一次,通常在模块初始化或者设备probe函数里完成。

另一个原因是schedule_delayed_work返回false,调用者没有检查返回值。工作项已经在队列里时,重复调度会被忽略。如果你的逻辑依赖每次调用都执行,就会觉得“工作函数丢了”。解决办法是用mod_delayed_work刷新延迟,或者为每次任务分配独立的delayed_work。

还有一种情况是工作项被调度到了已经销毁的工作队列上。如果你用alloc_workqueue创建了自定义队列,在销毁队列之前没有取消挂在上面的工作项,destroy_workqueue会等待所有工作完成,但如果工作项在销毁后又引用了队列,就会出错。正确顺序是先cancel_delayed_work_sync,再destroy_workqueue。

排查清单:检查INIT_DELAYED_WORK是否只调用一次;检查schedule_delayed_work返回值;检查工作队列是否在调度前已经创建、在取消后销毁;检查模块是否在卸载时执行了同步取消。

4.2 重复调度导致的重入与竞态

schedule_delayed_work本身有防重入保护:同一个work_struct在队列里时不会被重复添加。但这个保护只针对“队列里”的状态。如果工作函数已经开始执行,此时再次调用schedule_delayed_work,内核会允许,因为工作项已经不在队列里了。如果工作函数执行时间较长,新调度的工作可能在上一个还没结束时就开始,导致同一个工作函数并发执行。如果工作函数访问共享数据又没有锁,就会竞态。

解决重入有几种思路。第一种是在工作函数里用互斥锁保护共享数据,保证串行。第二种是用delayed_work自带的状态,在工作函数开头判断标志位。第三种是使用单线程工作队列,比如alloc_ordered_workqueue,它保证同一个队列上的工作项串行执行,不会并发。对于大多数驱动,用alloc_ordered_workqueue是最简单的防重入方案。

static struct workqueue_struct *my_wq; my_wq = alloc_ordered_workqueue("my_ordered_wq", 0); if (!my_wq) return -ENOMEM; /* 调度时使用自定义队列 */ queue_delayed_work(my_wq, &my_dwork, msecs_to_jiffies(100));

4.3 模块卸载崩溃:cancel_delayed_work_sync的正确用法

模块卸载时崩溃,十有八九是工作函数在模块代码已经释放后还在运行。cancel_delayed_work_sync会等待正在执行的工作函数结束,所以它必须在释放任何工作函数会访问的资源之前调用。但有一个隐蔽的坑:如果工作函数内部自己重新调度了自己,cancel_delayed_work_sync只能取消当前这一轮,工作函数返回前又调度了下一轮,导致取消不彻底。

比如周期性自唤醒的工作函数:

static void periodic_work(struct work_struct *work) { struct delayed_work *dwork = to_delayed_work(work); /* 做任务 */ /* 重新调度自己 */ schedule_delayed_work(dwork, msecs_to_jiffies(1000)); }

在卸载时调用cancel_delayed_work_sync,如果工作函数正在执行,它会等函数返回。但函数返回前又调用了schedule_delayed_work,所以取消之后又有一个新的工作项排队。模块卸载后,这个工作项还会执行,访问已释放的内存。正确的做法是在工作函数里检查一个“停止标志”,或者用cancel_delayed_work_sync之后再检查一次并取消。

static bool module_stopping; static void periodic_work(struct work_struct *work) { struct delayed_work *dwork = to_delayed_work(work); if (module_stopping) return; /* 做任务 */ if (!module_stopping) schedule_delayed_work(dwork, msecs_to_jiffies(1000)); } static void __exit demo_exit(void) { module_stopping = true; cancel_delayed_work_sync(&periodic_dwork); /* 再取消一次,防止竞态窗口 */ cancel_delayed_work_sync(&periodic_dwork); /* 释放资源 */ }

两次调用cancel_delayed_work_sync并不是多余的。第一次调用时,如果工作函数刚好在检查module_stopping之前通过了检查,它可能还会调度下一次。第一次取消返回后,第二次取消可以确保那个新排队的项也被取消。更严谨的做法是用锁保护标志位和工作调度,但两次取消在大多数场景下已经足够。

4.4 延迟时间不准或工作执行顺序混乱

延迟时间不准通常有三个原因。一是HZ配置较低,比如HZ=100时,jiffies粒度是10毫秒,任何小于10毫秒的延迟都会被向上取整,实际延迟至少10毫秒。二是工作队列的worker线程被其他长时间运行的工作占用,导致你的工作排队等待。三是系统负载高,CPU调度延迟大。如果对延迟精度要求高,应该用高精度定时器hrtimer,然后在定时器回调里调度工作,或者直接在hrtimer回调里做非睡眠操作。

执行顺序混乱是指多个延迟工作项本应按时间先后执行,实际却乱序。system_wq上多个工作项是并发执行的,不保证顺序。如果你需要严格顺序,应该使用alloc_ordered_workqueue,它保证队列上的工作项按入队顺序串行执行。另外,延迟时间相同的多个工作项,进入待执行队列的顺序也不确定,不能依赖它们之间的顺序。

现象可能原因排查方法解决方向
工作函数完全不执行未初始化、重复初始化、队列已销毁检查INIT_DELAYED_WORK调用次数;检查schedule返回值确保初始化一次;检查生命周期
工作函数执行多次重复调度且函数已开始执行在工作函数入口打印计数用ordered workqueue或加锁
卸载时崩溃工作函数访问已释放资源检查cancel_delayed_work_sync调用位置先取消再释放;加停止标志
延迟时间偏大HZ低、队列繁忙、负载高打印调度时刻和执行时刻jiffies用hrtimer;自定义队列
顺序错乱并发执行、延迟相同观察多个工作项的日志顺序用ordered workqueue

5. 进阶用法与实战注意事项

掌握了基本用法之后,还有一些进阶技巧能让代码更稳、更高效。这部分内容在API文档里往往一笔带过,但实际项目里很关键。

5.1 自唤醒延迟工作实现周期任务

周期性任务是延迟工作队列的经典用法:工作函数执行完任务后,重新调度自己,延迟时间就是周期。这个模式简单,但要注意几个点。第一,周期时间是从工作函数执行完开始算,还是从上次调度开始算?上面的写法是执行完后重新调度,所以实际周期等于“执行时间+延迟时间”。如果执行时间不可忽略,周期会漂移。要固定周期,应该在调度时计算下一次的绝对到期时间,用mod_delayed_work或者记录jiffies差值。

第二,自唤醒的工作函数如果在模块卸载时还在运行,必须用停止标志加同步取消来安全退出。第三,如果系统进入挂起状态,jiffies会停止增长,延迟工作可能被推迟到唤醒之后。如果你的周期任务需要在挂起期间暂停,这正好;如果需要唤醒后立即补执行,就要额外处理。

static void sensor_poll_work(struct work_struct *work) { struct delayed_work *dwork = to_delayed_work(work); unsigned long next_delay = msecs_to_jiffies(200); if (module_stopping) return; /* 读取传感器,可能睡眠 */ read_sensor(); /* 重新调度,保持200ms周期 */ schedule_delayed_work(dwork, next_delay); }

5.2 自定义工作队列控制并发度与CPU绑定

system_wq是共享资源,大量工作项堆积时会影响其他子系统。对于自己的驱动,如果工作项可能运行较长时间,建议创建自定义工作队列。alloc_workqueue的第二个参数是标志位,常用的有WQ_UNBOUND、WQ_HIGHPRI、WQ_CPU_INTENSIVE。WQ_UNBOUND表示工作项不绑定到特定CPU,由内核调度器决定在哪个CPU上运行,适合可能睡眠且执行时间不确定的任务。WQ_HIGHPRI提高工作线程优先级,适合对延迟敏感的任务。

alloc_ordered_workqueue是alloc_workqueue的封装,创建单线程串行队列。如果不想管理并发度,直接用alloc_ordered_workqueue最省心。创建队列后,用queue_delayed_work把工作项挂上去,卸载时先取消所有工作项,再destroy_workqueue。

static struct workqueue_struct *sensor_wq; sensor_wq = alloc_ordered_workqueue("sensor_wq", WQ_MEM_RECLAIM); if (!sensor_wq) return -ENOMEM; queue_delayed_work(sensor_wq, &sensor_dwork, msecs_to_jiffies(200)); /* 卸载 */ cancel_delayed_work_sync(&sensor_dwork); destroy_workqueue(sensor_wq);

注意:destroy_workqueue会等待所有已经排队的工作执行完,但不会等待延迟定时器。如果还有延迟工作项在等待,必须先取消。否则销毁队列后,定时器到期时工作项找不到队列,会触发内核警告甚至崩溃。

5.3 调试延迟工作队列的实用手段

调试工作队列问题,最直接的手段是加日志。在调度点、工作函数入口和出口打印jiffies和CPU号,可以看清延迟是否符合预期、工作函数在哪个CPU上执行。如果怀疑工作项丢失,可以打印schedule_delayed_work的返回值。内核还提供了workqueue相关的tracepoint和/sys/kernel/debug/workqueue接口,可以查看各个工作队列的繁忙程度、工作项数量。

# 查看工作队列状态(需要debugfs挂载) mount -t debugfs none /sys/kernel/debug cat /sys/kernel/debug/workqueue/*/busy

如果发现工作函数执行时间过长,可以用ftrace的函数图功能跟踪。配置function_graph跟踪器,过滤工作函数名,就能看到调用栈和耗时。对于偶发的卸载崩溃,可以在cancel_delayed_work_sync前后加printk,确认工作函数是否已经退出。另外,打开内核的CONFIG_DEBUG_OBJECTS_WORK和CONFIG_DEBUG_OBJECTS_TIMERS可以在工作项或定时器被错误使用时给出警告,对早期发现初始化问题很有帮助。

# ftrace 示例 cd /sys/kernel/debug/tracing echo function_graph > current_tracer echo demo_work_func > set_ftrace_filter echo 1 > tracing_on # 触发写入 echo 0 > tracing_on cat trace

我个人在实际项目里还习惯用一个简单的原子计数器统计工作函数执行次数,配合/proc或者sysfs导出,方便在压力测试时观察是否有漏执行或重复执行。这个方法比翻日志高效得多,尤其是在长时间运行的设备上。

最后再分享一个小技巧:如果你的延迟工作项需要在多个设备实例之间共享同一个工作函数,可以把delayed_work嵌入设备私有结构体,用container_of拿到设备指针。这样每个设备实例有独立的工作项,互不干扰,也不需要全局变量。初始化时对每个实例调用INIT_DELAYED_WORK,调度时使用各自的delayed_work,卸载时逐个取消。这个模式在USB、PCI、I2C驱动里非常常见,扩展性好,也避免了并发访问同一个工作项带来的重入问题。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 5:48:19

Spark ML ALS实现豆瓣电影推荐系统实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 5:47:42

STM32C5驱动IIS3DWB加速度计:IIC接口实现振动监测的完整方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 5:47:40

零基础用草图+AI生成网页:豆包实操指南

1. 一张A4草图&#xff0c;怎么就成了网页的设计图说出来你可能不信&#xff0c;我这个连HTML和CSS都分不清的人&#xff0c;上周用豆包做了一个能点按钮、能弹提示框的网页。全过程没有写一行代码&#xff0c;但也不是光靠嘴说&#xff0c;核心道具是一张手画的A4纸草图。先交…

作者头像 李华
网站建设 2026/9/30 5:47:33

强基计划笔试微积分备考:从知识断层到解题降维

1. 强基计划笔试里的微积分到底占多重的分量先说一个比较扎心的事实&#xff1a;每年强基计划校考结束&#xff0c;都能在各类考生群里看到一种声音——“高考数学平时能考140&#xff0c;结果强基笔试数学卷子拿回来一看&#xff0c;连题目在问什么都得琢磨半天”。这不是个别…

作者头像 李华
网站建设 2026/9/30 5:47:04

AI代码审查门禁:从误报率到采纳率,用数据驱动信任

最近和几个团队聊AI代码审查落地&#xff0c;聊得最多的反而不是模型能力&#xff0c;而是误报率。模型确实能抓出一些人类 reviewer 漏掉的问题&#xff0c;但开发者的耐心是有限度的——如果十条评论里有四条是“看了半天觉得没问题”&#xff0c;AI 助手很快就会被当成噪音直…

作者头像 李华
网站建设 2026/9/30 5:46:47

航拍操场人体检测:YOLOv8自定义数据集构建与小目标训练实战

无人机飞起来那一刻&#xff0c;取景器里满屏都是黑压压的人头。操场课间操、军训方阵、运动会入场式&#xff0c;这些俯视画面里“人”这个东西和平视行人检测里完全是两码事。我之前拿现成的YOLO行人检测模型直接往航拍视频上怼&#xff0c;结果不是漏检一大片&#xff0c;就…

作者头像 李华