本节摘要:驱动经常要"等一等"或"过会儿再干"。本节讲清内核的三种时间来源、忙等与睡眠等待的分界、定时器与延迟工作两种推迟机制,并用定时器把 LED 驱动的闪烁功能完整兑现——6.1 节修好的配置接口在这里第一次真正动起来。
硬件世界里到处是"等":上电后要等复位释放、写命令后要等芯片就绪、闪烁灯要按周期翻转电平。"等"的方式选错,轻则浪费处理器,重则在中断上下文里睡眠引发死锁。内核为"等"准备了一整个工具谱系,选择标准只有两个问题:要等多久、能不能睡。
内核时间有三套表示,各有来历。
时钟节拍是最古老的度量:内核按固定频率周期性跳动,每次跳动节拍计数加一。频率在内核配置时确定,常见每秒一百到一千次。毫秒转节拍的换算函数 msecs_to_jiffies 就是 6.1 节用到的那个——它把"人类单位"翻译成"节拍单位"。节拍的精度受限于跳动频率:配置为二百五十赫兹时,一次节拍四毫秒,你请求一毫秒的定时,实际拿到的是零到四毫秒之间的某个值。
高精度时钟以纳秒为单位单调递增,读一次开销极小,适合打时间戳、算间隔。网络驱动给收包盖时间戳、日志系统给消息标时刻,用的都是它。
墙钟时间是"现实世界的时间"——年月日时分秒,会随校时跳变。驱动里几乎不该用它做测量(校时一跳,间隔就成负数),它属于用户态展示层。
u64 t0 = ktime_get_ns(); /* 高精度起点 */ do_something(); u64 dt = ktime_get_ns() - t0; /* 纳秒级耗时,单调可靠 */ unsigned long j = jiffies; /* 节拍计数,做超时判断 */
按"等多久、能否睡"两个维度,等待工具排成四个档次。
忙等微秒:上电复位后等芯片稳定,量级几微秒。处理器空转原地踏步,最浪费但最简单——好在微秒级的浪费无伤大雅。中断上下文唯一可用的短等待手段。
忙等毫秒:把忙等拉长到毫秒级是反模式——几毫秒的空转等于烧掉几百万条指令的机会。除非持锁期间确需短暂等待(持锁不能睡),否则一律换成睡眠。
睡眠毫秒:让出处理器,定时醒来。进程上下文等硬件就绪的标准姿势,量级毫秒起步——再短的睡眠会被调度开销吃掉意义。
睡眠区间:告诉调度器"这之间醒来都行",调度器可把多个等待合并处理,省下不少唤醒。量级十毫秒以上的轮询等待优先选它。
udelay(10); /* 忙等十微秒:复位释放 */ usleep_range(1000, 2000); /* 睡眠一到两毫秒:给区间让调度优化 */ msleep(20); /* 睡眠二十毫秒:等芯片自检 */
⚠️ 常见坑:在中断上下文里调用毫秒级睡眠函数。中断上下文没有可睡眠的载体,这类调用轻则触发警告转储,重则系统挂死。中断里的等待只剩忙等一条路——所以中断处理必须快进快出,重活交给底半部。
"过会儿再干"有两种实现。
内核定时器:登记一个回调与一个到期节拍,到期后回调在软中断环境执行——不可睡眠,适合"到点翻个寄存器"这类轻量动作。LED 闪烁正是典型用例:
#include <linux/timer.h> static struct timer_list blink_timer; static int blink_enabled; /* 到点回调:翻转电平并重新排队,形成周期 */ static void blink_toggle(struct timer_list *t) { static int level; level = !level; iowrite32(level ? led_mask : 0, led_gpio_base); if (blink_enabled) mod_timer(&blink_timer, jiffies + blink_jiffies); /* 续约下一拍 */ } static void blink_start(void) { timer_setup(&blink_timer, blink_toggle, 0); blink_enabled = 1; mod_timer(&blink_timer, jiffies + blink_jiffies); } static void blink_stop(void) { blink_enabled = 0; del_timer_sync(&blink_timer); /* 同步删除:等回调退出再返回 */ }
三个细节见功力。mod_timer 的"续约"写在回调末尾,周期得以延续;del_timer_sync 保证删除时回调不在别的处理器上还在跑(普通删除返回后回调可能仍在执行,随后模块卸载就是灾难);回调里没碰任何锁——翻转电平与续约都是单线程软中断环境里的原子序列。这套代码与 6.1 节的配置接口拼起来:用户态设周期、锁保护配置序列、定时器兑现节奏,闪烁功能闭环完成。
延迟工作:把工作挂进工作队列并指定延迟,到点后在工作线程环境执行——可以睡眠。适合"两百毫秒后读一次传感器状态"这类到点还要干重活的场景。
| 机制 | 回调环境 | 能否睡眠 | 精度 | 典型用途 |
|---|---|---|---|---|
| 忙等 | 当前上下文 | — | 微秒 | 复位等待、时序缝隙 |
| 睡眠函数 | 当前上下文 | 需可睡 | 毫秒 | 等硬件就绪 |
| 内核定时器 | 软中断 | 否 | 节拍级 | 周期性轻量动作 |
| 延迟工作 | 工作线程 | 是 | 节拍级 | 到点干重活 |
别在驱动里造绝对时间。比较两个时刻用高精度时钟的差值,别拿墙钟做算术;判断超时用"节拍差大于等于阈值"的接口,别手写减法——节拍计数会回绕,手写比较在回绕点必错。
定时精度向下兼容。请求一毫秒定时、实际拿到四毫秒是配置决定的正常现象,对时序敏感的逻辑要在设计时就把节拍精度算进预算,而不是抱怨"定时器不准"。
到点回调里只做最小事。定时器回调跑在软中断环境,干重活会拖累所有软中断;需要重活就回调里只"点火",把火递给工作队列。
追问一:忙等那么浪费,为什么不把它从内核里删掉? 因为它有不可替代的岗:极短延时(几微秒以内)的场景下,睡下去再醒来的调度开销比忙等本身还大,而且部分代码路径(持自旋锁中、中断上下文)根本没有"睡"这个选项。硬件复位后的等待、总线切换前的建立时间,都是忙等的常驻客户。判断式很朴素:延时可睡就走睡眠档,不可睡且足够短才轮到忙等——把忙等用在长延时上,是把最贵的等待方式用在最不缺精确性的地方,双重浪费。
追问二:定时器到期时系统正忙,回调晚到了很久,数据会错吗? 看你把"什么时候干活"写在哪儿。定时器只承诺"不早于这个时刻执行",晚点是调度常态;对时序敏感的逻辑要把"回调执行时刻"与"动作基准时刻"分开——回调里先读当前时刻再计算目标状态,而不是假设回调准点到达、直接按预定步进推算。LED 闪烁这类人眼场景容错极大,但同样的思维搬到工业控制的脉冲输出,准点假设就会出真事故。回调里重新对表,是所有定时逻辑的安全写法。
时间节奏稳了,下一节处理驱动的"生死时刻"——系统休眠与唤醒时,驱动要兑现哪些承诺。