linux中iowait的形成原因和内核分析

经常我们碰到一些问题,进程的写文件,写入的速度非常慢,而且当时总的IO的数量(BI,BO)也非常低,但是vmstat显示的iowait却非常高,就是下图中的wa选项。
iowait_oenhan

man vmstat手册如下解释:

 


wa: Time spent waiting for IO. Prior to Linux 2.5.41, included in idle.
CPU花费到IO等待上的时间,也就是说进程的io被CPU调度出去了,并没有进行立即执行,导致整个执行时间延长,程序性能降低。

那么wa是如何统计的呢?

vmstat是procps内实现的

oenhan@oenhan ~ $ dpkg-query -S /usr/bin/vmstat
procps: /usr/bin/vmstat
在procps中,wa中通过getstat读取/proc/stat实现的。

void getstat(jiff *restrict cuse, jiff *restrict cice,
             jiff *restrict csys, jiff *restrict cide,
             jiff *restrict ciow, jiff *restrict cxxx,
             jiff *restrict cyyy, jiff *restrict czzz,
         unsigned long *restrict pin, unsigned long *restrict pout,
             unsigned long *restrict s_in,unsigned long *restrict sout,
         unsigned *restrict intr, unsigned *restrict ctxt,
         unsigned int *restrict running, unsigned int *restrict blocked,
         unsigned int *restrict btime, unsigned int *restrict processes)
{
  static int fd;
  unsigned long long llbuf = 0;
  int need_vmstat_file = 0;
  int need_proc_scan = 0;
  const char* b;
  buff[BUFFSIZE-1] = 0;  /* ensure null termination in buffer */
 
  if(fd){
    lseek(fd, 0L, SEEK_SET);
  }else{
    fd = open("/proc/stat", O_RDONLY, 0);
    if(fd == -1) crash("/proc/stat");
  }
  read(fd,buff,BUFFSIZE-1);
  *intr = 0;
  *ciow = 0;  /* not separated out until the 2.5.41 kernel */
  *cxxx = 0;  /* not separated out until the 2.6.0-test4 kernel */
  *cyyy = 0;  /* not separated out until the 2.6.0-test4 kernel */
  *czzz = 0;  /* not separated out until the 2.6.11 kernel */
 
  b = strstr(buff, "cpu ");
  if(b) sscanf(b,  "cpu  %Lu %Lu %Lu %Lu %Lu %Lu %Lu %Lu",
          cuse, cice, csys, cide, ciow, cxxx, cyyy, czzz);
proc/stat在内核中通过show_stat实现,统计cpustat.iowait获取最终结果:

iowait = cputime64_add(iowait, get_iowait_time(i));

static cputime64_t get_iowait_time(int cpu)
{
 u64 iowait_time = -1ULL;
 cputime64_t iowait;

 if (cpu_online(cpu))
  iowait_time = get_cpu_iowait_time_us(cpu, NULL);

 if (iowait_time == -1ULL)
  /* !NO_HZ or cpu offline so we can rely on cpustat.iowait */
  iowait = kstat_cpu(cpu).cpustat.iowait;
 else
  iowait = usecs_to_cputime64(iowait_time);

 return iowait;
}

而cpustat.iowait的值在account_system_time统计计算,通过统计rq->nr_iowait的值判断来递增。

void account_system_time(struct task_struct *p, int hardirq_offset,
    cputime_t cputime)
{
 struct cpu_usage_stat *cpustat = &kstat_this_cpu.cpustat;
 runqueue_t *rq = this_rq();
 cputime64_t tmp;

 p->stime = cputime_add(p->stime, cputime);

 /* Add system time to cpustat. */
 tmp = cputime_to_cputime64(cputime);
 if (hardirq_count() - hardirq_offset)
  cpustat->irq = cputime64_add(cpustat->irq, tmp);
 else if (softirq_count())
  cpustat->softirq = cputime64_add(cpustat->softirq, tmp);
 else if (p != rq->idle)
  cpustat->system = cputime64_add(cpustat->system, tmp);
 else if (atomic_read(&rq->nr_iowait) > 0)
  cpustat->iowait = cputime64_add(cpustat->iowait, tmp);
 else
  cpustat->idle = cputime64_add(cpustat->idle, tmp);
 /* Account for system time used */
 acct_update_integrals(p);
}

附加一句:account_system_time可以用来具体统计更详细的CPU占用情况。

而控制的rq->nr_iowait的函数是io_schedule和io_schedule_timeout。

void __sched io_schedule(void)
{
 struct runqueue *rq = &per_cpu(runqueues, raw_smp_processor_id());

 delayacct_blkio_start();
 atomic_inc(&rq->nr_iowait);
 schedule();
 atomic_dec(&rq->nr_iowait);
 delayacct_blkio_end();
}

long __sched io_schedule_timeout(long timeout)
{
 struct runqueue *rq = &per_cpu(runqueues, raw_smp_processor_id());
 long ret;

 delayacct_blkio_start();
 atomic_inc(&rq->nr_iowait);
 ret = schedule_timeout(timeout);
 atomic_dec(&rq->nr_iowait);
 delayacct_blkio_end();
 return ret;
}

因为没有实际应用场景,针对调用代码做一下分析,具体的应用场景可以参考
查看调用代码如下:

 


先看io_schedule函数,调用它的有sync_buffer,sync_io,dio_awit_one,direct_io_worker,get_request_wait等,只要你有对磁盘的写入操作,就会调用io_schedule,尤其以直接写入(DIO)最明显,每次写入都会调用。普通写入也只是积攒到buffer里面,统一刷新buffer的时候才会调用一次io_schedule。即使没有主动的写入IO,也有可能产生iowait,在sync_page函数中,当缓存和硬盘数据进行同步的时候,就会被调用,比如常用的mlock锁内存时就会__lock_page触发缓存同步,一样会增加iowait,一些读硬盘也会触发。

 


虽然io_schedule调用的非常多,但真正的大杀器还是io_schedule_timeout,io_schedule由内核调用,整个调度时间还是比较短的,io_schedule_timeout则基本都是指定了HZ/10的时间段。如balance_dirty_pages中每次写入文件都会对脏页进行平衡处理,wb_kupdate定时器般进行缓存刷新,try_to_free_pages则在释放内存时刷缓存时使用,虽然不是每次都使用io_schedule_timeout,但综合各个的判断条件看,当内存中的缓存足够多的时候,会极大触发io_schedule_timeout,水线一般是dirty_background_ratio和dirty_ratio。

如上所分析的,避免iowait需要注意的有:

1.高性能要求的进程最好在内存中一次读完所有数据,全部mlock住,不需要后面再读磁盘,

2.不需要写或者刷新磁盘(一般搞不定,就让单独的进程专门干这个活)。

3.控制整个缓存的使用情况,但从工程上不容易处理,虚拟化估计更有效,相对与系统虚拟化(KVM),进程虚拟化(cgroup)应该更简单有效。

时间: 2024-09-24 04:27:46

linux中iowait的形成原因和内核分析的相关文章

解决linux中iowait 过高的问题

I/O问题一直是一个比较难定位的问题,今天线上环境遇到了I/O 引起的CPU负载问题,看到了如下这篇比较好的文章,完饭后的我还在和西红柿和黄瓜在减肥的路上抗争,正好将原文翻译成中文,供广大同胞品鉴   Linux has many tools available for troubleshooting some are easy to use, some are more advanced. Linux 有许多可用来查找问题的简单工具,也有许多是更高级的 I/O Wait is an issue

浅析Linux中的时间编程和实现原理(三) Linux内核的工作

引子 时间系统的工作需要软硬件以及操作系统的互相协作,在上一部分,我们已经看到大多数时间函数都依赖内核系统调用,GlibC 仅仅做了一次请求的转发.因此必须深入内核代码以便了解更多的细节. 内核自身的正常运行也依赖于时钟系统.Linux 是一个典型的分时系统,CPU 时间被分成多个时间片,这是多任务实现的基础.Linux 内核依赖 tick,即时钟中断来进行分时. 为了满足应用和内核自己的需求,内核时间系统必须提供以下三个基本功能: 提供系统 tick 中断(驱动调度器,实现分时) 维护系统时间

Linux 中的 DTrace :BPF 进入 4.9 内核

随着 BPF 追踪系统(基于时间采样)最后一个主要功能被合并至 Linux 4.9-rc1 版本的内核中,现在 Linux 内核拥有类似 DTrace 的原生追踪功能.DTrace 是 Solaris 系统中的高级追踪器.对于长期使用 DTrace 的用户和专家,这将是一个振奋人心的里程碑!现在在 Linux 系统上,你可以在生产环境中使用安全的.低负载的定制追踪系统,通过执行时间的柱状图和频率统计等信息,分析应用的性能以及内核. 用于 Linux 的追踪项目有很多,但是这个最终被合并进 Lin

Linux中的DTrace:BPF进入4.9内核

随着 BPF 追踪系统(基于时间采样)最后一个主要功能被合并至 Linux 4.9-rc1 版本的内核中,现在 Linux 内核拥有类似 DTrace 的原生追踪功能.DTrace 是 Solaris 系统中的高级追踪器.对于长期使用 DTrace 的用户和专家,这将是一个振奋人心的里程碑!现在在 Linux 系统上,你可以在生产环境中使用安全的.低负载的定制追踪系统,通过执行时间的柱状图和频率统计等信息,分析应用的性能以及内核. 用于 Linux 的追踪项目有很多,但是这个最终被合并进 Lin

Linux中不编译内核,如何挂载mount ntfs分区

Linux中不编译内核,如何挂载mount ntfs分区? 找到对应内核版本(uhttp://www.aliyun.com/zixun/aggregation/11696.html">name -a)的ntfsrpm,安装即可.以原装rh8为例,未升级或编译内核1. 上google.com搜索并下载 kernel-ntfs-2.4.18-14.i686.rpm2. rpm -ivh kernel-ntfs-2.4.18-14.i686.rpm3. mkdir /mnt/c4. mount

Linux中如何安装内核源码包?

Linux中如何安装内核源码包? 有些时候,我们需要对系统的内核进行升级操作,我们可以使用rpm命令来完成. 下载新的iso光盘镜像文件,并挂载iso文件到光盘. 把你光盘上的内核源码包装上即可,我们使用如下命令: rpm -i *kernel*source*.rpm

wdcp/wdlinux 在 UBUNTU/linux 中安装失败原因之创建用户

根本原因在于安装时创建的用户www 使用了和ubuntu已创建的用户相同的UID: 1000,冲突了自然创建不了用户. 你可以修改lanmp.sh脚本中创建www用户时的代码,将1000改为其他数字. 也可以修改当前用户的UID(我选择改了当前用户). 修改方法:退出当前身份,root或其他用户登陆:# usermod -u 1010 username,1010 为该用户新的UID   还有wdcpu用户使用了999的UID,如果你用VirtualBox创建的虚拟机,它在linux中的用户uid

Linux中errno使用

  当linux中的C api函数发生异常时,一般会将errno变量(需include errno.h)赋一个整数值,不同的值表示不同的含义,可以通过查看该值推测出错的原因,在实际编程中用这一招解决了不少原本看来莫名其妙的问题.但是errno是一个数字,代表的具体含义还要到errno.h中去阅读宏定义,而每次查阅是一件很繁琐的事情.有下面几种方法可以方便的得到错误信息 (1)void perror(const char *s) 函数说明 perror ( )用来将上一个函数发生错误的原因输出到标

在Linux中使用线程

我并不假定你会使用Linux的线程,所以在这里就简单的介绍一下.如果你之前有过多线程方面的编程经验,完全可以忽略本文的内容,因为它非常的初级. 首先说明一下,在Linux编写多线程程序需要包含头文件pthread.h.也就是说你在任何采用多线程设计的程序中都会看到类似这样的代码: #include 当然,进包含一个头文件是不能搞定线程的,还需要连接libpthread.so这个库,因此在程序连接阶段应该有类似这样的指令: gcc program.o -o program -lpthread.h>