在Linux Block IO层,说到关键数据结构我想可能就只有标题中描述的三种了。我们今天就来详细描述这三种数据结构在文件系统和块设备层扮演的角色以及他们之间的联系。
PAGE
page在内核中被称为缓存页,在文件系统中扮演最核心的角色。Linux使用内存缓存文件数据,而所有的文件内容都被分割成page然后通过一定方式组织起来,便于查找。
page大小固定,当前一般为4KB。一个大文件的缓存可能会占据很多page。
BUFFER_HEAD
buffer_head顾名思义,表示缓冲区头部。这个缓冲区缓冲的是磁盘等块设备数据,而buffer_head则是描述缓冲区的元数据。
前面说过,内核以page为单位管理文件内容,page典型大小为4KB,而一般块设备的访问以扇区为单位,扇区的一般大小为512 byte,当然随着技术的进步,这个大小也在不断提升。而文件系统最小逻辑可寻址单元称为块。块的大小要比扇区大,但又比页小,典型大小为1K。 内核执行磁盘的所有操作是按照块来操作的。
在如此背景之下,便诞生了buffer_head这样的数据结构,它是内核page与磁盘上物理数据块之间的桥梁。一方面,每个page包含多个buffer_head(一般4个),另外一方面,buffer_head中又记录了底层设备块号信息。这样,通过page->buffer_head->block就能完成数据的读写。
page与buffer_head数据结构之间关系如下图所示:假设page大小为4KB,而文件系统块大小为1KB。
page通过private字段索引该page的第一个buffer_head,而所有的buffer_head通过b_this_page形成一个单循环链表;
buffer_head中的b_data指向缓存文件的块数据;
buffer_head内还通过b_page指向其所属的page(图中未画出)
由于buffer_head描述的是文件系统块缓存,既然缓存,便存在数据一致性问题:缓存中的数据可能比磁盘数据落后,缓存中的数据也可能比磁盘数据新。因此,需要一种机制来描述这些状态。buffer_head中需要一系列的状态位。
enum bh_state_bits {
BH_Uptodate,
BH_Dirty,
BH_Lock,
BH_Req,
BH_Uptodate_Lock,
BH_Mapped,
BH_New,
BH_Async_Read,
BH_Async_Write,
BH_Delay,
BH_Boundary,
BH_Write_EIO,
BH_Unwritten,
BH_Quiet,
BH_Meta,
BH_Prio,
BH_Defer_Completion,
BH_PrivateStart,
}
其中:
BH_Uptodate: 表示缓存数据与磁盘数据一致
BH_Dirty: 表示缓存数据被更新,有待同步至磁盘上。
这些状态何时会被更新呢?
以BH_Uptodate为例,当文件系统向块设备层发起一次读请求时,会注册一个完成时的回调函数end_buffer_read_sync,在IO层返回读取结果后,该函数内根据读取结果:
- 如果读取成功,则说明buffer_head缓冲区中的数据与磁盘上一致,设置BH_Update。
- 如果读取失败,说明buffer_head缓冲区的数据处于一种不一致状态,此时,需要清除buffer_head的BH_Update。
static void __end_buffer_read_notouch(struct buffer_head *bh, int uptodate)
{
if (uptodate) {
set_buffer_uptodate(bh);
} else {
clear_buffer_uptodate(bh);
}
unlock_buffer(bh);
}
void end_buffer_read_sync(struct
buffer_head *bh, int uptodate)
{
__end_buffer_read_notouch(bh, uptodate);
put_bh(bh);
}
BIO
在 Linux 2.6 版本以前,buffer_head 是 kernel 中非常重要的数据结构,它曾经是 kernel 中 I/O 的基本单位(现在已经是 bio 结构),它曾被用于为一个块映射一个页,它被用于描述磁盘块到物理页的映射关系,所有的 block I/O 操作也包含在 buffer_head 中。但是这样也会引起比较大的问题:buffer_head 结构过大(现在已经缩减了很多),用 buffer head 来操作 I/O 数据太复杂,kernel 更喜欢根据 page 来工作(这样性能也更好);另一个问题是一个大的 buffer_head 常被用来描述单独的 buffer,而且 buffer 还很可能比一个页还小,这样就会造成效率低下;第三个问题是 buffer_head 只能描述一个 buffer,这样大块的 I/O 操作常被分散为很多个 buffer_head,这样会增加额外占用的空间。因此 2.6 开始的 kernel (实际 2.5 测试版的 kernel 中已经开始引入)使用 bio 结构直接处理 page 和地址空间,而不是 buffer。
因此,当前的内核在向块设备层提交读写请求时,都会将buffer_head封装在bio结构中,而不再使用原来的buffer_head,例如下面这段代码是ext2文件系统向磁盘写数据的实现:
static int submit_bh_wbc(int rw, struct buffer_head *bh,
unsigned long bio_flags, struct writeback_control *wbc)
{
struct bio *bio;
if (test_set_buffer_req(bh) && (rw & WRITE))
clear_buffer_write_io_error(bh);
bio = bio_alloc(GFP_NOIO, 1);
if (wbc) {
wbc_init_bio(wbc, bio);
wbc_account_io(wbc, bh->b_page,
bh->b_size);
}
bio->bi_iter.bi_sector = bh-
>b_blocknr * (bh->b_size >> 9);
bio->bi_bdev = bh->b_bdev;
bio_add_page(bio, bh->b_page, bh-
>b_size, bh_offset(bh));
bio->bi_end_io = end_bio_bh_io_sync;
bio->bi_private = bh;
bio->bi_flags |= bio_flags;
......
}