第 5 章 等待的艺术:定时器、线程与工作队列
5.1 延迟的艺术——到底该睡多久?
在用户空间写程序时,等待是一件再简单不过的事。你想让程序暂停一秒?sleep(1)。想精确到毫秒?usleep。操作系统会帮你打理好一切:你的进程进入休眠,CPU 资源让给别人,时间到了操作系统再把你摇醒。
但在内核里,这件事变得棘手起来。
想象你正在写一个驱动,需要向一个慢速的硬件设备发送指令。硬件手册上写着:「写入命令后,必须等待至少 5 微秒,才能发送下一条指令」。
如果你在用户空间,这根本不是问题。但在内核里,你立刻会面临一个尴尬的选择:
- 选项 A:死循环等待(忙等待)。CPU 在那里空转,数着 cycles 过日子。这当然很精确,但 CPU 资源被浪费了——这就像你在微波炉前面盯着倒计时看,什么都不干。
- 选项 B:进程休眠。你告诉调度器「我要睡会儿」,CPU 转而去处理别的进程。这很高效,但问题是——你能睡吗?
如果你此刻正运行在中断上下文里,或者手里拿着自旋锁,那么「睡觉」是被严格禁止的。在这种不允许阻塞的场景下,你唯一的出路就是忙等待。
这就是本章要解决的核心矛盾:如何在内核安全、高效地处理时间流逝。
这一章我们要讲的,就是内核提供的三种处理「稍后做这件事」的机制。它们各有各的使用场景,选错了工具,轻则系统性能下降,重则直接死锁。
- 短延迟:无论是忙等待还是短暂休眠,怎么选才对?
- 内核定时器:像闹钟一样,在未来的某个时间点敲门。
- 内核线程:把后台任务扔到一个独立的线程里慢慢跑。
- 工作队列:最常用的「推迟执行」手段,把繁重的工作扔给内核专用的线程去处理。
这些机制并非彼此孤立,它们在不同的上下文中互为补充。理解它们何时可用、何时不可用,是编写健壮内核代码的关键。
5.1 内核中的时间延迟:忙等待 vs. 休眠
让我们先从最基础的场景开始:我现在就要等一会儿。
这个需求非常普遍,比如硬件时序要求。内核根据你是否允许进程调度(是否允许 CPU 切换去做别的事),将延迟 API 分为截然不同的两类:原子延迟和阻塞延迟。
原子延迟 —— 我不能睡,只能数羊
当你处于原子上下文中——比如正在处理中断、持有自旋锁,或者禁用了抢占——你绝对不能让 CPU 调度出去。此时若试图让进程休眠,内核会毫不犹豫地给你抛出一个 BUG() 或者直接死锁。
在这种场景下,我们使用 *delay() 系列函数。它们的本质是忙循环。
内核提供了三个精度的原子延迟 API:
ndelay(unsigned long nsecs):纳秒级延迟。udelay(unsigned long usecs):微秒级延迟。mdelay(unsigned long msecs):毫秒级延迟。
它是怎么算出来的?
你可能会好奇,udelay(1) 怎么就能保证只等 1 微秒?CPU 的频率是会变的,这怎么算准?
这里就要提到 BogoMIPS 这个古老而重要的概念了。
BogoMIPS(Bogus MIPS,伪 MIPS)是内核在启动时校准的一个数值。它测量的是 CPU 在一秒钟内能执行多少次「什么也不做」的空循环。这个数值被记录在内核的 loops_per_jiffy 变量中。
当你调用 udelay() 时,内核根据 BogoMIPS 算出需要跑多少次循环才能抵消掉这段延迟时间。
⚠️ 踩坑预警
千万别在原子上下文里用 mdelay() 去等待太长时间,或者更糟,用 mdelay() 去等秒级的延迟。这会让 CPU 像「疯狗」一样空转,占用率 100%,系统响应卡顿。mdelay() 仅适用于毫秒级的极短等待,且确实无法调度的情况。
阻塞延迟 —— 我要睡会儿,别吵我
如果你现在的状态是进程上下文,且手里没有锁,那么你可以做更有教养的事:让出 CPU。
这时我们使用 *sleep() 系列函数。这会调用调度器,将当前进程从 CPU 运行队列中移除,放入等待队列,直到时间到了再被唤醒。
usleep_range(unsigned long usecs_min, unsigned long usecs_max)msleep(unsigned int msecs)ssleep(unsigned int seconds)
为什么 usleep_range() 是个范围?
你可能觉得奇怪,为什么不能像 usleep() 那样指定一个精确值?答案是:为了节能和性能优化。
如果要求系统「必须在 1000 微秒时准时唤醒」,系统可能需要使用高精度定时器,这会阻止 CPU 进入深度省电模式。 如果你告诉系统「我在 1000 到 1500 微秒之间醒过来都行」,调度器就有了更大的自由度。它可以把这短时间稍微延长一点(这就是 timer slack 概念),从而让 CPU 多睡一会儿,减少唤醒频率,降低功耗。
推荐做法:
- 小于 10 微秒:用
udelay()(忙等待)。 - 10 微秒到 20 毫秒:用
usleep_range()。 - 大于 20 毫秒:用
msleep()。
msleep() 与 msleep_interruptible()
传统的 msleep() 是不可中断的。一旦睡下去,要么时间到了,要么系统崩溃,否则谁也叫不醒你。
而 msleep_interruptible() 允许被信号打断。这对于需要响应用户空间操作(比如 Ctrl+C)的内核线程非常有用。它的返回值是剩余的时间(如果被提前打断)。
瞧瞧实际效果 —— 时间到底准不准?
让我们用代码说话。我们会在内核模块中测试这些延迟函数,并用高精度定时器(HRT)来打表,看看实际延迟到底有多少。
我们需要获取高精度的时间戳。内核提供了 ktime_get_real_ns(),它返回自 Epoch(1970-01-01 00:00:00 UTC)以来的纳秒数。
代码演示
// (...)
ktime_t start, end;
s64 actual_time_ns;
pr_info("Testing delay APIs...\n");
/* 1. 测试 udelay (忙等待,约 2ms) */
start = ktime_get_real_ns();
mdelay(2);
end = ktime_get_real_ns();
actual_time_ns = end - start;
pr_info("mdelay(2) expected: 2000000 ns, actual: %lld ns", actual_time_ns);
/* 2. 测试 msleep (休眠,约 20ms) */
start = ktime_get_real_ns();
msleep(20);
end = ktime_get_real_ns();
actual_time_ns = end - start;
pr_info("msleep(20) expected: 20000000 ns, actual: %lld ns", actual_time_ns);
/* 3. 测试 usleep_range (允许波动,约 5000-5500 us) */
start = ktime_get_real_ns();
usleep_range(5000, 5500);
end = ktime_get_real_ns();
actual_time_ns = end - start;
pr_info("usleep_range(5000, 5500) expected min: 5000000 ns, actual: %lld ns",
actual_time_ns);
// (...)
当你加载这个模块并查看 dmesg 输出时,你会发现一些有趣的现象:
mdelay()非常精准,因为它在死等。msleep()和usleep_range()的实际值通常比预设值要长。因为调度器唤醒进程是需要时间的(调度延迟),而且你醒过来之后还得排队等 CPU 资源。
⚠️ 记住:内核中的延迟永远是「至少」这么多时间,而不是「精确」这么多时间。如果你对硬件时序有硬性要求(如「必须少于 10us」),只能用忙等待;如果只是想缓冲一下数据,用休眠,把 CPU 让给更重要的人。