最近在调一块ARM嵌入式板子的功耗,发现一个特别典型的症状:CPU占用不到1%,系统负载几乎为零,但整机电流怎么都降不下去。用示波器抓了一圈,最后定位到是内核时钟子系统的问题——周期性tick中断一直在跑,每几毫秒就把CPU从睡眠状态拉起来一次,功耗自然下不来。想彻底解决这个问题,绕不开两个关键概念:调度定时器(sched_timer)和动态时钟(nohz)。这篇文章就把这两块机制的原理、代码路径和实际操作中踩过的坑一次讲透,适合正在做内核调优、低功耗优化,或者单纯想搞明白tick机制的同学参考。
1. 调度器为什么离不开那个“心跳”:sched_timer的作用
1.1 tick不只是一个时间计数器,它是调度器的生命线
Linux内核里有一个非常基础的概念叫“tick”,也就是时钟节拍。内核编译时会配置一个CONFIG_HZ参数,常见的是100、250、1000,单位是Hz,表示每秒触发多少次时钟中断。比如HZ=250,代表每隔4毫秒,本地APIC定时器就会触发一次中断,这个中断就是系统的“心跳”。
很多人误以为tick只是用来维护系统时间的,其实它的作用远不止这么简单。每次tick中断到来时,内核会依次做几件事:更新jiffies系统时钟、更新进程的CPU时间统计、处理到期的低精度定时器,还有最关键的——调用调度器的周期性处理函数scheduler_tick()。换句话说,整个调度器的公平性、实时性、负载均衡,都依赖这个周期性的“敲打”。
你可以把tick想象成医院的护士查房。护士每隔一段时间来病房看一眼,量体温、换药、看看病人有没有异常。如果护士不来了,病房里短期看不出大问题,但遇到突发状况(比如任务被唤醒、CPU空闲了很久、某个进程一直在独占CPU),整个系统就会陷入混乱。sched_timer本质上就是负责安排这个“查房时间表”的机制。
1.2 scheduler_tick一次中断里到底要干多少活
update_process_times()被tick中断触发后,会进一步调用scheduler_tick()。这个函数是调度器周期性工作的核心入口,它做的事情比大多数人想象中复杂得多:
- 更新当前任务的运行时间统计:CFS调度器需要基于虚拟运行时间(vruntime)来选择下一个运行的任务。每次tick都要把当前任务的运行时间累加进去,更新vruntime,否则任务的优先级权重就“算不清账”。
- 检查时间片是否耗尽:CFS虽然没有传统的时间片概念,但sched_period和sysctl_sched_latency等参数需要周期推进;调度实体的运行预算(比如
cfs_bandwidth)要靠tick来扣减。 - 实时任务和deadline任务的时长检测:RT调度器和DL调度器需要检测任务是否超过运行时限,超过就要触发抢占或报错。
- 触发负载均衡:每个调度域会周期性地调用
load_balance(),tick是一个重要的触发点。 - 各种统计数据的更新:比如
/proc/stat里的cpu时间、perf事件、schedstat等,都需要tick驱动。
有一次我在排查一个“进程偶尔卡顿”的问题时,通过perf sched看到scheduler_tick的耗时波动接近几十微秒,深入查才发现是某个模块在timer softirq里做了太多事拖累了tick处理。这个经历让我深刻认识到,tick路径上的小问题会被放大到整个系统的调度质量上。
1.3 sched_timer的本质:一个挂在hrtimer上的tick驱动
从实现层面看,sched_timer并不是什么神秘的独立组件,它其实是tick_sched结构体里的一个hrtimer。结构体定义大致长这样:
c复制struct tick_sched {
struct hrtimer sched_timer;
unsigned long check_clocks;
enum tick_nohz_mode nohz_mode;
ktime_t last_tick;
int inidle;
int tick_stopped;
...
};
`tick_setup_sched_
