专栏名称: Linux就该这么学
专注于Linux运维技术培训,让您学习的每节课都有所收获,订阅本号后可每天获得最新Linux运维行业资讯、最实用的Linux免费教程以及独家Linux考证资料,三十多万技术小伙伴的选择,Linux就该这么学!
目录
相关文章推荐
Linux爱好者  ·  “WePhone创始人被前妻逼死”案件最新进 ... ·  2 天前  
Linux就该这么学  ·  标普:谷歌独占约7成去年新车所搭载车机系统份 ... ·  2 天前  
Linux就该这么学  ·  当了leader才发现,大厂最想裁掉的,不是 ... ·  2 天前  
Linux就该这么学  ·  三分钟读懂 Linux ... ·  3 天前  
Linux就该这么学  ·  “ 加班程序员:从没这样想洋人死过 ... ·  3 天前  
51好读  ›  专栏  ›  Linux就该这么学

CPU离奇飙到100%!开发者挖出16年老Bug:这么多年竟无人发现?

Linux就该这么学  · 公众号  · linux  · 2025-01-23 08:02

正文


作者 | Doug Brown      翻译 | 郑丽媛  出品 | CSDN(ID:CSDNnews)

【CSDN 编者按】每一次对旧设备的升级都仿佛是一场跨越时代的冒险。本文作者致力于将基于 PXA166 的 Chumby 8 设备从 Linux 2.6.28 版本升级到现代 6.x 版本,然而,在看似一切硬件外设都已顺利工作的背后,却出现了一个令他费解的谜团——CPU 使用率居高不下,甚至时常飙升至 100%。通过深入分析内核源码、探讨定时器寄存器的读取方式,本文作者最终找到了隐藏在代码中的 bug 并成功修复,该补丁也已被合并到了 Linux 6.2 版本中。

原文链接:https://www.downtowndougbrown.com/2024/04/why-is-my-cpu-usage-always-100-upgrading-my-chumby-8-kernel-part-9/

一直以来,我都在记录把我那台基于 PXA166 的 Chumby 8 从 Linux 2.6.28升级到现代 6.x 版本的经历。到目前为止,这个项目中的所有主要硬件外设都已正常工作。不过,当我运行 top 命令时,发现了一个很奇怪的现象:CPU 的使用率总是非常高,且原因不明。

Mem: 47888K used, 55968K free, 168K shrd, 3116K buff, 27480K cachedCPU: 100% usr   0% sys   0% nic   0% idle   0% io   0% irq   0% sirqLoad average: 0.00 0.00 0.00 2/51 269PID  PPID USER     STAT   VSZ %VSZ %CPU COMMAND267   200 root     R     2936   3% 100% top100     1 root     S    12240  12%   0% /sbin/udevd -d1     0 root     S     2936   3%   0% init200     1 root     S     2936   3%   0% -sh65     1 root     S     2936   3%   0% /sbin/syslogd -n71     1 root     S     2936   3%   0% /sbin/klogd -n34     2 root     SW       0   0%   0% [irq/56-mmc0]10     2 root     IW       0   0%   0% [kworker/0:1-eve]11     2 root     IW       0   0%   0% [kworker/u2:0-ev]41     2 root     IW<      0   0%   0% [kworker/0:2H-kb]8     2 root     IW       0   0%   0% [kworker/0:0-lib]22     2 root     IW<      0   0%   0% [kworker/0:1H-mm]32     2 root     SW       0   0%   0% [irq/55-mmc1]14     2 root     IW       0   0%   0% [rcu_preempt]17     2 root     IW       0   0%   0% [kworker/u2:1-ev]15     2 root     SW       0   0%   0% [kdevtmpfs]27     2 root     IW       0   0%   0% [kworker/0:2-pm]2     0 root     SW       0   0%   0% [kthreadd]13     2 root     SW       0   0%   0% [ksoftirqd/0]3     2 root     SW       0   0%   0% [pool_workqueue_]

你看,这真是太奇怪了!为什么 top 命令会占用我这么多的 CPU 资源?尤其是第二行,直接显示 usr(用户空间)占用了 100%。有时候,CPU 使用率还会显示为 50% usr 和 50% sys(系统空间),有时又会显示 sys 占用 100%。只有在极少数情况下,top 命令才会如我预期般显示 0% 的使用率。之前 2.6.28 版的 Linux 内核就没有这个问题,所以我猜新版内核中肯定有些不同之处。

我开始猜测问题可能出在哪里,不过也只是瞎猜:也许是我之前成功配置的某个驱动完全占用了 CPU,导致看起来像是其他进程消耗了全部的 CPU;又或者是我缺少某种 CPU 空闲支持,导致处理器一直处在 100% 的高负载下运行,无法休息;还有可能是主线内核中缺少了某些电源管理支持等……无论如何,我需要找到一种方法来缩小问题范围。


确认 bug 并非出自最新的 Linux 6.x

为了解决这个难题,我想到的第一步就是回溯过往。几年前,我曾尝试让 Linux 3.13 在 Chumby 8 上运行,但由于时间不够和经验不足,我最终放弃了这个项目。不过,这段经历在今天变得非常有用,因为我可以尝试启动旧的 3.13 版内核,看看它是否也存在相同的问题。

结果不出所料,3.13 版内核也有完全相同的问题。这一发现很有价值,因为它表明问题并非出自最新版本。而且,由于在那个旧内核中运行的驱动程序并不多,这也排除了很多很多可能的原因。于是,我告别了旧内核,并感谢它提供的线索。

随后,我尝试通过启用 CONFIG_PROFILING 来对现代内核进行分析,并在内核命令行中添加了 profile=2。同时,我还确保将 Linux 编译目录下的 System.map 文件复制到了 Chumby 的 /boot 目录中。这样一来,我就做好了性能分析的准备。

readprofile -r    # resets the counterstop               # run top and wait for a while, then type q to exitreadprofile       # this prints out the final profiling results

readprofile 的结果非常有趣。整个输出太长,无法在这里全部展示,但以下是它输出的最后一部分:

...38 lock_is_held_type                          0.08888 debug_lockdep_rcu_enabled                  0.10004 __schedule                                 0.00191 preempt_schedule_irq                       0.00681 __mutex_unlock_slowpath                    0.00121 mutex_trylock                              0.00223 __mutex_lock                               0.00171 down_read_killable                         0.00933974 default_idle_call                         24.23174 _raw_spin_lock                             0.04351 _raw_spin_lock_irq                         0.00781 _raw_spin_lock_irqsave                     0.008129 _raw_spin_unlock_irq                       0.2788118 _raw_spin_unlock_irqrestore                0.84290 *unknown*4722 total                                      0.0006

从总计来看,default_idle_call 占用了绝大多数时间,这在我看来很正常。经过这次测试,我对那些外设驱动程序不再怀疑,确定 CPU 确实是处于空闲状态——但为什么 top 命令不这么认为呢?

为了弄清楚 default_idle_call 做了什么,我追踪到了它最终调用的 arch_cpu_idle 函数。在 ARM 架构上,如果没有定义 arm_pm_idle 函数,则会调用 cpu_do_idle。而在这个特定的 ARM 设备上,arm_pm_idle 未被使用,因此我继续追踪 cpu_do_idle。最终,通过对编译后的 vmlinux 文件进行反汇编,我确认对于 PXA168 处理器而言,它调用了 cpu_mohawk_do_idle。

根据下面 Linux 6.8 版本的代码,这个函数看起来并不复杂:

ENTRY(cpu_mohawk_do_idle)    mov r0, #0    mcr p15, 0, r0, c7, c10, 4      @ drain write buffer    mcr p15, 0, r0, c7, c0, 4       @ wait for interrupt    ret lr

它们基本上是相同的,只不过新内核还会清空写缓冲区。此外,我发现 2.6.28 版本的内核中并未定义 CONFIG_ENABLE_COREIDLE,这点差异比较大。为了确保万无一失,我用 objdump 又仔细检查了一下 2.6.28 内核:

c0207000 :c0207000:       e3a00000        mov     r0, #0c0207004:       e1a0f00e        mov     pc, lr

没错,2.6.28 版内核中确实没有“等待中断”的操作。这让我有了一些新思路,开始觉得自己可能找到了问题的关键。我试着将现代内核中的 cpu_mohawk_do_idle 函数修改得与旧版本完全相同,但说实话,这在逻辑上并不合理:移除一个“等待中断”指令,难道不会让 CPU 的空闲管理变得更差吗?我强迫自己暂时不去思考这些,反正试试看也没什么损失。

尽管我以为自己有了突破性的进展,但这个方向最终证明是一条死胡同。无论我在 cpu_mohawk_do_idle 中做了什么调整,top 依然显示 CPU 使用率为 100%——本以为我马上就要解决这个问题,瞬间又回到了原点。


从头理解 top 命令的工作原理

当我暂时放下这个问题后,我意识到自己的方法过于随意,缺乏逻辑性。因此,我决定换一种方式来处理这个问题。有一个简单的问题启发了我:top 究竟是如何计算并显示 CPU 使用率的?我突然发现自己对 top 的工作原理知之甚少,完全是凭感觉。于是,我查阅了 BusyBox 的源代码,试图理解它是如何工作的。

原来,top 所显示的所有信息都是来自 procfs,它挂载在 /proc 目录下。在源文件的开头,你可以看到一段注释:“在启动时,当前目录会切换到 /proc,所有的读取操作相对于该目录进行。”关于这个切换工作目录的操作,也在第 1150 行得到了确认。

top 所做的就是读取几个文件,包括 /proc/stat、/proc/meminfo,以及每个运行进程对应的 /proc//stat 文件。在 BusyBox 的 top 实现中,遍历所有进程都是由 procps_scan 函数完成的。源代码中的注释提到,man 5 proc 可以提供更多关于这些文件内容的信息。于是我把重点放在了 /proc/stat 上,为它负责提供 top 输出第二行中显示的 CPU 负载分布情况,例如用户空间、系统空间和空闲时间等。

手册页中提到,/proc/stat 文件中以“cpu”开始的行包含了一系列数字,这些数字对应着 CPU 在不同状态下消耗的时间。以下是从我的台式电脑上获取的一些输出示例:

cpu  33032 1025 8433 426488 1422 0 314 0 0 0

从左到右,这些数字分别表示:用户模式时间、优先级较低的用户模式时间(nice)、系统模式时间、空闲时间、I/O 等待时间、硬中断(irq)、软中断(softirq)、被其他虚拟机占用的时间、运行虚拟机的时间以及低优先级的运行虚拟机的时间。这些时间单位是 USER_HZ,通常是 1/100 秒,但在不同系统上可能会有所差异。而在我自己的 x86_64 架构机器上,这个单位确实就是 1/100 秒。

$ getconf CLK_TCK100

然而,在我使用 buildroot 生成的 Chumby 根文件系统中,并没有 getconf 命令可用。我本可以启用一个包来添加它,但我觉得直接写一个简单的 C 程序来解决这个问题也很容易。

#include #include 
int main(int argc, char *argv[]){printf("%ld\n", sysconf(_SC_CLK_TCK));return 0;}

结果显示它与我的台式电脑相同,同样是 1/100 秒:

# /tmp/test100

既然知道了时间单位是 1/100 秒,我就开始尝试做一些实验。看起来 top 命令只是周期性地检查 /proc/stat 中的值,然后通过比较每次迭代之间数值的变化来计算负载。所以我推测,如果 CPU 大部分时间都处于空闲状态,那么我查看文件内容、等待 10 秒、再次查看文件内容后,空闲计数应该会增加大约 1000(因为 1000 * 1/100 秒 = 10 秒)。于是我决定,在我的台式电脑上试试这个想法。

$ cat /proc/stat | grep 'cpu' ; sleep 10 ; cat /proc/stat | grep 'cpu'cpu  49815 370 14743 25000206 3471 0 169 0 0 0cpu0 2606 12 713 1563406 121 0 2 0 0 0cpu1 2605 31 680 1563428 194 0 0 0 0 0cpu2 4065 56 804 1562009 106 0 2 0 0 0cpu3 2688 11 812 1563209 160 0 1 0 0 0cpu4 3100 10 840 1562442 505 0 0 0 0 0cpu5 2820 13 1032 1562568 150 0 1 0 0 0cpu6 2714 42 821 1562976 241 0 1 0 0 0cpu7 4316 25 1250 1560486 175 0 2 0 0 0cpu8 3397 6 1132 1562333 117 0 2 0 0 0cpu9 3366 16 1035 1562254 75 0 5 0 0 0cpu10 3435 34 748 1562466 207 0 6 0 0 0cpu11 3044 6 803 1562672 67 0 98 0 0 0cpu12 3202 17 1007 1562161 287 0 2 0 0 0cpu13 2612 18 1158 1562700 400 0 0 0 0 0cpu14 2827 49 940 1562505 422 0 39 0 0 0cpu15 3014 18 963 1562585 238 0 2 0 0 0

... 10 秒过后,输出如下:

cpu  49844 370 14756 25016161 3472 0 169 0 0 0cpu0 2606 12 713 1564405 121 0 2 0 0 0cpu1 2606 31 680 1564426 194 0 0 0 0 0cpu2 4066 56 804 1563008 106 0 2 0 0 0cpu3 2689 11 815 1564204 160 0 1 0 0 0cpu4 3101 10 840 1563440 505 0 0 0 0 0cpu5 2822 13 1032 1563566 150 0 1 0 0 0cpu6 2714 42 821 1563976 241 0 1 0 0 0cpu7 4316 25 1250 1561484 175 0 2 0 0 0cpu8 3398 6 1132 1563332 117 0 2 0 0 0cpu9 3367 16 1035 1563252 75 0 5 0 0 0cpu10 3441 34 750 1563458 207 0 6 0 0 0cpu11 3047 6 806 1563666 67 0 98 0 0 0cpu12 3206 17 1007 1563156 287 0 2 0 0 0cpu13 2614 18 1160 1563696 400 0 0 0 0 0cpu14 2829 49 942 1563503 422 0 39 0 0 0cpu15 3015 18 964 1563583 238 0 2 0 0 0

原来除了一个总数之外,每个 CPU 还会有一行单独的统计数据。请记住,空闲计数是第四列。以 cpu13 为例,我们来看看前后的对比。

cpu13 2612 18 1158 1562700 400 0 0 0 0 0cpu13 2614 18 1160 1563696 400 0 0 0 0 0

看起来没错!该 CPU 的空闲计数增加了 996,接近 1000。我们再来看看“总 CPU”行的对比:

cpu  49815 370 14743 25000206 3471 0 169 0 0 0cpu  49844 370 14756 25016161 3472 0 169 0 0 0

总空闲计数增加了 15955,大约是 1000 乘以 16。我的 CPU 是 AMD Ryzen 5700G,拥有 8 个核心和 16 个线程——在 Linux 中,每个线程都被视为一个 CPU。好的,数学验证通过了!我感觉自己终于理解了 top 是如何计算的。

接下来,我在我的现代 Chumby 内核上进行了类似的对比。由于只有一个CPU,所以它打印出的 cpu 和 cpu0 行数据相同。我决定进一步过滤,只显示总 CPU 统计数据:

# cat /proc/stat | grep 'cpu ' ; sleep 10 ; cat /proc/stat | grep 'cpu 'cpu  420 0 3204 4 0 0 599 0 0 0cpu  421 0 3213 6 0 0 600 0 0 0

在追踪这个问题上,我终于取得了一些实质性进展,但输出结果明显不对。尽管两行数据之间相隔了 10 秒,但空闲计数只增加了 2,说明 CPU 在这段时间内只有 0.02 秒处于空闲状态。这个结果毫无意义,因为其他列的总增加值为 1 + 9 + 1 = 11 次 = 0.11 秒。

这时,我兴奋地启动了旧版 2.6.28 内核。该版内核在 top 命令下表现正常,于是我重复了这个实验:

# cat /proc/stat | grep 'cpu ' ; sleep 10 ; cat /proc/stat | grep 'cpu 'cpu  4 0 198 4067 5 1 16 0 0cpu  4 0 199 5067 5 1 17 0 0

这次,空闲计数增加了恰好 1000 次,也就是 10 秒——正如我预期的那样!这给了我一个可以开始着手调查的新线索,因为新版内核并没有增加空闲计数(偶尔也会增加,但频率极低),且原因不明。

这完美地解释了为什么 top 总是显示 CPU 被 100% 占用:因为 top 只是根据每次检查时,每列的刻度总和与总刻度之间的差异来计算百分比,但由于每次空闲刻度的差异都是 0 或接近 0,所以 CPU 通常显示为被 100% 占用也就不足为奇了。


适用于 OLPC 的解决方法,也成功解决了我的问题

既然已经明确了问题所在,下一个问题就是:为什么会这样?内核的哪一部分负责增加空闲刻度计数?是什么东西被破坏了,才导致这种情况只在 PXA16x 上发生?

经过一些搜索,我发现了一个针对 OLPC 内核的提交。该提交非常简单明了:在 xo_4_defconfig 中禁用了 CONFIG_NO_HZ。提交信息中,Jon Nettleton 描述的问题与我的情况极为相似。

在内核中启用 CONFIG_NO_HZ 时,似乎存在一个计数问题,它会导致系统报告的 CPU 使用率异常偏高。而这个问题进一步破坏了快速用户挂起(fast user suspend)的功能。

有趣的是,我还注意到了一个提交标题以“arm: mmp3”开头,表明它是比我手上这款更新的一款 Marvell CPU,所以我对此非常感兴趣。内核文档中详细解释了 CONFIG_NO_HZ,其作用是在 CPU 空闲时禁用调度器所使用的周期性定时器中断。而我发现,我的内核配置了CONFIG_NO_HZ_IDLE=y,这实际上等同于旧版本中所定义的 CONFIG_NO_HZ=y。这意味着我的内核配置与 OLPC 在遇到类似问题时的配置非常相似!

这一发现为我提供了新的思路。很幸运,前面提到的内核文档中解释了所有相关配置选项的新旧名称。特别是,它告诉我,禁用 CONFIG_NO_HZ 在新版内核中的等效配置是启用 CONFIG_HZ_PERIODIC=y。它还告诉我,可以通过添加一个额外的内核命令行参数“nohz=off”来暂时禁用该选项,而无需重新编译内核。所以,我尝试了这个方法并重新运行测试。

Mem: 50240K used, 53616K free, 200K shrd, 3116K buff, 27276K cachedCPU:   0% usr   0% sys   0% nic  99% idle   0% io   0% irq   0% sirq

结果立竿见影:top 不再显示 100% 的 CPU 使用率,而是正确地报告了接近 99% 的空闲时间——看到这样的输出,我非常开心!同样,使用 /proc/stat 进行的相同测试也得到了正确结果。

# cat /proc/stat | grep 'cpu ' ; sleep 10 ; cat /proc/stat | grep 'cpu 'cpu  345 0 731 7170 67 0 0 0 0 0cpu  347 0 733 8167 67 0 0 0 0 0

显然,OLPC 的解决方法也成功解决了我的问题!


原来,这是一个寄存器读取时序问题

虽然找到了解决方案,但我并不满足于此。一切看起来有点神奇,我想要深入理解问题的本质。理论上来说,CONFIG_NO_HZ_IDLE 应该能正常工作,所以我怀疑存在更深层次的 bug。我希望能够使我的 CPU 在 dyntick-idle 模式下工作!

于是,我决定从 /proc/stat 开始反向追踪。内核中哪个函数负责提供 /proc/stat 的内容?它又是如何获取空闲时间的数据?

查阅资料后,我发现 /proc/stat 的内容是由 fs/proc/stat.c 中的 show_stat 函数提供的。在文件底部,你可以看到它调用了 proc_create,将“stat”设置为 /proc 中的一个文件。总之,show_stat 调用 get_idle_time,而后者又调用了 get_cpu_idle_time_us。最后一个函数非常有意思,因为它上方的注释提到:“这个时间是通过计数而不是采样来测量的。”

get_cpu_idle_time_us 通过调用 get_cpu_sleep_time_us 来查看时间,而 get_cpu_sleep_time_us 又调用了 ktime_get 来获取当前时间以便进行计数。我不认为这里的数学计算会出错,否则不同架构的众多用户都会遇到相同的问题。事实上,在另一个 Marvell ARCH_MMP 处理器上也发生了相同的事情,这是一个重要的线索。

那么,ktime_get 返回什么值,它是如何工作的呢?其背后的理念很简单:它返回一个表示单调递增计时器的 ktime_t 类型,单位是纳秒。具体来说,文档中提到 ktime_get 返回的时间从系统启动开始,但在挂起期间会暂停。最终,它需要调用特定于硬件的代码,这也是我认为问题所在的地方。

如果你沿着 ktime_get 的调用链向下跟踪,你会经过 kernel/time/timekeeping.c 中的一些函数,最终到达 tk_clock_read,它会调用一个由时钟源提供的 read 函数。正如内核源码所述,clocksource 是“一个自由运行计数器的硬件抽象。最后,我找到了通向硬件相关代码的路径。

我在内核的 arch/arm/mach-mmp 源目录中搜索与 clocksource 相关的代码。果然,我发现了关键线索:

time.c: *   Support for clocksource and clockeventstime.c:static u64 clksrc_read(struct clocksource *cs)time.c:static struct clocksource cksrc = {time.c:  .name    = "clocksource",time.c:  clocksource_register_hz(&cksrc, rate);

进一步查看在这个文件中注册的时钟源时,我发现获取当前时间的相关函数是 clksrc_read:

static struct clocksource cksrc = {    .name       = "clocksource",    .rating     = 200,    .read       = clksrc_read,    .mask       = CLOCKSOURCE_MASK(32),    .flags      = CLOCK_SOURCE_IS_CONTINUOUS,};

这个函数实际上是 timer_read 的一个包装器,而 timer_read 才是从硬件计时器读取数据的实际代码。

/* * FIXME: the timer needs some delay to stablize the counter capture */static inline uint32_t timer_read(void){int delay = 100;
__raw_writel(1, mmp_timer_base + TMR_CVWR(1));
while (delay--) cpu_relax();
return __raw_readl(mmp_timer_base + TMR_CVWR(1));}

这段代码比我预想的要复杂得多。我本以为它只是一个简单的寄存器读取操作,但实际上,它需要先向寄存器写入一个 1,然后等待一段时间,再读取同一个寄存器的值。该函数的注释中还有一个非常显眼的 FIXME(待修复),这立刻引起了我的警觉。

为了更好地理解这个问题,我决定回到 Armada 16x 的软件手册,查阅硬件定时器的相关内容。起初,我怀疑可能是定时器配置了错误的时钟频率,但经过确认,我发现计时器确实像代码中说的那样,被配置为了3.25 MHz。在 PDF 第 683 页底部,我发现了关于定时器计数寄存器的一个有趣说明:

定时器值在读取时存在亚稳态风险。因此,读取每个 CR 寄存器的值有两种方法:

1、双重读取并比较:读取两次并比较两个读取值,以确保值是有效的。

2、使用 CVWR 寄存器:这对于快速时钟计时器尤其有效。

显然,Linux 内核中的代码采用了第二种方案。我查阅了 Marvell 的 CVWR 寄存器文档,里面关于该寄存器的文档非常简略,只提到了该寄存器可防止在读取计数值时出现不稳定的风险,并提供了简单的读写规则。

● 写入:

0x0:无效

0x1:捕获 CRn 的值

● 读取:

返回捕获的 CRn 寄存器的值

没错,这就是内核想要实现的功能。于是,我随便试了一下,将现有代码替换为直接读取计数寄存器,完全忽略亚稳态风险:

static inline uint32_t timer_read(void){return __raw_readl(mmp_timer_base + TMR_CR(1));}

结果竟然成功了!进行此更改后,top 命令显示出了正确的 CPU 空闲时间。我确信自己找到了问题的根源,于是我又回到原来的代码,并进行了更多尝试。如果我将延迟从 100 次增加到 500 次,原始代码就开始正常工作了。进一步调整时,又发现 200 次不行,但 300 次却可以。

我终于找到了问题的根本原因。我对问题的最初猜测完全错了——这只是一个简单的寄存器读取时序问题。现有代码在请求计时器捕获最新值后等待的时间不够长,因此返回的是上一次读取尝试的值,而不是最新的值。函数上方的 FIXME 注释是对的,可惜软件手册没有提供关于延迟应该设置为多少的具体信息。

就在这时,我回想起 Chumby 的原始 2.6.28 内核工作正常,于是我决定查看它的代码版本。不出所料,它有一个不同版本的 timer_read。

首先,它使用的是定时器 0,而不是定时器 1,而更大的区别在于,它通过多次读取寄存器而不是空循环来实现延迟,同时它还在捕获期间禁用了中断。

我继续搜索 Marvell 的其他内核,看看它们是否有不同的定时器读取方式:

● https://github.com/kumajaya/android_kernel_samsung_lt02/blob/24c62c9af6bf6d424aaa3f249099b0eef8ccbec4/arch/arm/mach-mmp/time.c#L60

● https://github.com/embeddedTS/linux-2.6.34-ts471x/blob/c3de2e89f23d328e99253b2320efd8944e524b08/arch/arm/mach-mmp/time.c#L61

第一个链接中的内核似乎实现了 Marvell 建议的双重读取方法,但前提是计时器被配置为32.768 KHz,否则它的操作方式与 Chumby 2.6.28 内核类似,但没有禁用中断;第二个链接中的内核似乎完全忽略了亚稳态风险,当定时器配置为 32.768 KHz 时也不做任何处理,其他情况下则采用了类似旧版 Chumby 内核的解决方案。


这个 bug,自 2009 年起就存在

这个 bug 从 MMP 架构加入内核以来就一直存在。2009 年初提交的最早添加 PXA168 支持的补丁就包含了有 bug 的代码,但那时使用的是定时器 0 而不是定时器 1。所以,这段代码实际上是 Chumby 内核代码和新版主线代码的混合版本。那时,注释中也还没有 FIXME。

/* * Note: the timer needs some delay to stablize the counter capture */static inline uint32_t timer_read(void){int delay = 100;
__raw_writel(1, TIMERS_VIRT_BASE + TMR_CVWR(0));
while (delay--) cpu_relax();
return __raw_readl(TIMERS_VIRT_BASE + TMR_CVWR(0));}

深入研究后,我发现 FIXME 注释出现在内核 Git 历史中的初始提交中,这意味着当时一定有人意识到代码中还有问题。看起来,后来的改动将定时器从 0 改为 1,目的是确保 clockevents 拥有自己独立的计时器,从而修复 OLPC XO-1.75 上观察到的一个 bug。

令我惊讶的是,这么多年来,这个 bug 竟然没有引起主线内核开发者的注意。更准确地说,OLPC 项目确实发现了这个问题,但由于他们已经有了一个变通方案,所以并没有将其视为大问题。因此,我决定着手解决这个潜在问题。

由于 Marvell 厂商的内核使用多次读取寄存器来实现 CVWR 方式的延迟,所以我也选择了这种方法。首先,我需要决定延迟的迭代次数。Chumby 的内核总共对 CVWR 寄存器进行了 5 次读取,其他两个内核则进行了 3 次读取,所以我选择了 4 次作为折中,以防 Chumby 有充分理由进行更多次迭代。至于是否在延迟期间禁用中断,我认为没有必要这样做——最坏的情况不过是两次定时器读取几乎同时发生,第一次读取返回的时间值略晚于原本应返回的时间,或者第一次读取被取消并返回旧的定时器值。虽然这可能是个问题,但现有的代码也没有禁用中断。

我第一次提交这个问题的修复是在 2022 年 9 月,但没有收到任何回复。后来,我在同年重新提交了修复,并且第二次抄送给了主要的 SoC 维护者,他们处理了我的修复并合并到了代码中。最终,我的补丁被合并到了 Linux 6.2 版本中,并且也被反向移植到了多个 4.x、5.x 和 6.x 版本的内核中。自从实施这个修复后,我的 Chumby 设备上再也没有出现过 CPU 时间报告方面的问题。

这就是我如何发现我的 Chumby 8 上 CPU 使用率总是显示为 100% 的故事。经过一番对 BusyBox、/proc 和多层内核代码的探索,我终找到了读取 PXA168 定时器计数寄存器的代码中隐藏的小 bug。能解决这个问题我感到非常满意!我花费的时间也完全值得,因为我了解了 procfs 的工作原理以及 top 命令如何获取 CPU 使用情况的信息。尽管我仍然觉得自己对 Linux 内核的内部机制了解得不够多,但能解决这样的问题是我初步接触内核开发的一个绝佳契机。

END

官方站点:www.linuxprobe.com

Linux命令大全:www.linuxcool.com

刘遄老师QQ:5604215

Linux技术交流群:2636170

(新群,火热加群中……)

想要学习Linux系统的读者可以点击"阅读原文"按钮来了解书籍《Linux就该这么学》,同时也非常适合专业的运维人员阅读,成为辅助您工作的高价值工具书!