Linux中page、buffer_head、bio的联系

本文深入探讨了Linux Block I/O层中的三大关键数据结构:page、buffer_head与bio。详细介绍了它们在文件系统与块设备层的作用及相互间的联系,并解释了如何通过这些数据结构确保数据的一致性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在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;
    ......
}
<think>好的,用户想了解内核级文件缓冲区的位置或实现细节。首先,我需要回忆一下Linux内核中文件缓冲的相关知识。记得文件缓冲通常与Page Cache有关,对吧?Page Cache是内核用来缓存磁盘数据的主要机制,通过将磁盘数据缓存在内存中,减少直接访问磁盘的次数,提高性能。 接下来,用户提到的“位置”可能指的是在内核代码中的具体实现位置。根据引用[1]中的信息,ramfs文件系统的注册函数是在fs/ramfs/inode.c中的init_ramfs_fs函数里调用了register_filesystem。这可能与文件系统的缓冲区管理相关,但需要进一步确认。不过ramfs本身是基于内存的文件系统,可能普通的磁盘文件系统的缓冲机制有所不同。 然后,考虑到文件缓冲区的实现,应该涉及struct address_space结构体,这个结构体管理文件的页面缓存。每个打开的文件都有一个关联的address_space,负责页面的缓存同步。另外,buffer_head结构体可能用于块设备的缓冲,但现代内核更多使用bio结构来处理块I/O操作。 关于代码位置,Page Cache的主要实现可能在mm/filemap.c中,包括读写操作的处理,比如generic_file_read_itergeneric_file_write_iter函数。这些函数处理文件的读写请求,并利用Page Cache进行数据的缓存。 另外,引用[3]提到NFS架构中的缓冲缓存可能导致本地远程视图的不一致,这说明内核在处理网络文件系统时,缓冲机制需要额外的同步措施。例如,NFS客户端可能需要与服务器同步缓存的数据,这涉及到缓存的失效更新机制,可能在fs/nfs目录下的相关代码中实现。 用户还可能关心如何查找这些实现的具体代码位置。例如,使用gdb调试内核时,可以在相关函数设置断点,如引用[1]中提到的init_ramfs_fs函数。但文件缓冲区的核心代码可能在mm目录下的文件,如filemap.c,以及特定文件系统的实现中。 另外,引用[2]提到cephfs客户端中的objectcacher,虽然这是用户态的实现,但内核中的Page Cache机制与之类似,都是通过缓存数据减少磁盘访问。因此,内核的文件缓冲区主要在Page Cache层实现,涉及address_space操作、页面管理、读写回写等机制。 总结一下,内核级文件缓冲区的位置主要在Page Cache,实现细节涉及struct page、address_space、读写函数等,相关代码位于mm/filemap.c及各个文件系统的具体实现中。需要结合内核代码中的具体函数结构体来详细说明。</think>内核级文件缓冲区主要通过 **Page Cache** 实现,其核心代码位于 Linux 内核的 **mm/filemap.c** **fs/buffer.c** 等文件中。以下是关键实现细节: --- ### 1. **核心数据结构** - **struct page**:表示物理内存页,用于缓存文件内容。每个页对应文件的一个块(通常4KB)。 - **struct address_space**:关联文件与缓存的页,通过 **page_tree**(基数树)管理缓存页。定义在 **include/linux/fs.h**。 - **struct buffer_head**:历史遗留结构,用于块设备缓冲区(现多被 **struct bio** 取代)。 --- ### 2. **缓存位置与生命周期** - **缓存位置**:文件数据缓存在物理内存中,通过 **页框分配器(Page Frame Allocator)** 管理。 - **缓存写入**:修改后的脏页(Dirty Page)由 **pdflush** 或 **kworker** 线程异步刷回磁盘[^3]。 - **缓存淘汰**:采用 **LRU算法** 或 **Clock-Pro算法**,代码见 **mm/vmscan.c**。 --- ### 3. **关键函数与流程** - **读流程**: ```c generic_file_read_iter() → do_generic_file_read() → find_get_page() // 查找缓存页 ``` 若未命中缓存,触发 **page fault** 并调用 **readpage()** 从磁盘加载数据[^3]。 - **写流程**: ```c generic_file_write_iter() → __generic_file_write_iter() → write_begin()/write_end() ``` 写入数据到缓存页并标记为脏页,延迟写回磁盘。 --- ### 4. **代码定位示例** - **Page Cache 核心函数**: - **add_to_page_cache()**(mm/filemap.c):将页加入缓存。 - **filemap_fault()**(mm/filemap.c):处理文件内存映射缺页。 - **缓冲区同步**: - **sync_filesystem()**(fs/sync.c):同步文件系统所有脏页。 - **writeback_inodes()**(fs/fs-writeback.c):回写脏页到磁盘[^1][^3]。 --- ### 5. **性能优化** - **预读机制**:通过 **readahead** 提前加载连续数据,见 **mm/readahead.c**。 - **写合并**:延迟写入并合并相邻 I/O 请求,减少磁盘操作。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值