cpu iowait 到底算 busy 还是 idle?

我们在追查线上问题的时候,经常会碰到 cpu iowait 很高的 case,并且通常这种机器伴随着难登录、操作卡顿等现象,一看 cpu idle 还非常低,以为机器 cpu 被打爆了

其实不然,真正的罪魁祸首是 io,而不是 cpu。你看到的 cpu idle 很低,其实是因为部分 idle 时间被算到了 iowait 里面,导致看起来 cpu idle 很低而已。而纯粹的 iowait 很高,并不会引起系统卡顿,如果你发现系统卡顿,大概率是因为 io 异常的磁盘正好是根分区而已,不过今天我们不讨论 io,只讨论 cpu

那么问题来了,cpu iowait 到底是算 busy 还是 idle?

  1. 如果 cpu iowait 不算 busy,那衡量一个 cpu 的繁忙程度的指标到底是什么?
  2. 如果 cpu iowait 算 busy,这和直觉又不符合啊?既然是 io wait,cpu 都被切换出去执行其他进程了,又何来 busy 一说
  3. 初次之外,内核是如何统计 cpu iowait 的?

1)实验:初探究竟

理论上来看,如果 iowait 不是 busy,那么当我们有进程需要调度的时候,理论上应该可以得到执行。所以要证明这个,最简单的办法就是来做个实验

找一个 iowait 很高的机器(模拟一下),比如这个:

然后我在机器上了,起了一堆死循环进程,来模拟是否可以使用 iowait “状态” 的 cpu,结果。。。

iowait 从之前的 40% 立马就降到了 9.2%,从这个测试来看,iowait 应该不算 cpu busy

2)证明:iowait 时间的内核实现

top 命令的 wa 是从 /proc/stat 文件里读取的,top 源码我就不展开了,有兴趣的可以自己看下 procps 的源码实现,很简单

/proc/stat 文件里有总cpu,单独每个cpu的各种时间片统计,比如 busy, idle, iowait, nice, user, system 等等

cat /proc/stat 
cpu  499156648 12569604 302856986 18424661695 8940778 0 7630333 0 0 0
cpu0 3589203 122206 3657401 141727241 1324795 0 15213 0 0 0
cpu1 5980461 277889 3434600 139724851 81725 0 936559 0 0 0
cpu2 6815015 284360 3833044 138603267 28226 0 872182 0 0 0
cpu3 6846086 272672 4168392 138210450 18639 0 919854 0 0 0
cpu4 8652682 154532 5455878 135270952 22629 0 879416 0 0 0
cpu5 7390439 151983 4619196 137384075 20464 0 869934 0 0 0

/proc 本身是一个特殊的文件系统,其内核实现的所有代码,在 fs/proc 下面:https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/fs/proc?h=v4.10

其中 /proc/stat 的实现对应 fs/proc/stat.c 源码,把代码展开

static int show_stat(struct seq_file *p, void *v)
{
    u64 user, nice, system, idle, iowait, irq, softirq, steal;
    ....

    for_each_possible_cpu(i) {
        user += kcpustat_cpu(i).cpustat[CPUTIME_USER];
        nice += kcpustat_cpu(i).cpustat[CPUTIME_NICE];
        system += kcpustat_cpu(i).cpustat[CPUTIME_SYSTEM];
        idle += get_idle_time(i);
        iowait += get_iowait_time(i);
        irq += kcpustat_cpu(i).cpustat[CPUTIME_IRQ];
        softirq += kcpustat_cpu(i).cpustat[CPUTIME_SOFTIRQ];
        steal += kcpustat_cpu(i).cpustat[CPUTIME_STEAL];
        guest += kcpustat_cpu(i).cpustat[CPUTIME_GUEST];
        guest_nice += kcpustat_cpu(i).cpustat[CPUTIME_GUEST_NICE];
        sum += kstat_cpu_irqs_sum(i);
        sum += arch_irq_stat_cpu(i);
        ....
    }
    ...
}

cpu在进程切换的时候,会把当前进程的所有运行时间,比如 user,system 等时间累加到当前 cpu 的私有变量 cpustat 上,当用户读取 /proc/stat 文件的时候,通过 proc 文件系统的 seq_file 回调接口,进入到 show_stat 这个函数里面,这里面会便利所有 cpu 的 cpustat,得到累加的 user, system 等 cpu 时间

get_iowait_time() -> kcpustat_cpu(cpu).cpustat[CPUTIME_IOWAIT],统计 CPUTIME_IOWAIT 的是 account_idle_wait() 函数,这个函数原本是用来计算 cpu 的 idle 时间的,从这个实现里看,似乎可以看出来,如果当前 cpu 是 idle 的,但是队列里有进程在等待 io 操作,那么当前的等待时间,算 iowait,不算 idle

这似乎就已经证明了我们刚才的猜想

/*
 * Account for idle time.
 * @cputime: the cpu time spent in idle wait
 */
void account_idle_time(u64 cputime)
{
    u64 *cpustat = kcpustat_this_cpu->cpustat;
    struct rq *rq = this_rq();

    if (atomic_read(&rq->nr_iowait) > 0)
        cpustat[CPUTIME_IOWAIT] += cputime;
    else
        cpustat[CPUTIME_IDLE] += cputime;
}

void account_process_tick(struct task_struct *p, int user_tick)
{
    ....
    if (user_tick)
        account_user_time(p, cputime);
    else if ((p != rq->idle) || (irq_count() != HARDIRQ_OFFSET))
        account_system_time(p, HARDIRQ_OFFSET, cputime);
    else
        account_idle_time(cputime);   // 如果不是 user 或者 sys,那就进入 idle 统计逻辑,iowait 和 idle 都在这里
}

---------------------- kernel/time/timer.c  --------------------
/*
 * Called from the timer interrupt handler to charge one tick to the current
 * process.  user_tick is 1 if the tick is user time, 0 for system.
 */
void update_process_times(int user_tick)
{
    struct task_struct *p = current;

    /* Note: this timer irq context must be accounted for as well. */
    account_process_tick(p, user_tick);
    run_local_timers();
    rcu_check_callbacks(user_tick);
    ....
}

nr_iowait 是什么时候才会发生变化呢?nr_iowait 是在调度的核心函数 __scheduler 里修改的

static void __sched notrace __schedule(bool preempt)
{
    struct task_struct *prev, *next;
    ...
    cpu = smp_processor_id();
    rq = cpu_rq(cpu);
    prev = rq->curr;

    schedule_debug(prev);

    /*
     * Make sure that signal_pending_state()->signal_pending() below
     * can't be reordered with __set_current_state(TASK_INTERRUPTIBLE)
     * done by the caller to avoid the race with signal_wake_up().
     */
    rq_lock(rq, &rf);
    smp_mb__after_spinlock();

    /* Promote REQ to ACT */
    rq->clock_update_flags <<= 1;
    update_rq_clock(rq);

    switch_count = &prev->nivcsw;
    if (!preempt && prev->state) {
        if (unlikely(signal_pending_state(prev->state, prev))) {
            prev->state = TASK_RUNNING;
        } else {
            deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK);
            prev->on_rq = 0;

            if (prev->in_iowait) {
                atomic_inc(&rq->nr_iowait);        ========> 这里
                delayacct_blkio_start();
            }

__schedule 是内核进程调度最核心的函数,这个函数负责执行上下文切换。从内核代码上看,进程在切换的时候,如果当前正在让出 cpu 的进程(prev)处于 in_iowait 状态,并且 prev 不在 cpu 运行队列中。这说明什么?说明 cpu 已经 idle 下来了,否则不可能 __schedule 在选择下一个进程来调度的时候,prev 不在 rq 内

3)结论:iowait 属于 idle 时间

由此可以证明,cpu iowait 严格来说属于 idle,不属于 busy。衡量 cpu 繁忙程度的指标,不应该只看 cpu idle,而是要看 total – cpu idle – cpu iowait

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注