Linux内核定时器开发实战:从struct timer_list到timer_setup
1. 项目概述在Linux内核驱动开发中定时器是一个极其基础但又至关重要的组件。无论是实现按键消抖、周期性状态轮询、超时处理还是简单的延时任务都离不开它。很多刚接触内核编程的朋友面对struct timer_list、jiffies、mod_timer这些概念和API时常常感到无从下手照着网上的代码抄一遍能跑但稍微想改点逻辑或者排查问题就懵了。我自己在早期做驱动开发时也踩过不少坑比如定时器回调函数里不能睡眠、在多核环境下删除定时器的竞争问题等等。这篇文章我就以一个从业者的视角结合一个从简单到复杂的完整示例把Linux内核定时器的使用掰开揉碎了讲清楚。我们不仅会看到“怎么做”更会深入探讨“为什么这么做”以及在实际项目中那些手册里不会写的“坑”和技巧。无论你是正在学习驱动开发的学生还是需要在实际产品中应用定时器的工程师希望这篇近万字的干货能成为你手边可靠的参考。2. 内核定时器核心机制与设计思路在动手写代码之前我们必须先理解内核定时器是怎么工作的。这就像开车前得知道油门、刹车和方向盘是干嘛的否则直接上路就是马路杀手。2.1 为什么需要内核定时器想象一下这个场景你写了一个触摸屏驱动手指按下时会产生中断但为了过滤掉硬件抖动导致的误触发你需要在中断发生后等待几毫秒等信号稳定了再读取坐标。这个“等待几毫秒”的操作如果直接用mdelay()或udelay()这类忙等待函数在这几毫秒里CPU核心就傻傻地空转什么别的活也干不了严重浪费资源。内核定时器就是为了解决这类“在未来的某个时间点执行某个任务”的需求而生的。它把任务回调函数挂起设定一个未来的唤醒时间然后CPU就可以放心地去处理其他进程等时间到了内核会自动调用你的回调函数。这是一种异步、非阻塞的延迟执行机制。2.2 核心数据结构struct timer_list内核定时器的所有信息都封装在struct timer_list这个结构体中。理解它的关键成员是正确使用的第一步。struct timer_list { struct list_head entry; // 用于将定时器挂入内核管理链表的节点 unsigned long expires; // 定时器到期时刻以jiffies为单位 void (*function)(unsigned long); // 到期时执行的回调函数 unsigned long data; // 传递给回调函数的参数 struct tvec_base *base; // 指向内部管理此定时器的基准结构通常无需驱动开发者关心 // ... 可能还有其他内部成员取决于内核版本 };expires(到期时间)这是定时器的“闹钟时刻”。它的值不是“还有多久到期”而是“在哪个绝对时间点到期”。这个时间点是以jiffies为单位的。所以我们通常用当前jiffies值 延迟的jiffies数来计算它。例如jiffies msecs_to_jiffies(1000)表示1秒后到期。function(回调函数)时间到点时内核要替你执行的函数。它的函数签名是固定的void function(unsigned long data)。这个函数运行在中断上下文具体是软中断上下文这意味着在它内部你不能调用任何可能导致睡眠的函数比如kmalloc(GFP_KERNEL)、mutex_lock、down_interruptible等。也不能进行长时间的计算否则会阻塞其他软中断和下半部影响系统响应。data(参数)一个unsigned long类型的参数会在回调函数被调用时传给它。你可以用它传递一个整数值或者更常见的传递一个指向你的设备私有数据结构的指针通过(unsigned long)priv进行强制类型转换。这是定时器区分不同实例或获取上下文信息的关键。entry和base这些是内核用于内部管理的字段驱动开发者通常不需要直接操作它们。2.3 时间基准jiffies与HZjiffies是内核的一个全局变量记录系统启动以来经过的“滴答”数。这个“滴答”的频率由内核编译配置HZ决定。HZ100表示每秒有100次滴答每个滴答间隔10毫秒。HZ1000则表示每秒1000次滴答间隔1毫秒定时精度更高但也会增加定时器处理的开销。jiffies32位系统上是一个32位无符号整数大约每(2^32 / HZ)秒会回绕wrap around。例如HZ1000时大约49.7天回绕一次。内核提供了time_after、time_before等宏来安全地比较可能发生回绕的时间。get_jiffies_64()在64位系统或某些配置下可以直接安全地获取64位的jiffies值避免了回绕问题。在驱动中为了代码的健壮性和可移植性我强烈建议使用get_jiffies_64()来获取当前时间而不是直接使用jiffies。msecs_to_jiffies()一个非常实用的辅助函数它将毫秒数转换为对应的jiffies数。由于HZ可能不同直接写jiffies 100并不代表100毫秒而可能是100个滴答即100/HZ秒。所以timer.expires get_jiffies_64() msecs_to_jiffies(1000);才是“1秒后到期”的正确写法。注意在定时器回调函数内部如果你需要再次计算未来的时间点比如实现循环定时也应该使用get_jiffies_64()获取“当前”时间再加上偏移量。因为从定时器到期到你的回调函数被执行中间可能有微小的调度延迟。2.4 单次定时与循环定时的设计抉择这是定时器应用的两个基本模式选择哪一种取决于你的业务逻辑。单次定时就像设定一个一次性的闹钟响完就结束。适用于超时处理、延迟初始化等场景。例如在驱动probe函数中延迟2秒再初始化某个慢速硬件。循环定时就像设定一个重复响铃的闹钟。需要在每次回调函数执行末尾重新设置定时器的到期时间expires并再次激活它。适用于周期性任务如传感器数据采集、LED心跳灯、网络链路状态检测等。关键区别在于单次定时器在回调函数执行后内核会自动将其从活动链表中移除。而循环定时需要你在回调函数中手动调用mod_timer()来重新设定并激活。如果你在回调函数里什么都不做这个定时器就失效了。3. 定时器API详解与使用要点了解了原理我们来看看具体怎么用。内核提供了一组API来操作struct timer_list这些函数看似简单但每个都有需要注意的细节。3.1 初始化从 init_timer 到 timer_setup在较老的内核版本大约4.15之前标准的初始化流程是init_timer(timer);初始化结构体内部链表等字段。手动赋值timer.expires,timer.function,timer.data。然而在现代内核4.15及以上中init_timer()已被标记为废弃。社区引入了新的timer_setup()宏它更安全特别是在防止回调函数参数类型错误方面。// 老式方法已过时不推荐在新代码中使用 init_timer(timer); timer.expires ...; timer.function timer_handler; timer.data (unsigned long)dev; // 现代方法推荐 #include linux/timer.h struct timer_list timer; timer_setup(timer, timer_handler, 0); // 然后单独设置 expires mod_timer(timer, get_jiffies_64() msecs_to_jiffies(1000)); // 或者如果需要传递data可以在回调函数中通过timer_list结构体本身获取timer_setup()的第三个参数是flags通常设为0。它的一个巨大优点是回调函数timer_handler的第一个参数就是指向这个struct timer_list本身的指针你可以通过from_timer()宏来获取包含它的结构体通常是你的设备私有结构体这是一种更面向对象、更安全的参数传递方式。我们会在后面的高级用法里详细展开。实操心得在开始一个新驱动项目时首先通过uname -r确认你的目标内核版本并查阅对应版本的include/linux/timer.h头文件。如果看到timer_setup就毫不犹豫地使用新API。这能让你的代码在未来更具兼容性。3.2 激活与修改add_timer 与 mod_timeradd_timer(timer)将一个**已经初始化好expires,function,data**的定时器加入到内核的内部管理链表中。一旦加入定时器就开始倒计时。mod_timer(timer, expires)这是一个更强大、更常用的函数。它的作用是如果定时器还未被激活即不在活动链表中它的行为等同于add_timer()。如果定时器已经被激活它会先修改定时器的到期时间为新的expires然后将其重新排序到正确的链表位置。mod_timer是线程安全的。这意味着你可以在一个CPU核心上调用mod_timer来修改定时器同时定时器可能在另一个CPU核心上即将到期或正在执行回调。内核内部通过锁机制处理好了这种竞争。这正是实现循环定时的核心技巧在定时器回调函数中计算下一个到期时间new_expires然后调用mod_timer(timer, new_expires)。这样当本次回调执行完毕定时器就已经为下一个周期设置好了。static void timer_handler(struct timer_list *t) // 新API风格 { struct my_device *dev from_timer(dev, t, my_timer); // 获取设备结构体 // ... 处理任务 ... // 重新设定为1秒后再次触发 mod_timer(t, get_jiffies_64() msecs_to_jiffies(1000)); }3.3 删除与同步删除del_timer 与 del_timer_sync当驱动被卸载、设备关闭或不再需要定时器时必须将其删除。del_timer(timer)尝试从活动链表中删除一个定时器。如果删除成功即定时器尚未到期返回1如果定时器已经到期可能正在执行或已执行完回调返回0。del_timer_sync(timer)这是del_timer()的同步版本。它会等待定时器回调函数执行完毕后才返回。这确保了当你从模块退出函数module_exit中调用它时不会发生回调函数还在运行而它依赖的数据结构比如设备私有数据已经被释放了的悲剧。如何选择在进程上下文如ioctl,read,write系统调用路径或模块退出函数中删除定时器几乎总是应该使用del_timer_sync()。这是最安全的选择避免了use-after-free的竞态条件。在中断上下文或任何不能睡眠的上下文中必须使用del_timer()因为del_timer_sync()可能因为等待回调完成而睡眠。static void __exit my_exit(void) { // 在模块退出函数中安全地等待定时器回调完成 del_timer_sync(timer); printk(KERN_INFO Driver unloadedn); }注意事项即使你使用了del_timer_sync()在定时器回调函数中你仍然需要小心地处理共享数据。因为del_timer_sync()只是保证在它返回后回调函数不会再次被调用但在它被调用的一瞬间回调函数可能正在另一个CPU上并发执行。因此对共享数据的访问通常需要额外的锁如自旋锁spinlock_t来保护。4. 完整示例从单次到循环再到高级用法现在我们把所有知识点串起来写几个有代表性的例子。我会先用老式API写一个基础版再用现代API重构并加入错误处理和资源管理。4.1 基础单次定时器老式API示例这个例子对应你提供的原始代码我们加上详细的注释和打印信息。#include linux/init.h #include linux/kernel.h #include linux/module.h #include linux/timer.h MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(A simple one-shot timer example); static struct timer_list my_timer; // 定时器回调函数 static void my_timer_callback(unsigned long data) { // data 是我们之前传入的参数 520这里打印它 printk(KERN_INFO Timer fired! data %lun, data); // 这是一个单次定时器回调执行后定时器会自动变为非活动状态 // 无需调用 del_timer } static int __init timer_init(void) { printk(KERN_INFO Timer module initializingn); // 1. 初始化定时器结构体 (老式方法) init_timer(my_timer); // 2. 设置定时器参数 my_timer.expires jiffies msecs_to_jiffies(1000); // 1秒后到期 my_timer.function my_timer_callback; my_timer.data 520; // 可以传递任意unsigned long数据这里传个幸运数字 // 3. 激活定时器 add_timer(my_timer); printk(KERN_INFO Timer set to fire in 1 secondn); return 0; // 初始化成功 } static void __exit timer_exit(void) { // 4. 删除定时器。使用同步删除确保安全。 int ret del_timer_sync(my_timer); if (ret) { printk(KERN_INFO Timer was still pending and has been removedn); } else { printk(KERN_INFO Timer had already fired or was not activaten); } printk(KERN_INFO Timer module exitingn); } module_init(timer_init); module_exit(timer_exit);编译与测试# 假设你的Makefile正确配置了内核路径 make sudo insmod timer_demo.ko # 使用 dmesg 查看内核日志 sudo dmesg | tail -20 # 你应该能看到 Timer module initializing 和 Timer set to fire... # 大约1秒后看到 Timer fired! data 520 sudo rmmod timer_demo # 再次 dmesg看到删除相关的信息4.2 循环定时器与现代API重构接下来我们用现代的timer_setup()API重写一个循环定时器的例子并展示如何优雅地传递设备上下文。#include linux/init.h #include linux/kernel.h #include linux/module.h #include linux/timer.h MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(A periodic timer using modern API); // 定义一个简单的设备结构体用于承载定时器和一些状态 struct my_device { struct timer_list periodic_timer; int count; // 记录触发了多少次 char name[32]; }; static struct my_device my_dev; // 现代定时器回调函数签名第一个参数是 struct timer_list * static void periodic_timer_callback(struct timer_list *t) { // 使用 from_timer 宏从定时器指针反推回包含它的设备结构体指针 // 参数1: 设备结构体类型 // 参数2: 在回调中收到的 timer_list 指针 // 参数3: 设备结构体中 timer_list 成员的字段名 struct my_device *dev from_timer(dev, t, periodic_timer); dev-count; printk(KERN_INFO [%s] Periodic timer tick #%d at jiffies%llun, dev-name, dev-count, get_jiffies_64()); // 实现循环定时的关键重新设定定时器 // 计算下一次触发时间当前时间 1秒 unsigned long next_expires get_jiffies_64() msecs_to_jiffies(1000); mod_timer(t, next_expires); } static int __init timer_init(void) { printk(KERN_INFO Periodic timer module initializingn); // 初始化设备结构体 strncpy(my_dev.name, MyTimerDevice, sizeof(my_dev.name) - 1); my_dev.name[sizeof(my_dev.name) - 1]