Linux内核延时函数选型:delay与sleep区别及原子上下文避坑
1. 从一次传感器上电时序卡死说起delay 和 sleep 根本是两套东西前阵子帮人看一个 I2C 温湿度传感器的驱动现象很典型单独跑测试用例一切正常一旦并入整机系统开机跑几分钟就会刷出一片BUG: sleeping function called from invalid context或者scheduling while atomic伴随系统响应变慢、看门狗报警。代码翻出来一看问题就在一个不起眼的msleep(20)上——它被写在了request_irq注册的硬中断处理函数里。这类问题在内核驱动开发里出现频率极高根因只有一个很多人把延时函数当成了一类东西以为它们只是等一会儿的不同写法随便挑一个能编过就行。实际上Linux 内核里名字带delay和名字带sleep的函数走的是两条完全不同的实现路径一条是占着 CPU 空转另一条是让出 CPU 去睡觉。选错了轻则延时不准、功耗飙升重则直接触发内核 BUG、系统挂死。这篇就把ndelay、udelay、mdelay、msleep、ssleep、msleep_interruptible、usleep_range、fsleep这一串函数掰开揉碎讲一遍它们底层各自靠什么实现精度和代价差在哪什么上下文能用、什么上下文绝对不能用以及在真实驱动里到底该怎么选。如果你在写字符设备驱动、I2C/SPI 外设驱动或者做嵌入式内核移植这些内容基本是绕不开的基本功。1.1 同样是等 1 毫秒两条路差了十万八千里先把最核心的分野说清楚。udelay(1000)和msleep(1)表面上都是等大约 1 毫秒但内核做的事情截然不同。udelay属于忙等待busy-wait。它拿到一个校准过的循环次数loops_per_jiffy然后老老实实在 CPU 上跑一段空循环一圈一圈数下来数够了就返回。这期间 CPU 一直在跑指令调度器插不进来别的任务别想上这个核。它的好处是延时相对精确微秒级可用坏处是白白烧电。msleep属于睡眠等待sleep。它把当前任务的状态设成TASK_UNINTERRUPTIBLE然后调用调度相关的接口主动让出 CPU同时向内核定时器注册一个唤醒时间点。任务从运行队列里摘出去CPU 立刻去跑别的任务或者进入空闲状态等到时间到了定时器把任务重新标记为可运行调度器再把它捞回来继续跑。用一个生活化的类比udelay就像你站在电梯门口一直盯着楼层数字什么都不干就等着电梯来msleep则是按了按钮之后坐到旁边椅子上刷手机电梯到了会响一声叫你。前者保证你第一时间看到电梯代价是你这一分钟啥也干不了后者这几十秒还能干点别的但取决于别人什么时候叫你。提示判断用哪个系列第一句话永远是问自己——我当前处在哪个上下文如果是中断上下文、软中断、持有自旋锁、关抢占的临界区只能用*delay如果是普通进程上下文优先考虑*sleep。1.2 内核里的等待其实有三条路不是两条实际写代码时等待这件事会被拆成三种语义搞清楚这三种选型就不容易错。第一种是纯时间关系且不能被打断比如某个芯片复位后必须保持 5ms 低电平这 5ms 内不能有任何调度行为把时序拉长就用忙等。第二种是纯时间关系但可以被打断比如等一个传感器完成一次转换超时了就报错退出。这种场景只关心最多等多久中途被信号打断也无所谓适合用msleep_interruptible或者usleep_range。第三种是等待某个条件成立超时只是兜底比如等网卡寄存器某个状态位置位、等 DMA 完成标志。这时候时间只是保险丝真正决定什么时候返回的是条件本身。这类场景压根不该用固定延时轮询而应该用wait_event系列或者readx_poll_timeout这类封装。我见过不少驱动把第三种硬写成while (!(readl(reg) FLAG)) mdelay(10);然后配一个计数器防死循环。这种写法编译能过、测试也过但在负载高的系统里会出现两个问题一是忙等把 CPU 占满二是固定 10ms 的轮询间隔可能让一个本来 100 微秒就能完成的操作拖到 10 毫秒整机性能莫名其妙变差。举个我在做的以太网 PHY 驱动里的实测对比同样是等待链路协商完成写法底层机制CPU 占用典型延迟说明while(!link) mdelay(10)忙等全程 100%平均 5ms负载高时挤占其他任务while(!link) msleep(10)睡眠轮询接近 0平均 10ms至少多等一个间隔wait_event_timeout()事件唤醒接近 0事件到达即返回需要驱动侧有唤醒点readx_poll_timeout()微秒级轮询低可控微秒~毫秒通用性最好这张表里的差异在单核嵌入式平台上会被放大得很明显。1.3 怎么判断自己在不在原子上下文很多报错日志里写的是sleeping function called from invalid context翻译过来就是你在这个地方不允许睡眠。判断依据是内核里的抢占计数preempt_count()以及in_interrupt()、in_atomic()这组宏。大致可以这么记以下场景一律属于原子上下文msleep、ssleep、usleep_range、schedule_timeout全都不能碰。硬中断处理函数request_irq注册的那个 handler 本体软中断、tasklet、raise_softirq相关的上下文持有spin_lock系列自旋锁的临界区显式调用preempt_disable()之后关中断local_irq_save之后定时器回调timer_list.function里早期内核新内核 timer 回调仍在软中断上下文反过来workqueue的工作函数、线程化中断处理函数request_threaded_irq的 thread_fn、内核线程、系统调用路径、probe函数里都是可以睡眠的进程上下文。注意in_atomic()在部分配置下并不完全等价于不能睡眠最稳的判断方式是直接调用might_sleep()内核你不好直接调或者在开发阶段打开CONFIG_DEBUG_ATOMIC_SLEEP让内核在真正出问题之前就把调用栈打出来。这个调试选项我建议在每个新驱动项目初期都打开。另外提一句request_threaded_irq是个好东西它把中断处理拆成硬中断里只做最紧急的事剩下的丢给内核线程两段线程化的那一段就是进程上下文可以放心msleep。上面那个传感器的例子正确的修法就是把msleep(20)从硬中断挪到线程化处理函数里。说到这里顺带回答一个常被问到的概念问题sleep 和 wait 的区别是什么。在用户态语义里sleep 是我主动休息一段时间wait 是我等某个条件成立。在内核里这两个概念是融合的——schedule_timeout同时接受时间参数和任务状态本质上就是睡到时间到 / 被唤醒为止而wait_event系列是在它之上包了一层条件判断让你既能等条件、又有超时兜底。理解了这层关系后面看源码就不会迷路。2. 忙等三兄弟 ndelay、udelay、mdelayBogoMIPS、MAX_UDELAY_MS 和背后的校准逻辑搞清楚了上下文限制接下来把忙等这一族拆开看。这三个函数在头文件里挨着定义但能力边界完全不同很多人以为只是单位不同其实不是。2.1 loops_per_jiffy 是怎么来的calibrate_delay 与 BogoMIPSudelay的实现要点在于内核并不知道1 微秒在这颗 CPU 上对应多少条指令因为不同主频、不同流水线、不同编译器优化都会影响执行速度。所以它必须在启动阶段做一次运行时校准。校准发生在calibrate_delay()里。做法大致是先设一个初始的循环次数用__delay()跑一遍同时用周期性时钟中断jiffies计时看这一个 jiffy 里到底循环了多少次。不断迭代逼近最后得到一个相对稳定的loops_per_jiffy值。交叉编译时你在启动日志里看到的那行Calibrating delay loop... 1993.93 BogoMIPS (lpj9969664)这个 BogoMIPS 就是从loops_per_jiffy换算出来的公式大致是bogomips loops_per_jiffy * HZ / 500000。它只是校准结果的副产品不代表真实性能指标所以你在网上看到的MIPS 分数对比基本没有参考价值——同一颗芯片在不同编译选项下都能跑出不同数字。校准完成之后udelay(us)内部会把这个微秒数换算成一个大整数用的是定点数技巧把us乘以约2^32/1000000的系数再交给__const_udelay()去跑循环。为什么要做定点换算因为在内核里用浮点数是禁忌会破坏 FPU 状态、增加上下文切换成本只能用整数乘除加移位来模拟。由此可以推出一个很重要的结论udelay的精度取决于校准质量和 CPU 频率稳定性。如果你的平台开启了动态调频DVFSCPU 主频在运行中被拉低而loops_per_jiffy还是启动时按高频校准的值那么实际延时就会被拉长。这是嵌入式平台上一个非常经典的延时莫名变长的原因。2.2 udelay 的精度边界与 MAX_UDELAY_MS 这条红线udelay在大多数架构上单次调用的上限是MAX_UDELAY_MS通常是 5 毫秒。超过这个值会发生什么答案是不确定的——有的架构会把参数截断有的会触发编译期错误有的干脆给你一个完全错误的延时。这不是可能会不准而是属于未定义行为绝对不要试探。大量通用代码里定义了一个经典的mdelay#define mdelay(n) (\ (__builtin_constant_p(n) (n) MAX_UDELAY_MS) ? udelay((n) * 1000) : \ ({unsigned long __ms (n); while (__ms--) udelay(1000);}))翻译成人话如果你传的是一个编译期常量而且不超过MAX_UDELAY_MS那就直接展开成一次udelay否则老老实实循环调用udelay(1000)一次一毫秒地累加。这个实现直接暴露了mdelay的两个缺点。第一它是纯忙等期间 CPU 完全被占用。mdelay(100)意味着这个核在 100 毫秒里不会执行任何其他任务。如果是启动阶段一次性初始化忍了如果是运行时的高频路径那基本等于给系统判了缓刑。第二它的精度其实不如想象中好。因为要循环 N 次udelay(1000)每次调用都要做一遍定点乘法和循环计数每次的误差会累积起来。mdelay(100)的实际耗时可能比 100ms 多出几个百分点。那ndelay呢很多架构上它压根没有独立的实现直接转发给udelay#ifndef ndelay #define ndelay(n) udelay(DIV_ROUND_UP(n, 1000)) #endif也就是说你写ndelay(500)它可能实际执行的是udelay(1)——向上取整到 1 微秒。所以想靠ndelay做几百纳秒级别的精确时序控制在通用内核代码里是不现实的真正的纳秒级时序得靠硬件外设定时器、PWM、DMA 触发来保证。2.3 mdelay 为什么在代码评审里总被点名在任何一个稍有规模的内核社区提交里出现mdelay基本都会被 reviewer 追问一句为什么不能睡。这不是吹毛求疵而是有具体代价的。假设你的 SoC 是单核 1GHz跑一个实时性要求一般的系统。mdelay(200)意味着这 200 毫秒里所有等待 CPU 的任务都得排队。如果刚好有个高优先级的音频线程要出数据就会出现爆音xrun。如果这个核正在跑的是实时任务那就是 200 毫秒的实时性违约。功耗上CPU 空转不能进低功耗状态对电池设备是实打实的掉电。在多核系统上影响小一些但也占了一个核的算力还会影响调度器的负载均衡判断。所以业内的普遍共识可以总结成一句话mdelay只用于必须原子且时长很短的场合而且时长最好控制在几百微秒以内。凡是能在进程上下文里做的等待一律换成msleep或usleep_range。一个很典型的、确实必须用忙等的场景某些 SPI 从设备的片选建立时间CS setup time要求从拉低 CS 到发出第一个时钟之间至少保持 100ns。这种时序用睡眠是绝对不行的因为调度延迟本身就远超这个量级只能用ndelay或者干脆靠硬件 SPI 控制器内置的 CS delay 功能来做。再提一句热词里出现的stm32 延时函数 delay 卡死其实和内核这套逻辑是同一个道理的两面。裸机环境下的delay通常是空循环一旦被高优先级中断抢占或者循环计数依赖的时钟源被改掉就会出现卡死的错觉——程序其实在跑只是循环次数远超预期。解决办法也是一样的思路要么用硬件定时器做非阻塞延时要么把长延时的循环拆段中间插一次喂狗或者状态检查。3. 睡眠家族的共同底座schedule_timeout 与 jiffies 粒度忙等讲完了来看睡眠这一族。这一族函数数量多、名字相似但底层的调用链高度收敛搞懂一个就都懂了。3.1 msleep、ssleep 与 schedule_timeout_uninterruptible 的调用链先看最基础的schedule_timeout()。它做的事情是接收一个以 jiffies 为单位的时间把当前任务挂起注册一个定时唤醒然后调用schedule()让出 CPU返回剩余的 jiffies 数如果中途被唤醒返回值就是还没睡完的部分。这里有个必须记住的前提schedule_timeout本身不会帮你设置任务状态你必须自己先设好。标准写法是set_current_state(TASK_UNINTERRUPTIBLE); timeout schedule_timeout(timeout);如果你忘了设置状态任务仍然是TASK_RUNNINGschedule()一看这任务还想跑转头又把它调度回来了——结果就是变成一个死循环每个 jiffy 醒一次跑满 CPU日志里什么都不报排查起来极其痛苦。基于这个底座内核包了几个常用的壳/* 不可中断睡眠除非时间到否则不会被信号唤醒 */ void __sched msleep(unsigned int msecs) { unsigned long timeout msecs_to_jiffies(msecs); while (timeout) timeout schedule_timeout_uninterruptible(timeout); } /* 直接按秒 */ void __sched ssleep(unsigned int seconds) { msleep(seconds * 1000); }注意msleep里那个while循环。为什么要循环因为schedule_timeout在某些情况下可能返回一个非零的剩余值比如内核里存在其他唤醒源、或者计时的边界情况循环能保证总睡眠时间不短于请求值。这也解释了一个常见现象msleep的实际睡眠时间倾向于比请求值长绝不会比请求值短。ssleep就是msleep乘 1000没有额外逻辑。它的存在纯粹是为了代码可读性——你写ssleep(1)比写msleep(1000)更不容易看错一个零。但要注意参数类型ssleep接受的是unsigned int秒如果传一个表达式的计算结果注意别发生整型溢出。3.2 msleep_interruptible 的返回值陷阱msleep_interruptible是最容易用错的一个。它的签名是unsigned long msleep_interruptible(unsigned int msecs);返回的是剩余未睡完的毫秒数如果睡满了返回 0如果中途被信号打断返回剩下没睡的部分。这个语义和schedule_timeout保持一致但和前面那个 void 返回的msleep形成了对比。我踩过的坑在这里曾经写了一段等待外部模块上电的代码用了msleep_interruptible(500)然后紧接着去访问硬件。结果在某些情况下系统里有个信号打过来函数 5 毫秒就返回了硬件还没上电后续访问直接拿到全 0 的数据表现为偶发的读不到设备。查了很久才定位到是返回值没判断。正确姿势长这样unsigned long left msleep_interruptible(500); if (left) { dev_warn(dev, power-up wait interrupted, %lu ms left\n, left); /* 要么重试要么直接报错退出 */ return -ERESTARTSYS; }另外需要强调msleep_interruptible的可中断是指被信号唤醒只对进程上下文有意义。在中 interrupts 上下文里调用它是没有意义的虽然它本身也会因为原子上下文而报错。还有一点容易忽略msleep_interruptible会响应SIGKILL所以如果你在驱动路径里用了它用户态kill一下就能把你的等待打断。对于必须完成的硬件操作比如写 flash 的擦除等待应该用不可中断的msleep避免被打断后留下不一致的状态。3.3 usleep_range 与高精度定时器10 微秒到 20 毫秒的甜点区如果你的等待时长在几十微秒到几十毫秒之间msleep太粗udelay太浪费这时候该上usleep_range(min, max)。它为这个区间专门设计底层用的是高精度定时器hrtimer而不是传统的 jiffies 定时器轮。这意味着它不受HZ粒度限制理论上可以做到微秒级唤醒精度。它的核心设计思路是给一个区间而不是一个精确值usleep_range(50, 100); /* 意思是至少在 50us 之后、100us 之前把我叫醒 */为什么给区间两个原因。第一给调度和定时器子系统留出合并唤醒的空间。内核可以把你和其他时间点接近的唤醒请求合并到同一次硬件定时器编程里减少 CPU 从低功耗状态被唤醒的次数对电池续航有明显帮助。第二避免请求一个无法保证的精确时刻。就算你写usleep_range(60, 60)实际唤醒也会有几百纳秒到几微秒的调度抖动与其给一个假的精确值不如明确告诉内核你能接受的窗口。使用usleep_range有几个硬性约束必须在进程上下文原子上下文里会直接报错。min应该小于max两者差距不要拉得太夸张比如usleep_range(10, 100000)否则相当于放弃了对唤醒时刻的控制实际行为和msleep差不多但代价更高。参数是微秒别和msleep的毫秒搞混。usleep_range(5000, 6000)是 5~6 毫秒不是 5 秒。内核在 5.9 之后还加了一个更省心的封装fsleep()static inline void fsleep(unsigned long usecs) { if (usecs 10) udelay(usecs); else if (usecs 20000) usleep_range(usecs, 2 * usecs); else msleep(DIV_ROUND_UP(usecs, 1000)); }这个函数把上面讲的选型规则直接编码进去了10 微秒以内用忙等因为睡眠的开销比等待时间本身还大20 毫秒以内用usleep_range再长就用msleep。如果你不确定该用哪个又确定自己在进程上下文直接调fsleep是个不掉坑的选择。它比udelay聪明的地方在于udelay传一个很大的值是非法的而fsleep会自动帮你切换实现。4. 选型不靠感觉一张决策表加几个真实场景理论讲完落到实际写代码。这一节给出可直接对照的选型依据以及几个我在真实驱动里反复遇到的场景。4.1 上电时序、复位脉冲、传感器转换等待该怎么写这三种等待在外设驱动里出现频率最高但需求各不相同代码写法也不一样。上电时序数据手册通常写上电后等待不少于 X ms 再进行访问。这种属于纯时间关系且发生在probe或者resume的进程上下文里直接用msleep(x)。如果手册写的是典型 1ms最大 10ms我一般会分成两步先usleep_range(1000, 1500)快速试一次失败了再退到msleep(10)重试这样正常路径快、异常路径稳。复位脉冲要求某根 GPIO 拉低保持 X 微秒再拉高且拉低期间不能被调度拉长。这种虽然理论上应该用忙等但如果 X 大于几百微秒udelay又超过MAX_UDELAY_MS就只能考虑用硬件方案——比如把复位信号接到 PWM 或者定时器输出上让硬件精确产生脉冲软件只负责触发。我见过不少项目在 1.5ms 的复位脉冲上用mdelay在轻负载下没问题一旦系统繁忙多任务抢占会让脉冲宽度出现可观测的波动个别器件就会随机初始化失败。传感器转换等待这是最典型的等待条件成立 超时兜底。以一款常见的温度传感器为例触发单次转换后需要等约 30ms但实际完成时间取决于内部振荡器会有几个毫秒的浮动。写法推荐两种。写法一先固定等待再读/* 典型转换时间 30ms留足余量 */ usleep_range(30000, 35000); ret regmap_read(regmap, REG_TEMP, val);写法二轮询状态位更推荐ret regmap_read_poll_timeout(regmap, REG_STATUS, val, (val STATUS_READY), 2000, /* 每次轮询间隔 2ms */ 50000); /* 总超时 50ms */ if (ret) return ret; /* -ETIMEDOUT 表示器件没响应 */写法二的优势在于正常情况 30ms 就返回了不会白等器件故障时能在 50ms 内报错而不是傻等一个固定值然后读到垃圾数据。regmap_read_poll_timeout内部会根据sleep_us参数自动选择用udelay还是usleep_range——传 0 表示纯忙等可用在原子上下文传大于 0 的值表示每次轮询之间睡眠。4.2 原子上下文里非等不可怎么办有时候确实躲不开中断处理函数里必须等一个硬件状态变化而且这个等待必须有时间上限。这种场景的处理思路是尽量缩到最小 用忙等 可分次。首先要做的第一件事是尽量把等待挪出去。request_threaded_irq的线程化处理函数、workqueue、tasklet 都能承接后续工作硬中断里只做最小动作。这是最干净的方案。如果确实挪不出去比如某个芯片要求中断应答后必须在 20 微秒内完成一次寄存器写那就只能忙等。这时候要注意几点优先用ndelay/udelay控制在MAX_UDELAY_MS以内。轮询时加cpu_relax()在部分架构上它能提示 CPU 这是个自旋循环有助于降低总线和功耗开销。循环里一定要有超时计数防止硬件异常导致死循环。不要在自旋锁里做长忙等这会把其他核也卡住。一个折中的技巧是分次短忙等把 5ms 的等待拆成 50 次 100 微秒的udelay每次之间检查一下need_resched或者其他快速条件。不过要注意在硬中断里这并不能真正让出 CPUneed_resched也要等中断返回后才会被处理所以它主要的价值是让逻辑更可控而不是真的减轻 CPU 占用。4.3 等条件而不是等时间wait_event 的适用边界前面反复提到很多场景本质上应该用事件机制而不是固定延时。这里把wait_event系列和*_poll_timeout系列的适用边界说清楚。wait_event_timeout(wq, condition, timeout)是事件驱动的它会在wq这个等待队列上挂起只有别人显式调用wake_up相关接口时才会被唤醒或者超时。它的返回值语义是条件成立返回剩余 jiffies正数超时返回 0负数理论上不会出现但也要处理。用它的前提是你的驱动里有一个明确的唤醒点比如中断里读到硬件事件后调wake_up(wq)。readx_poll_timeout(op, addr, val, cond, sleep_us, timeout_us)是轮询驱动的它按固定间隔反复读寄存器直到条件满足或超时。它不需要唤醒点适合那种硬件不会产生中断、只能靠读状态位判断的器件。选哪个的判断很简单硬件能不能产生中断或者别的唤醒源能就用 wait_event不能就用 poll_timeout。这里还有个小细节值得说。usleep_range在底层会调用schedule()如果你的等待队列和 hrtimer 混用要注意唤醒的时序。我遇到过一种情况用wait_event_timeout配 10ms 超时同时中断里也会wake_up结果在极端负载下出现了中断已经 wake_up 了但任务还在 hrtimer 的睡眠里没被叫醒的错觉。实际原因是任务当时在usleep_range里睡而不是在等待队列上——这两个等待是串行的不能并行。解决办法是把两者改成二选一或者在睡眠之后补一次条件检查。5. 实测误差与踩坑排查延时不准、睡不够、睡过头的根因上面讲的都是该怎么写这一节讲为什么实际跑起来和预期不一样。同一份代码在不同平台、不同内核配置下表现可能差很多根源基本都在下面几处。5.1 为什么 msleep(1) 实际睡了十几毫秒这是新手最常见的困惑。msleep(1)请求 1 毫秒实测可能是 4ms、10ms 甚至更多。原因有三层逐层叠加。第一层是 jiffies 取整。msecs_to_jiffies(1)的计算是向上取整的。不同HZ配置下结果差很多CONFIG_HZ1 jiffy 等于msecs_to_jiffies(1)理论最短睡眠10010ms110ms2504ms14ms10001ms11ms表格里最关键的信息是msleep的最小有效粒度就是 1 个 jiffy。当HZ100时msleep(1)和msleep(10)的效果是一样的都是 10 毫秒。这就是为什么在很多老内核配置默认HZ100上微秒级和毫秒级的睡眠函数区分度特别差。第二层是定时器精度。传统定时器基于 jiffies 时间轮唤醒点被对齐到 jiffy 边界。在CONFIG_NO_HZ_IDLE大多数现代发行版和嵌入式配置都开着下定时器是按需编程的不需要等下一个周期性 tick所以唤醒点大致就是那个 jiffy 对应的时刻误差主要来自 jiffies 取整而不是 tick 对齐。第三层是调度延迟。定时器到期只是把任务标记为可运行真正被调度上 CPU 还要看当前有没有更高优先级的任务在跑。在负载较高的系统上几百微秒到几毫秒的额外延迟很正常。如果你在一个SCHED_FIFO实时任务旁边跑msleep那延迟可能到几十毫秒。排查方法很直接在目标板子上打时间戳对比。ktime_t t0 ktime_get(); msleep(1); ktime_t t1 ktime_get(); pr_info(msleep(1) actual %lld us\n, ktime_to_us(ktime_sub(t1, t0)));跑几百次取分布你就能看出实际抖动范围。我一般在HZ250的 ARM 平台上看到msleep(1)实测在 4.2ms 到 6ms 之间波动这属于正常范围。5.2 延时被拉长动态调频与 CPU 空闲的干扰如果实测延时明显长于预期而且抖动很大除了调度延迟还要怀疑两个平台相关因素。一个是动态调频DVFS。前面说过loops_per_jiffy是启动时校准的通常按最高频算。如果系统运行中把 CPU 降到一半频率udelay的实际时长会翻倍。这在udelay/mdelay上尤其明显。规避方法是在关键时序代码附近用clk框架临时提升主频或者干脆把这段时序交给硬件外设产生。另一个是CPU 进入深度空闲状态。低功耗配置下CPU 进入 C-state 后从唤醒到真正开始执行需要一段时间exit latency。如果usleep_range请求的时间很短比如 20 微秒CPU 可能还没来得及进入深度空闲就被叫醒或者进去之后唤醒延迟超过了睡眠时间导致实际时间明显拉长。这时候可以适当调整请求区间或者用pm_qos相关接口临时限制 CPU 的休眠深度。这里有个经验值可以参考在一颗典型的 ARM Cortex-A 平台上如果 C-state exit latency 是 50 微秒那么所有小于 100 微秒的usleep_range请求实际上都不会让 CPU 进入深度休眠直接用udelay反而更省事。所以fsleep那个10 微秒以内用忙等的阈值是有道理的。5.3 系统卡死的几类典型症状与对应排查路径最后把我在实际项目里遇到的几类卡死症状和排查路径列一下遇到类似现象可以直接对照。症状一日志刷屏BUG: sleeping function called from invalid context。直接原因就是在原子上下文里调了睡眠函数。排查方式是打开CONFIG_DEBUG_ATOMIC_SLEEP让内核在调用点直接打印栈回溯一眼就能看到哪个函数干的。修复方式是把睡眠挪到线程化处理函数、workqueue或者改用忙等。症状二系统响应极慢但没有报错。通常是某处有个长mdelay在运行时路径上被高频调用。用ftrace的函数图功能或者perf top看一下热点很容易抓到。也可以用CONFIG_PREEMPT配合latencytop之类的工具量化调度延迟。症状三某个任务永远不醒。大概率是schedule_timeout前面忘了set_current_state或者等待条件的检查位置写错了——比如在循环外面检查一次条件就进睡眠条件已经成立但检查发生在设置状态之前就会永久睡下去。标准写法一定是设置状态 → 检查条件 → 必要时才schedule这个顺序wait_event系列内部就是这么实现的所以能用现成的宏就别手写。症状四偶发的时序错误重试就好。这类最难查基本是延时余量不够。建议把关键延时全部用ktime_get打点跑几千次记录最小值和分布。如果最小值已经贴近理论下限就说明余量不足需要加长如果最小值远大于理论值说明有别的东西在干扰顺着 DVFS 和中断风暴的方向查。症状五中断里udelay时间明显偏长。检查这个核上是不是有频繁的其他中断或者 NMI它们会抢占你的忙等循环把实际耗时拉长。忙等的精度是建立在这段代码不被打断的前提上的一旦被打断累计误差就很可观。回过头看这一整套函数的设计逻辑其实很统一内核给你提供从纳秒到秒的全尺度等待能力代价是精度、功耗、上下文限制三者之间的权衡。选型时先问我在什么上下文再问我要等多久最后问我在等时间还是等条件三个问题回答完函数自然就定了。真正容易出错的从来不是函数本身而是开发时没问这三个问题就去写代码。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →