ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

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_workschedule_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是250100个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) clean3.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配置较低比如HZ100时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 workqueue5. 进阶用法与实战注意事项掌握了基本用法之后还有一些进阶技巧能让代码更稳、更高效。这部分内容在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驱动里非常常见扩展性好也避免了并发访问同一个工作项带来的重入问题。
返回列表