1. 从一次延时引发的卡死说起:内核等时间的两条路
刚接手一块板子的驱动调试时,我遇到过最典型的一个场景:一个传感器上电后需要等 20ms 才能读寄存器,同事在 GPIO 中断处理函数里写了msleep(20),结果系统直接抛出一长串BUG: scheduling while atomic,中断下半部再也没跑起来。同一份代码,把msleep换成mdelay就活了,但 CPU 占用率肉眼可见地飙了一段时间。这个对比基本就是 linux 内核睡眠函数(delay 与 sleep)这一整套机制的入口:内核里"等一段时间"并不是一个动作,而是两套完全不同的实现路径,选错了轻则延迟不准,重则把调度器惹毛。
先给不太熟悉内核的读者一个整体印象。所谓*delay家族,指的是ndelay、udelay、mdelay,它们的共同点是忙等待:在 CPU 上跑一段空循环,靠指令执行速度凑出时间,期间不让出处理器,什么都不干但也没闲着。*sleep家族则是msleep、ssleep、usleep_range以及底层的schedule_timeout,它们把当前任务挂到调度队列里,交还 CPU,等时钟中断或高精度定时器到期再把自己唤醒。前者准但费电、阻塞上下文,后者省电但有精度下限、必须能睡眠。这两个特性组合起来,就决定了任何一个延时场景的选型。
很多做嵌入式的朋友对这类问题不陌生。裸机上用 STM32 的HAL_Delay(),本质是靠 SysTick 中断累加计数,只要当前中断优先级比 SysTick 高,计数永远不涨,函数就卡死在里面。内核里的msleep逻辑上是同一类依赖:它要等时钟中断把jiffies推上去,或者等高精度定时器到期的时间事件触发,如果这段时间内中断被屏蔽、或者当前上下文根本不允许调度,那它同样等不到唤醒,表现就是任务挂死。所以本文不是简单地列一张 API 对照表,而是把两条路径的实现原理、适用边界、参数换算、踩坑现场都摊开讲一遍,无论你是刚看内核代码的新手,还是写了好几年驱动的老手,都能从里面找到可以直接抄走的判断依据。
1.1 忙等待和睡眠等待,到底差在哪里
把话说透一点,udelay(100)编译出来大致是这样一个结构:先通过loops_per_jiffy换算出 100 微秒对应多少次空循环,然后执行这段循环。整个过程不需要任何外部事件配合,CPU 有多少算多少,所以你把它放进中断处理、放进自旋锁保护的临界区、放进关中断的代码片段里,它都能工作,这是它最大的价值。代价也很直白:这段时间 CPU 什么正事都干不了,而且循环次数是按启动时校准的频率算的,如果运行过程中 CPU 降频(比如调频策略切到了省电档),实际延时会被拉长。这一点在功耗敏感的移动设备上特别明显,一大堆mdelay堆在一起,电量就会被打包带走。
msleep(100)走的是另一条路:调用点先把当前任务状态设成不可中断睡眠(或者可中断睡眠),然后主动调schedule()让出 CPU。任务从运行队列里摘出去,直到定时器到期把它重新标记为可运行,才回到调度队列。这段时间 CPU 可以去跑别的任务,甚至可以进低功耗空闲状态,非常省电。但前提条件非常硬:调用点必须处在进程上下文、且不能持有自旋锁、不能关中断、不能处在 RCU 读临界区,否则就是拿调度器开玩笑。
我一般的判断顺序是这样的:先问"这段代码能不能睡"。如果答案是"不能",比如中断处理函数、spin_lock临界区、raw_local_irq_save之后,那就只能用*delay,并且要控制时长。如果能睡,第二个问题才是"精度要求多高",微秒级以内用udelay,几十微秒到毫秒级优先usleep_range,毫秒级往上直接msleep,如果是"等某个条件成立"而不是"等固定时间",那应该用wait_event系列而不是死等。
1.2 jiffies、HZ 与时间精度换算表
要理解 sleep 家族的精度,绕不开HZ这个编译期常量。内核用定时器中断周期性地推进一个全局计数jiffies,HZ就是一秒钟推进多少次,也就是 1 jiffy 等于多少秒的倒数。这个值在不同架构和不同发行版内核里不一样,服务器上常见 250 或 1000,桌面发行版多为 1000,某些嵌入式配置会降到 100。它直接决定了msleep这类基于 jiffies 的睡眠函数的时间分辨率。
下面这张表是我实际对着几台机器验证过的,单位是毫秒,测试方法是在驱动里连续调用同一个函数,用ktime_get前后打点算差值:
| HZ 取值 | 1 jiffy 时长 | msleep(1)实测区间 | msleep(10)实测区间 |
|---|---|---|---|
| 100 | 10 ms | 10 ~ 20 ms | 20 ~ 30 ms |
| 250 | 4 ms | 4 ~ 8 ms | 12 ~ 16 ms |
| 1000 | 1 ms | 1 ~ 2 ms | 10 ~ 11 ms |
看到没有,msleep(1)在 HZ=100 的机器上实际睡了十几毫秒,这跟很多人的直觉差得很远。原因有两个:一是msleep内部会把毫秒换算成 jiffies 并向上取整,msecs_to_jiffies会算成至少 1 个 jiffy;二是实现里还额外加了 1,防止传入极小值被舍入成 0 导致根本不睡。这两个动作叠加,就是上面表格里"起步多一个 jiffy"的现象来源。所以如果你需要 1 毫秒级别的准确延时,msleep是靠不住的,要么换usleep_range,要么把 HZ 提高。
usleep_range走的是高精度定时器(hrtimer)而不是 jiffies,精度可以做到微秒级,代价是需要系统里有一个可用的高精度时钟源。现代 SoC 基本都带着各种 timer,问题不大,但在非常早期的启动阶段(时钟源还没注册完),高精度定时器还不一定能用。
提示:评估一个延时需求时,先看需求的绝对误差能不能接受 1 个 jiffy。不能接受的走 hrtimer 路径,能接受的走 jiffies 路径,这是最省事的分类方法。
2. 忙等待三兄弟:udelay、ndelay、mdelay 的边界在哪
忙等待这三个函数看起来最简单,实际最容易写出事故。我见过不止一次这样的情况:代码里udelay(us)传了个变量进来,变量在某些分支下变成了几万,结果一行调用直接把系统卡住几百毫秒;也见过有人用mdelay(1000)在中断里做"软延时"等待硬件状态稳定,一个中断进来就霸占 CPU 一秒钟。要避免这类问题,得先知道它们背后的时间是怎么来的,以及参数上限卡在什么地方。
2.1 BogoMIPS 校准:延时循环是怎么"数"出微秒的
内核启动日志里有一行很出名的话,大意是 "Calibrating delay loop... 数值 BogoMIPS"。这个校准过程在calibrate_delay()里完成,思路非常朴素:内核先利用定时器测出一个 jiffy 的时长,然后在一个 jiffy 里执行一个极简的循环体,看能跑多少遍,得到一个loops_per_jiffy。有了这个数,1 微秒对应多少圈就成了除法问题,udelay就是按这个换算出来的圈数去跑空循环。
这个机制有一个很直接的副作用:延时精度依赖启动时校准的数值,而校准值反映的是启动那一刻的 CPU 频率。如果系统跑起来之后调频策略把主频从 1.5GHz 降到 800MHz,udelay(100)还是会按 1.5GHz 时的循环次数去跑,实际耗时自然被拉长约一倍。反过来,如果 CPU 被超频使用(有些板子确实这么干),延时会缩短。所以对时间敏感的硬件时序,比如某些单总线协议的微秒级时序、EEPROM 的写周期等待,如果用的是忙等待,一定要把调频因素考虑进去,或者在关键路径上锁定频率。
另外,ndelay在绝大多数平台上其实都是靠udelay折算出来的。纳秒级别的延时要靠 CPU 循环实现,误差非常大,一次函数调用本身的开销可能就超过目标时间。我个人的经验是ndelay只在极少数对时序特别敏感的地方有意义,而且必须配合示波器实测,不然就是心理安慰。
2.2 编译期上限检查与 __bad_udelay 报错
这是新手最容易撞上的一堵墙。如果你写了udelay(10000),编译大概率会失败,报一个找不到__bad_udelay函数的错误。这不是编译器抽风,而是内核故意设计的保护:udelay、ndelay被定义成宏,对编译期常量参数做上限检查,超限就展开成__bad_udelay(),而这个函数只有声明没有定义,链接时就会失败。参数上限由各架构的MAX_UDELAY_MS决定,常见取值在 2 到 5 之间,折算下来单次udelay的最大合法值大约在 2000 到 5000 微秒之间。
这个限制背后的理由是:单个忙等待循环太长,会长时间霸占 CPU 且不受任何调度干预,内核不鼓励这么干。如果你的延时确实超过几毫秒,应该改走睡眠路径。
mdelay的情况稍微特殊。它允许更长的时间,但它的典型实现是"如果参数是常量且小于一个 jiffy,就直接udelay;否则用循环逐毫秒地调用udelay(1000)"。老版本内核里这个展开方式有个隐藏问题:如果传进来的是带副作用的表达式(比如mdelay(i++)),会被求值多次,导致诡异的行为。虽然现在很多版本的实现已经用临时变量规避了,但我的建议一直是:给*delay和*sleep传参时,永远先用一个局部变量接一下,不要直接塞表达式进去。
顺带说一个很多人忽略的点:udelay传变量时是不做上限检查的。编译器只对常量做检查,变量值到底多大,运行时才知道。这意味着udelay(some_var)里面如果some_var是 50000,你能得到一段 50 毫秒的忙等待,而且在中断上下文里它不会被任何东西打断。写代码时如果参数来源不确定,务必自己加一层范围判断。
/* 推荐写法:显式钳位,避免运行时超限 */ static void safe_delay_us(unsigned long us) { if (us <= 100) udelay(us); else if (us <= 20000) usleep_range(us, us + 100); else msleep(DIV_ROUND_UP(us, 1000)); }上面这段代码其实和内核较新版本提供的fsleep()思路一致,后面会专门讲。
2.3 什么场景只能用它:中断与原子上下文
忙等待真正不可替代的场景就两类:中断上下文和原子上下文(持有自旋锁、关中断、RCU 读临界区)。这两类场景下调用msleep会直接触发告警,因为调度器没有合法的任务上下文可以挂起。开启了CONFIG_DEBUG_ATOMIC_SLEEP的内核会打印sleeping function called from invalid context,这个检查非常值得打开,尤其是在调试新驱动的时候,它能帮你把"不该睡的地方睡了"全部揪出来。
实际写代码时我会遵循这么一条经验规则:中断处理函数里需要等待硬件时,优先重新设计成"上半部只做最小动作,延时操作丢到底半部或者工作队列里"。比如等一个芯片上电稳定的 20ms,完全可以放到request_work之后的工作队列里做,没必要在中断里硬扛。只有那些外部条件根本不允许延后、且等待时长短到μs级别的场景,才用udelay顶着。
再说说实时内核的情况。打了PREEMPT_RT补丁的内核里,自旋锁会变成可睡眠的互斥量,理论上在某些自旋锁保护区内允许睡眠了。但我不建议你依赖这一点写出"RT 上能跑、非 RT 上崩"的代码。可移植的写法永远是把睡眠操作放到自旋锁之外,这是最稳的。
注意:中断上下文里
mdelay(100)看起来只是慢,实际上影响面比你想象的大:这 100 毫秒里所有依赖该 CPU 的中断都被推后,网络包开始丢失,看门狗可能触发,实时性要求高的任务全部延迟。中断里超过 100 微秒的忙等待,都应该成为代码评审里的重点。
3. 睡眠函数家族:msleep、ssleep、usleep_range 怎么挑
睡眠这几个函数名字很像,但底层走的路径、精度、可中断性都不一样。选型时我会把它们分成三档:毫秒级粗粒度用msleep/ssleep,微秒级精细控制用usleep_range,需要"等固定时间但要自己控制超时逻辑"用schedule_timeout。下面逐个拆。
3.1 msleep 与 msleep_interruptible 的实现差异和返回值陷阱
msleep的实现短得可以背下来:
void msleep(unsigned int msecs) { unsigned long timeout = msecs_to_jiffies(msecs) + 1; while (timeout) timeout = schedule_timeout_uninterruptible(timeout); }关键点有三个。第一,msecs_to_jiffies是向上取整的,传入 1ms 在 HZ=250 的机器上就是 1 个 jiffy(4ms),再加 1 就是 8ms 起步。第二,循环是为了处理"被提前唤醒但没有完全睡够"的情况,schedule_timeout_*返回剩余时间,非零就继续睡。第三,msleep是不可中断的,任务状态是TASK_UNINTERRUPTIBLE,意味着这段时间内kill -9也杀不掉它,用户态程序等着的时候按 Ctrl+C 也没反应。如果这段睡眠意外变得很长(比如参数算错了,睡了几十秒),现场看起来就像进程僵死。
msleep_interruptible的区别在于任务状态是TASK_INTERRUPTIBLE,收到信号会提前醒来,并且返回值是剩余的毫秒数。这个返回值经常被忽略,但它在处理信号时很有用:驱动里想"最多等 100ms,被信号打断就立刻返回错误",就需要看这个返回值判断是不是被打断了。我见过有代码把msleep_interruptible当普通msleep用,信号一来就提前醒,后面读到的硬件状态还没准备好,于是出现偶发性的读取失败,定位了半天。
ssleep就是msleep(secs * 1000)的包装,用起来方便,但没有额外的精度或行为差异。参数是秒级,写的时候注意别把单位搞混,ssleep(1)是一秒不是一毫秒。
再强调一次那个最容易踩的坑:msleep(0)依然会睡至少 1 个 jiffy 再加 1,因为msecs_to_jiffies(0)是 0,加 1 之后变成 1,最终还是绕进定时器里睡一个 jiffy。如果你的本意是"让出 CPU 一下",应该用cond_resched()或schedule(),不要用msleep(0)。
3.2 usleep_range 为什么取代了 usleep
老代码里常见的usleep(100),在新的内核上编译会直接报隐式声明错误,因为它已经被移除了,官方文档给出的替代方案就是usleep_range(min, max)。这个函数收两个参数而不是一个,刚接触的人会困惑:我只要睡 100 微秒,为什么还要给个范围?
原因在于高精度定时器的实现方式。内核要为你这段睡眠设置一个 hrtimer 时间事件,而多个任务的时间事件会被归并到同一批定时器中断里处理,以降低唤醒次数、节省功耗。你给的范围越宽,内核越容易把你的唤醒点和其他任务合并到一次中断里;范围越窄,唤醒越精确,但中断次数越多、功耗越高。所以这个范围本质上是你和系统之间的一次"精度换功耗"的谈判。
我的经验值是这样的,供参考:
| 场景 | 建议范围 | 说明 |
|---|---|---|
| 硬件时序严格要求(如某些传感器转换等待) | usleep_range(us, us + 1) | 范围极小,接受功耗代价 |
| 一般外设等待 | usleep_range(us, us + 100) | 常用折中,唤醒偏差百微秒级 |
| 对精度不敏感的轮询间隔 | usleep_range(us, 2 * us) | 给调度器充分合并空间 |
需要注意usleep_range的下限保证:它在大部分实现里保证"至少睡 min",而不是"精确睡 min",实际唤醒点落在区间内任意位置。另外第一参数不能大于第二参数,写反了会触发警告。还有一点,usleep_range只能用在能睡眠的上下文,中断里调用同样会报错,这点和msleep一致。
至于usleep的消失,如果你维护的是老代码,直接把它替换成usleep_range(us, us + 1)就能编译通过,行为上也基本等价。但如果是新写的代码,我建议还是按上面的表格选一个合理的范围,别一律用 +1。
3.3 schedule_timeout 与 fsleep:底层原语和推荐封装
schedule_timeout是上面这些函数共同的底座,它接收一个 jiffies 单位的时间,把当前任务挂起,返回剩余 jiffies(0 表示睡满了,非零表示中途被唤醒)。它本身不会修改任务状态,这是个非常重要的细节:调用前你必须自己把状态设好,否则任务会带着TASK_RUNNING状态去睡,schedule()一看你还是可运行的,根本不会让出 CPU,直接返回,你等于白睡。
正确的手工用法是这样:
set_current_state(TASK_UNINTERRUPTIBLE); schedule_timeout(HZ / 10); /* 睡 100ms */ __set_current_state(TASK_RUNNING);schedule_timeout还有两个常用封装:schedule_timeout_interruptible()和schedule_timeout_uninterruptible(),前者内部会设置TASK_INTERRUPTIBLE,后者设置TASK_UNINTERRUPTIBLE,调用时就不需要自己设状态了。写驱动的时候如果你要在一个循环里等待某个条件、同时需要控制总超时时间,用这对封装比msleep灵活得多,因为你可以把剩余时间拿回来继续用,实现"总超时 500ms,每 10ms 检查一次条件"这种逻辑。
较新的内核(大约 5.8 之后)提供了fsleep(),它做的事情就是根据时间长短自动在三档之间选择:10 微秒以内走udelay,20 毫秒以内走usleep_range,再长就走msleep。这个函数的出现挺说明问题的——它把大家多年来手工写的选型逻辑固化成了内核 API。如果你的代码面向较新的内核,直接用fsleep是省心的选择;如果需要兼容老内核,就照着它的实现自己包一层,功能一样。
4. sleep 与 wait:wait_event 系列里的睡眠
很多人问过sleep和wait的区别,这个问题在内核语境下其实不太成立,因为它们是包含关系:wait_event系列内部就是在睡眠,只不过睡眠的唤醒条件不是"时间到"而是"某个条件变成真"。硬要区分的话,可以理解成sleep是"我等一会儿",wait是"我等一个事情发生,最多等到某个时间点"。实现上后者更复杂,也更容易写出 bug。
4.1 wait_event 家族的条件循环结构
wait_event宏展开之后大致是这么个形状:
#define wait_event(wq, condition) \ do { \ might_sleep(); \ if (condition) \ break; \ for (;;) { \ prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE); \ if (condition) \ break; \ schedule(); \ } \ finish_wait(&wq, &__wait); \ } while (0)这段代码里有几个设计非常值得琢磨。第一,进循环前先判断一次条件,如果已经满足就完全不睡,这叫"快路径",避免了不必要的上下文切换。第二,在prepare_to_wait之后、真正schedule()之前又判断了一次条件,这一次判断是关键,它堵住了"条件刚被设置、唤醒还没来得及发出去"的时间窗口。第三,唤醒条件永远由wake_up配合条件变量一起完成,标准做法是先改条件,再调用wake_up,顺序反了就会丢唤醒。
wait_event_interruptible是它最常用的变体,任务状态是TASK_INTERRUPTIBLE,能被信号打断,返回值可以区分是条件满足还是被信号打断。驱动里等待用户态可干预的操作,基本都该用这个版本,否则用户按 Ctrl+C 没反应,体验很差。还有wait_event_timeout、wait_event_interruptible_timeout,带超时参数,返回值是剩余 jiffies,用的时候记得判断是超时了还是条件满足了。
实际使用时要注意条件变量的写法。它必须是能被其他上下文修改的共享数据,配套的修改通常需要加锁(比如自旋锁或者互斥量),并且在锁的保护下修改、修改完再唤醒。如果条件变量是个普通整型而没有任何同步,编译器优化和 CPU 乱序都可能让你读到旧值,导致任务明明该醒却继续睡。
4.2 丢失唤醒、内存屏障与 set_current_state
前面提到wait_event内部两次判断条件,就是为了防丢失唤醒。但如果你手工写类似的逻辑,很容易踩到两个坑。第一个是忘设任务状态:
/* 错误写法:条件不满足就 schedule,任务状态还是 RUNNING */ if (!condition) schedule();这段代码看起来挺合理,实际上schedule()发现当前任务是可运行状态,直接返回不切换,等于没睡。后面依赖条件成立的代码就会在条件还没成立时执行,出各种诡异问题。
第二个坑是内存屏障。set_current_state()内部带有全屏障语义,保证"设置任务状态"和"检查条件"这两个操作不会被编译器或 CPU 重排到彼此前面去。如果你绕过它直接赋值current->state = TASK_UNINTERRUPTIBLE,在某些弱内存序架构(某些 ARM 型号)上就可能出现:任务已经把自己挂进等待队列了,但状态还没写进内存,wake_up那一刻看到状态还是RUNNING,于是跳过唤醒,任务永久睡着。这种 bug 在 x86 上测不出来,换到嵌入式平台才复现,定位起来非常折磨人。
所以我的建议很直接:不要手写等待队列逻辑,能用wait_event就用wait_event。它把状态设置、屏障、条件复检、清理这几个容易出错的环节都封装好了,自己写一遍基本是给自己挖坑。只有极少数场景(比如要在等待过程中做更复杂的状态机切换)才需要手工操作prepare_to_wait这一层,那时候也必须成对使用prepare_to_wait/finish_wait,并在中间用schedule()而不是schedule_timeout()。
另外补充一句历史知识:老内核里还有sleep_on和interruptible_sleep_on这类函数,它们早就被移除了,原因是内部存在竞态窗口,用起来非常容易丢唤醒。如果你的代码是移植自很老的版本,看到这两个名字,一定要重写成wait_event系列。
5. 实战排查与速查:从日志定位到参数选择
理论和 API 都讲完了,最后落到我最想分享的部分——真出问题时怎么查。延时相关的 bug 有个共同特点:它们在 x86 服务器上往往不出现,一到嵌入式板子上就偶尔复现,原因多半是没有开对应的调试选项。下面这几个报错和现场,都是我实际处理过的。
5.1 几个高频报错与现场定位思路
BUG: scheduling while atomic:这是最典型的一个,含义是"我在一个不允许调度的上下文里调用了会睡眠的函数"。日志里会带着调用栈,直接看栈顶往下几层,就能找到到底是谁睡的。常见触发点:中断处理函数里调了msleep、spin_lock保护的临界区里调了usleep_range、local_irq_disable之后调了wait_event。定位到之后的标准改法是把这部分逻辑搬到进程上下文,用工作队列或者底半部承接。
sleeping function called from invalid context:和上面类似,是CONFIG_DEBUG_ATOMIC_SLEEP开启后might_sleep()检查报出来的。它比第一个更早发现问题,因为might_sleep()是在函数入口就检查的,不一定非要走到真正schedule()那一步。调试新驱动时我会一直开着这个选项,代价是略微的性能开销,收益是能在测试阶段而不是上线后暴露问题。
任务卡死不返回:没有报错,进程就是不动了。这种要先看/proc/<pid>/stack和/proc/<pid>/wchan,前者能显示内核栈,一眼就能看出卡在哪个函数。如果是卡在schedule_timeout后面,说明定时器没到期或者被反复唤醒;如果是卡在wait_event里,那要看唤醒条件是不是根本没满足,以及有没有别的地方本该wake_up却漏了。我遇到过一回,某个错误分支里忘了调wake_up,结果那个等待任务永远醒不来,最后是看栈里停在schedule才反应过来。
延时明显比预期长:这种多数是 HZ 和 jiffies 换算的问题。最快的验证方法是看/proc/sys/kernel/之外的编译配置(CONFIG_HZ),或者在目标机上跑一个简单的测试模块,循环调用目标函数并用ktime_get打点统计分布。统计之前不要凭感觉,我之前也"笃定"某个函数很准,一测发现误差有 40%,就是向上取整加上 HZ 太低的组合效果。
看门狗被触发:这类问题通常是某个忙等待太长,把中断响应周期拉爆了。排查方法是开 ftrace 的irqsoff跟踪器,它会把关中断时间最长的路径抓出来。几乎所有"看门狗莫名奇妙复位"的问题,用这个方法都能查到真凶。
5.2 精度、功耗、实时性的三角权衡
选延时函数,本质是在精度、功耗、实时影响这三者中间做取舍。udelay精度高、不依赖定时器、但烧 CPU 且影响实时性;usleep_range精度中高、省电、但唤醒有抖动;msleep精度低到毫秒级、非常省电、但因为不可中断可能导致长尾延迟。没有哪一个是"最好"的,只有合不合适。
举几个我实际做过的权衡。电池供电的便携设备上,所有毫秒级以上的等待一律走msleep,哪怕多睡几个 jiffy也无所谓,省下的电比那几毫秒重要得多。工业控制里的通信时序等待,微秒级的部分用udelay并且锁定 CPU 频率,毫秒级的部分用usleep_range收窄范围,宁可多耗一点也要保证时序窗口。跑实时任务的内核上,我尽量让每个忙等待都短于 50 微秒,并且用 ftrace 反复验证最坏情况下的关中断时间。
还有一个容易被忽略的维度是唤醒频率。usleep_range(100, 100)这种极窄范围的调用,如果放在一个高频轮询循环里,每秒可能产生上万次定时器中断,功耗会非常难看。把范围放宽到usleep_range(100, 1000),让内核有机会合并唤醒,功耗能降下来一个档次。这属于"用一点点延迟精度换系统整体效率"的典型操作。
5.3 常见问题速查表与参数选择决策
把前面这些内容压成两张表,方便你排查的时候直接对照。
| 现象 | 可能原因 | 处理方向 |
|---|---|---|
编译报__bad_udelay未定义 | udelay常量参数超过架构上限(约 2~5ms) | 改用msleep或usleep_range |
| 中断里调用后系统挂死 | 中断上下文调用了睡眠函数 | 改用udelay,或把逻辑挪到工作队列 |
msleep(1)实际睡了十几毫秒 | HZ 偏低 + 向上取整 + 额外加 1 | 改用usleep_range,或接受误差 |
任务永久卡在schedule | 丢失唤醒,条件设置与唤醒顺序错了 | 检查是否先改条件再wake_up |
| 禁用信号后无法杀掉等待进程 | 用了不可中断版本 | 换msleep_interruptible或wait_event_interruptible |
| 看门狗偶发复位 | 某处忙等待过长,中断响应被推迟 | 用 ftraceirqsoff定位并缩短 |
| 延时随 CPU 频率变化 | 忙等待依赖启动时校准的循环数 | 锁频,或改用定时器类延时 |
再把选型逻辑整理成一句话版本,方便你在写代码时快速过一遍:能不能睡(中断、自旋锁、关中断里不能睡)→ 要等多久(10μs 内udelay,20ms 内usleep_range,更长msleep,不确定直接fsleep)→ 是等固定时间还是等条件(等条件用wait_event系列)→ 需不需要被信号打断(需要就用_interruptible变体)。这四步走过一遍,基本上不会选错。
最后分享一个小技巧:如果你的模块将来要面对不同 HZ 配置的内核,不要在代码里写死"1 个 jiffy 等于 1 毫秒"这种假设,一律用msecs_to_jiffies、usecs_to_jiffies这类换算宏。我早年写过一段mdelay(1)当作毫秒基准的代码,在一个 HZ=100 的定制内核上跑偏了将近十倍,硬件时序直接不满足,返工重测花了两天。换算宏加上实测打点,这两个习惯能帮你省掉大部分和延时相关的返工。