一、背景
在之前的博客 获取进程或线程级别的iodelay的方法-CSDN博客 和 线程每次iodelay监控及D状态开始和结束监控并做堆栈记录-CSDN博客 里,我们讲到了iowait的概念,但是是针对线程或进程而言的,线程和进程的iowait有单独的iodelay概念来描述和统计,通常意义上来说,iowait更多是指cpu的概念,top里就有这一项统计,top里的iowait的数值的统计,从原理上来说和线程和进程的iodelay在实现细节上有一定的相似之处,但是也有明显的不同的地方。我们在这篇博客里来展开,并用例子来说明。
二、iowait的原理阐述
2.1 iowait的概念解释
通常意义来说,我们说的iowait都是cpu的概念,因为top里有这一项统计,每个cpu都有一个iowait指标,然后系统整体也有一个iowait指标,是各个cpu的iowait的平均值。
我们通过man mpstat(mpstat也是一个可以看到包括iowait等cpu状态的工具)可以看到iowait指标的解释:
如上图,要被统计进iowait要满足两个条件:
1)cpu是idle的
2)系统正在等一些I/O的请求完成
这里需要再次重点强调的是第一点,也就是要计算进iowait,cpu这时候得是idle的,也就是说,如果cpu这时候有在运行别的任务,那么这个期间就算有在等一些I/O的请求完成,这个期间也是不会计算进iowait指标里的。
所以,这里就存在一个概念,上面说的cpu这时候得时idle的,这句话如何理解?
idle在我们通常理解上来说,它是指cpu并没有做任何的事情。而在top工具看来,如果这时候cpu没有做任何的事情,但是有I/O的请求正在等待完成,那么这时候并不属于idle的状态,而是属于iowait的状态。如下面的图示,暂时忽略中断时间等其他杂项统计,把整个时间拆分成usr、sys、及通常意义上的idle,通常意义上的idle里分为iowait部分,及top工具里认为的idle:
这里就存在一个问题,cpu进入通常意义上的idle之后,哪段时间会被统计进iowait呢?我们在下面一节里展开。
2.2 相关的内核逻辑
我们看一下iowait统计的开始时间和结束时间是如何确定的。
刚才也说到了iowait是一个per cpu的一个概念。cpu在进入通常意义上的idle之后(也就是没有任务需要继续使用cpu之后),会根据当前cpu的状态,来决定是否开始计时iowait。
要开始统计进cpu A上的iowait除了cpu A要进入通常意义上的idle以外,还有一个条件就是cpu A上有D状态进程,等待着IO相关的任务完成。
要注意,一个进程是D状态,但是不一定是iowait状态,D状态有很多种情况,就比如内核逻辑里执行msleep就会导致任务变成D状态,而iowait状态则是一定发生了IO相关的操作,在需要等IO完成时执行了下图里的io_schedule:
如上图,在执行schedule前会进行io_schedule_prepare,在里面会进行进程有关的in_iowait的标志:
然后cpu上如果在进入通常意义的idle状态时,在执行到__schedule时,会根据prev任务是否在iowait状态,进行当前rq上的nr_iowait数量的+1:
上图里的delayacct_blkio_start函数我们在之前的博客 获取进程或线程级别的iodelay的方法-CSDN博客 里的 4.2 一节已经进行了分析,是进行任务的iodelay的统计,而cpu的iowait的统计是基于采样的逻辑,有关采样的逻辑,我们在之前的 /proc/<pid>/下的节点的读取及相关内核逻辑分析及getrusage-CSDN博客 博客里的 2.2.2 一节有过相关的介绍,里面涉及到一个采样和校准的逻辑。这里有关iowait的统计的相关采样和统计的函数如下:
上图里的红色框出的rq的nr_iowait就是刚才描述过的在schedule时根据prev的in_iowait状态来去累积加的当前cpu上的等io的任务的总计数,如果总计数大于0,则当采样时cpu是通常意义上的idle状态,就统计这次采样的时间算在iowait里。
上面说的account_idle_time是在account_process_tick函数里执行的:
2.2.1 增加dump_stack来确定跟踪account_idle_time的相关调用
要注意,内核里虽然不少函数是能通过cat /proc/kallsyms | grep可以找到对应的符号的:
但是,并不是每次相关函数的调用都是走的函数来调用,它是会被优化成直接inline方式来调用的,这时候,通过kprobe就捕获不到,这一点要特别注意。我们在通过kprobe来捕获account_idle_time就遇到了相关情况,因为并不是所有调用account_idle_time的地方都是非inline方式。
我们抓到了account_idle_time相关的调用栈如下:
在do_idle时会间接调用到account_idle_time:
另外,account_process_tick的调用链(account_process_tick里也是会调用account_idle_time):
如上图,account_process_tick是通过tick_sched_timer间接调用到,而tick_sched_timer则是tick的hrtimer的timer回调:
相关的内容部分的改动:
static u32 _needoutput = 0;
static u32 _outputtimes = 0;
void enable_output(void) {_needoutput = 1;
}
EXPORT_SYMBOL_GPL(enable_output);void disable_output(void) {_needoutput = 0;_outputtimes = 0;
}
EXPORT_SYMBOL_GPL(disable_output);/** 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;elsecpustat[CPUTIME_IDLE] += cputime;if (_needoutput) {if (_outputtimes == 0) {_outputtimes = 1;dump_stack();}}
}
相关的进行dump_stack的内核模块:
#include <linux/module.h>
#include <linux/capability.h>
#include <linux/sched.h>
#include <linux/uaccess.h>
#include <linux/proc_fs.h>
#include <linux/ctype.h>
#include <linux/seq_file.h>
#include <linux/poll.h>
#include <linux/types.h>
#include <linux/ioctl.h>
#include <linux/errno.h>
#include <linux/stddef.h>
#include <linux/lockdep.h>
#include <linux/kthread.h>
#include <linux/sched.h>
#include <linux/delay.h>
#include <linux/wait.h>
#include <linux/init.h>
#include <asm/atomic.h>
#include <trace/events/workqueue.h>
#include <linux/sched/clock.h>
#include <linux/string.h>
#include <linux/mm.h>
#include <linux/interrupt.h>
#include <linux/tracepoint.h>
#include <trace/events/osmonitor.h>
#include <trace/events/sched.h>
#include <trace/events/irq.h>
#include <trace/events/kmem.h>
#include <linux/ptrace.h>
#include <linux/uaccess.h>
#include <asm/processor.h>
#include <linux/sched/task_stack.h>
#include <linux/nmi.h>
#include <asm/apic.h>
#include <linux/version.h>
#include <linux/sched/mm.h>
#include <asm/irq_regs.h>
#include <linux/kallsyms.h>
#include <linux/kprobes.h>
#include <linux/stop_machine.h>MODULE_LICENSE("GPL");
MODULE_AUTHOR("zhaoxin");
MODULE_DESCRIPTION("Module for account_dile_time.");
MODULE_VERSION("1.0");struct kprobe _kp1;u32 bhasoutput = 0;extern void enable_output(void);
extern void disable_output(void);int kprobecb_pre(struct kprobe* i_k, struct pt_regs* i_p)
{if (!bhasoutput) {bhasoutput = 1;dump_stack();}return 0;
}void kprobecb_post(struct kprobe *p, struct pt_regs *regs,unsigned long flags)
{
}int kprobe_register_func_account_idle_time(void)
{int ret;memset(&_kp1, 0, sizeof(_kp1));_kp1.symbol_name = "account_process_tick";_kp1.pre_handler = kprobecb_pre;_kp1.post_handler = kprobecb_post;ret = register_kprobe(&_kp1);if (ret < 0) {printk("register_kprobe fail!\n");return -1;}printk("register_kprobe success!\n");return 0;
}void kprobe_unregister_func_account_idle_time(void)
{unregister_kprobe(&_kp1);
}static int __init testaccountidletime_init(void)
{enable_output();kprobe_register_func_account_idle_time();return 0;
}static void __exit testaccountidletime_exit(void)
{disable_output();kprobe_unregister_func_account_idle_time();
}module_init(testaccountidletime_init);
module_exit(testaccountidletime_exit);
2.3 进入iowait状态的一些场景总结
会导致cpu iowait升高的一些场景总结:
1)打开或读取磁盘上的文件,且磁盘上的文件当前系统并没有与之对应的pagecache
这一项在后面的博客里会重点提及一个相关场景,与缺页异常有关
2)使用O_DIRECT方式写文件
3)系统的writeback相关的kworker内核线程执行脏页回写
4)nvme相关的jdb2_journal相关内核线程形如jdb2/nvme0**
5)coredump触发落盘导致
6)nfs场景也会统计iowait
nfs场景导致的iowait的调用链举例:
filp_close->nfs4_file_flush->nfs_wb_all->filemap_write_and_wait_range->__filemap_fdatawait_rangge->wait_on_page_writeback->folio_wait_writeback->folio_wait_bit->folio_wait_bit_common
三、运行实例来说明
这一章,我们编写一个内核模块来人为修改任务的iowait的标志位后,再进行msleep来做一些实验,来说明上面 2.1 里说的概念。
3.1 D状态不一定是iowait
如下代码,在绑核10核上,进行insmod,insmod时进行msleep:
#include <linux/module.h>
#include <linux/delay.h>MODULE_LICENSE("GPL");
MODULE_AUTHOR("zhaoxin");
MODULE_DESCRIPTION("Module for iowait.");
MODULE_VERSION("1.0");static int __init testdtask_init(void)
{//current->in_iowait = 1;msleep(10000);//current->in_iowait = 0;return -EINVAL;
}static void __exit testdtask_exit(void)
{}module_init(testdtask_init);
module_exit(testdtask_exit);
如上面代码,我们只是做了一个msleep。如下图,insmod时绑核10核执行:
可以从上图看到,insmod的进程是处于D的状态,但是通过top按1看10核上的iowait,可以如下图看到,iowait并没有彪高,且idle是100%:
3.2 如果cpu繁忙,就算cpu上有进程处于iodelay状态,该cpu也不属于iowait
我们改下一下程序,在msleep前标志上in_iowait状态,然后绑核执行看,是否对应核上的iowait彪高:
#include <linux/module.h>
#include <linux/delay.h>MODULE_LICENSE("GPL");
MODULE_AUTHOR("zhaoxin");
MODULE_DESCRIPTION("Module for iowait.");
MODULE_VERSION("1.0");static int __init testdtask_init(void)
{current->in_iowait = 1;msleep(10000);current->in_iowait = 0;return -EINVAL;
}static void __exit testdtask_exit(void)
{}module_init(testdtask_init);
module_exit(testdtask_exit);
可以如下图看到这时候cpu的iowait是彪高的:
但是,如果这时候,我们执行一个死循环程序也执行到cpu10上,如下运行:
这时候看top可以看到,这时候user变成了100%,但是iowait是0%: