ykrocku

Less is more.

深入分析diskstats

背景

内核很多重要子系统均通过proc文件的方式,将自身的一些统计信息输出,方便最终用户查看各子系统的运行状态,这些统计信息被称为metrics。 直接查看metrics并不能获取到有用的信息,一般都是由特定的应用程序(htop/sar/iostat等)每隔一段时间读取相关metrics,并进行相应计算,给出更具用户可读性的输出。 常见的metrics文件有:

  • cpu调度统计信息的/proc/stat
  • cpu负载统计信息的/proc/loadavg

通用块设备层也有一个重要的统计信息

  • /proc/diskstats 内核通过diskstats文件,将通用块设备层的一些重要指标以文件的形式呈现给用户。

因为本文档牵涉到通用块设备层很多细节,建议先了解IO调度器的相关知识。

初探diskstats

首先来看下diskstats里面都有些什么,下面截取的是一个diskstats文件内容:

1
2
3
4
5
6
7
8
9
# cat /proc/diskstats 
   8       0 sda 8567 1560 140762 3460 0 0 0 0 0 2090 3440
   8       1 sda1 8565 1557 140722 3210 0 0 0 0 0 1840 3190
   8      16 sdb 8157 1970 140762 2940 0 0 0 0 0 1710 2890
   8      17 sdb1 8155 1967 140722 2900 0 0 0 0 0 1670 2850
   8      32 sdc 8920 1574 206410 7870 430 0 461 250 0 6820 8120
   8      33 sdc1 8918 1571 206370 7840 430 0 461 250 0 6790 8090
   8      48 sdd 209703 1628 341966 1318450 3109063 331428 943042901 9728000 0 8943570 11015280
   8      49 sdd1 209701 1625 341926 1318200 3109063 331428 943042901 9728000 0 8943320 11015030

虽然如上面提到的,这些数字看上去完全没有规律。不过若想研究内核通用块设备层的统计实现方式,还是得一个一个字段的分析。

简单的说,每一行对应一个块设备,分别有ram0-ram15、loop0-loop7、mtdblock0-mtdblock5,剩下的sdxx就是硬盘和分区了。 这里以sda设备的数据为例,分别列举各字段的意义:

1
8       0 sda 8567 1560 140762 3460 0 0 0 0 0 2090 3440

根据内核文档iostats.txt中描述,各字段意义如下:

Value Quoted 解释
F1 8 major number 此块设备的主设备号
F2 0 minor mumber 此块设备的次设备号
F3 sda device name 此块设备名字
F4 8567 reads completed successfully 成功完成的读请求次数
F5 1560 reads merged 读请求的次数
F6 140762 sectors read 读请求的扇区数总和
F7 3460 time spent reading (ms) 读请求花费的时间总和
F8 0 writes completed 成功完成的写请求次数
F9 0 writes merged 写请求合并的次数
F10 0 sectors written 写请求的扇区数总和
F11 0 time spent writing (ms) 写请求花费的时间总和
F12 0 I/Os currently in progress 次块设备队列中的IO请求数
F13 2090 time spent doing I/Os (ms) 块设备队列非空时间总和
F14 3440 weighted time spent doing I/Os (ms) 块设备队列非空时间加权总和

基本上都是数量、时间的累加值,按照读、写分开统计。

流程图

下图是Linux内核通用块设备层IO请求处理的完整流程,如图例所示,所有的统计相关处理均有用不同颜色标注。 在进行深入分析前,请大致浏览图片,对整个流程有一个大致印象。

实现分析

proc入口

在内核代码中grep “diskstats”即可找到定义在block/genhd.c中的diskstats_show函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
while ((hd = disk_part_iter_next(&piter))) {
  cpu = part_stat_lock();
  part_round_stats(cpu, hd);
  part_stat_unlock();
  seq_printf(seqf, "%4d %7d %s %lu %lu %llu "
         "%u %lu %lu %llu %u %u %u %u\n",
         MAJOR(part_devt(hd)), MINOR(part_devt(hd)),
         disk_name(gp, hd->partno, buf),
         part_stat_read(hd, ios[READ]),
         part_stat_read(hd, merges[READ]),
         (unsigned long long)part_stat_read(hd, sectors[READ]),
         jiffies_to_msecs(part_stat_read(hd, ticks[READ])),
         part_stat_read(hd, ios[WRITE]),
         part_stat_read(hd, merges[WRITE]),
         (unsigned long long)part_stat_read(hd, sectors[WRITE]),
         jiffies_to_msecs(part_stat_read(hd, ticks[WRITE])),
         part_in_flight(hd),
         jiffies_to_msecs(part_stat_read(hd, io_ticks)),
         jiffies_to_msecs(part_stat_read(hd, time_in_queue))
      );

此段代码用seq_printf函数将保存在hd_struct结构体内的统计信息组成了diskstats文件。

数据结构

用到的数据结构都定义在<linux/genhd.h>中,主要有disk_stats和hd_struct两个结构体,意义见注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
struct disk_stats {
    /*
     *sectors[0] <--> F6 
     *sectors[1] <--> F10
     */
    unsigned long sectors[2];    /* READs and WRITEs */

    /*
     *ios[0] <--> F4 
     *ios[1] <--> F8
     */
    unsigned long ios[2];

    /*
     *merges[0] <--> F5 
     *merges[1] <--> F9
     */
    unsigned long merges[2];

    /*
     *ticks[0] <--> F7 
     *ticks[1] <--> F11
     */
    unsigned long ticks[2];

    /*F13, time spent doing IOs*/
    unsigned long io_ticks;

    /*F14, weighted time spent doing I/Os (ms)  */
    unsigned long time_in_queue;
};

struct hd_struct {
  unsigned long stamp;

    /*F12 I/Os currently in progress*/
  atomic_t in_flight[2];

    /*如果支持SMP则需要使用“每CPU”变量,否则需要加锁*/
#ifdef   CONFIG_SMP
  struct disk_stats __percpu *dkstats;
#else
  struct disk_stats dkstats;
#endif
  atomic_t ref;
  struct rcu_head rcu_head;
};

F7/F11 ticks

见下一节

F4/F8 ios

如流程图所示,在每个IO结束后,都会调用blk_account_io_done函数,来对完成的IO进行统计。 blk_account_io_done统计了 ios(F4/F8)ticks(F7/F11),还处理了in_flight(后续节有分析)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
static void blk_account_io_done(struct request *req)
{
  /*
     * 不统计Flush请求,见 http://en.wikipedia.org/wiki/Disk_buffer#Cache_flushing
  */
  if (blk_do_io_stat(req) && !(req->cmd_flags & REQ_FLUSH_SEQ)) {
        /*
         * duration是当前时间(IO完成时间)减去此IO的入队时间(见流程图)
         */
      unsigned long duration = jiffies - req->start_time;

        /*从req获取请求类型:R / W*/
      const int rw = rq_data_dir(req);
      struct hd_struct *part;
      int cpu;

      cpu = part_stat_lock();
        /*获取请求对应的partition(part)*/
      part = req->part;
        /*
         * 该partition的ios[rw]加1
         * part_stat_inc定义在<linux/genhd.h>中
         * part_stat_inc这个宏用来处理SMP和非SMP的细节,见上面的结构体定义
         */
      part_stat_inc(cpu, part, ios[rw]);

        /*
         * 将此IO的存活时间加进ticks
         */
      part_stat_add(cpu, part, ticks[rw], duration);

      part_round_stats(cpu, part);

        /*
         *完成了一个IO,也就是in_flight(正在进行)的IO少了一个
         */
      part_dec_in_flight(part, rw);

      hd_struct_put(part);
      part_stat_unlock();
  }
}

F5/F9 merges

内核每执行一次Back Merge或Front Merge,都会调用drive_stat_acct。 其实in_flight也是在这个函数中统计的,new_io参数用来区分是新的IO,如果不是新IO则是在merge的时候调用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static void drive_stat_acct(struct request *rq, int new_io)
{
  struct hd_struct *part;
    /*从req获取请求类型:R / W*/
  int rw = rq_data_dir(rq);
  int cpu;

  if (!blk_do_io_stat(rq))
      return;

  cpu = part_stat_lock();

  if (!new_io) {
      part = rq->part;
        /*
         * 非新IO,merges[rw]加1
         */
      part_stat_inc(cpu, part, merges[rw]);
  } else {
        .....
      part_round_stats(cpu, part);
        /*
         * 新提交了一个IO,也就是in_flight(正在进行)的IO多了一个
         */
      part_inc_in_flight(part, rw);
  }

  part_stat_unlock();
}

F6/F10 sectors

读写扇区总数是在blk_account_io_completion函数中统计的,如流程图中所示,这个函数在每次IO结束后调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void blk_account_io_completion(struct request *req, unsigned int bytes)
{
    if (blk_do_io_stat(req)) {
        const int rw = rq_data_dir(req);
        struct hd_struct *part;
        int cpu;

        cpu = part_stat_lock();
        part = req->part;
        /*
         *bytes是此IO请求的数据长度,右移9位等同于除以512,即转换成扇区数
         *然后加到sectors[rw]中
         */
        part_stat_add(cpu, part, sectors[rw], bytes >> 9);
        part_stat_unlock();
    }
}

F12 in_flight

in_flight这个统计比较特别,因为其他统计都是计算累加值,而它是记录当前队列中IO请求的个数。统计方法则是:

  • 新IO请求插入队列(被merge的不算)后加1
  • 完成一个IO后减1 实现见上面章节中的blk_account_io_done和drive_stat_acct函数内的注释。

F14 time_in_queue

见下一节。

F13 io_ticks

io_ticks统计块设备请求队列非空的总时间,统计时间点与in_flight相同,统计代码实现在part_round_stats_single函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void part_round_stats_single(int cpu, struct hd_struct *part,
                  unsigned long now)
{
  if (now == part->stamp)
      return;
    /*
     *块设备队列非空
     */
  if (part_in_flight(part)) {
        /*
         *统计加权时间 队列中IO请求个数 * 耗费时间
         */
      __part_stat_add(cpu, part, time_in_queue,
              part_in_flight(part) * (now - part->stamp));

        /*
         *统计队列非空时间
         */
      __part_stat_add(cpu, part, io_ticks, (now - part->stamp));
  }
  part->stamp = now;
}

整个代码实现的逻辑比较简单,在新IO请求插入队列(被merge不算),或完成一个IO请求的时候均执行如下操作:

  • 队列为空

    1. 记下时间stamp = t1
  • 队列不为空

    1. io_ticks[rw] += t2-t1
    2. time_in_queue += in_flight * (t2-t1)
    3. 记下时间stamp = t2

下面是一个实际的例子,示例io_ticks和time_in_queue的计算过程:

ID Time Ops in_flight stamp gap io_ticks time_in_queue
0 100.00 新IO请求入队 0 0 —– 0 0
1 100.10 新IO请求入队 1 100.00 0.10 0.10 0.10
3 101.20 完成一个IO请求 2 100.10 0.80 1.20 1.70
4 103.50 完成一个IO请求 1 100.20 1.30 2.50 3.00
5 153.50 新IO请求入队 0 103.50 —– 2.50 3.00
6 154.50 完成一个IO请求 1 153.50 1.00 3.50 4.00

总共时间 54.50s, IO队列非空时间3.50s