12. Linux内核定时器¶
在嵌入式Linux驱动开发的世界里,时间是一个不可或缺的维度,我们常常需要让设备在“未来的某个时刻”做某事,或者“每隔一段时间”重复做某事。 对于内核开发者而言,内核定时器就是这样一款核心的工具,它允许驱动程序在指定的未来时间点,调度执行一段特定的代码。 它不依赖硬件时钟芯片,而是完全由内核软件层面实现,是驱动中实现“延时”、“周期性任务”和“超时检测”的基础。
12.1. 内核定时器定义¶
Linux内核定时器是内核提供的一种基于时间触发的同步机制,用于实现“在指定时间后执行特定任务”的功能,本质是一种软定时器, 依赖内核时钟中断和jiffies(系统节拍计数器)实现计时,不具备硬件定时器的高精度,但足以满足绝大多数内核驱动、内核任务的时间触发需求。
内核定时器的核心特性是异步触发、非阻塞,定时器注册后,内核会在指定时间到期时,自动调用预先注册的回调函数,执行用户定义的任务。
内核定时器作用总结如下:
实现周期性任务(如LED周期性翻转);
实现超时检测(如设备通信超时、资源获取超时);
延迟执行特定任务(如延迟初始化硬件、延迟释放资源);
配合同步机制(如自旋锁),实现安全的周期性硬件操作。
12.2. 内核定时器分类¶
Linux内核提供多种定时器类型,适配不同精度、不同场景的需求:
普通定时器(timer_list):最基础、最常用,基于jiffies计时,支持一次性/周期性触发,可动态修改触发时间;
高精度定时器(hrtimer):高精度计时,支持纳秒级精度,基于硬件时钟,触发更精准;
延迟工作队列(delayed_work):基于工作队列和定时器,回调函数在进程上下文执行,支持延迟执行、周期性执行;
watchdog定时器(watchdog):硬件/软件结合,用于监控系统运行,超时未喂狗则触发系统复位。
本章节主要介绍普通定时器,其他定时器不作展开。
12.3. 内核定时器底层源码解析¶
Linux内核普通定时器定义于内核源码/include/linux/timer.h,底层依托jiffies、时钟中断和定时器链表实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // 内核源码/include/linux/timer.h
struct timer_list {
struct hlist_node entry;
unsigned long expires;
void (*function)(struct timer_list *);
u32 flags;
unsigned long cust_data;
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
ANDROID_KABI_RESERVE(1);
ANDROID_KABI_RESERVE(2);
};
|
说明:
entry:内核定时器的挂载节点,内核会把所有定时器组织成哈希链表/双向链表统一管理,这个成员就是将当前定时器挂入内核定时器链表的“钩子”。
expires:定时器超时绝对时间。
function:定时器回调函数指针,定时器到期时,内核会自动调用这个函数。
flags:定时器控制标志位。
cust_data:用户自定义私有数据,开发者可以把自定义参数(如设备指针、状态值)存在这里。
lockdep_map:锁依赖调试,用于内核死锁检测、锁顺序验证,驱动开发中完全不用关心。
12.4. 系统节拍计数器¶
内核定时器的计时基础是jiffies,它是一个全局无符号长整型变量,用于记录系统启动以来的时钟中断次数,其值随时钟中断递增。 在struct timer_list里看到的expires,单位就是jiffies。
jiffies的增长由系统时钟中断驱动,内核有一个配置项HZ(每秒时钟中断次数),可执行以下命令确认系统HZ值:
1 2 3 4 5 6 7 8 9 10 | #查看系统HZ值
cat /boot/config-* | grep CONFIG_HZ
#信息输出如下
# CONFIG_HZ_PERIODIC is not set
# CONFIG_HZ_100 is not set
# CONFIG_HZ_250 is not set
CONFIG_HZ_300=y
# CONFIG_HZ_1000 is not set
CONFIG_HZ=300
|
jiffies和秒换算关系:
1 2 3 4 5 6 7 | #换算关系
1 jiffies = 1/HZ 秒
#可得到
HZ=100时, 1 jiffies = 10 毫秒
HZ=300时, 1 jiffies = 3.33 毫秒
HZ=1000时,1 jiffies = 1 毫秒
|
12.5. 内核定时器标准API函数¶
以下主要以普通定时器进行说明,普通定时器的标准API包括初始化、启动、重启、停止、查询等。
12.5.1. 定时器初始化¶
12.5.1.1. timer_setup函数¶
timer_setup函数用于初始化struct timer_list类型定时器,绑定回调函数,设置定时器标志位,是使用定时器的前置操作,必须在启动定时器前调用。
函数原型:
1 | void timer_setup(struct timer_list *timer, void (*function)(struct timer_list *), unsigned int flags);
|
参数说明:
timer:指向需要初始化的定时器结构体指针;
function:定时器到期后自动执行的回调函数指针;
flags:定时器标志位,通常填0(默认值),用于配置定时器的特殊行为(如是否支持多CPU调度)。
需注意,回调函数必须严格遵循void (*)(struct timer_list *)的原型定义,否则会导致内核编译报错或运行时指针异常;初始化需在启动定时器之前完成。
12.5.2. 定时器启动/重启¶
12.5.2.1. mod_timer函数¶
mod_timer函数用于设置定时器的到期时间,若定时器已处于活跃状态(已注册且未到期),则更新其到期时间;若定时器未活跃,则直接启动定时器,是实现定时器周期性触发的核心API。
函数原型:
1 | int mod_timer(struct timer_list *timer, unsigned long expires);
|
参数说明:
timer:指向已初始化的定时器结构体指针;
expires:定时器到期时间,单位为jiffies,通常设置为“当前jiffies + 时间间隔”。
返回值:
int类型,返回1表示定时器已成功更新到期时间或启动;返回0表示定时器未初始化或已被删除。
可重复调用mod_timer函数,用于动态调整定时器的到期时间;若expires设置为小于当前jiffies的值,定时器会立即触发回调函数。
12.5.3. 定时器删除¶
12.5.3.1. del_timer函数¶
del_timer函数用于异步删除指定的定时器,若定时器未到期,则取消其触发;若定时器已到期或未活跃,则忽略该操作,属于异步删除方式,不等待回调函数执行完毕。
函数原型:
1 | int del_timer(struct timer_list *timer);
|
参数说明:
timer:指向需要删除的定时器结构体指针。
返回值:
int类型,返回1表示定时器已成功删除;返回0表示定时器未启动或已删除。
需注意,如果非同步删除,可能存在回调函数正在执行的风险,易引发并发竞态,不建议在中断上下文使用。
12.5.3.2. del_timer_sync函数¶
del_timer_sync函数用于同步删除指定的定时器,与del_timer的区别是,会等待定时器回调函数执行完毕后,再删除定时器, 确保无并发风险,是驱动开发中推荐的定时器停止方式。
函数原型:
1 | int del_timer_sync(struct timer_list *timer);
|
参数说明:
timer:指向需要删除的定时器结构体指针。
返回值:
int类型,返回1表示定时器已成功删除;返回0表示定时器未启动或已删除。
推荐在进程上下文使用,避免在原子上下文使用(会导致阻塞);驱动移除时必须调用该函数,避免定时器残留引发资源泄漏。
12.5.4. 定时器状态查询¶
12.5.4.1. timer_pending函数¶
timer_pending函数用于查询指定定时器是否处于活跃状态。
函数原型:
1 | int timer_pending(const struct timer_list *timer);
|
参数说明:
timer:指向需要查询的定时器结构体指针。
返回值:
int类型,返回1表示定时器处于活跃状态;返回0表示定时器未活跃(未启动、已到期或已删除)。
仅用于查询状态,不可用于控制定时器的启动或停止;参数为const指针,不可通过该指针修改定时器成员。
12.5.5. 私有数据访问¶
12.5.5.1. from_timer宏¶
from_timer是内核提供的容器of宏,用于从定时器指针(timer),获取其所在的父结构体指针(ptr), 核心用于定时器回调函数中访问私有数据。
宏原型:
1 2 | #define from_timer(var, callback_timer, timer_fieldname) \
container_of(callback_timer, typeof(*var), timer_fieldname)
|
参数说明:
ptr:父结构体指针名,用于接收获取到的父结构体指针;
timer:定时器指针;
member:定时器在父结构体中的成员名。
返回值:
无显式返回值,通过宏运算直接将父结构体指针赋值给ptr。
需注意,必须确保member参数与父结构体中定时器的成员名完全一致,否则会导致指针越界、系统崩溃;仅用于回调函数中,结合定时器指针获取私有数据。
12.6. 系统节拍转换API函数¶
12.6.1. 毫秒转系统节拍¶
12.6.1.1. msecs_to_jiffies函数¶
msecs_to_jiffies函数是内核标准时间转换API,将用户态常用的毫秒单位时间,转换为定时器所需的jiffies(系统节拍)单位。
函数原型:
1 | unsigned long msecs_to_jiffies(const unsigned int msecs);
|
参数说明:
msecs:待转换的毫秒数,为非负整数。
返回值:
转换后的jiffies节拍数,可直接赋值给定时器间隔参数或expires字段。
需注意,输入毫秒数不可为0或负数,否则会导致定时器异常触发;转换结果自动适配系统HZ配置(如HZ=100、HZ=300), 无需手动计算,避免硬编码出错。
12.6.2. 微秒转系统节拍¶
12.6.2.1. usecs_to_jiffies函数¶
usecs_to_jiffies函数是内核标准时间转换API,将微秒单位时间转换为jiffies节拍数,适用于更短延时的定时器场景,弥补msecs_to_jiffies最小毫秒级的局限,适配短周期定时需求。
函数原型:
1 | unsigned long usecs_to_jiffies(const unsigned int usecs);
|
参数说明:
usecs:待转换的微秒数,非负整数,适用于短周期定时场景。
返回值:
转换后的jiffies节拍数。
需注意,普通定时器基于jiffies,精度受HZ限制,微秒级转换仅为理论值,实际触发精度仍为节拍级;高精度短周期场景建议改用hrtimer高精度定时器。
12.6.3. 系统节拍转毫秒¶
12.6.3.1. jiffies_to_msecs函数¶
jiffies_to_msecs函数是内核标准时间转换API,将jiffies系统节拍数,反向转换为毫秒单位,常用于调试打印、日志输出,方便开发者直观查看定时器间隔时长。
函数原型:
1 | unsigned int jiffies_to_msecs(const unsigned long j);
|
参数说明:
j:待转换的jiffies节拍数,对应定时器间隔或到期节拍。
返回值:
转换后的毫秒数。
需注意,转换结果受系统HZ配置影响,仅为近似值(普通定时器非高精度);不可用于高精度计时场景,仅做调试、日志展示使用。
12.8. 内核定时器实验¶
Linux内核定时器回调函数属于软中断上下文(原子上下文),绝对不能睡眠,因此:
优先用:原子操作(简单变量)、自旋锁(复杂共享资源)
禁止用:信号量、互斥体(会引发内核崩溃)
本实验在自旋锁实验基础上添加内核定时器部分,设备树插件完全一致,驱动仅说明修改部分,重复部分不作说明。
本章的示例代码目录为: linux_driver/15_timer
12.8.1. 实验代码讲解¶
结构体与定时器定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | /* 定义 LED 字符设备结构体 */
struct led_chrdev {
/* 字符设备结构体 */
struct cdev dev;
/* 数据寄存器的虚拟地址,用于设置输出的电压 */
unsigned int __iomem *va_dr;
/* 数据方向寄存器的虚拟地址,用于设置输入或者输出 */
unsigned int __iomem *va_ddr;
/* 引脚高低位 */
unsigned int hl_pos;
/* 引脚偏移量 */
unsigned int led_pin;
/* LED 的设备树子节点 */
struct device_node *device_node;
/* 自旋锁 */
spinlock_t spinlock;
/* 定时器 */
struct timer_list timer;
/* LED 状态 */
int led_state;
/* 定时器间隔时间 */
unsigned long timer_interval;
};
/* 定义 led_chrdev 结构体指针,用于动态分配内存管理 LED 硬件信息 */
static struct led_chrdev *led_cdev;
|
将定时器作为设备私有资源,与LED硬件资源、自旋锁绑定,既能通过定时器实现LED周期性控制,又能通过自旋锁保证定时器回调函数与write函数的临界区安全。
定时器初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | static int pdrv_led_probe(struct platform_device *pdev)
{
// 内存分配、设备树解析、字符设备注册(省略重复代码)
/* 初始化自旋锁 */
spin_lock_init(&led_cdev->spinlock);
/* 初始化定时器 */
timer_setup(&led_cdev->timer, led_timer_callback, 0);
/* timer_interval单位为jiffies
* 当HZ=100时,此处定时器实际间隔时间为 HZ/2 个 jiffies,即 HZ/2 * (1/HZ)= 0.5秒
* 当HZ=300时,此处定时器实际间隔时间为 HZ/2 个 jiffies,即 HZ/2 * (1/HZ)= 1秒
*/
led_cdev->timer_interval = HZ / 2;
/* 默认关闭 LED */
led_cdev->led_state = 0;
// 错误处理(省略重复代码)
}
|
说明:
初始化顺序:先初始化自旋锁,再初始化定时器,因为定时器回调函数中会使用自旋锁;
回调函数绑定:led_timer_callback是定时器到期后执行的回调函数,负责LED电平翻转;
默认间隔:HZ/2表示间隔为系统节拍的一半,确保LED默认闪烁频率;
默认状态:led_state = 0表示默认关闭LED。
需注意,probe函数并没有启动定时器,而是在write函数中触发启动。
定时器回调函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | /* 定时器回调函数 */
static void led_timer_callback(struct timer_list *t)
{
struct led_chrdev *led_cdev = from_timer(led_cdev, t, timer);
unsigned int val;
/* 用于保存中断状态信息 */
unsigned long flags;
/* 获取自旋锁并保存中断状态 */
spin_lock_irqsave(&led_cdev->spinlock, flags);
/* 读取数据寄存器的值 */
val = ioread32(led_cdev->va_dr);
/* 判断 LED 状态 */
if (led_cdev->led_state) {
/* 设置高 16 位的使能位 */
val |= ((unsigned int)0x1 << (led_cdev->led_pin + 16));
/* 设置低 16 位的对应引脚输出,翻转电平 */
val ^= ((unsigned int)0x1 << (led_cdev->led_pin));
/* 将修改后的值写回到数据寄存器 */
iowrite32(val, led_cdev->va_dr);
}
/* 重新启动定时器 */
mod_timer(&led_cdev->timer, jiffies + led_cdev->timer_interval);
/* 释放自旋锁并恢复中断状态 */
spin_unlock_irqrestore(&led_cdev->spinlock, flags);
}
|
说明:
第4行:从定时器指针t,获取其所在的led_chrdev结构体指针,实现私有数据(如GPIO寄存器地址、引脚偏移量)的访问;
第11行:获取自旋锁,因为回调函数中操作GPIO寄存器(临界区),需与write函数中的GPIO操作互斥,避免并发错乱;
第21行:通过val ^= ((unsigned int)0x1 << (led_cdev->led_pin)) 翻转对应引脚的电平,实现LED闪烁;
第27行:重启定时器,设置下一次到期时间为“当前jiffies + 间隔时间”,实现周期性触发;
第30行:释放自旋锁,避免锁泄漏。
定时器的启动、停止与参数修改
通过write函数接收用户态指令,实现定时器的启动、停止、间隔时间修改,结合LED状态控制,代码片段如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | static ssize_t pdrv_led_write(struct file *filp, const char __user *buf,
size_t count, loff_t *ppos)
{
/* 用于存储用户输入 */
char input[32] = {0};
/* 用于存储用户输入的时间参数 */
unsigned long interval = 0;
unsigned long val = 0;
/* 从文件结构体的私有数据中获取 led_chrdev 结构体指针 */
struct led_chrdev *led_cdev = filp->private_data;
/* 用于保存中断状态信息 */
unsigned long flags;
/* 打印设备写操作信息 */
printk("pdrv_led write \r\n");
/* 从用户空间读取输入 */
if (copy_from_user(input, buf, min(count, sizeof(input) - 1))) {
return -EFAULT;
}
/* 解析用户输入 */
if (sscanf(input, "%lu", &val) >= 1) {
printk("val = %ld \n", val);
/* 获取自旋锁并保存中断状态 */
spin_lock_irqsave(&led_cdev->spinlock, flags);
switch (val) {
/* 关闭 LED */
case 0:
led_cdev->led_state = 0;
/* 停止定时器 */
del_timer_sync(&led_cdev->timer);
/* 读取数据寄存器的值 */
val = ioread32(led_cdev->va_dr);
/* 设置高 16 位的使能位 */
val |= ((unsigned int)0x1 << (led_cdev->led_pin + 16));
/* 设置低 16 位的对应引脚输出高电平 */
val |= ((unsigned int)0x01 << (led_cdev->led_pin));
/* 将修改后的值写回到数据寄存器 */
iowrite32(val, led_cdev->va_dr);
break;
/* 开启 LED 闪烁 */
case 1:
led_cdev->led_state = 1;
/* 启动定时器 */
mod_timer(&led_cdev->timer, jiffies + led_cdev->timer_interval);
break;
/* 修改定时器时间 */
case 2:
if (sscanf(input, "%lu %lu", &val, &interval) == 2 && interval > 0) {
/* 转换为 jiffies */
led_cdev->timer_interval = msecs_to_jiffies(interval);
printk("Timer interval set to %lu ms\n", interval);
} else {
printk(KERN_ERR "Invalid interval value\n");
}
break;
default:
/* 释放自旋锁并恢复中断状态 */
spin_unlock_irqrestore(&led_cdev->spinlock, flags);
return -EINVAL;
}
}
/* 释放自旋锁并恢复中断状态 */
spin_unlock_irqrestore(&led_cdev->spinlock, flags);
return count;
}
|
说明:
停止定时器(case 0):设置led_state = 0,调用del_timer_sync同步停止定时器,避免回调函数继续执行,同时将LED置为低电平;
启动定时器(case 1):设置led_state = 1,调用mod_timer启动定时器,若定时器已活跃,则更新到期时间;
修改间隔(case 2):将用户输入的毫秒数(interval)通过msecs_to_jiffies转为jiffies,更新timer_interval,下一次定时器到期后,将使用新的间隔时间;
自旋锁保护:write函数中修改定时器参数、LED状态,与回调函数中的GPIO操作互斥,确保数据一致性;需注意copy_from_user是可能阻塞的函数,自旋锁持有期间禁止任何阻塞/调度,因此所有用户态拷贝、数据解析放在锁外面。
定时器的资源释放
驱动移除时,需同步删除定时器,避免资源泄漏,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | static int pdrv_led_remove(struct platform_device *pdev)
{
struct led_chrdev *led_cdev = platform_get_drvdata(pdev);
/* 打印平台驱动移除信息 */
printk("pdrv_led remove\n");
/* 删除定时器 */
del_timer_sync(&led_cdev->timer);
/* 销毁设备节点 */
device_destroy(class, devno);
/* 删除字符设备 */
cdev_del(&led_cdev->dev);
/* 释放设备号 */
unregister_chrdev_region(devno, DEV_CNT);
/* 销毁设备类 */
class_destroy(class);
return 0;
}
|
必须使用del_timer_sync而非del_timer,因为驱动移除时,定时器可能仍在活跃状态,同步删除可等待回调函数执行完毕,避免并发风险和资源泄漏。
12.8.3. 程序运行结果¶
如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。
12.8.3.1. 实验操作¶
在本节实验中,鲁班猫系列板卡,系统设备树中均默认使能了 LED 的设备功能,需要关闭设备树的leds节点,可以修改leds节点的 status = "okay"; 为 status = "disabled";,然后编译设备树进行替换,也可以在板卡中直接使用以下命令关闭系统leds驱动对LED的控制:
1 2 3 4 5 6 7 8 | #心跳灯命名可能为sys_status_led或sys_led,需先确认
ls /sys/class/leds/
#如果为sys_status_led
sudo sh -c 'echo 0 > /sys/class/leds/sys_status_led/brightness'
#如果为sys_led
sudo sh -c 'echo 0 > /sys/class/leds/sys_led/brightness'
|
将led的亮度调为0,与此同时led的触发条件自动变为none,从而取消leds驱动对LED的控制。
设备树插件加载方法和设备树插件实验完全一致,加载设备树插件并重启板卡后使用以下命令加载驱动:
1 2 3 4 5 6 7 8 | #加载驱动
sudo insmod timer_led.ko
#信息输出如下
[ 145.169694] led platform driver init
[ 145.170026] led platform driver probe
[ 145.170070] GPIO_BASE address: 0xFDD60000
[ 145.170211] major=236, minor=0
|
通过驱动代码,最后会在/dev下创建led设备,可以使用echo命令来测试我们的led驱动是否正常。 我们使用以下命令控制灯的亮灭:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #停止定时器,关闭LED
sudo sh -c "echo 0 > /dev/timer_led"
#开启定时器,LED闪烁
sudo sh -c "echo 1 > /dev/timer_led"
#定时器间隔为200ms
sudo sh -c "echo 2 200 > /dev/timer_led"
#定时器间隔为500ms
sudo sh -c "echo 2 500 > /dev/timer_led"
#修改定时器间隔为500ms信息打印如下
[ 4312.904165] pdrv_led open
[ 4312.904281] pdrv_led write
[ 4312.904305] val = 2
[ 4312.904323] Timer interval set to 500 ms
[ 4312.904348] pdrv_led release
|
12.9. 内核定时器使用注意事项¶
回调函数上下文约束:普通定时器回调函数运行在原子上下文,禁止睡眠、延时、调度操作(如msleep、schedule、copy_from_user、get_user),禁止使用可中断的同步机制(如mutex_lock_interruptible)。
自旋锁配合使用:若回调函数中操作共享资源(如GPIO寄存器、全局变量),需与其他操作(如write、read函数)通过自旋锁保护,避免并发竞态。
定时器启动与停止规范:启动定时器用mod_timer,停止定时器优先用del_timer_sync,避免使用del_timer导致的并发问题;
到期时间设置:expires参数必须是jiffies类型,不可直接使用毫秒/秒,需通过msecs_to_jiffies等宏转换;避免设置过期时间小于当前jiffies。
私有数据访问:通过from_timer宏获取父结构体指针时,必须确保member参数与父结构体中定时器的成员名一致,否则会导致指针越界、系统崩溃。
资源释放:驱动移除、模块退出时,必须删除定时器,避免定时器残留导致回调函数继续执行,引发资源泄漏或系统异常。
避免递归触发:回调函数中重启定时器时,需确保间隔时间合理,避免定时器频繁触发,占用过多CPU资源。