## 块I/O流程与 I/O调度器 * 一个块IO的一生:从page cache到bio 到 request * 块设备层的数据结构 * page和bio的关系,request 和bio的关系 * O_DIRECT 和O_SYNC * blktrace , ftrace * IO调度和CFQ调度算法 * CFQ和ionice * cgroup与IO * io性能测试: iotop, iostat 主要内容:从应用程序发起一次IO行为,最终怎么到磁盘,以及在这个路径上有什么trace的方法和 配置。 每次应用程序写磁盘,都是到pagecache 。三进三出 讲解 bio的一生,都是在pagecache以下。 ------- 首先,在一次普通的sys_write过程中,会触发以下的函数调用。 ``` sys_write -> vfs_write -> generic_file_write_iter 1) generic_perform_write -> write_begin -> copy_data ->write_end -> mark_buffer_dirty -> set_buffer_dirty/ set_page_dirty/ set_inode_dirty 2) generic_file_direct_write -> filemap_write_and_write_range -> mapping-> a_ops-> direct_IO ``` 如果是buffer IO , 脏页回写是异步的,并且由块设备层负责。 ### 块设备层的数据结构 struct page -> struct inode -> struct bio -> struct request 真正开始做IO之前,即操作block子系统之前,包括以下步骤: ![](https://box.kancloud.cn/f4820ff05337d046a3a1be22547a2239_2148x1362.png) 应用程序读一个文件,首先会去查page cache是否命中,下一次再读page cache是命中的。 应用程序去写硬盘时,首先会去写page cache,至于什么时候开始写硬盘,由linux flush线程通过(pdflush->每一个设备的flush ->工作队列)实现。当然也可以是线程本身,通过direct IO去写硬盘。 如上图三个task_struct进程,同时打开一个文件,在内存中生成file数据结构,记录文件打开的实例。 -------- inode是真实存在硬盘里,当inode结构体中的i_mapping,对应的地址空间address_space。一个4M的文件,被分成很多4K单元存在于内存,通过地址 去radix tree来查page cache是否命中。如果查到了,就从radix tree对应的page返回。 如果没有page cache对应,就会通过 address_space_operations(文件系统实现的数据结构) 去 readpage,从硬盘里的块读到pagecache。 ------- free命令看到的buffers和cache有什么区别? 答:在内核层面,全部都是pagecache。前者对应的是裸分区产生的page cache,后者对应的是挂载文件系统之后,文件系统目录产生的page cache。如下图内核代码的截图: ![](https://box.kancloud.cn/bcaa240b5857af1aa1d76588d90b0771_1668x1164.png) 文件系统的管理单元是block,内存管理是以page为单位,扇区(section)是硬件读写的最小单元。 假设你把ext4文件系统格式化成1k/block,那么一个page对应4个block,此时读一个内存的page,文件系统要操作4个block。 假设你把ext4文件系统格式化成4k/block,那么page和block可以一一对应。 ------- /proc/meminfo中的 buffer ram如何计算出? 调用 nr_blockdev_pages(),这个函数会遍历所有原始块设备(list_for_each_entry),把所有原始块设备的inode中的i_mapping , 指向 address_space_operation。包括radix tree管理的pagecache,和 pagecache读写硬盘的硬件操作的接口。 nr_pages: page cache中已经命中的page。 ### Page和 bio的关系 所谓的bio,是指文件系统的block与内存page的对应。有时候文件系统格式化block为1k,一次读写page的请求,最终转换成操作多个block。bio为I/O请求提供了一个轻量级的表示方法,内核用一个bio的结构体来描述一次块IO操作。 bio结构体如下: ``` struct bio { sector_t bi_sector; /*我们想在块设备的第几个扇区上进行io操作(起始扇区),此处扇区大小是按512计算的*/ struct bio *bi_next; struct block_device *bi_bdev; /*指向块设备描述符的指针,该io操作是针对哪个块设备的*/ unsigned long bi_rw; /*该io操作是读还是写*/ unsigned short bi_vcnt; /* bio的bio_vec数组中段的数目 */ unsigned short bi_idx; /* bio的bio_vec数组中段的当前索引值 */ unsigned short bi_phys_segments; //合并之后bio中(内存)物理段的数目 unsigned int bi_size; /* 需要传送的字节数 */ bio_end_io_t *bi_end_io; /* bio的I/O操作结束时调用的方法 */ void *bi_private; //通用块层和块设备驱动程序的I/O完成方法使用的指针 unsigned int bi_max_vecs; /* bio的bio vec数组中允许的最大段数 */ atomic_t bi_cnt; /* bio的引用计数器 */ struct bio_vec *bi_io_vec; /*指向bio的bio_vec数组中的段的指针 */ struct bio_set *bi_pool; struct bio_vec bi_inline_vecs[0];/*一般一个bio就一个段,bi_inline_vecs就可满足,省去了再为bi_io_vec分配空间*/ } struct bio_vec { struct page *bv_page; //指向段的页框对应页描述符的指针 unsigned int bv_len; //段的字节长度,长度可以超过一个页 unsigned int bv_offset; //页框中段数据的偏移量 }; ``` 一个bio可能有很多个bio段,这些bio段在内存是可能不连续,位于不同的页,但在磁盘上对应的位置是连续的。一般上层构建bio的时候都是只有一个bio段。(新的DMA支持多个不连续内存的数据传输) 可以看到bio的段可能指向多个page,而bio也可以在没有buffer_head的情况下构造。 ### request 和bio的关系 在IO调度器中,上层提交的bio被构造成request结构,每个物理设备会对应一个request_queue,里面顺序存放着相关的request。每个请求包含一个或多个bio结构,bio之间用有序链表连接起来,按bio起始扇区的位置从小到大,而且这些bio之间在磁盘扇区是相邻的,也就是说一个bio的结尾刚好是下一个bio的开头。 通常,通用块层创建一个仅包含一个bio结构的请求,可能存在新bio与请求中已存在的数据物理相邻的情况,就把bio加入该请求,否则用该bio初始化一个新的请求。 ------ buffer 和cache,在linux内核实现上没有区别,在计数上有区别。最后都是 address_space ->radix tree -> read/write pages。 buffer_head: 是内核封装的数据结构。它是内核page与磁盘上物理数据块之间的桥梁。一方面,每个page包含多个buffer_head(一般4个),另外一方面,buffer_head中又记录了底层设备块号信息。这样,通过page->buffer_head->block就能完成数据的读写。 ### O_DIRECT 和 O_SYNC ![](https://box.kancloud.cn/75a7d7de13aad909d8cee7f60fff501f_1594x1116.png) 两者区别,O_DIRECT 把一片地址配置成不带cache的空间 , 直接导硬盘 , 而 O_SYNC 类似CPU cache的write through. 当应用程序使用O_SYNC写page cache时,直接写穿到硬盘。 当应用程序同时使用O_SYNC和 O_DIRECT,可能会出现page cache的不一致问题。 ### 三进三出 讲解 bio ![](https://box.kancloud.cn/11188132ab54b7aec4343d4d6e9c1760_1546x1014.png) page cache只有通过 address_space_operations里的 write_pages和 read_pages,才能产生对文件系统的block产生IO行为。 block io( bio ) : 讲文件系统上哪些block 读到内存哪些 page, 文件系统的 block bitmap 和 inode table,bio是从文件系统解析出来,从block到page之间的关系。 inode_table : 指针指向哪些 page cache和 硬盘数据 通过linux readpages函数关联时,会把文件系统的四个block,转化成4个bio。bio里的指针指向数据在硬盘的位置,也会有把这些数据读出来之后放置到page cache的哪些page。 当一个page对应多个block的情况,一个page 对应几个bio:size(page) > size(block), ![](https://box.kancloud.cn/1a4c98780b23ac0e47f3386672b338c4_1632x1048.png) bio --> request 每个进程有自己的plug队列,先把bio先发送到plug队列。在plug队列里,把bio转化成request。第一个bio一定是一个request,之后的bio就会查看多个bio是否可以merge到一个request,如果不能合并就产生一个新的request。 bio边发plug队列,边转成request。而这些request 先放到 elevator电梯队列,IO调度电梯做的类似路由器中QoS的功能->限制端口的流量。 deadline调度算法:优先考虑读,认为写不重要。应用程序写到pagecache以后,就已经写完了。 ### ftrace vfs_read/vfs_write 慢,到底层操作磁盘差十万八千里。要用ftrace分析,才能知道具体原因。 ![](https://box.kancloud.cn/5b4e7e9fc09867c7316db370bbf44abf_1656x1134.png) 对于文件读写这种非常复杂的流程,在工程里面可以使用的调试方式是 ftrace。 除了用ftrace工具进行函数级的分析之外,使用blktrace去跟踪整个block io 生命周期,比如什么时候进入plug队列,unplug,什么时候进入电梯调度,什么时候进到驱动队列。 ### blktrace ``` apt-get install sleuthkit blktrace #1: blktrace -d /dev/sda -o - | blkparse -i - > 1.trace #2: root@whale-indextest01-1001:/home/gzzhangyi2015/learningLinuxKernel/io-courses/bio-flow# dd if=read.c of=barry oflag=sync 0+1 records in 0+1 records out 219 bytes (219 B) copied, 0.000854041 s, 256 kB/s ``` ![](https://box.kancloud.cn/4679c9e20dd93e073574e8e1f9672674_763x182.png) 扇区号(294220992)/8 = block号(36777624) 再用debugfs -R 'icheck 块号' /dev/sda9 ![](https://box.kancloud.cn/87a6fdb951087f762905e5473361acb5_1228x192.png) 再用debugfs -R 'ncheck inode' /dev/sda9 ![](https://box.kancloud.cn/b691c681bcbcaad0ad42d824ab93e3ef_1700x194.png) 再用 blkcat /dev/sda9 块号 ![](https://box.kancloud.cn/99df9582fae9ae783405a9a637e13fa0_1686x534.png) 总结: blktrace 是在内核里关键函数点上加了一些记录,再把记录抓下来。主要是看操作的流程。 ftrace 看 函数的流程。 http://blog.51cto.com/xuclv/1176756 ## IO调度 和 CFQ调度算法 ![](https://box.kancloud.cn/52031a19472af05308e33b390676c551_1604x684.png) 主要从 `进程优先级` , `流量控制` 方面考虑,在`通用块层和 I/O调度层` 进行限速,限制带宽和 IOPS。 Noop:空操作调度算法,也就是没有任何调度操作,并不对io请求进行排序,仅仅做适当的io合并的一个fifo队列。合并的技术,不太适用于排序。适用于固态硬盘,因为固态硬盘基本上可以随机访问。 CFQ:完全公平队列调度类似进程调度里的CFS,指定进程的nice值。它试图给所有进程提供一个完全公平的IO操作环境。它为每个进程创建一个同步IO调度队列,并默认以时间片和请求数限定的方式分配IO资源,以此保证每个进程的IO资源占用是公平的,cfq还实现了针对进程级别的优先级调度。 CFQ对于IO密集型场景不适用,尤其是IO压力集中在某些进程上的场景。该场景下需要更多满足某个或某几个进程的IO响应速度,而不是让所有的进程公平的使用IO。 此时,deadline调度(最终期限调度)就更适应这样的场景。deadline实现了四个队列,其中两个分别处理正常read和write,按扇区号排序,进行正常io的合并处理以提高吞吐量.因为IO请求可能会集中在某些磁盘位置,这样会导致新来的请求一直被合并,于是可能会有其他磁盘位置的io请求被饿死。 于是,实现了另外两个处理超时read和write的队列,按请求创建时间排序,如果有超时的请求出现,就放进这两个队列,调度算法保证超时(达到最终期限时间)的队列中的请求会优先被处理,防止请求被饿死。由于deadline的特点,无疑在这里无法区分进程,也就不能实现针对进程的io资源控制。 从原理上看, cfq是一种比较通用的调度算法,是一种以进程为出发点考虑的调度算法,保证大家尽量公平。 deadline是一种以提高机械硬盘吞吐量为思考出发点的调度算法,只有当有io请求达到最终期限的时候才进行调度,非常适合业务比较单一并且IO压力比较重的业务,比如数据库。 而noop呢?其实如果我们把我们的思考对象拓展到固态硬盘,那么你就会发现,无论cfq还是deadline,都是针对机械硬盘的结构进行的队列算法调整,而这种调整对于固态硬盘来说,完全没有意义。对于固态硬盘来说,IO调度算法越复杂,效率就越低,因为额外要处理的逻辑越多。 所以,固态硬盘这种场景下,使用noop是最好的,deadline次之,而cfq由于复杂度的原因,无疑效率最低。 ## CFQ 和 ionice ![](https://box.kancloud.cn/d586ee0fd68c18aa45a71f1a040496cd_1648x1066.png) ### demo 把linux的IO调度算法改成CFQ,并且运行两个不同IO nice值的进程。 ``` 目前的调度算法是 deadline root@whale-indextest01-1001:/sys/block/sda/queue# cat scheduler noop [deadline] cfq root@whale-indextest01-1001:/sys/block/sda/queue# echo cfq > scheduler root@whale-indextest01-1001:/sys/block/sda/queue# cat scheduler noop deadline [cfq] root@whale-indextest01-1001:/sys/block/sda/queue# ionice -c 2 -n 0 cat /dev/sda9 > /dev/null& [1] 6755 root@whale-indextest01-1001:/sys/block/sda/queue# ionice -c 2 -n 7 cat /dev/sda9 > /dev/null& [2] 6757 root@whale-indextest01-1001:/sys/block/sda/queue# iotop ``` ![](https://box.kancloud.cn/2c2cebfa41199ffb0e994146988460ff_1534x874.png) ``` root@whale-indextest01-1001:/sys/fs/cgroup/blkio# cgexec -g blkio:A dd if=/dev/sda9 of=/dev/null iflag=direct & [3] 7550 root@whale-indextest01-1001:/sys/fs/cgroup/blkio# cgexec -g blkio:B dd if=/dev/sda9 of=/dev/null iflag=direct & [4] 7552 ``` FAQ: 1. 什么是Bufferd IO/ Direct IO? 如何解释cgroup的blkio对buffered IO是没有限速支持的? 答:这里面的buffer的含义跟内存中buffer cache有概念上的不同。实际上这里Buffered IO的含义,相当于内存中的buffer cache+page cache,就是IO经过缓存的意思。 cgroup针对IO的资源限制实现在了通用块设备层,对哪些IO操作有影响呢? 原则上说都有影响,因为绝大多数数据都是要经过`通用块设备层写入存储`的,但是对于应用程序来说感受可能不一样。在一般IO的情况下,应用程序很可能很快的就写完了数据(在数据量小于缓存空间的情况下),然后去做其他事情了。这时应用程序感受不到自己被限速了,而内核在处理write-back的阶段,由于没有相关page cache中的inode是属于那个cgroup的信息记录,所以所有的page cache的回写只能放到cgroup的root组中进行限制,而不能在其他cgroup中进行限制,因为root组的cgroup一般是不做限制的。 而在Sync IO和Direct IO的情况下,由于应用程序写的数据是不经过缓存层的,所以能直接感受到速度被限制,一定要等到整个数据按限制好的速度写完或者读完,才能返回。这就是当前cgroup的blkio限制所能起作用的环境限制。