首先介绍struct disk_stats的字段,接着介绍如何基于这些字段生成/proc/diskstats,然后介绍如何基于/proc/diskstats生成iostat的输出。本文基于linux kernel 3.19.8。
struct disk_stats (1)
这个结构体定义在include/linux/genhd.h中。它针对一个part(它可能代表一个分区也可能代表一整个disk),统计从系统启动到当前时刻的所有read/write请求。
1 | struct disk_stats { |
sectors[2]: read/write扇区的数量;ios[2]: read/write请求数;merges[2]: read/write请求的合并次数;ticks[2]: read/write请求从初始化到完成消耗的jiffies累计;io_ticks: 该分区上存在请求(不管是read还是write)的jiffies累计;time_in_queue: 该分区上存在的请求数量(不管是read还是write)与逝去jiffies的加权累计;
sectors字段 (1.1)
sectors字段是在请求结束阶段统计的:
1 | void blk_account_io_completion(struct request *req, unsigned int bytes) |
part_stat_add是一个macro,它累加part的struct disk_stats的某个字段。注意,如果part->partno为0,那么这个part其实代表的是一整个disk;否则part->partno不为0,这个part代表的是disk的一个分区,在这种情况下,还要累加整个disk的同一字段(part_to_disk((part))得到disk,其part0字段就是代表整个disk的part)。其定义如下:
1 |
|
rq_data_dir(req)拿到req的方向(direction),即read(0)还是write(1)。bytes >> 9是根据字节数计算扇区数。part_stat_add(cpu, part, sectors[rw], bytes >> 9)就是做相应的累加。
ios字段 (1.2)
ios字段也是在请求结束阶段统计的。请求结束时的调用是这样的:
1 | blk_end_request |
如1.1节所述,sectors的统计是在blk_account_io_completion中完成的。而ios的统计是在blk_account_io_done中完成的:
1 | void blk_account_io_done(struct request *req) |
part_stat_inc是一个macro,调用1.1节中介绍过的part_stat_add。它完成的工作是给part的struct disk_stats某个字段(这里是ios字段)加1;当然,如果part代表的是一个分区,它还会给disk的同一字段加1。
1 |
|
ticks字段 (1.3)
ticks字段也是在前述blk_account_io_done函数中统计的。首先通过duration = jiffies - req->start_time计算请求从初始化到完成的jiffies,然后通过part_stat_add累加到part的ticks(若该part代表的是分区,还会累加到disk的ticks)。req->start_time是在blk_rq_init中设定的:
1 | void blk_rq_init(struct request_queue *q, struct request *rq) |
io_ticks字段和time_in_queue字段 (1.4)
io_ticks和ticks关系不大:如1.3节所述,后者是各个read/write请求从初始化(blk_rq_init)到完成经历的jiffies的累计;前者是part上存在请求(不管是read还write)的时间(jiffies)的累计。怎么理解呢?在part上的请求的数量发生变化的地方(如请求开始、结束和merge的地方),去看刚刚逝去的这一段时间(jiffies)里part上是不是存在请求。若存在,这段jiffies就累加到io_ticks上;若不存在,则不累加。
反而io_ticks和time_in_queue的关系更大:time_in_queue是分区上存在的请求数量与jiffies的加权累计。什么意思呢?和统计io_ticks一样,还是在part上的请求数量发生变化的地方,去看刚刚逝去的这一段时间(jiffies)里part上是不是存在请求。不同的是,若存在请求,则把(存在的请求数量 * jiffies)累加到time_in_queue,否则不累加。
大致可以这么理解:io_ticks更注重server忙的时间;time_in_queue更注重client等待的时间。比如超市收银员,我们想看他的繁忙程度:从他早晨上班开始,io_ticks统计的是结账队列不空的时间总和;time_in_queue统计的是所有顾客排队结账消耗的时间总和。
这两者的统计都是在part_round_stats中完成的。这个函数在part上的请求数量发生变化的时候被调用(如前面的blk_account_io_done函数)。
1 | void part_round_stats(int cpu, struct hd_struct *part) |
和前述part_stat_add一样,若part代表的是一个分区,则不但要为分区作统计,而且还要为它所在的disk作统计。具体统计的过程是这样的:
1 | static void part_round_stats_single(int cpu, struct hd_struct *part, |
我们看这段代码,每次被调用时:
- 计算距离上次被调用逝去的时间(
now - part->stamp);
- 计算距离上次被调用逝去的时间(
- 看是否存在请求(if (inflight));
- 若存在,则
time_in_queue累加上(请求数*逝去时间);io_ticks累加上逝去时间;
- 若存在,则
- 更新被调用时间,为下次被调用做准备(
part->stamp = now);
- 更新被调用时间,为下次被调用做准备(
存在的定义是:part上的in_flight大于0。in_flight是在这两个函数中完成的:
1 | static inline void part_inc_in_flight(struct hd_struct *part, int rw) |
很明显,这两个函数分别是增加和减小part上的in_flight值(当part代表分区时,也会对disk做同样的统计)。part_dec_in_flight在前面的blk_account_io_done中被调用。part_inc_in_flight在blk_account_io_start中被调用。blk_account_io_start和blk_account_io_done是对称的,一个在请求开始阶段,一个在请求结束阶段。
在blk_account_io_start中,我们只看new_io为true的情况(为false的情况见下文1.5节),除去异常分成相当直观:
1 | void blk_account_io_start(struct request *rq, bool new_io) |
总结来说:io_ticks和time_in_queue的统计是这样的:
- 在一个请求产生的时候(part的请求数量发生变化):1.调用
part_round_stats(是否把最近这一段时间累计到io_ticks和time_in_queue);2.in_flight++; - 在一个请求结束的时候(part的请求数量发生变化):1.调用
part_round_stats(是否把最近这一段时间累计到io_ticks和time_in_queue);2.in_flight--;
这里需要强调一点:in_flight是进入elevator的数量。只要elevator不空,io_ticks就累计。所以io_ticks代表的是elevator不空的时间。也就是说,基于它得到的磁盘繁忙程度(iostat的util,见下文)其实不能精确代表物理硬件的繁忙程度,若把elevator往下(包含elevator)看成一个黑盒的话,它代表的是这个黑盒的繁忙程度。
merges字段 (1.5)
merges字段在blk_account_io_start函数中统计(见1.4节):new_io为false,表示本请求不是一个新请求,而是一个合并的请求,所以累加merges,不难理解。bio_attempt_back_merge和bio_attempt_front_merge在合并成功的时候,以new_io为false来调用本函数。