0%

linux iostat详解

首先介绍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
2
3
4
5
6
7
8
struct disk_stats {
unsigned long sectors[2]; /* READs and WRITEs */
unsigned long ios[2];
unsigned long merges[2];
unsigned long ticks[2];
unsigned long io_ticks;
unsigned long time_in_queue;
};
  • 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
2
3
4
5
6
7
8
9
10
11
12
13
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;
part_stat_add(cpu, part, sectors[rw], bytes >> 9);
part_stat_unlock();
}
}

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
2
3
4
5
6
#define part_stat_add(cpu, part, field, addnd)  do {      \
__part_stat_add((cpu), (part), field, addnd); \
if ((part)->partno) \
__part_stat_add((cpu), &part_to_disk((part))->part0, \
field, addnd); \
} while (0)

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
2
3
4
5
6
7
blk_end_request
----> blk_end_bidi_request
----> blk_update_bidi_request
----> blk_update_request
----> blk_account_io_completion
----> blk_finish_request
----> blk_account_io_done

如1.1节所述,sectors的统计是在blk_account_io_completion中完成的。而ios的统计是在blk_account_io_done中完成的:

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
void blk_account_io_done(struct request *req)
{
/*
* Account IO completion. flush_rq isn't accounted as a
* normal IO on queueing nor completion. Accounting the
* containing request is enough.
*/
if (blk_do_io_stat(req) && !(req->cmd_flags & REQ_FLUSH_SEQ)) {
unsigned long duration = jiffies - req->start_time;
const int rw = rq_data_dir(req);
struct hd_struct *part;
int cpu;

cpu = part_stat_lock();
part = req->part;

part_stat_inc(cpu, part, ios[rw]);
part_stat_add(cpu, part, ticks[rw], duration);
part_round_stats(cpu, part);
part_dec_in_flight(part, rw);

hd_struct_put(part);
part_stat_unlock();
}
}

part_stat_inc是一个macro,调用1.1节中介绍过的part_stat_add。它完成的工作是给part的struct disk_stats某个字段(这里是ios字段)加1;当然,如果part代表的是一个分区,它还会给disk的同一字段加1。

1
2
#define part_stat_inc(cpu, gendiskp, field)       \
part_stat_add(cpu, gendiskp, field, 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void blk_rq_init(struct request_queue *q, struct request *rq)
{
memset(rq, 0, sizeof(*rq));

INIT_LIST_HEAD(&rq->queuelist);
INIT_LIST_HEAD(&rq->timeout_list);
rq->cpu = -1;
rq->q = q;
rq->__sector = (sector_t) -1;
INIT_HLIST_NODE(&rq->hash);
RB_CLEAR_NODE(&rq->rb_node);
rq->cmd = rq->__cmd;
rq->cmd_len = BLK_MAX_CDB;
rq->tag = -1;
rq->start_time = jiffies;
set_start_time_ns(rq);
rq->part = NULL;
}

io_ticks字段和time_in_queue字段 (1.4)

io_ticksticks关系不大:如1.3节所述,后者是各个read/write请求从初始化(blk_rq_init)到完成经历的jiffies的累计;前者是part上存在请求(不管是read还write)的时间(jiffies)的累计。怎么理解呢?在part上的请求的数量发生变化的地方(如请求开始、结束和merge的地方),去看刚刚逝去的这一段时间(jiffies)里part上是不是存在请求。若存在,这段jiffies就累加到io_ticks上;若不存在,则不累加。

反而io_tickstime_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
2
3
4
5
6
7
8
void part_round_stats(int cpu, struct hd_struct *part)
{
unsigned long now = jiffies;

if (part->partno)
part_round_stats_single(cpu, &part_to_disk(part)->part0, now);
part_round_stats_single(cpu, part, now);
}

和前述part_stat_add一样,若part代表的是一个分区,则不但要为分区作统计,而且还要为它所在的disk作统计。具体统计的过程是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void part_round_stats_single(int cpu, struct hd_struct *part,
unsigned long now)
{
int inflight;

if (now == part->stamp)
return;

inflight = part_in_flight(part);
if (inflight) {
__part_stat_add(cpu, part, time_in_queue,
inflight * (now - part->stamp));
__part_stat_add(cpu, part, io_ticks, (now - part->stamp));
}
part->stamp = now;
}

我们看这段代码,每次被调用时:

    1. 计算距离上次被调用逝去的时间(now - part->stamp);
    1. 看是否存在请求(if (inflight));
    1. 存在,则 time_in_queue累加上(请求数*逝去时间);io_ticks累加上逝去时间;
    1. 更新被调用时间,为下次被调用做准备(part->stamp = now);

存在的定义是:part上的in_flight大于0。in_flight是在这两个函数中完成的:

1
2
3
4
5
6
7
8
9
10
11
12
13
static inline void part_inc_in_flight(struct hd_struct *part, int rw)
{
atomic_inc(&part->in_flight[rw]);
if (part->partno)
atomic_inc(&part_to_disk(part)->part0.in_flight[rw]);
}

static inline void part_dec_in_flight(struct hd_struct *part, int rw)
{
atomic_dec(&part->in_flight[rw]);
if (part->partno)
atomic_dec(&part_to_disk(part)->part0.in_flight[rw]);
}

很明显,这两个函数分别是增加和减小part上的in_flight值(当part代表分区时,也会对disk做同样的统计)。part_dec_in_flight在前面的blk_account_io_done中被调用。part_inc_in_flightblk_account_io_start中被调用。blk_account_io_startblk_account_io_done是对称的,一个在请求开始阶段,一个在请求结束阶段。

blk_account_io_start中,我们只看new_io为true的情况(为false的情况见下文1.5节),除去异常分成相当直观:

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
void blk_account_io_start(struct request *rq, bool new_io)
{
struct hd_struct *part;
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;
part_stat_inc(cpu, part, merges[rw]);
} else {
part = disk_map_sector_rcu(rq->rq_disk, blk_rq_pos(rq));
if (!hd_struct_try_get(part)) {
/*
* The partition is already being removed,
* the request will be accounted on the disk only
*
* We take a reference on disk->part0 although that
* partition will never be deleted, so we can treat
* it as any other partition.
*/
part = &rq->rq_disk->part0;
hd_struct_get(part);
}
part_round_stats(cpu, part);
part_inc_in_flight(part, rw);
rq->part = part;
}

part_stat_unlock();
}

总结来说:io_tickstime_in_queue的统计是这样的:

  • 在一个请求产生的时候(part的请求数量发生变化):1.调用part_round_stats(是否把最近这一段时间累计到io_tickstime_in_queue);2. in_flight++
  • 在一个请求结束的时候(part的请求数量发生变化):1.调用part_round_stats(是否把最近这一段时间累计到io_tickstime_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_mergebio_attempt_front_merge在合并成功的时候,以new_io为false来调用本函数。

/proc/diskstats (2)

iostat (3)

写的不错,有赏!