iostat IO统计原理linux内核源码分析----基于单通道SATA盘

先上一个IO发送submit_bio流程图,本文基本就是围绕该流程讲解。

内核版本 3.10.96
详细的源码注释:https://github.com/dongzhiyan-stack/kernel-code-comment

1 iostat基本知识与涉及的内核数据结构

在排查IO问题时,iostat 命令还是挺常用的。

[root@localhost ~]# iostat -dmx 1
Device:           rrqm/s   wrqm/s     r/s     w/s    rMB/s    wMB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
sda               0.00     0.01    0.28    0.14     0.01     0.00    53.54     0.01   20.43   15.73   29.62   3.98   0.17
sdb               0.00     0.00    0.00    0.00     0.00     0.00    32.20     0.00    5.54    6.04    4.42   4.62   0.00

像util代表的IO使用率、await代表的IO wait时间、r/s和w/s代表的IOPS、rrqm/s和wrqm/s代表的读写IO合并数、rMB/s和wMB/s代表的读写速率等等,在linux内核是怎么统计的?本文主要讲解IO发送、IO传输、IO传输完成这几个过程中涉及的IO使用率等参数,在内核里是怎么统计计算的。本文讲解的是基于单通道SATA盘,多通道队列mq下篇讲解。

iostat命令实际是读取/proc/diskstats 获取的IO数据,然后计算IO使用率等数据,对应的内核函数是diskstats_show()。

首先需要介绍一下基本数据结构和函数

struct hd_struct 表示一个块设备分区,也表示整个块设备。比如我们把磁盘sda分成sda1,sda2,sda3三个分区,则sda、sda1、sda2、sda3都对应一个struct hd_struct结构。
struct hd_struct结构体中与IO使用率等有关的成员

struct hd_struct {int  partno;//块设备主分区时不为0,块设备分区时为0unsigned long stamp;//记录当前系统时间jiffiesatomic_t in_flight[2];// in_flight [rw]表示IO队列中读写请求个数struct disk_stats __percpu *dkstats;//记录IO使用率等原始数据
}

struct disk_stats结构体的成员都是IO使用率等有关的数据

struct disk_stats {unsigned long  sectors[2];//读写扇区总数,blk_account_io_completion()中更新unsigned long  ios[2];//blk_account_io_done()中更新,传输完成的读写IO个数,IOPSunsigned long  merges[2];//合并bio的个数,drive_stat_acct(blk_account_io_start)更新合并计数unsigned long  ticks[2];//blk_account_io_done()中更新,req传输耗时unsigned long  io_ticks;//IO使用率用的这个参数,文章最后详解unsigned long  time_in_queue;// IO在队列中的时间,受IO队列中IO个数的影响
};

submit_bio(int rw, struct bio *bio)是我们熟悉IO请求发送函数,它传入的是struct bio结构,表示本次IO读写的磁盘起始地址和大小、IO是读还是写、要读写的数据在内存中的地址等等。接着流程是submit_bio-> generic_make_request->blk_queue_bio,在blk_queue_bio()函数中,引入一个新的结构struct request,感觉之所以再费事引入这个结构,主要目的是为了IO请求合并。我想大家多多少少都应该听说过,在内核block层有个电梯调度算法(elv),可以把多个IO请求合并为一个(IO读写属性要一致),这些IO读写的磁盘扇区地址范围首尾相邻,这样只用进行一次IO磁盘数据传输。比如有三个IO请求IO1、IO2、IO3,读写的磁盘扇区地址范围分别是0–10,10–15,15–25,如果把这3个IO合并成一个新的IO,对应的磁盘扇区地址范围0–25,之后只用进行一次实际的磁盘数据传输就行,否则就得针对IO0、IO1、IO3分别进行3次实际的磁盘数据传输,传输效率就会变低,SATA盘这种机械硬盘受影响更大。这些操作大部分在blk_queue_bio()完成,先来该函数流程图。struct request这里简称为req。

2 submit_bio源码分析

void  blk_queue_bio(struct request_queue *q, struct bio *bio)
{/*依次取出进程plug->list链表上已有的rq,判断rq代表的磁盘范围是否挨着本次的bio的范围,是则把bio合并到rq合并成功返回真,直接return返回*/if (attempt_plug_merge(q, bio, &request_count))return;spin_lock_irq(q->queue_lock);/*在elv调度器里查找是否有可以合并的req,这是合并bio的req,是调用具体的IO调度算法函数寻找可以合并的
req。这些req所在的IO调度算法队列是多进程共享的,就像上边所述,进程访问这些队列要加锁,其他进程要等待。
函数返回值 ELEVATOR_BACK_MERGE(前项合并的req)、ELEVATOR_FRONT_MERGE(前项合并)、
ELEVATOR_NO_MERGE(没有找到可以合并的req)*/el_ret = elv_merge(q, &req, bio);if (el_ret == ELEVATOR_BACK_MERGE) {//后项合并,bio合并到req对应的磁盘空间范围后边bio_attempt_back_merge(q, req, bio);/*二次合并,看能否把req合并到IO调度队列里的next req. 传说中的更高阶的合并吧,比如原IO调度算法队
列挨着的req1和req2,代表的磁盘空间范围分别是req1:0--5,req2:11--16,bio的磁盘空间是6--10,则先执行
bio_attempt_back_merge()把bio后项合并到req1,此时req1:0--10,显然此时req1和req2可以进行二次合并,
attempt_back_merge()函数就是这个作用吧,合并成功返回1,否则0*/attempt_back_merge(q, req);}else if (el_ret == ELEVATOR_FRONT_MERGE) {//back merge合并bio_attempt_front_merge(q, req, bio);//二次合并,看能否把req合并到IO调度队列里的前一个prev reqattempt_front_merge(q, req);}//走到这里,应该没有找到可以合并bio的rq,那就针对bio新分配一个rq,里边有可能休眠req = get_request(q, rw_flags, bio, GFP_NOIO);
//根据bio的信息对新分配的req初始化init_request_from_bio(req, bio);plug = current->plug;if (plug) {//当前进程的plug链表非NULL//如果plug链表上的req过多,实际测试一般不成立if (request_count >= BLK_MAX_REQUEST_COUNT) {//把plug链表上的req刷入IO调度算法队列,强制启动硬件IO传输 blk_flush_plug_list(plug, false);}//新分配的req添加到plug->list链表list_add_tail(&req->queuelist, &plug->list);//更新主块设备和块设备分区的time_in_queue和io_ticks数据,增加队列flight中req计数drive_stat_acct(req, 1);//就是blk_account_io_start}else{spin_lock_irq(q->queue_lock);//新分配的rq添加到rq队列。更新主块设备和块设备分区的time_in_queue和io_ticks数据,//增加队列flight中req计数add_acct_request(q, req, where);//启动rq队列,通知底层驱动,新的IO要传输__blk_run_queue(q);spin_unlock_irq(q->queue_lock);}
}

1执行attempt_plug_merge()函数。在该函数中,判断当前进程current->plug如果非NULL,则循环取出current->plug链表上已经存在的每一个req,执行blk_try_merge()判断本次的bio能否合并到该req。判断标准是二者的磁盘扇区地址范围是否挨着。有两种情况:情况1,假设bio代表的磁盘范围是0–5,req代表的磁盘范围是5–10,bio的磁盘范围在req的前边,这叫front merge。bio合并到req(合并过程见bio_attempt_ front _merge函数),req的代表的磁盘范围空间0–10;情况2,bio代表的磁盘范围是10–15,req代表的磁盘范围是5–10,bio的磁盘范围在req的后边,这叫back merge。bio合并到req(合并过程见bio_attempt_back_merge函数),req的代表的磁盘空间范围是5–15。如果合并成功,blk_queue_bio()函数直接返回,不再向下执行。

attempt_plug_merge函数的示意代码如下:


```c
static bool attempt_plug_merge(struct request_queue *q, struct bio *bio,unsigned int *request_count)
{struct blk_plug *plug;struct request *rq;  plug = current->plug;//依次取出进程plug->list链表上已有的struct request req,然后判断req代表的磁盘范围是否挨着本次//的bio的范围,是则把bio合并到reqlist_for_each_entry_reverse(rq, &plug->list, queuelist) {//检查bio和rq代表的磁盘范围是否挨着,挨着则可以合并el_ret = blk_try_merge(rq, bio);if (el_ret == ELEVATOR_BACK_MERGE) {//二者挨着,req向后合并本次的bioret = bio_attempt_back_merge(q, rq, bio);if (ret)break;} else if (el_ret == ELEVATOR_FRONT_MERGE) {//二者挨着,req向前合并本次的bioret = bio_attempt_front_merge(q, rq, bio);if (ret)break;}}
}

2 到这里说明bio没有合并到current->plug链表上的req,并且该块设备设置了IO调度算法(本案例以常用的deadline调度算法为例),则执行elv_merge()判断本次的bio能否合并到IO调度算法队列中的req。注意,这里是bio能否合并到IO调度算法队列中的req,合并源与第1步不一样!判断标准也是二者的磁盘扇区地址范围是否挨着,也是分back merge和front merge两种合并。大体过程是:先遍历fifo队列中的req,看bio能否合并到back merge到req。接着遍历rb tree队列中的req,看bio能否合并到front merge合并到req。fifo队列和rb tree队列是deadline调度算法用的两种数据结构,都是保存req的队列,fifo队列是以req代表的磁盘扇区起始地址为key,而构成的一个链表。从该链表中取出的req,如果req代表的起始扇区地址加上磁盘扇区大小等于bio的磁盘扇区起始地址,req start sector+req sectors=bio start sector,则bio合并到req的后边。rb tree队列是以req代表的磁盘扇区起始地址进行排序,从该队列中取出的req,如果req的起始扇区地址等于bio的磁盘扇区起始地址加上磁盘扇区大小,即bio start sector+bio sectors= req start sector,则bio合并到req的前边。这里,注释代码里提到了二次合并,大体意思是bio合并到req后,req对应的磁盘空间范围扩大了,又与IO算法队列里的其他req代表的磁盘空间范围挨着了,这样就可以进一步合并,源码里有较为详细的举例,这里就不再介绍了。

3 如果执行elv_merge()后判断新的bio能不能合并到IO调度算法队列中已经存在的req,则执行get_request()分配一个新的struct req结构,接着执行init_request_from_bio()用bio的数据对req初始化。

4 接着分两种情况,当前进程是否使用plug队列,即blk_queue_bio()函数判断current->plug是否NULL。如果非NULL,则执行先执行list_add_tail(&req->queuelist, &plug->list)把当前req放入plug链表,然后执行drive_stat_acct(高版本内核换成blk_account_io_start)函数,进行一些IO使用率数据的统计。在该函数里,重点执行part_round_stats()更新struct disk_stats结构的time_in_queue和io_ticks两个变量、struct hd_struct的stamp变量。然后执行part_inc_in_flight(),使struct hd_struct的in_flight [rw]加1,表示IO队列中新增了一个IO请求。time_in_queue、io_ticks、stamp、in_flight [rw]这几个变量前文有解释。

下方是drive_stat_acct()、part_round_stats()、part_inc_in_flight()三个函数的源码简单解释。

static void drive_stat_acct(struct request *rq, int new_io) //高版本函数名字是 blk_account_io_start
{// new_io为0,表示发生了bio合并,说明新的bio合并到了req,只用增加IO合并计数merges则返回if (!new_io) { part = rq->part;//增加IO合并数到struct disk_stats结构的merges变量part_stat_inc(cpu, part, merges[rw]); }else {//更新主块设备和块设备分区的time_in_queue和io_ticks数据part_round_stats(cpu, part);//有一个新的req加入队列了,增加flight计数part_inc_in_flight(part, rw);rq->part = part;}
}
//有一个新的req加入队列了,增加req个数flight
static  inline void part_inc_in_flight(struct hd_struct *part, int rw)
{//增加当前块设备分区的req个数flightatomic_inc(&part->in_flight[rw]);if (part->partno) //增加当前块设备主分区的req个数flight,不用管atomic_inc(&part_to_disk(part)->part0.in_flight[rw]);
}//更新主块设备和块设备分区的time_in_queue和io_ticks计数,并更新part->stamp 为当前系统时间jiffies
void part_round_stats(int cpu, struct hd_struct *part)
{unsigned long now = jiffies;if (part->partno)//更新块设备主分区io_ticks计数,并更新part->stamp 为当前时间jiffies,不用管part_round_stats_single(cpu, &part_to_disk(part)->part0, now);//更新当前要读写的块设备分区的io_ticks,并更新part->stamp 为当前时间jiffiespart_round_stats_single(cpu, part, now);
}//更新主块设备和块设备分区的time_in_queue和io_ticks计数,并更新part->stamp 为当前系统时间jiffies
static void part_round_stats_single(int cpu, struct hd_struct *part,unsigned long now)
{//时间差必须大于一个jiffies,否则不更新time_in_queue、io_ticks、part->stamp。就是说每两次执行//part_round_stats_single函数时间间隔不能太短,避免统计的太频繁。if (now == part->stamp)return;//in_flight 不为0,说明IO队列有req,则更新time_in_queue和io_ticks两个IO计数if (part_in_flight(part)) {__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 为当前时间part->stamp = now;
}

5 如果current->plug为NULL,则执行add_acct_request(),该函数中执行drive_stat_acct(高版本内核换成blk_account_io_start)函数,更新time_in_queue、io_ticks、stamp 、in_flight这几个IO使用率有关的使用计数。接着add_acct_request()中执行__elv_add_request()把新的req加入IO算法队列。回到blk_queue_bio函数,接着执行__blk_run_queue()强制启动磁盘控制器,进行磁盘硬件IO数据传输,之后就能磁盘控制器传输完数据,执行硬中断和软中断函数,唤醒等待IO数据传输完成而休眠的进程。__elv_add_request()涉及到把req加入IO调度算法队列的很多细节,有机会另外写文章再讲解。但是__blk_run_queue()函数还有必要介绍的,它负责调用磁盘控制器的启动IO数据传输函数。

3 __blk_run_queue() 启动IO数据传输和struct bio_vec结构分析

__blk_run_queue()函数的函数调用流程是,__blk_run_queue ->__blk_run_queue_uncond->scsi_request_fn。

static void scsi_request_fn(struct request_queue *q)
{struct scsi_device *sdev = q->queuedata;struct Scsi_Host *shost;struct scsi_cmnd *cmd;struct request *req;for (;;) {/*1 循环执行__elv_next_request(),从q->queue_head队列取出待进行IO数据传输的req2 分配一个struct scsi_cmnd *cmd,使用req对cmd进行部分初始化cmd->request=req,req->special = cmd,还有cmd->transfersize传输字节数、cmd->sc_data_direction DMA传输方向。3 先遍历req上的每一个bio,再得到每个bio的bio_vec,把bio对应的文件数据在内存中的首地址bvec->bv_pag+bvec->bv_offset写入scatterlist。catterlist是磁盘数据DMA传输有关的数据结构,scatterlist保存到bidi_sdb->table.sgl,bidi_sdb是req的struct scsi_data_buffer成员。*/req = blk_peek_request(q);if (!req || !scsi_dev_queue_ready(q, sdev))break;…………//blk_queue_start_tag()中调用blk_start_request(),表示开始传输IO数据了。if (!(blk_queue_tagged(q) && !blk_queue_start_tag(q, req)))blk_start_request(req);sdev->device_busy++;…………//cmd和req已经相互赋值过了,包含了本次的SCSI命令cmd = req->special;…………//发送SCSI  IO传输命令rtn = scsi_dispatch_cmd(cmd);
}
}
void blk_start_request(struct request *req)//表示要启动底层硬件传输了
{//把req从队列剔除blk_dequeue_request(req);req->resid_len = blk_rq_bytes(req);if (unlikely(blk_bidi_rq(req)))req->next_rq->resid_len = blk_rq_bytes(req->next_rq);//req->timeout和req->deadline赋值,把req插入q->timeout_list链表,启动request_queue->timeout//定时器blk_add_timer(req);
}

scsi_request_fn()的实际调用流程还是很复杂的,重点应该是blk_peek_request()函数。这个函数大体分析了一下,有一些地方没有搞清楚,分析的可能有问题。大体意思应该是,分配一个struct scsi_cmnd
*cmd结构,该结构负责与磁盘控制器驱动打交道,包含本次要传输IO数据在内存中的地址,传输数据量等等。由于磁盘控制器实际传输数据使用的是DMA,DMA负责把从IO数据在内存中的地址取出数据,搬运到磁盘控制器有关的寄存器(这个没看到代码,推测),不用CPU参与。这里以以IO写为例,IO读则应是DMA把本次要读取的数据从磁盘控制有关的寄存器搬运到bio指定的内存地址。DMA传输数据又有个要求,必须是从一片连续的物理内存中搬运到指定的内存或者寄存器,但是req对应的多个bio的磁盘数据在内存中的地址,一般都不会是连续的。这里又要引入一个结构,struct bio_vec,表示bio对应的磁盘数据在内存中的一片地址,一个struct bio_vec表示一个page。

比如要进程在内存 1K–2K 和5K–8K保存了本次要写入的文件数据,要写入的磁盘空间是2K–6K,生成一个bio请求bio1。则要生成两个bio_vec结构:bio_vec1和bio_vec2,bio_vec1代表内存1k–2k这片空间,bio_vec2代表5K–8K这片内存空间,bio1对应bio_vec1和bio_vec2。并且进程还在内存空间8K–12k保存了文件数据,要写入磁盘空间的是6k–10k,则又生成了一个bio请求bio2,还生成一个bio_vec结构bio_vec3,bio2对应bio_vec3。由于bio1传输的IO磁盘空间(2K–6K)与bio2传输的IO磁盘空间(6k–10k)临近,二者就合并到了一个req。

最后在磁盘控制器在进行IO数据传输时,并不管bio1或者bio2,而是要从通过bio1和bio2找到bio_vec1、bio_vec2、bio_vec3找到实际要传输的IO数据在内存中的地址,要传输的数据量。而每次DMA传输IO数据又要求要传输数据的内存空间是连续的,所以一次肯定传输不完bio_vec1、bio_vec2、bio_vec3这3片内存数据,这里又引入了一个新的结构struct scatterlist,简单来说它负责组织这种不连续的物理内存的DMA数据传输,blk_peek_request()函数除了分配struct scsi_cmnd结构表示本次传输的SCSI命令,还调用建立bio_vec1、bio_vec2、bio_vec3这3片不连续的内存的内存映射。struct scatterlist、struct scsi_cmnd和req都是通过成员相互连续的。

通过以上分析,有种被骗的感觉,之前觉得req可以保证IO数据一次性传输完,传输完产生一次中断,然后唤醒等待IO数据传输的进程就一切搞定。但是现在看来,由于bio的对应的要传输的磁盘数据在内存中的地址不连续,得传输很多次才能完成呀,也就是要产生多次中断,才能最终传输完req对应的bio磁盘数据呀。我感觉,就看req所有的bio对应的磁盘数据保存的内存空间,分成几片,就要DMA传输几次,然后产生对应次数的中断,最终才能传输完所有的数据。这点是猜测的,涉及的源码后续也得再研究一下。

4 进程current->plug链表与req的发送过程讲解

关于进程current->plug链表,涉及到了req的两种发送方式,其中还是有东西要讲解的。
第一种情况:

submit_bio(rw, &bio)
wait_for_completion(&complete)

进程执行submit_bio后,因为current->plug链表为NULL,所以blk_queue_bio()函数最后,执行add_acct_request-> __elv_add_request 把新的req添加到IO算法队列,然后直接执行__blk_run_queue启动磁盘硬件IO数据传输了。这里我有个疑问,如果block没有设置IO调度算法,那req该怎么添加到IO算法队列?????要研究一下。

第二种情况:

blk_start_plug(&plug)
journal_submit_data_buffers(journal, commit_transaction,write_op)
blk_finish_plug(&plug)

blk_start_plug函数会设置current->plug链表,然后调用journal_submit_data_buffers()函数。该函数会大量调用submit_bio-> blk_queue_bio最后,因为current->plug非NULL,则执行list_add_tail(&req->queuelist, &plug->list)把req添加到plug->list链表。接着执行blk_finish_plug函数。

void blk_finish_plug(struct blk_plug *plug)
{blk_flush_plug_list(plug, false);if (plug == current->plug)current->plug = NULL;
}
void blk_flush_plug_list(struct blk_plug *plug, bool from_schedule)
{LIST_HEAD(list);list_splice_init(&plug->list, &list);
//对plug链表上的req排序,应该是按照每个req的起始扇区地址排序,起始扇区小的排在前list_sort(NULL, &list, plug_rq_cmp);local_irq_save(flags);//整个发送过程关中断//依次取出进程plug链表上的req依次插入IO调度算法队列上while (!list_empty(&list)) {//取出reqrq = list_entry_rq(list.next);//从plug链表删除reqlist_del_init(&rq->queuelist);//如果req有REQ_FLUSH | REQ_FUA属性if (rq->cmd_flags & (REQ_FLUSH | REQ_FUA))__elv_add_request(q, rq, ELEVATOR_INSERT_FLUSH);else//否则,在这里把每一个req插入到IO调度算法队列里,尝试与IO算法队列上的req合并__elv_add_request(q, rq, ELEVATOR_INSERT_SORT_MERGE);}
// queue_unplugged-> __blk_run_queue 中启动磁盘硬件IO数据传输queue_unplugged(q, depth, from_schedule);local_irq_restore(flags);
}

__elv_add_request传递的ELEVATOR_INSERT_FLUSH和ELEVATOR_INSERT_SORT_MERGE参数,二者实质的区别是啥?????需要研究一下

好的,到这里blk_queue_bio()函数整理流程讲解完成,主要是一个新分配的req是怎么影响IO使用率等有关的数据,涉及到drive_stat_acct()、part_round_stats()、part_inc_in_flight()几个函数。其实还有一个重要环节,影响IO使用率等数据的统计,就是IO数据传输完成,磁盘控制器先产生硬中断,然后出发软中断,然后执行blk_account_io_done()和blk_account_io_completion()。

5 IO数据传输完成过程详解

blk_account_io_done()和blk_account_io_completion()函数调用流程如下:

0xffffffffb8f45380 : part_round_stats+0x0/0x100 [kernel]0xffffffffb8f48430 : blk_account_io_done+0x110/0x170 [kernel]0xffffffffb8f484fc : blk_finish_request+0x6c/0x130 [kernel]0xffffffffb90dd546 : scsi_end_request+0x116/0x1e0 [kernel]0xffffffffb90dd7d8 : scsi_io_completion+0x168/0x6a0 [kernel]0xffffffffb90d2c8c : scsi_finish_command+0xdc/0x140 [kernel]0xffffffffb90dcd22 : scsi_softirq_done+0x132/0x160 [kernel]0xffffffffb8f4f8f6 : blk_done_softirq+0x96/0xc0 [kernel]0xffffffffb8ca2155 : __do_softirq+0xf5/0x280 [kernel]0xffffffffb8f47e90 : blk_account_io_completion+0x0/0xb0 [kernel]0xffffffffb8f47f89 : blk_update_request+0x49/0x360 [kernel]0xffffffffb90dd464 : scsi_end_request+0x34/0x1e0 [kernel]0xffffffffb90dd7d8 : scsi_io_completion+0x168/0x6a0 [kernel]0xffffffffb90d2c8c : scsi_finish_command+0xdc/0x140 [kernel]0xffffffffb90dcd22 : scsi_softirq_done+0x132/0x160 [kernel]0xffffffffb8f4f8f6 : blk_done_softirq+0x96/0xc0 [kernel]0xffffffffb8ca2155 : __do_softirq+0xf5/0x280 [kernel]

再梳理一下两个函数的调用流程,栈回溯信息有些函数没打印出来

scsi_end_request –> 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

前文说过,一个req可能合并了多个bio,发送一次IO数据传输SCSI命令,可能并不会把req上所有的bio对应的磁盘数据传输完。比如req上合并了5个bio,而这5个bio可能又有更多的bio_vec,就是说要传输磁盘IO数据在物理内存的数据肯能分了很多片,而传输磁盘IO数据又得使用DMA,DMA每次只能传输一片连续的物理内存中的数据。所以假设第一次发送IO数据传输命令,只传输了3个bio对应的磁盘数据。数据传输完成后,产生中断执行scsi_end_request –> blk_end_request-> blk_end_bidi_request-> blk_update_bidi_request-> blk_update_request-> blk_account_io_completion 函数,在blk_account_io_completion函数中,统计一下刚才传输完成IO数据量,把传输完成的bio相关休眠的进程唤醒,设置下一次传输的bio等等。接着就返回了,不再执行blk_finish_request-> blk_account_io_done,继续进行下一次IO传输,应该是执行scsi_end_request->scsi_next_command->scsi_run_queue->__blk_run_queue进行下一次IO传输,这点不太确定。还有就是这个流程是怎么设置本次传输的bio有关的IO数据到磁盘控制器的,也不太清楚???需要专门研究一下req、bio、SCSI命令的转化关系,尤其是一个req的bio分多次传输的情况!

好的,假设新设置完下一次传输的bio后,也传输完成了,此时又产生中断。但是因为req上的bio都传输完成了,所以函数执行流程是,执行scsi_end_request –> blk_end_request-> blk_end_bidi_request-> blk_update_bidi_request-> blk_update_request-> blk_account_io_completion ,统计一下本次传输完成IO数据量,把传输完成的bio相关休眠的进程唤醒等等。因为 现在req上的bio全部传输完成了,返回到blk_update_bidi_request函数后,就会执行blk_end_bidi_request-> blk_finish_request-> blk_account_io_done,在blk_account_io_completion函数中,统计ios、ticks、time_in_queue、io_ticks、in_flight等IO使用计数。

直接上blk_account_io_completion和blk_account_io_done源码

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;//增加struct disk_stats结构的sectors IO使用计数,即传输的扇区数part_stat_add(cpu, part, sectors[rw], bytes >> 9);part_stat_unlock();}
}
//req对应的bio磁盘数据全部传输完成了,增加ios、ticks、time_in_queue、io_ticks、flight等使用计数
static void blk_account_io_done(struct request *req)
{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;//增加struct disk_stats结构的ios计数,即IOPSpart_stat_inc(cpu, part, ios[rw]);//增加struct disk_stats结构的ticks计数part_stat_add(cpu, part, ticks[rw], duration);//更新主块设备和块设备分区的time_in_queue和io_ticks数据part_round_stats(cpu, part);//req传输完成,IO队列中的req数见1,即struct hd_struct结构的 flight减1part_dec_in_flight(part, rw);hd_struct_put(part);part_stat_unlock();}
}

6 最后用流程图总结一下

跟IO计数有关的流程全介绍完成了,下边再详细展示下整个过程。

这是submit_bio主函数流程,黄色的方框表示一个主函数的开始,棕色的方框表示该函数代码里执行流程,青色的表示对应函数代码里的主要函数。跟IO使用率统计有关的两个函数是part_round_stats()、drive_stat_acct(同blk_account_io_start),part_round_stats()被调用的频繁很高,在IO传输完成、cat /proc/diskstat都会调用到。如下是流程图

part_round_stats()、drive_stat_acct(同blk_account_io_start)两个函数的主流程是:

下边总结一下,块设备IO使用率有关的几个统计项数据:time_in_queue、io_ticks、in_flight、merges 、sectors、ios、ticks、part->stamp。

  • 1 time_in_queue表示当前块设备的IO队列中的总IO数在IO队列中的时间,是一个累加值,具体看源码。更新流程是:part_round_stats->part_round_stats_single->__part_stat_add(cpu,part,time_in_queue,part_in_flight(part)*(now-part->stamp))

  • 2 io_ticks最常用,跟iostat看到IO使用率有关,更新流程是
    part_round_stats->part_round_stats_single->__part_stat_add(cpu, part,io_ticks, (now -part->stamp))。它表示,单位时间内已经提交IO到队列但是还没有传输完成的IO花费的时间(实际以jiffies为单位表示,是个累加值),。什么意思?它与in_flight紧密相关,in_flight表示已经提交IO到队列但是还没有传输完成的IO个数。如果1s内,有800ms in_flight都大于0,说明”单位时间内已经提交IO到队列但是还没有传输完成的IO花费的时间”是800ms,则该段时间的IO使用率就是80%。

  • 3 in_flight表示已经提交IO到队列但是还没有传输完成的IO个数,准确点说是在IO队列中的IO个数+已经从IO队列中取出发送给磁盘控制器驱动传输但还未传输完成的IO个数。需要强调一点,以前我一直以为,in_flight仅仅表示IO队列中的IO个数,大错特错。当从IO队列中取出一个req发送给磁盘控制器驱动,in_flight并没有减1,而是等req对应的磁盘数据全部传输完成执行blk_account_io_done函数in_flight才会减1。另外的场景是,当一个新的req(即IO)加入队列,in_flight加1。当发生IO二次合并,被合并的req就要消失,则in_flight减1。如下是in_flight加减流程:

新分配的req加入IO队列,in_flight加1。 使用进程plug链表:blk_queue_bio->drive_stat_acct(req, 1)(blk_account_io_start)->> part_inc_in_flight()->atomic_inc(&part->in_flight[rw])

不使用进程plug链表:blk_queue_bio->add_acct_request->drive_stat_acct(req,1)(blk_account_io_start)->part_inc_in_flight()->atomic_inc(&part->in_flight[rw])

当一个req传输完成,in_flight减1:blk_account_io_done->part_dec_in_flight->atomic_dec(&part->in_flight[rw])

当发生IO二次合并,被合并的req就要出队列,in_flight减1:blk_queue_bio>attempt_front_merge()/attempt_back_merge() ->attempt_merge()->blk_account_io_merge()->part_dec_in_flight()->atomic_dec(&part->in_flight[rw])

这里有个疑问,发生IO二次合并,只是in_flight减1,为什么不merges那个IO参数也减1呢?merges参数针对bio合并到req的情况,只有bio合并到进程plug链表的req或者IO算法队列的req,merges才加1,不会减少。

  • 4 merges表示当前块设备bio合并到进程plug链表的req或者IO算法队列的req的个数,累加值。前边已经解释过,不再啰嗦。它的更新流程是1

bio合并到IO算法队列中的req,merges加1:blk_queue_bio>bio_attempt_front_merge/bio_attempt_back_merge->drive_stat_acct(req, 0)(同blk_account_io_start)->part_stat_inc(cpu, part, merges[rw])

bio合并到进程plug链表的req,merges加1:blk_queue_bio->attempt_plug_merge->blk_try_merge->bio_attempt_front_merge->bio_attempt_back_merge->drive_stat_acct(req, 0)(同blk_account_io_start)->part_stat_inc(cpu, part, merges[rw])

  • 5 sectors 表示当前块设备的req传输完成的扇区数,累加值。更新流程是blk_account_io_completion->part_stat_add(cpu, part, sectors[rw],
    bytes >> 9)注意,实际观察发现,一个req代表的总扇区数一次传输并不能传输完,会产生好几次中断,每次中断后的软中断都会执行到blk_account_io_completion(),然后累计一下当前完成传输的扇区数。

  • 6 ios表示当前块设备传输完成的req个数,累加值。只有req代表的扇区数全部传输完,执行blk_account_io_done->part_stat_inc(cpu,
    part, ios[rw]) 令ios累加1,这应该是就是iostat看到的IOPS。

  • 7 ticks应该表示req的传输耗时,是一个累计值,即累计该块设备的所有的req传输耗时,更新流程是blk_account_io_done->part_stat_add(cpu, part, ticks[rw],duration)。在req分配时会记录当前系统时间jiffies,流程是blk_queue_bio->get_request->__get_request->blk_rq_init,即req->start_time=jiffies。然后等req对应的磁盘数据全部传输完,执行blk_account_io_done,先执行duration= jiffies - req->start_time,duration则是该req传输耗时,然后把这个req传输耗时累加到ticks。

  • 8 part->stamp其实跟块设备的IO使用率统计项没有多大关系。它用来限制part_round_stats()函数的执行,在part_round_stats->part_round_stats_single函数最后,用part->stamp记录jiffies。然后下次执行part_round_stats->part_round_stats_single,如果part->stamp如果与当前系统时间jiffies相等,直接返回,不会再更新time_in_queue、io_ticks这两项数据。

static void part_round_stats_single(int cpu, struct hd_struct *part,unsigned long now)
{//时间差必须大于一个jiffies,否则执行返回if (now == part->stamp)return;//in_flight 不为0,说明in flight队列有req,此时才会更新time_in_queue、io_ticksif (part_in_flight(part)) {__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 为当前时间part->stamp = now;
}

最后还是要提一下执行最频繁的part_round_stats()函数,只要前后两次执行该函数时间差大于一个jiffes,并且IO队列的req个数不为0,即in_flight不为0,就会更新time_in_queue和io_ticks两个数据。该函数的执行实际时机有。

1 新的IO加入IO队列,流程是blk_queue_bio->drive_stat_acct(req, 1)(blk_account_io_start)->part_round_stats() 和blk_queue_bio->add_acct_request->drive_stat_acct(req, 1)(blk_account_io_start)->part_round_stats()。

2 req对应的磁盘扇区数据全部传输完,执行blk_account_io_done-> part_round_stats()

3 cat /proc/diskstat后内核执行vfs_read-> proc_reg_read-> seq_read-> diskstats_show-> part_round_stats()

4 发生IO二次合并执行blk_queue_bio->attempt_front_merge()/attempt_back_merge()->attempt_merge()->blk_account_io_merge()->part_round_stats()

iostat IO统计原理linux内核源码分析----基于单通道SATA盘相关推荐

  1. Linux内核源码分析--内核启动之(4)Image内核启动(setup_arch函数)(Linux-3.0 ARMv7)【转】...

    原文地址:Linux内核源码分析--内核启动之(4)Image内核启动(setup_arch函数)(Linux-3.0 ARMv7) 作者:tekkamanninja 转自:http://blog.c ...

  2. Linux内核源码分析—从用户空间复制数据到内核空间

    Linux内核源码分析-从用户空间复制数据到内核空间 本文主要参考<深入理解Linux内核>,结合2.6.11.1版的内核代码,分析从用户空间复制数据到内核空间函数. 1.不描述内核同步. ...

  3. Linux内核源码分析《进程管理》

    Linux内核源码分析<进程管理> 前言 1. Linux 内核源码分析架构 2. 进程原理分析 2.1 进程基础知识 2.2 Linux进程四要素 2.3 进程描述符 task_stru ...

  4. Linux内核源码分析之内存管理

    本文站的角度更底层,基本都是从Linux内核出发,会更深入.所以当你都读完,然后再次审视这些功能的实现和设计时,我相信你会有种豁然开朗的感觉. 1.页 内核把物理页作为内存管理的基本单元. 尽管处理器 ...

  5. Linux内核源码分析方法—程序员进阶必备

    一.内核源码之我见 Linux内核代码的庞大令不少人"望而生畏",也正因为如此,使得人们对Linux的了解仅处于泛泛的层次.如果想透析Linux,深入操作系统的本质,阅读内核源码是 ...

  6. 【技术分享篇】Linux内核——手把手带你实现一个Linux内核文件系统丨Linux内核源码分析

    手把手带你实现一个Linux内核文件系统 1. 内核文件系统架构分析 2. 行行珠玑,代码实现 [技术分享篇]Linux内核--手把手带你实现一个Linux内核文件系统丨Linux内核源码分析 更多L ...

  7. Linux内核源码分析--内核启动之(3)Image内核启动(C语言部分)(Linux-3.0 ARMv7) 【转】...

    原文地址:Linux内核源码分析--内核启动之(3)Image内核启动(C语言部分)(Linux-3.0 ARMv7) 作者:tekkamanninja 转自:http://blog.chinauni ...

  8. Linux内核源码分析方法

    说明:这是一个刚接触内核三个月的同学,通过对一个内核特性的分析,总结出一套分析内核的方法. 一.内核源码之我见 Linux内核代码的庞大令不少人"望而生畏",也正因为如此,使得人们 ...

  9. 内核大佬讲述,Linux内核源码分析方法(建议收藏)

    一.内核源码之我见 Linux内核代码的庞大令不少人"望而生畏",也正因为如此,使得人们对Linux的了解仅处于泛泛的层次.如果想透析Linux,深入操作系统的本质,阅读内核源码是 ...

最新文章

  1. asp.net httpmodule 访问页面控件 备忘
  2. R语言泊松回归模型案例:基于AER包的affair数据分析
  3. Linux中的用户和组
  4. JavaWeb -- Struts2 ResultType细化, 国际化
  5. python 中的static-method (静态函数), classmethod(类函数 ), 成员函数
  6. Akka-CQRS(2)- 安装部署cassandra cluster,ubuntu-16.04.1-LTS and MacOS mojave
  7. a标签写链接地址跳转到下载企业微信app页面
  8. 程序开发中常用的密码学家的算法推荐清单
  9. php全站文章搜索,phpspider在列表页生成内容采集url.(又名:如何通过搜狗搜索关键词,爬取新浪新闻)...
  10. (实战3)tasklist(查看进程)和taskkill(结束进程)的使用
  11. Logstash过滤器之Mutate过滤器详解
  12. AngularJS报错:[$injector:unpr] Unknown provider: $templateRequestProvider
  13. 【dojo】dojo.ready(dojo.addOnLoad) “前传”
  14. 创建MySQL桌面快捷方式的方法
  15. 信息安全竞赛解决方案
  16. ssh no matching host key type found. Their offer: ssh-rsa
  17. 吉他学习教程1 之 认识吉他
  18. 基于QT实现的商品销售管理系统
  19. Influxdb CQ RP
  20. OpManager 虚拟化管理

热门文章

  1. UX用户体验研究:选择指标的2个模型
  2. 什么是.NET?什么是.NET Core?.NET和.NET Core区别又是什么呢?
  3. JavaScript 19. 正则表达式
  4. vs下的输出目录/输出文件/工作目录-总结
  5. SQL 中日期的比较
  6. 计算机系张馨文,2020届魅力毕业生 | 【信息学部计算机与信息学院】张馨文——努力且热爱,做一朵乐观而坚韧的向日葵...
  7. <selectKey>标签详解
  8. 【数据结构】图的详细分析(全)
  9. JMeter自动化测试工具
  10. SOUI的一个动态创建控件的小例子