找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

603

积分

0

好友

75

主题
发表于 5 天前 | 查看: 20| 回复: 0

在上一篇文章中,我们介绍了如何正确理解 Linux 系统中的 iowait 指标。简单来说,iowait 是 CPU 空闲时间的一部分,它表示 CPU 因为等待 I/O 操作(主要是块设备 I/O)完成而无所事事的时间占比。它并不是 I/O 本身消耗的 CPU 时间,而是 CPU 在等待 I/O 时的“空闲”时间

我们通常使用 vmstatiostatmpstat 等命令来查看 iowait。这些命令的数据源头,都是 /proc/stat 这个伪文件。它包含了由内核维护的原始 CPU 统计信息。

查看 /proc/stat 文件获取CPU统计信息

/proc/stat 中的数据完全来自 Linux 内核的内部计数器,由内核在运行时动态统计和更新。那么,内核究竟是如何统计出 iowait 时间的呢?让我们深入源码,一探究竟。

首先,我们需要找到 /proc/stat 在内核中的实现。其核心是一个名为 show_stat 的函数。

内核中 /proc/stat 文件的初始化代码

show_stat 函数中,内核会遍历每一个在线的 CPU,获取其 kernel_cpustat 结构体。这个结构体里记录了 CPU 在用户态、系统态、空闲、中断以及 iowait 等各个方面所花费的时间。获取 iowait 时间的函数是 get_iowait_time

show_stat 函数中获取各CPU统计信息的核心代码

get_iowait_time 函数的逻辑是:它会尝试调用 get_cpu_iowait_time_us 来获取以微秒为单位的 iowait 时间。

get_iowait_time 函数实现

我们继续追踪 get_cpu_iowait_time_us。可以看到,这个函数实际上就是读取每个 CPU 私有变量 tick_cpu_sched(一个 tick_sched 结构体)中的 iowait_sleeptime 成员值。

get_cpu_iowait_time_us 函数定义与注释

那么,iowait_sleeptime 是在哪里被更新的呢?在内核中搜索可以发现,更新发生在 tick_nohz_stop_idle 函数中。这个函数是 Linux 内核 NO_HZ 机制的关键部分,它的作用是在 CPU 退出空闲状态时被调用。

当 CPU 上没有进程可运行时,它会进入空闲状态,内核可能会停止周期性的时钟中断(NO_HZ)以节省功耗。当有中断发生或进程被唤醒时,CPU 需要退出空闲,tick_nohz_stop_idle 便负责处理这个“退出”逻辑。

这个时间点恰恰是更新 iowait 计时的最佳时机。因为一段等待 I/O 的时间要想被计入 iowait,前提是 CPU 在这段时间内必须是空闲的。函数会计算本次空闲的时长 delta(当前时间减去进入空闲的时间 idle_entrytime)。关键判断来了:如果当前 CPU 的 nr_iowait_cpu 返回值大于 0,那么这段空闲时间 delta 就被累加到 iowait_sleeptime 上;否则,它只能被计入普通的 idle_sleeptime

tick_nohz_stop_idle 函数中更新 iowait_sleeptime 的逻辑

由此可见,一段 CPU 空闲时间能否“升级”为 iowait 时间,完全取决于 nr_iowait_cpu 这个函数的返回值。我们来看它的实现:

nr_iowait_cpu 函数实现

这个函数非常简单,就是返回指定 CPU 运行队列(struct rq)中的 nr_iowait 成员值。顾名思义,nr_iowait 很可能就表示该 CPU 上有多少个正在因等待 I/O 而被阻塞的进程

为了验证,我们需要在内核中搜索 nr_iowait 是在哪里被设置的。可以追踪到 __block_task 函数。这个函数由核心调度函数 __schedule 调用。熟悉 Linux 进程调度的同学都知道,__schedule 是执行任务切换的核心。

当一个进程执行了会导致阻塞的操作(比如等待 I/O)而主动让出 CPU 时,最终就会调用到 __schedule。此时,正是更新运行队列 nr_iowait 计数的最佳时机。从代码中可以看到,如果当前进程的 in_iowait 标志为非零,那么就会将当前 CPU 运行队列的 nr_iowait 值加一。

__block_task 函数中更新 nr_iowait 的代码

这样一来,逻辑就串起来了:在 tick_nohz_stop_idle 中,正是因为 nr_iowait_cpu 读到了大于 0 的值(表示有进程在等 I/O),空闲时间才被计入了 iowait。而 nr_iowait 增加的关键,在于有进程的 in_iowait 标志被置位且该进程进入了阻塞状态。

那么,进程的 in_iowait 标志又是在哪里被设置的呢?我们继续搜索,找到了以下代码:

io_schedule 相关函数中设置 in_iowait 标志的代码

io_scheduleio_schedule_timeout 这类函数中,内核会调用 io_schedule_prepareio_schedule_finish。在进程即将调用 schedule() 让出 CPU 之前,io_schedule_prepare 会将当前进程的 in_iowait 标志置为 1,表明本进程即将因等待 I/O 而睡眠。当进程所等待的 I/O 完成并被唤醒后,io_schedule_finish 会被调用,将 in_iowait 标志恢复为 0。

正向梳理:一次完整的 iowait 统计旅程

以上我们是逆向从 /proc/stat 的输出追溯到了源头。现在让我们正向梳理一遍整个过程:

  1. 进程发起 I/O 等待:当一个进程需要等待 I/O 完成时,它(通常是内核驱动或文件系统代码)会调用 io_schedule()io_schedule_timeout()。这两个函数会先将当前进程的 in_iowait 标志置为 1。
  2. 进程让出 CPU:随后,进程通过 schedule() 系列函数进入睡眠状态。在核心调度器 __schedule 的路径上,会检查当前进程的 in_iowait 标志。如果为 1,则将该进程所在 CPU 运行队列的 nr_iowait 计数加 1。
  3. CPU 进入空闲:如果此时该 CPU 上没有其他可运行的进程,它就会进入空闲状态,并记录下进入空闲的时间点(idle_entrytime)。
  4. CPU 退出空闲并统计时间:当 I/O 完成中断到来或其他事件唤醒某个进程时,CPU 需要退出空闲状态。此时 tick_nohz_stop_idle 函数被调用。它计算出自进入空闲到此刻的时间差 delta关键判断:检查当前 CPU 运行队列的 nr_iowait 是否大于 0。如果是,说明在刚刚这段空闲期里,至少有一个进程在等待 I/O,那么这段时间 delta 就累加到 iowait_sleeptime;否则,delta 只累加到普通的 idle_sleeptime
  5. 进程唤醒,清理状态:被唤醒的进程在 io_schedule_finish 中将自己的 in_iowait 标志清 0。当它被重新调度上 CPU 运行时,如果它再次让出 CPU,在 __block_task 中会使 nr_iowait 减 1(通过对应的原子递减操作)。
  6. 用户读取统计:最后,当用户或监控工具(如 vmstat)读取 /proc/stat 时,内核便将每个 CPU 的 iowait_sleeptime 汇总并输出。

vmstatiostatmpstat 这些 系统监控工具 的工作原理,就是定期读取 /proc/stat,然后计算相邻两次采样之间 iowait 时间的差值,再除以总的耗时,从而得到这段时间内的 iowait 百分比。这也解释了为什么这些工具能以极低的开销提供精确的 CPU 统计信息——所有繁重的计数工作都已在内核中完成,用户空间只是做简单的读取和计算。

希望这篇从内核源码角度出发的剖析,能帮助你更深刻地理解 iowait 这个关键性能指标的来龙去脉。如果你想深入探讨更多 Linux 系统内核或性能优化相关的话题,欢迎在云栈社区进行交流与分享。




上一篇:SQL字符串处理核心指南:从拼接、截取到正则匹配与JSON解析的45个函数详解
下一篇:Python数据分析入门:零基础3步安装Pandas与10分钟掌握数据读取
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-24 04:03 , Processed in 0.301002 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表