南寨小子 Help

CPU iowait计算原理

1. Timer Interrupt

timer interrupt是操作系统的重要机制,用于驱动进程调度、更新系统时间、处理定时器事件、更新进程运行时间等。

timer interrupt由定时器周期性触发,周期间隔由HZ定义,默认是1秒执行100次。

// /home/kangxiaoning/workspace/linux-3.10.0-1160/include/asm-generic/param.h # define HZ CONFIG_HZ /* Internal kernel timer frequency */
// /home/kangxiaoning/workspace/linux-3.10.0-1160/include/generated/autoconf.h #define CONFIG_HZ 100

2. Periodic Tick

当CPU收到timer interrupt信号时,就会暂停当前任务,切换到预先定义的中断服务程序(Interrupt Service Routine),ISR就是处理timer interrupt这种事件的handler。

在Linux kernel中, timer interrupt的具体实现是periodic ticktimer interrupt这类事件对应的handler函数是tick_handle_periodic ,具体代码如下。

/* * Event handler for periodic ticks */ void tick_handle_periodic(struct clock_event_device *dev) { int cpu = smp_processor_id(); ktime_t next; tick_periodic(cpu); #if defined(CONFIG_HIGH_RES_TIMERS) || defined(CONFIG_NO_HZ_COMMON) /* * The cpu might have transitioned to HIGHRES or NOHZ mode via * update_process_times() -> run_local_timers() -> * hrtimer_run_queues(). */ if (dev->event_handler != tick_handle_periodic) return; #endif if (dev->mode != CLOCK_EVT_MODE_ONESHOT) return; /* * Setup the next period for devices, which do not have * periodic mode: */ next = ktime_add(dev->next_event, tick_period); for (;;) { if (!clockevents_program_event(dev, next, false)) return; /* * Have to be careful here. If we're in oneshot mode, * before we call tick_periodic() in a loop, we need * to be sure we're using a real hardware clocksource. * Otherwise we could get trapped in an infinite * loop, as the tick_periodic() increments jiffies, * when then will increment time, posibly causing * the loop to trigger again and again. */ if (timekeeping_valid_for_hres()) tick_periodic(cpu); next = ktime_add(next, tick_period); } }

2.1 tick_periodic()

tick_periodic()Periodic tick的核心处理函数,负责管理系统的周期性的定时事件,确保系统时间的准确性和进程调度的及时性。

其中的update_process_times()函数主要作用是更新进程的运行时间信息,在CPU时间片结束或发生时间更新时被调用, iowait通过这个函数的执行得到更新。

/* * Periodic tick */ static void tick_periodic(int cpu) { if (tick_do_timer_cpu == cpu) { write_seqlock(&jiffies_lock); /* Keep track of the next tick event */ tick_next_period = ktime_add(tick_next_period, tick_period); do_timer(1); write_sequnlock(&jiffies_lock); update_wall_time(); } update_process_times(user_mode(get_irq_regs())); profile_tick(CPU_PROFILING); }

2.2 update_process_times()

update_process_times()通过传入的user_mode(get_irq_regs())判断处理器当前所处的模式,调用account_process_tick(p, user_tick)将CPU时间统计到对应的指标上,比如top命令中的ussyidwahisi等。

/* * 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; int cpu = smp_processor_id(); /* Note: this timer irq context must be accounted for as well. */ account_process_tick(p, user_tick); run_local_timers(); rcu_check_callbacks(cpu, user_tick); #ifdef CONFIG_IRQ_WORK if (in_irq()) irq_work_tick(); #endif scheduler_tick(); run_posix_cpu_timers(p); }

2.3 account_process_tick()

该函数用于为进程记录CPU时间。根据参数user_tick的值,决定是用户态时间还是内核态时间。根据进程是否是空闲进程,以及是否在硬中断上下文中,分别调用不同的会计函数来记录用户态时间、内核态时间和空闲时间。

  • account_user_time()更新该进程用户态使用的CPU时间。

  • account_system_time()更新该进程内核态使用的CPU时间,具体包括硬中断、软中断、system消耗的CPU时间。

  • account_idle_time()更新该进程的空闲时间,包含CPU没有被任何活动进程使用的时段,以及等待IO的时间

/* * Account a single tick of cpu time. * @p: the process that the cpu time gets accounted to * @user_tick: indicates if the tick is a user or a system tick */ void account_process_tick(struct task_struct *p, int user_tick) { cputime_t one_jiffy_scaled = cputime_to_scaled(cputime_one_jiffy); struct rq *rq = this_rq(); if (vtime_accounting_enabled()) return; if (sched_clock_irqtime) { irqtime_account_process_tick(p, user_tick, rq); return; } if (steal_account_process_tick()) return; if (user_tick) account_user_time(p, cputime_one_jiffy, one_jiffy_scaled); else if ((p != rq->idle) || (irq_count() != HARDIRQ_OFFSET)) account_system_time(p, HARDIRQ_OFFSET, cputime_one_jiffy, one_jiffy_scaled); else account_idle_time(cputime_one_jiffy); }

2.4 account_idle_time()

account_idle_time()的主要功能是更新当前CPU的空闲时间等待I/O的时间

这里也可以看出来等待I/O的时间本身也属于CPU idle时间的一部分。

  • 如果CPU当前是空闲状态 ,并且有进程正在等待I/O ,时间将计入iowait

  • 如果CPU当前是空闲状态 ,并且没有任何进程等待I/O ,时间将计入idle

/* * Account for idle time. * @cputime: the cpu time spent in idle wait */ void account_idle_time(cputime_t cputime) { u64 *cpustat = kcpustat_this_cpu->cpustat; struct rq *rq = this_rq(); if (atomic_read(&rq->nr_iowait) > 0) cpustat[CPUTIME_IOWAIT] += (__force u64) cputime; else cpustat[CPUTIME_IDLE] += (__force u64) cputime; }

如果有进程正在等待I/O,但是其它进程被调度到了当前CPU上,此时该CPU在执行其它进程的指令,处于繁忙状态,代码逻辑就执行不到统计iowait的部分,所以CPU繁忙时iowait反映不了进程等待I/O的事实

考虑这么一个场景:A程在等待I/O,B进程在CPU上执行,此时CPU并不是空闲状态,而是被B进程使用,这种情况下CPU时间会统计到B进程的us/sy指标上,不会统计到A进程的iowait指标,也就是当系统中有CPU密集型任务时,即使有进程在等待I/O,iowait指标也体现不出来

结论:CPU繁忙的情况下,即使有进程等待I/O,也不会体现在iowait指标上。

3. CPU iowait不可靠

从上面分析可以看出来, CPU iowait并不是一个可靠的监控指标, iowait高不代表一定有I/O瓶颈, iowait低也不代表没有I/O瓶颈。

判断是否有进程在等待I/O,可以通过vmstatb列来判断。

参考

Linux's iowait statistic and multi-CPU machines

Understanding Linux IOWait

Last modified: 27 October 2024