第十四章 块I/O层

块I/O层

系统中能够随机(不需要按顺序)访问固定大小数据片(chunks)的硬件设备称为块设备,这些固定大小的数据片就称作块,硬盘是最常见的块设备。

如果一个硬件设备是以字符流(顺序访问)的方式被访问,就归于字符设备;如果一个设备是随机访问的,它就属于块设备。

1.块设备

块设备中最小可寻址单元是扇区,扇区的大小是设备的物理属性。

块是文件系统的一种抽象,物理磁盘寻址按照扇区进行,但内核执行的所有磁盘操作是按照块进行。因为扇区是设备的最小可寻址单元,所以块不能比扇区还小,只能数倍于扇区。内核要求块的大小是2的幂,不能超过一个页的大小。所以通常块大小为512字节、1KB、4KB,即扇区:块=1:N。

扇区称硬扇区或设备块;块有时称文件块或I/O块。

扩展阅读:Device file


2.缓冲区和缓冲区头

每个缓冲区对应一个块,它相当于磁盘块在内存中的表示。

内核定义一个buffer_head结构体来表示一个缓冲区,每个缓冲区都有这样一个描述符,它包含内核操作缓冲区的全部信息。

buffer_head:linux/buffer_head.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct buffer_head {
unsigned long b_state; /* 缓冲区状态标志 buffer state bitmap (see above) */
struct buffer_head *b_this_page;/* 页面中的缓冲区 circular list of page's buffers */
struct page *b_page; /* 存储该缓冲区的页面 the page this bh is mapped to */
sector_t b_blocknr; /* 起始块号 start block number */
size_t b_size; /* size of mapping */
char *b_data; /* pointer to data within the page */
struct block_device *b_bdev; /* 相关联的块设备 */
bh_end_io_t *b_end_io; /* I/O完成方法 I/O completion */
void *b_private; /* reserved for b_end_io */
struct list_head b_assoc_buffers; /* 映射链表 associated with another mapping */
struct address_space *b_assoc_map; /* 相关的地址空间 mapping this buffer is
associated with */
atomic_t b_count; /* 引用计数 users using this buffer_head */
};

b_state域表示缓冲区的状态,相关的标志位定义在bh_state_bits枚举中。


3. bio结构体

bio结构体表示内核操作块I/O的基本容器,代表了正在活动的以segment链表形式组织的块I/O操作。一个segment是一小块连续的内存,这样不需要保证单个缓冲区一定要连续,即使一个缓冲区分散在内存的多个位置,bio结构体也能对内核保证I/O操作的执行。这样的向量I/O就是所谓的聚散I/O。

bio结构体定义在:linux/blk_types.h

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
struct bio {
struct bio *bi_next; /* request queue link */
struct block_device *bi_bdev; /* 相关块设备 */
int bi_error;
unsigned int bi_opf; /* bottom bits req flags,
* top bits REQ_OP. Use
* accessors.
*/
unsigned short bi_flags; /* status, command, etc */
unsigned short bi_ioprio;
struct bvec_iter bi_iter;
/* Number of segments in this BIO after
* physical address coalescing is performed.
*/
unsigned int bi_phys_segments; /* 结合后的segment数目 */
/*
* 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; /* 最后一个可合并的段大小 */
atomic_t __bi_remaining;
bio_end_io_t *bi_end_io; /* I/O完成方法 */
void *bi_private; /* 拥有者的私有方法 */
#ifdef CONFIG_BLK_CGROUP
/*
* Optional ioc and css associated with this bio. Put on bio
* release. Read comment on top of bio_associate_current().
*/
struct io_context *bi_ioc;
struct cgroup_subsys_state *bi_css;
#endif
union {
#if defined(CONFIG_BLK_DEV_INTEGRITY)
struct bio_integrity_payload *bi_integrity; /* data integrity */
#endif
};
unsigned short bi_vcnt; /* bio_vecs偏移的个数 how many bio_vec's */
/*
* Everything starting with bi_max_vecs will be preserved by bio_reset()
*/
unsigned short bi_max_vecs; /* bio_vecs数目上限 max bvl_vecs we can hold */
atomic_t __bi_cnt; /* 使用计数 pin count */
struct bio_vec *bi_io_vec; /* bio_vecs链表 the actual vec list */
struct bio_set *bi_pool;
/*
* 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.
*/
struct bio_vec bi_inline_vecs[0]; /* 内嵌bio向量 */
};
3.1 I/O向量

bi_io_vec域指向一个bio_vec结构体数组,该结构体链表包含了一个特定的I/O操作所需要用到的所有片段。每个bio_vec结构是形式为的向量,它描述了一个特定的片段:<片段的物理页, 块在物理页中的偏移位置, 从给定偏移量开始的块长度>。

bio_io_vec结构体数组整个一起表示了一个完整的缓冲区。bio_vec结构定义在:linux/bvec.h

1
2
3
4
5
struct bio_vec {
struct page *bv_page; /* 指向这个segment所驻留的物理页 */
unsigned int bv_len; /* 以字节为单位的大小*/
unsigned int bv_offset; /* 偏移量 */
};

bio结构体代表I/O操作,可以包含内存中的一个或多个页;buffer_head结构体代表一个缓冲区,描述的是磁盘上的一个块。利用bio代替buffer_head好处有:

  • 不需要连续存储区,也不需要分割I/O操作
  • bio很容易处理高端内存,因为它处理的是物理页而不是直接指针
  • bio既可以代表普通页,也可以代表直接I/O
  • bio便于执行分散——集中(矢量化)块I/O操作,操作中的数据可以来自多个物理页
  • bio相比缓冲区头属于轻量级结构体

4.请求队列

块设备将他们挂起的块I/O请求保存在请求队列中,由request_queue结构体表示,请求队列只要不为空,队列对应的块设备驱动程序就会从队列头获取请求,然后将其送入对应的块设备上去。

请求队列request_queue定义在:linux/blkdev.h,包含一个双向请求链表以及相关控制信息,请求队列的每一项是一个request结构体。一个请求可能操作多个连续的磁盘块,所以每个请求可以由多个bio结构体构成。request结构体同样定义在:linux/blkdev.h


5. I/O调度程序

直接简单地将请求依次直接发送给块设备性能是不好的,磁盘寻址是整个计算机中最慢的操作之一,为了优化寻址操作,内核既不会简单地按请求次序,也不会立即将其提交给磁盘,在提交前他会合并和排序的预操作,从而大大提高性能,内核负责提交I/O请求的子系统称为I/O调度程序

I/O调度程序通过两种方法减少磁盘寻址时间:合并和排序。

  • 合并:将多个请求结合成一个新请求。将多次请求的开销压缩成一次,合并后只需要传递一条寻址指令给磁盘。
  • 排序:I/O调度算法【整理】Linux I/O调度

    1. Linus电梯
    当一个请求加入到队列中时,会依次执行:

  • 如果队列中已存在一个队相邻磁盘扇区操作的请求,那么新请求将和这个已经存在的请求合并成一个请求,包括:向前合并和向后合并
  • 如果队列中存在一个驻留时间过长的请求,那么新请求将被插入到队列尾部,以防请求饥饿
  • 如果队列中以扇区方向为序存在合适的插入位置,那么新请求将被插入到该位置,保证队列中请求是以被访问磁盘物理位置为序进行排列
  • 如果队列中不存在合适的请求插入位置,将请求插入队列尾部
  1. The Deadline I/O Scheduler
    为了解决Linus电梯的饥饿问题,writes-starving-reads问题
    文档
    block/deadline-iosched.c

配置I/O调度

1
2
3
4
5
6
7
8
$ echo 'deadline' > /sys/block/sda/queue/scheduler
$ cat /sys/block/sda/queue/scheduler
noop anticipatory [deadline] cfq
title Red Hat Enterprise Linux Server (2.6.9-67.EL)
root (hd0,0)
kernel /vmlinuz-2.6.9-67.EL ro root=/dev/vg0/lv0 elevator=deadline
initrd /initrd-2.6.9-67.EL.img

  1. The Anticipatory I/O Scheduler

https://lwn.net/Articles/21274/

  1. The Complete Fair Queuing I/O Scheduler
    注意与CFS的区别与联系,CFQ每个提交I/O的进程都有自己的队列。
    block/cfq-iosched.c
  2. The Noop I/O Scheduler
    专为随机访问设备设计。
    block/noop-iosched.c
  3. The Kyber I/O scheduler
    block/kyber-iosched.c
  4. Budget Fair Queueing (BFQ) scheduler

6. 小结

  • bio:表示活动的I/O操作
  • buffer_head:表示块到页的映射
  • request:表示具体I/O请求
  • I/O调度程序

相关资料
Budget Fair Queueing (BFQ) Storage-I/O Scheduler
Linux I/O Scheduler
http://blog.csdn.net/vanbreaker/article/details/8278358
https://lwn.net/Articles/601799/