华为的鸿蒙到底有多牛?为什么称作遥遥领先?
2026/6/18 23:41:45
title: buffer
categories:
https://github.com/wdfk-prog/linux-study
fs/buffer.c及其实现的**缓冲区缓存(Buffer Cache)**是Linux/Unix系统中最古老、最核心的性能优化机制之一。它最初是为了解决一个根本性的问题:物理磁盘I/O操作极其缓慢。
缓冲区缓存通过在物理内存(RAM)中缓存磁盘块的内容来解决这个问题。当内核需要读取一个磁盘块时,它首先检查该块是否已经在缓存中。如果在,就直接从内存中读取,避免了昂贵的物理I/O。同样,写操作可以先写入缓存(标记为“脏”),然后由内核在稍后的“最佳”时机批量写回磁盘。
缓冲区缓存的发展史是Linux I/O栈演进的核心部分。
mmap),内核引入了页缓存(Page Cache),它以内存页(Page,通常为4KB)为单位来缓存文件内容。这导致了一段时间内,文件数据可能同时存在于页缓存和缓冲区缓存中,造成了所谓的“双重缓存”(Double Caching)问题,浪费了内存。struct buffer_head)被重新定位,主要扮演两个角色:buffer_head演变为页缓存中一个页的描述符。一个内存页可以包含多个磁盘块,因此一个struct page可以关联一组struct buffer_head,每个buffer_head精确描述了页内某个块的状态(如是否脏、是否已映射到磁盘等)。这消除了内存浪费,同时保留了buffer_head对块级状态管理的优势。fs/buffer.c是Linux内核存储子系统中极其稳定和基础的部分。它不是一个经常出现新功能特性的领域,但其代码的正确性、稳定性和性能对整个系统至关重要,因此一直在被积极地维护和优化。它是所有块设备I/O操作的必经之路,被所有传统文件系统(如ext4, XFS, Btrfs)广泛用于元数据管理,同时也是mkfs、fsck等工具访问裸设备的基础。
fs/buffer.c的核心是围绕struct buffer_head数据结构和相关的哈希表进行管理。
struct buffer_head):这是缓冲区缓存的基本单元。它代表了一个物理磁盘块在内存中的映像。其关键字段包括:b_data):指向缓存了该块内容的物理内存地址(通常位于某个页缓存页内)。b_state):用位图(bitflags)表示块的当前状态,如BH_Uptodate(数据有效且与磁盘同步)、BH_Dirty(数据已被修改,需写回磁盘)、BH_Lock(正在进行I/O,被锁定)、BH_Mapped(已映射到磁盘上的一个有效块)。sb_bread()),内核会:buffer_head。BH_Uptodate),内核会锁定该buffer_head并直接返回其数据指针。buffer_head,将其与一个页缓存中的页关联起来,然后向块设备层提交一个读请求。I/O完成后,内核用从磁盘读回的数据填充内存,并将buffer_head标记为BH_Uptodate,最后返回给请求者。mark_buffer_dirty()。这个函数仅仅是在对应的buffer_head中设置BH_Dirty标志位,并不会立即写盘。这种机制被称为写回缓存(Write-back Cache)。真正的写盘操作由内核的后台回写(flusher)线程在稍后(如内存压力大时、周期性同步或用户调用sync()时)批量执行,这样可以将多次小的、随机的写入合并成一次大的、顺序的写入,提高效率。buffer_head结构体,当缓存大量小文件时,这部分元数据开销可能比较可观。buffer_head和缓冲区缓存是以下场景中不可或缺的标准解决方案:
sb_bread,sb_getblk)来操作这些元数据块。mkfs,fdisk,dd)打开一个设备文件(如/dev/sda1)并进行读写时,这些I/O请求会直接通过缓冲区缓存进行处理。buffer_head来管理日志缓冲区和提交事务。read_mapping_page)。虽然底层仍然会使用buffer_head作为描述符,但上层接口由页缓存统一管理,这样可以更好地利用预读、内存管理等高级特性。O_DIRECT标志打开文件,彻底绕过缓冲区缓存和页缓存。| 特性 | Buffer Cache (fs/buffer.c) | Page Cache (mm/filemap.c) | Direct I/O (O_DIRECT) |
|---|---|---|---|
| 缓存单元 | 块 (Block):大小可变,与文件系统块大小一致。 | 页 (Page):大小固定,与CPU内存页大小一致 (通常4KB)。 | 无缓存:直接在用户空间缓冲区和磁盘之间传输数据。 |
| 主要内容 | 文件系统元数据、原始块设备数据。 | 文件数据、可执行文件代码。 | 不缓存任何内容。 |
| 与硬件关系 | 面向块设备的抽象。 | 面向内存管理和文件对象的抽象。 | 直接与块设备驱动交互,绕过通用块层的大部分缓存逻辑。 |
| 数据一致性 | 通过BH_Dirty标志管理,由内核回写线程负责同步。 | 通过页的Dirty标志管理,与buffer_head的脏标志联动。 | 应用程序需要自己保证数据一致性(例如,通过fsync来同步元数据)。 |
| 性能特点 | 对元数据和重复的块访问有极高的加速效果。 | 对文件读写、内存映射有极高的加速效果,支持预读等优化。 | 避免了内核缓存的内存拷贝和CPU开销,对大型顺序I/O或自缓存应用有利。 |
| 使用接口 | 内核接口:sb_bread(),mark_buffer_dirty()等。 | 内核接口:read_mapping_page()用户接口: read(),write(),mmap() | 用户接口:open()时指定O_DIRECT标志。 |
| 总结 | I/O栈的底层缓存,负责块级的具体实现。 | I/O栈的高层缓存,负责文件级的抽象和优化。 | 一种“旁路”机制,用于特定高性能场景。 |
// 定义缓冲区高速缓存的初始化函数。// __init 宏表示该函数仅在内核初始化阶段执行,执行完毕后其占用的内存会被释放。void__initbuffer_init(void){unsignedlongnrpages;// 用于存储页数的变量intret;// 用于存储函数调用的返回值// 使用 KMEM_CACHE 宏为 buffer_head 结构体创建一个 SLAB 缓存池,并将其句柄存入全局变量 bh_cachep。// SLAB 是内核中一种高效的对象缓存机制。// 参数说明:// buffer_head: 指定这个缓存池中存放的对象类型是 struct buffer_head。// SLAB_RECLAIM_ACCOUNT: 标志位,指示该缓存池占用的内存是可回收的。当系统内存紧张时,内核会尝试收缩这个缓存池。// SLAB_PANIC: 标志位,如果缓存池创建失败,则会引发内核恐慌(panic)。这表明该缓存池对系统运行至关重要。bh_cachep=KMEM_CACHE(buffer_head,SLAB_RECLAIM_ACCOUNT|SLAB_PANIC);/* * 将 buffer_head 的内存占用限制在 ZONE_NORMAL 区域的10%。 * ZONE_NORMAL 是常规内存区域。 */// 调用 nr_free_buffer_pages() 获取可用于缓冲区的物理内存页总数,然后计算其10%的值。// 这样做是为了给 buffer_head 结构体的总大小设定一个合理的上限,防止其元数据消耗过多内存。nrpages=(nr_free_buffer_pages()*10)/100;// 计算允许存在的 buffer_head 结构体的最大数量。// (PAGE_SIZE / sizeof(struct buffer_head)) 计算出一页内存可以容纳多少个 buffer_head。// 再乘以之前计算出的页数(nrpages),得到最终的上限值,并存入全局变量 max_buffer_heads。max_buffer_heads=nrpages*(PAGE_SIZE/sizeof(structbuffer_head));// 向CPU热插拔(CPUHP)子系统注册一个状态回调。// 这用于在CPU被移除时,执行一些清理工作。在不支持热插拔的系统上(如STM32),此回调永远不会被触发。// 参数说明:// CPUHP_FS_BUFF_DEAD: 指定回调函数触发的时机,即在文件系统之后、CPU完全死亡之前的某个阶段。// "fs/buffer:dead": 为这个回调状态起一个易于调试的名称。// NULL: 安装回调,这里没有安装时需要执行的函数。// buffer_exit_cpu_dead: 卸载回调,当CPU下线时,会调用此函数来清理与该CPU相关的缓冲区资源。ret=cpuhp_setup_state_nocalls(CPUHP_FS_BUFF_DEAD,"fs/buffer:dead",NULL,buffer_exit_cpu_dead);// 检查CPU热插拔回调的注册是否成功。// 如果 ret 小于0(表示出错),WARN_ON 会在内核日志中打印一个警告信息和堆栈跟踪。// 它不会使内核恐慌,因为即使注册失败,系统在大多数情况下仍可继续运行。WARN_ON(ret<0);}