关注了就能看到更多这么棒的文章哦~
A kernel without buffer heads
By Jonathan Corbet
May 1, 2023
DeepL assisted translation
https://lwn.net/Articles/930173/
在 Linux 内核中发现的所有数据结构(最起码从 Linus Torvalds 的开发机中成功毕业出来的代码里)都没有比 buffer head 结构更古老的了。就像 Linux 早期的许多其他遗留问题一样,buffer head 多年来一直都是人们想要删除的目标。不过,尽管它们带来了一些问题,但目前仍然活得好好的。现在,Christoph Hellwig 已经发布了一组 patch, 希望能够编译出一个没有 buffer head 的内核,但是目前来看要想实现这个目标所付出的代价将让大多数人不愿接受。
Linux 内核的第一个公开版本是 0.01 版,struct buffer_head 就是它的一部分:
struct buffer_head {
char * b_data; /* pointer to data block (1024 bytes) */
unsigned short b_dev; /* device (0 = free) */
unsigned short b_blocknr; /* block number */
unsigned char b_uptodate;
unsigned char b_dirt; /* 0-clean,1-dirty */
unsigned char b_count; /* users using this block */
unsigned char b_lock; /* 0 - ok, 1 -locked */
struct task_struct * b_wait;
struct buffer_head * b_prev;
struct buffer_head * b_next;
struct buffer_head * b_prev_free;
struct buffer_head * b_next_free;
};
虽然几十年前最好的磁盘驱动器当时已经被人们认为是很 "fast" 的了,但访问磁盘上的数据的速度仍然比访问主内存中的数据要慢几个数量级。因此,早在 Linux 诞生之前,人们就已经充分认识到需要对文件数据进行缓存操作的重要性了。当时普遍使用的方法是对磁盘块(disk block)进行缓存,文件系统代码对缓存中的数据进行操作;Torvalds 在 Linux 中沿用了这种模式。因此,从一开始,Linux 内核就包括了一个 "buffer cache",用来保存系统磁盘 block 的副本。
buffer_head 结构是管理 buffer cache 的关键结构。b_dev 和 b_blocknr 字段组合起来可以唯一地确定一个指定 buffer cache 条目所指向的 block,而 b_data 则指向缓存下来的数据本身。其他字段则用来跟踪该 block 内容是否需要被回写到磁盘上、它有多少用户等等信息。这是内核的 block I/O 子系统的核心部分,也是其内存管理代码的核心部分。
随着时间的推移,人们发现如果实现 file caching 的时候采用直接对 file 数据进行缓存(而不是按磁盘块缓存)来实现,那么效果会更好。在 1.3 开发周期中,Torvalds 开始实施一种新的功能,即 "page cache",它会管理来自文件中的数据 page,而不是来自磁盘 block 了。这一改动带来了许多好处;例如,如果能在 cache 中找到文件数据,那么许多对文件数据的操作就可以完全避免调用文件系统代码了。在更高层级上对数据进行缓存就可以更好地配合使用数据的场景了,而且能够对整个 page 进行缓存(比当时通常用的 512 字节 block size 要大 8 倍),提高了效率。
唯一的问题是,buffer cache 与 block 子系统和文件系统的实现都有很深的联系,所以这个 cache 还是一直存在着,与 page cache 共存了好几年,直到两者最终统一起来。哪怕在统一起来之后,buffer cache 也还是用于 block I/O 的 API 的核心部分。这并不是一个理想情况,因为文件系统致力于在磁盘上尽量连续地存放数据,而 page cache 可以在内存中以至少 page 这个粒度来将数据放在一起,但 buffer head 接口则需要将每个 I/O 操作分解成 512 字节的 block,每个 block 都有自己的 buffer head 结构。这个开销很大,尤其是增加了存储部分驱动程序的工作,它必须要尝试重新组装出更大的 block 以获得合理的 I/O 性能。
2.5 开发系列(旧的发布模式下的最后一个奇数版的开发内核)解决了这个问题,围绕一个新的数据结构 "bio" 重新设计了 block 层代码,可以更有效地表示 block I/O request。多年来,随着需要支持越来越高的 I/O 速率,bio 已经有了很大的进展,但它仍然是采用组装和管理 block I/O request 的方式来工作。
同时,尽管如此,在当前的内核中仍然可以找到 struct buffer_head。而且,更重要的是,一些文件系统仍然在使用它。buffer head 曾经在缓存管理中扮演的角色早已结束,但是它们仍然在内核的某些部分处理着一项重要的任务:维护管理缓存在内存中的数据和该数据所在的持久性存储(persistent storage)的位置之间的关联。内核有一个比较现代的接口(iomap)来实现这个目的,但不是所有的子系统都使用了它。
其中的一个例外是 ext4,它仍然大量使用了 buffer head。这个文件系统众所周知是从 ext2 派生出来的,ext2 是在 1993 年初的 0.99.7 版本中首次进入了内核。Ext2 是基于 block pointer 块指针的;每个文件都有一个与之相关的 list,包含磁盘上存放该文件数据的 block 的编号。这样的布局中,磁盘上的每一个 block 都是一个独立的实体(即使文件系统试图把它们关联在一起),非常符合 buffer head 模型。因此,buffer head 被深深嵌入到了 ext2 中,并且不出意料地在 30 年后的 ext4 中仍然存在,尽管 ext4 在 2006 年获得了对 extents 的支持,这是一种对大文件更高效的表示方法。
显然,buffer head 仍然可以正常使用,但它们还是增加了文件 I/O 的开销,也阻碍了开发者对内存管理和文件系统层的修改,包括正在进行的 folio 的工作。因此,很久以来人们一直希望摆脱 buffer head,并且这个愿望似乎越来越强烈了。
但是,正如 Hellwig 的这组 patch 所示,ext4 并不是唯一存在 buffer head 的模块。这组 patch 在经过一番重构后,增加了一个新的 BUFFER_HEAD 配置选项,用来控制 buffer head 支持是否编译进来。任何需要 buffer head 的代码都会 select 这个选项;如果在编译一个内核时其中并没有任何需要使用 buffer head 的代码,那么生成的内核就不会支持它了。这样的内核将缺少一些重要的功能,比如 ext4 文件系统,还有 F2FS、FAT、GFS2、HFS、ISO9660(CDROM)、JFS、NTFS、NTFS3 和 device-mapper layer 等。往好处想的话,还是可以构建一个支持 Btrfs 和 XFS 的不带有 buffer head 的内核的。
近期来看似乎还是不太可能会有很多不支持 buffer-head 的内核。然而,这项工作确实使我们更容易看到剩余的使用者都是哪些,这会有助于将工作重点放在真正摆脱 buffer head 上去。这项工作仍然可能需要一些时间,毕竟人们不会在一个大量使用的文件系统上匆忙地做大手术,而且它可能会加速一些旧的、不受欢迎的文件系统(如 JFS)被移除的过程。不过今后几年,将有可能可以抛弃这个从 kernel 一开始就存在的核心内核数据结构了。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~