Linux内核设计与实现(14)第十四章:块I/O层

1. 字符设备和块设备

I/O设备主要有2类:字符设备和块设备

1.1 字符设备

以字符为单位发送和接收数据的,只能顺序读写设备中的内容。
字符设备按照字节流的方式被有序访问。

	比如:串口设备,键盘,打印机、路由器、网关

1.2 块设备

能够随机读写设备中的内容,比如 硬盘,U盘
块设备访问的固定大小的数据片就称为块

1.3 字符设备和块设备的区别

1.是否可以被随机访问

2.内核管理块设备比管理字符设备复杂得多,因为字符设备仅仅需要控制一个位置,即当前位置;
而块设备访问的位置必须能够在介质的不同区间前后移动。

2. 块和扇区

2.1 块

块设备访问的固定大小的数据片就称为块

2.2 扇区

块设备中最小的可寻址单元为扇区
扇区的大小是2的整数倍,一般是 512字节。

2.3 块和扇区:

文件系统按块进行访问,块是较高层次的抽象
扇区是物理上的最小寻址单元,而逻辑上的最小寻址单元是块
块包含一个或多个扇区,但大小不超过一页
物理磁盘按扇区寻址,但是内核执行的所有磁盘操作都是按照块进行的
块调入内存时,需要先加载到缓冲区,每个缓冲区与一个块对应

2.4 查看扇区和块大小:fdisk

(centos7 虚机)

[root@localhost home]#fdisk -l

Disk /dev/sda: 21.5 GB, 21474836480 bytes, 41943040 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes	// 1. Sector size 就是扇区的值 512
I/O size (minimum/optimal): 512 bytes / 512 bytes		// 2. I/O size 就是 块的值 512
Disk label type: dos
……
[root@localhost home]#

3. 内核访问块设备的方法

3.1 块和内存页

内核通过文件系统访问块设备时,需要先把块读入到内存中。
所以文件系统为了管理块设备,必须管理块和内存页之间的映射。
在读入后或等待写出时,一个块会被调入内存,并存储在一个缓冲区中,每个缓冲区与一个块对应,它相当于是磁盘块在内存中的表示。

3.2 内核中有2种方法来管理块和内存页之间的映射。

缓冲区和缓冲区头
bio

3.3 缓冲区和缓冲区头

缓冲区(buffer),它是内存空间的一部分。
也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。

缓冲区和缓存
https://blog.csdn.net/lqy971966/article/details/104497600

3.3.1 缓冲区头 buffer_head

每个缓冲区都有一个对应的描述符,用buffer_head结构体表示,称作缓冲区头

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;                    /* 引用计数 */
};

3.3.2 缓冲区头作用:

描述磁盘块和物理内存缓冲区之间的映射关系。

3.3.3 缓冲区和缓冲区头的缺点

在2.6之前的内核中,主要就是通过缓冲区头来管理 块 和内存之间的映射的。
用缓冲区头来管理内核的 I/O 操作主要存在以下2个弊端。

1.效率低下

	对内核而言,操作内存页是最为简便和高效的。
	所以如果通过缓冲区头来操作的话,效率低下。
	因为:缓冲区 即块在内存中映射,可能比页面要小

2.负担和空间浪费

	每个缓冲区头只能表示一个块,所以内核在处理大数据时,
	会分解为对一个个小的块的操作,造成不必要的负担和空间浪费。

所以在2.6开始的内核中,缓冲区头的作用大大降低了。

3.4 bio

内核中块I/O操作的基本容器由bio结构体表示,该结构体代表了正在现场的(活动的)以片段链表形式组织的块I/O操作。

3.4.1 bio 背景:

bio 结构体的出现就是为了改善上面缓冲区头的2个弊端,它表示了一次 I/O 操作所涉及到的所有内存页。

struct bio {
	bio_vec //结构体表示 I/O 操作使用的片段

3.4.2 bio 优点:

bio结构体很容易处理高端内存,因为它处理的是内存页而不是直接指针
bio结构体既可以代表普通页I/O,也可以代表直接I/O
bio结构体便于执行分散-集中(矢量化的)块I/O操作,操作中的数据可以取自多个物理页面

3.4.3 bio 图

在这里插入图片描述

3.5 缓冲区头和bio的比较

1. 缓冲区头和bio并不是相互矛盾的。
	bio只是缓冲区头的一种改善,将以前缓冲区头完成的一部分工作移到bio中来完成。

2. bio中对应的是内存中的一个个页,而缓冲区头对应的是磁盘中的一个块。

3. 对内核来说,配合使用bio和缓冲区头比只使用缓冲区头更加的方便高效。

4. bio相当于在缓冲区上又封装了一层。
	使得内核在 I/O操作时只要针对一个或多个内存页即可,不用再去管理磁盘块的部分。

4. 内核I/O调度程序

4.1 背景:

缓冲区头和bio都是内核处理一个具体I/O操作时涉及的概念。
但是内核除了要完成I/O操作以外,还要调度好所有I/O操作请求,
尽量确保每个请求能有个合理的响应时间。

4.1.1 请求队列

块设备将它们挂起的I/O请求保存在请求队列中,该队列由 request_queue 结构体表示。
内核中的高层代码,如文件系统将请求加入到队列中,只要队列不为空,队列对应的块设备驱动程序就会从队列头获取请求,然后将其送入对应的块设备上去。
队列中的请求由结构体request表示,因为一个请求可能要操作多个连续的磁盘块,所以每个请求可以由多个bio结构体组成。

4.2 定义:

在内核中负责提交I/O请求的子系统称为I/O调度程序。
内核不会简单地以产生请求的次序直接将请求发向块设备,这样性能太差。

4.3 功能:

1. 内核不会简单地以产生请求的次序直接将请求发向块设备,这样性能太差。
	为了优化寻址操作,内核在提交请求前会先执行合并、排序等预操作:
	将请求队列中挂起的请求合并、排序。
	
	合并:指将两个或多个请求结合成一个新请求
	排序:则是一种类似电梯调度的算法
	
		
2. I/O调度程序的工作就是管理块设备的请求队列,
	它决定队列中的请求排序顺序以及在什么时刻派发请求到块设备
	以此减少磁盘寻址时间,从而提高全局吞吐量。

实际的磁盘调度算法有多种,如Linus电梯、最终期限I/O、预测I/O调度等,略

4.4 (1): linus 电梯

4.4.1 背景:

为了保证磁盘寻址的效率,一般会尽量让磁头向一个方向移动,等到头了再反过来移动,这样可以缩短所有请求的磁盘寻址总时间。
主要考虑了系统的全局吞吐量。

4.4.2 定义:

磁头的移动有点类似于电梯,所有这个 I/O 调度算法也叫电梯调度。
第一个电梯调度算法就是 linus本人所写的,所有也叫做 linus 电梯

4.4.3 功能/过程:

linus电梯调度主要是对I/O请求进行合并和排序。

当一个新请求加入I/O请求队列时,可能会发生以下4种操作:

1. 如果队列中已存在一个对相邻磁盘扇区操作的请求,那么新请求将和这个已存在的请求合并成一个请求
2. 如果队列中存在一个驻留时间过长的请求,那么新请求之间查到队列尾部,防止旧的请求发生饥饿
3. 如果队列中以扇区方向为序存在合适的插入位置,那么新请求将被插入该位置,
	保证队列中的请求是以被访问磁盘物理位置为序进行排列的。
4. 如果队列中不存在合适的请求插入位置,请求将被插入到队列尾部

4.4.4 linus 电梯已经被废弃:

linus电梯调度程序在2.6版的内核中被其他调度程序所取代了。

4.5 (2):最终期限I/O调度 deadline

4.5.1 背景:

linus电梯调度主要考虑了系统的全局吞吐量,对于个别的I/O请求,还是有可能造成饥饿现象。
读请求一般和提交它的应用程序时同步执行,应用程序等获取到读的数据后才会接着往下执行。

因此在 linus 电梯调度程序中,还可能造成 写-饥饿-读(wirtes-starving-reads)这种特殊问题。

4.5.2 最终期限I/O调度:

为了尽量公平的对待所有请求,同时尽量保证读请求的响应时间,提出了最终期限I/O调度算法。
最终期限I/O调度算法给每个请求设置了超时时间,默认情况下,读请求的超时时间500ms,写请求的超时时间是5s

4.5.3 优点:

最终期限I/O调度算法也不能严格保证响应时间,但是它可以保证不会发生请求在明显超时的情况下仍得不到执行。

4.6 (3):预测I/O调度 as(Linux内核缺省调度)

4.6.1 背景:

最终期限I/O调度算法优先考虑读请求的响应时间,但系统处于写操作繁重的状态时,会大大降低系统的吞吐量。
因为读请求的超时时间比较短,所以每次有读请求时,都会打断写请求,让磁盘寻址到读的位置,完成读操作后再回来继续写。
这种做法保证读请求的响应速度,却损害了系统的全局吞吐量(磁头先去读再回来写,发生了2次寻址操作)

4.6.2 预测I/O调度:

预测I/O调度算法是为了解决上述问题而提出的,它是基于最终期限I/O调度算法的。
预测I/O调度算法中最重要的是保证等待期间不要浪费,也就是提高预测的准确性,

目前这种预测是依靠一系列的启发和统计工作,预测I/O调度程序会跟踪并统计每个应用程序的I/O操作习惯,以便正确预测应用程序的读写行为。
如果预测的准确率足够高,那么预测I/O调度和最终期限I/O调度相比,既能提高读请求的响应时间,又能提高系统吞吐量。

注: 预测I/O调度是linux内核中缺省的调度程序

4.7 (4):完全公正的排队I/O调度 cfq

4.7.1 完全公正的排队I/O调度:

完全公正的排队(Complete Fair Queuing, CFQ)I/O调度:是为专有工作负荷设计的,它和之前提到的I/O调度有根本的不同。

CFQ I/O调度 算法中,每个进程都有自己的I/O队列,
CFQ I/O调度程序以时间片轮转调度队列,从每个队列中选取一定的请求数(默认4个),然后进行下一轮调度。

CFQ I/O调度在进程级提供了公平

4.8 (5):空操作的I/O调度 noop

空操作(noop)I/O调度几乎不做什么事情,这也是它这样命名的原因。
空操作I/O调度只做一件事情,当有新的请求到来时,把它与任一相邻的请求合并。

4.8.1 优点:

空操作I/O调度主要用于闪存卡之类的块设备,这类设备没有磁头,没有寻址的负担

猜你喜欢

转载自blog.csdn.net/lqy971966/article/details/119804495
今日推荐