块IO层

Linux中设备分为字符设备,块设备,网络设备三大类。对于字符设备与块设备区别在于能否随机访问数据,对于字符设备键盘来说,键盘驱动程序会按照和用户输入完全相同的顺序读取字符,并且得到一个字符流。但是对于块设备硬盘来说,硬盘驱动程序可能需要读取磁盘上任意块的内容,这些块不一定是连续的,所以说可以被随机访问。

所以事实上内核不必提供一个专门的子系统来管理字符设备,但是对块设备的管理却必须要有一个专门的提供服务的子系统。不仅仅是因为块设备的复杂性远远高于字符设备,更重要的原因是块设备对执行性能的要求很高﹔对硬盘每多一份利用都
会对整个系统的性能带来提升。另外,我们将会看到,块设备的复杂性会为这种优化留下很大的施展空间。对块设备和块设备的请求进行管理,在内核中称作块IO层。

块设备

块设备中最小的可寻址单元为扇区,扇区最常见的大小为512字节。块是文件系统的一种抽象–只能基于块来访问文件系统。虽然物理磁盘寻址是按照扇区级进行的,但是内核执行的所有磁盘操作都是按照块进行的。
内核(对有扇区的硬件设备)要求块大小是2的整数倍,而且不能超过一个页的长度。所以,对块大小的最终要求是,必须是扇区大小的2的整数倍,并且要小于页面大小。所以通常块大小是512KB、1KB或4KB。

块设备基本操作单元

缓冲区和缓冲区头

高速缓存这一层分为页缓存(page cache)和缓冲区(buffer cache),buffer cache是建立在page cache之上的。page cache是面向文件的抽象,而buffer cache则是面向块设备的抽象。

当一个块被调入内存时(也就是说,在读入后或等待写出时),它要存储在一个缓冲区中。每个缓冲区与一个块对应,它相当于是磁盘块在内存中的表示。前面提到过,块包含一个或多个扇区,但大小不能超过一个页面,所以一个页可以容纳一个或多个内存中的块。由于内核处理数据时需要一些相关的控制信息(比如块属于哪一个块设备,块对应于哪个缓冲区等),所以每一个缓冲区都有一个对应的描述符。该描述符用buffer_head结构体表示,称作缓冲区头,在文件<linux/buffer_head.h>(基于内核4.1.14)中定义,它包含了内核操作缓冲区所需要的全部信息。

随着时代的发展,访问单独磁盘块的场景越来越少,页缓存(page cache)慢慢占了主要地位,在Linux kernel 2.4以后,缓冲区(buffer cache)已经不单独实现了,而是建立在页缓存(page cache)的基础上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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; /* 块大小 */
char *b_data; /* 块起始位置,位于b_page中所指明的页面的某个位置上 */

struct block_device *b_bdev; /* 表示该请求指向哪个块设备。*/
bh_end_io_t *b_end_io; /* I/O 完成方法 */
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; /* 该缓冲区使用计数 */
};

b_count域表示缓冲区的使用记数,可通过两个定义在文件<linux/buffer_head.h>中的内联
函数对此域进行增减。

1
2
3
4
5
6
7
8
9
10
static inline void get_bh(struct buffer_head *bh)
{
atomic_inc(&bh->b_count);
}

static inline void put_bh(struct buffer_head *bh)
{
smp_mb__before_atomic();
atomic_dec(&bh->b_count);
}

在操作缓冲区头之前,应该先使用get_bh()函数增加缓冲区头的引用计数,确保该缓冲区头
不会再被分配出去;当完成对缓冲区头的操作之后,还必须使用put_bh()函数减少引用计数。

与缓冲区对应的磁盘物理块由b__blocknr-th域索引,该值是b__bdev域指明的块设备中的逻
辑块号。
与缓冲区对应的内存物理页由b_ page 域表示,另外,b_data域直接指向相应的块(它位于
b_ page域所指明的页面中的某个位置上),块的大小由b_size 域表示,所以块在内存中的起始位
置在b_data处,结束位置在(b_data + b_size)处。

在2.6内核以前,将缓冲区作为I/O操作单元带来了两个弊端。

  1. 缓冲区头是一个很大且不易控制的数据结构体(现在是缩减过的了),而且缓冲区头对数据的操作既不方便也不清晰。对内核来说,它更倾向于操作页面结构,因为页面操作起来更为简便,同时效率也高。
  2. 缓冲区头仅能描述单个缓冲区,当作为所有I/O的容器使用时,缓冲区头会促使内核把对大块数据的I/O操作(比如写操作)分解为对多个buffer_head结构体进行操作。这样做必然会造成不必要的负担和空间浪费。所以2.5开发版内核的主要目标就是为块I/O操作引入一种新型、灵活并且轻量级的容器,bio结构体

bio结构体

目前内核中块I/O操作的基本容器由bio结构体表示,它定义在文件<linux/blk_types.h>中。该结构体代表了正在现场的(活动的)以片断(segment)链表形式组织的块I/O操作,对缓冲区进行了封装,使用一个结构体数组存储多个缓冲区片段。结构体数组的一个元素是一小块连续的内存缓冲区,整个结构体数组表示了一个完整的缓冲区。这样的话,就不需要保证单个缓冲区一定要连续。 所以通过用片段来描述缓冲区,即使一个缓冲区分散在内存的多个位置上,bio结构体也能对内核保证I/O操作的执行。

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
66
67
68
69
70
struct bio {
struct bio *bi_next; /* request queue link */
struct gendisk *bi_disk;
unsigned int bi_opf; /* bottom bits req flags,
* top bits REQ_OP. Use
* accessors.
*/
unsigned short bi_flags; /* status, etc and bvec pool number */
unsigned short bi_ioprio;
unsigned short bi_write_hint;
blk_status_t bi_status;
u8 bi_partno;

/* Number of segments in this BIO after
* physical address coalescing is performed.
*/
unsigned int bi_phys_segments;

/*
* 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;

struct bvec_iter bi_iter;

atomic_t __bi_remaining;
bio_end_io_t *bi_end_io;

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;
#ifdef CONFIG_BLK_DEV_THROTTLING_LOW
void *bi_cg_private;
struct blk_issue_stat bi_issue_stat;
#endif
#endif
union {
#if defined(CONFIG_BLK_DEV_INTEGRITY)
struct bio_integrity_payload *bi_integrity; /* data integrity */
#endif
};

unsigned short bi_vcnt; /* how many bio_vec's 代表内存段的数目。*/

/*
* Everything starting with bi_max_vecs will be preserved by bio_reset()
*/

unsigned short bi_max_vecs; /* max bvl_vecs we can hold */

atomic_t __bi_cnt; /* pin count */

struct bio_vec *bi_io_vec; /* 记录了高速缓存层要提交给磁盘的数据。一个bio_io_vec可看作一个连续的内存段。 */

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结构体的目的主要是代表正在现场执行的I/O操作,所以该结构体中的主要城都是用来管理相关信息的,其中最重要的几个域是bi_io_vecs、bi_vcnt。 下图显示了bio结构体及其他结构体之间的关系。

I/O向量

bi_io_vec指针指向一个 bio_vec 结构体数组,该结构体链表包含了一个特定IO操作所需要使用到的所有片段。每个bio_vec结构都是一个形式为<page,offset,len>的向量,它描述的是一个特定的片段:片段所在的物理页、块在物理页中的偏移位置、从给定偏移量开始的块长度。整个bio_io_vec结构体数组表示了一个完整的缓冲区。bio_vec结构定义在<linux/bvec.h> 文件中:

1
2
3
4
5
6
7
8
struct bio_vec {
/*指向该缓冲区片段所在的页面*/
struct page *bv_page;
/* 缓冲区片段以字节为单位的长度*/
unsigned int bv_len;
/*该缓冲区片段在物理页面的偏移位置*/
unsigned int bv_offset;
};

总而言之,每一个块I0请求都通过一个bio结构体表示。每个请求包含一个或多个块,这些块存储在bio_vec 结构体数组中。这些结构体描述了每个片段在物理页中的实际位置,并且像向量一样被组织在一起。I/O 操作的第一个片段由b_io_vec结构体所指,其他的片段在其后依次放置,共有bi_vcnt个片段。当块I/O层开始执行请求、需要使用各个片段时,bi jidx 域会不断更新,从而总指向当前片段。

bi_cnt 域记录bio结构体的使用计数,如果该域值减为0,就应该撤销该bio结构体,并释放它占用的内存。通过下面两个函数管理使用计数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static inline void bio_get(struct bio *bio)
{
bio->bi_flags |= (1 << BIO_REFFED);
smp_mb__before_atomic();
atomic_inc(&bio->__bi_cnt);
}

static inline void bio_cnt_set(struct bio *bio, unsigned int count)
{
if (count != 1) {
bio->bi_flags |= (1 << BIO_REFFED);
smp_mb();
}
atomic_set(&bio->__bi_cnt, count);
}

前者增加使用计数,后者减少使用计数(如果计数减到0,则撤销bio结构体)。在操作正在活动的bio结构体时,-定要首先增加它的使用计数,以免在操作过程中该bio结构体被释放;相反在操作完毕后,要减少使用计数。

请求队列

块设备将它们挂起的块I/O请求保存在请求队列中,该队列由reques_queue结构体表示,定义在文件<linux/blkdev.h>中,包含一个双向请求链表以及相关控制信息。通过内核中像文件系统这样高层的代码将请求加入到队列中。请求队列只要不为空,队列对应的块设备驱动程序就会从队列头获取请求,然后将其送入对应的块设备上去。请求队列表中的每一项都是一个单独的请求,由reques结构体表示。

队列中的请求由结构体requcst 表示,它定义在文件<linux/blkdev.h>中。因为一个请求可能要操作多个连续的磁盘块,所以每个请求可以由多个bio结构体组成。注意,虽然在磁盘上的块必须连续,但是在内存中这些块并不一定要连续,每一个bio结构体都可以描述多个片段(回忆一下,片段是内存中连续的小区域),而每个请求也可以包含多个bio结构体。

I/O调度程序

I/O调度程序虚拟块设备给多个磁盘请求,以便降低磁盘寻址时间,确保磁盘性能的最优化。

PS:前往公众号“Linux工坊”,可以查看最新内容!关注即可免费领取面试&Linux技术书籍!

------ 本文结束------
  • 文章标题: 块IO层
  • 本文作者: 你是我的阳光
  • 发布时间: 2020年08月06日 - 14:47:42
  • 最后更新: 2022年11月07日 - 16:45:00
  • 本文链接: https://szp2016.github.io/Linux/块IO层/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
0%