最近太忙,竟然过了2个月才更新第十四章。。。。linux
主要内容:ios
I/O设备主要有2类:算法
字符设备因为只能顺序访问,因此应用场景也很少,这篇文章主要讨论块设备。并发
块设备是随机访问的,因此块设备在不一样的应用场景中存在很大的优化空间。app
块设备中最重要的一个概念就是块设备的最小寻址单元。异步
块设备的最小寻址单元就是扇区,扇区的大小是2的整数倍,通常是 512字节。async
扇区是物理上的最小寻址单元,而逻辑上的最小寻址单元是块。ide
为了便于文件系统管理,块的大小通常是扇区的整数倍,而且小于等于页的大小。oop
查看扇区和I/O块的方法:大数据
[wangyubin@localhost]$ sudo fdisk -l WARNING: GPT (GUID Partition Table) detected on '/dev/sda'! The util fdisk doesn't support GPT. Use GNU Parted. Disk /dev/sda: 500.1 GB, 500107862016 bytes, 976773168 sectors Units = sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 4096 bytes I/O size (minimum/optimal): 4096 bytes / 4096 bytes Disk identifier: 0x00000000
上面的 Sector size 就是扇区的值,I/O size就是 块的值
从上面显示的结果,咱们发现有个奇怪的地方,扇区的大小有2个值,逻辑大小是 512字节,而物理大小倒是 4096字节。
其实逻辑大小 512字节是为了兼容之前的软件应用,而实际物理大小 4096字节是因为硬盘空间愈来愈大致使的。
具体的前因后果请参考:4KB扇区的缘由
内核经过文件系统访问块设备时,须要先把块读入到内存中。因此文件系统为了管理块设备,必须管理[块]和内存页之间的映射。
内核中有2种方法来管理 [块] 和内存页之间的映射。
每一个 [块] 都是一个缓冲区,同时对每一个 [块] 都定义一个缓冲区头来描述它。
因为 [块] 的大小是小于内存页的大小的,因此每一个内存页会包含一个或者多个 [块]
缓冲区头定义在 <linux/buffer_head.h>: include/linux/buffer_head.h
struct buffer_head { unsigned long b_state; /* 表示缓冲区状态 */ struct buffer_head *b_this_page;/* 当前页中缓冲区 */ struct page *b_page; /* 当前缓冲区所在内存页 */ sector_t b_blocknr; /* 起始块号 */ size_t b_size; /* buffer在内存中的大小 */ char *b_data; /* 块映射在内存页中的数据 */ struct block_device *b_bdev; /* 关联的块设备 */ bh_end_io_t *b_end_io; /* I/O完成方法 */ void *b_private; /* 保留的 I/O 完成方法 */ struct list_head b_assoc_buffers; /* 关联的其余缓冲区 */ struct address_space *b_assoc_map; /* 相关的地址空间 */ atomic_t b_count; /* 引用计数 */ };
整个 buffer_head 结构体中的字段是减小过的,之前的内核中字段更多。
各个字段的含义经过注释都很明了,只有 b_state 字段比较复杂,它涵盖了缓冲区可能的各类状态。
enum bh_state_bits { BH_Uptodate, /* 包含可用数据 */ BH_Dirty, /* 该缓冲区是脏的(说明缓冲的内容比磁盘中的内容新,须要回写磁盘) */ BH_Lock, /* 该缓冲区正在被I/O使用,锁住以防止并发访问 */ BH_Req, /* 该缓冲区有I/O请求操做 */ BH_Uptodate_Lock,/* 由内存页中的第一个缓冲区使用,使得该页中的其余缓冲区 */ BH_Mapped, /* 该缓冲区是映射到磁盘块的可用缓冲区 */ BH_New, /* 缓冲区是经过 get_block() 刚刚映射的,尚且不能访问 */ BH_Async_Read, /* 该缓冲区正经过 end_buffer_async_read() 被异步I/O读操做使用 */ BH_Async_Write, /* 该缓冲区正经过 end_buffer_async_read() 被异步I/O写操做使用 */ BH_Delay, /* 缓冲区还未和磁盘关联 */ BH_Boundary, /* 该缓冲区处于连续块区的边界,下一个块不在连续 */ BH_Write_EIO, /* 该缓冲区在写的时候遇到 I/O 错误 */ BH_Ordered, /* 顺序写 */ BH_Eopnotsupp, /* 该缓冲区发生 “不被支持” 错误 */ BH_Unwritten, /* 该缓冲区在磁盘上的位置已经被申请,但还有实际写入数据 */ BH_Quiet, /* 该缓冲区禁止错误 */ BH_PrivateStart,/* 不是表示状态,分配给其余实体的私有数据区的第一个bit */ };
在2.6以前的内核中,主要就是经过缓冲区头来管理 [块] 和内存之间的映射的。
用缓冲区头来管理内核的 I/O 操做主要存在如下2个弊端,因此在2.6开始的内核中,缓冲区头的做用大大下降了。
- 弊端 1
对内核而言,操做内存页是最为简便和高效的,因此若是经过缓冲区头来操做的话(缓冲区 即[块]在内存中映射,可能比页面要小),效率低下。
并且每一个 [块] 对应一个缓冲区头的话,致使内存的利用率下降(缓冲区头包含的字段很是多)
- 弊端 2
每一个缓冲区头只能表示一个 [块],因此内核在处理大数据时,会分解为对一个个小的 [块] 的操做,形成没必要要的负担和空间浪费。
bio结构体的出现就是为了改善上面缓冲区头的2个弊端,它表示了一次 I/O 操做所涉及到的全部内存页。
/* * I/O 操做的主要单元,针对 I/O块和更低级的层 (ie drivers and * stacking drivers) */ struct bio { sector_t bi_sector; /* 磁盘上相关扇区 */ struct bio *bi_next; /* 请求列表 */ struct block_device *bi_bdev; /* 相关的块设备 */ unsigned long bi_flags; /* 状态和命令标志 */ unsigned long bi_rw; /* 读仍是写 */ unsigned short bi_vcnt; /* bio_vecs的数目 */ unsigned short bi_idx; /* bio_io_vect的当前索引 */ /* Number of segments in this BIO after * physical address coalescing is performed. * 结合后的片断数目 */ unsigned int bi_phys_segments; unsigned int bi_size; /* 剩余 I/O 计数 */ /* * To keep track of the max segment size, we account for the * sizes of the first and last mergeable segments in this bio. * 第一个和最后一个可合并的段的大小 */ unsigned int bi_seg_front_size; unsigned int bi_seg_back_size; unsigned int bi_max_vecs; /* bio_vecs数目上限 */ unsigned int bi_comp_cpu; /* 结束CPU */ atomic_t bi_cnt; /* 使用计数 */ struct bio_vec *bi_io_vec; /* bio_vec 链表 */ bio_end_io_t *bi_end_io; /* I/O 完成方法 */ void *bi_private; /* bio结构体建立者的私有方法 */ #if defined(CONFIG_BLK_DEV_INTEGRITY) struct bio_integrity_payload *bi_integrity; /* data integrity */ #endif bio_destructor_t *bi_destructor; /* bio撤销方法 */ /* * We can inline a number of vecs at the end of the bio, to avoid * double allocations for a small number of bio_vecs. This member * MUST obviously be kept at the very end of the bio. * 内嵌在结构体末尾的 bio 向量,主要为了防止出现二次申请少许的 bio_vecs */ struct bio_vec bi_inline_vecs[0]; };
几个重要字段说明:
bio_vec 结构体很简单,定义以下:
struct bio_vec { struct page *bv_page; /* 对应的物理页 */ unsigned int bv_len; /* 缓冲区大小 */ unsigned int bv_offset; /* 缓冲区开始的位置 */ };
每一个 bio_vec 都是对应一个页面,从而保证内核可以方便高效的完成 I/O 操做
缓冲区头和bio并非相互矛盾的,bio只是缓冲区头的一种改善,将之前缓冲区头完成的一部分工做移到bio中来完成。
bio中对应的是内存中的一个个页,而缓冲区头对应的是磁盘中的一个块。
对内核来讲,配合使用bio和缓冲区头 比 只使用缓冲区头更加的方便高效。
bio至关于在缓冲区上又封装了一层,使得内核在 I/O操做时只要针对一个或多个内存页便可,不用再去管理磁盘块的部分。
使用bio结构体还有如下好处:
缓冲区头和bio都是内核处理一个具体I/O操做时涉及的概念。
可是内核除了要完成I/O操做之外,还要调度好全部I/O操做请求,尽可能确保每一个请求能有个合理的响应时间。
下面就是目前内核中已有的一些 I/O 调度算法。
为了保证磁盘寻址的效率,通常会尽可能让磁头向一个方向移动,等到头了再反过来移动,这样能够缩短全部请求的磁盘寻址总时间。
磁头的移动有点相似于电梯,全部这个 I/O 调度算法也叫电梯调度。
linux中的第一个电梯调度算法就是 linus本人所写的,全部也叫作 linus 电梯。
linus电梯调度主要是对I/O请求进行合并和排序。
当一个新请求加入I/O请求队列时,可能会发生如下4种操做:
linus电梯调度程序在2.6版的内核中被其余调度程序所取代了。
linus电梯调度主要考虑了系统的全局吞吐量,对于个别的I/O请求,仍是有可能形成饥饿现象。
并且读写请求的响应时间要求也是不同的,通常来讲,写请求的响应时间要求不高,写请求能够和提交它的应用程序异步执行,
可是读请求通常和提交它的应用程序时同步执行,应用程序等获取到读的数据后才会接着往下执行。
所以在 linus 电梯调度程序中,还可能形成 写-饥饿-读(wirtes-starving-reads)这种特殊问题。
为了尽可能公平的对待全部请求,同时尽可能保证读请求的响应时间,提出了最终期限I/O调度算法。
最终期限I/O调度 算法给每一个请求设置了超时时间,默认状况下,读请求的超时时间500ms,写请求的超时时间是5s
但一个新请求加入到I/O请求队列时,最终期限I/O调度和linus电梯调度相比,多出了如下操做:
最终期限I/O调度 算法也不能严格保证响应时间,可是它能够保证不会发生请求在明显超时的状况下仍得不到执行。
最终期限I/O调度 的实现参见: block/deadline-iosched.c
最终期限I/O调度算法优先考虑读请求的响应时间,但系统处于写操做繁重的状态时,会大大下降系统的吞吐量。
由于读请求的超时时间比较短,因此每次有读请求时,都会打断写请求,让磁盘寻址到读的位置,完成读操做后再回来继续写。
这种作法保证读请求的响应速度,却损害了系统的全局吞吐量(磁头先去读再回来写,发生了2次寻址操做)
预测I/O调度算法是为了解决上述问题而提出的,它是基于最终期限I/O调度算法的。
但有一个新请求加入到I/O请求队列时,预测I/O调度与最终期限I/O调度相比,多了如下操做:
预测I/O调度算法中最重要的是保证等待期间不要浪费,也就是提升预测的准确性,
目前这种预测是依靠一系列的启发和统计工做,预测I/O调度程序会跟踪并统计每一个应用程序的I/O操做习惯,以便正确预测应用程序的读写行为。
若是预测的准确率足够高,那么预测I/O调度和最终期限I/O调度相比,既能提升读请求的响应时间,又能提升系统吞吐量。
预测I/O调度的实现参见: block/as-iosched.c
注:预测I/O调度是linux内核中缺省的调度程序。
彻底公正的排队(Complete Fair Queuing, CFQ)I/O调度 是为专有工做负荷设计的,它和以前提到的I/O调度有根本的不一样。
CFQ I/O调度 算法中,每一个进程都有本身的I/O队列,
CFQ I/O调度程序以时间片轮转调度队列,从每一个队列中选取必定的请求数(默认4个),而后进行下一轮调度。
CFQ I/O调度在进程级提供了公平,它的实现位于: block/cfq-iosched.c
空操做(noop)I/O调度几乎不作什么事情,这也是它这样命名的缘由。
空操做I/O调度只作一件事情,当有新的请求到来时,把它与任一相邻的请求合并。
空操做I/O调度主要用于闪存卡之类的块设备,这类设备没有磁头,没有寻址的负担。
空操做I/O调度的实现位于: block/noop-iosched.c
2.6内核中内置了上面4种I/O调度,能够在启动时经过命令行选项 elevator=xxx 来启用任何一种。
elevator选项参数以下:
参数 |
I/O调度程序 |
as | 预测 |
cfq | 彻底公正排队 |
deadline | 最终期限 |
noop | 空操做 |
若是启动预测I/O调度,启动的命令行参数中加上 elevator=as