深入分析diskstats

本文详细分析了Linux内核中的diskstats文件,它提供了通用块设备层的统计信息。文章介绍了diskstats的各个字段含义,包括读写请求次数、合并的请求、扇区数量以及时间等,并通过流程图展示了IO请求处理的完整流程。同时,文章探讨了相关数据结构如disk_stats和hd_struct,以及如何通过blk_account_io_done等函数进行统计更新。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

深入分析diskstats - ykrocku

背景

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

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

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

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

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

初探diskstats

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

# 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中描述,各字段意义如下:

ValueQuoted解释
F18major number此块设备的主设备号
F20minor mumber此块设备的次设备号
F3sdadevice name此块设备名字
F48567reads completed successfully成功完成的读请求次数
F51560reads merged读请求的次数
F6140762sectors read读请求的扇区数总和
F73460time spent reading (ms)读请求花费的时间总和
F80writes completed成功完成的写请求次数
F90writes merged写请求合并的次数
F100sectors written写请求的扇区数总和
F110time spent writing (ms)写请求花费的时间总和
F120I/Os currently in progress次块设备队列中的IO请求数
F132090time spent doing I/Os (ms)块设备队列非空时间总和
F143440weighted 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的计算过程:

IDTimeOpsin_flightstampgapio_tickstime_in_queue
0100.00新IO请求入队00—–00
1100.10新IO请求入队1100.000.100.100.10
3101.20完成一个IO请求2100.100.801.201.70
4103.50完成一个IO请求1100.201.302.503.00
5153.50新IO请求入队0103.50—–2.503.00
6154.50完成一个IO请求1153.501.003.504.00

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值