Linux 页高速缓存 —— address_space
address_space 是一个文件在内存中的缓存视图,用于管理文件内容如何映射到内存页,每个打开的文件在内核中都有一个关联的 address_space。
1. 解决了“缓存对象单一”的问题:从“只为文件服务”到“为一切而服务”
早期的 System V Unix 引入页高速缓存时,它仅仅是文件系统数据的缓存,与 vnode(文件对象)深度绑定。这意味着,这种缓存机制天生就是为“文件”这一种对象设计的。
address_space 的解决之道
Linux 希望页高速缓存成为一个通用缓存层,能够缓存“任何基于页的对象”。这包括:
- 普通文件
- 块设备文件(如
/dev/sda1) - 内存映射(如共享库、匿名映射)
如何实现?
address_space 被设计为一个独立的、中立的“缓存总部”。它通过 host 指针(通常指向 inode)来关联一个具体对象,而不是成为那个对象的一部分。任何拥有“可按页寻址数据”概念的对象,都可以挂载一个 address_space 来利用页高速缓存。
目的:维持页高速缓存的普遍性,不将其绑定到具体文件或 inode。
小结:address_space 将页高速缓存从文件系统附属功能升级为内核的通用基础设施。
2. 解决了“索引方式低效”的问题:从“无法定位”到“快速查找”
内存以“页”为单位(如 4KB),磁盘以“块”为单位(如 512B)。一个页可包含多个,不连续的磁盘块。
如果用“设备名 + 块号”作为缓存键,会遇到:
问题一:一个页包含多个块,却只能有一个键
一个文件在磁盘上存储时,其数据可能分散在磁盘的不同位置(即不连续的块)。当我们把文件的一部分数据读入内存时,我们是以页为单位来读取的,但这一页数据对应的磁盘块可能是不连续的。 如果我们用“设备名+块号”作为键来索引缓存页,那么会遇到以下问题: 一个缓存页包含了多个块,这些块号可能不连续,那么我们该用哪个块号作为键?如果我们用这一页中第一个块的块号作为键,那么当我们要查找的数据位于这一页中的其他块时,我们无法直接通过这个块号找到该页,因为我们要查找的数据所在的块号可能并不是第一个块的块号。也就是说,一个缓存页对应多个块号,但键只能是一个,这就导致了用块号作为键时,只能通过其中一个块号找到该页,而其他块号则无法直接找到该页。
问题二:为每个块建缓存项导致“页被多次缓存”
如果我们为每一个块都建立一个缓存项,那么同一个页就会被缓存多次(因为一个页包含多个块),这显然是一种浪费。而且,同一个页被缓存多次,也会导致数据不一致的问题。
问题三:难以反查页包含的块
当我们有一个缓存页时,我们很难知道这个页缓存了哪些块。因为块号不连续,我们无法通过一个简单的计算得到这个页包含的所有块号。我们需要额外的数据结构来记录这个页包含的块号列表,这增加了复杂性和开销。
这些问题导致“按块号”查找缓存效率极低。
address_space 的解决方式:使用“文件逻辑页偏移量”作为索引键
将文件视为由固定大小的逻辑页组成,每个逻辑页有唯一的 page index(页偏移量)。
访问文件某位置时:
- 计算该位置属于哪一个逻辑页(页偏移量)。
- 在
address_space的 radix tree 中查找该页缓存。 - 命中 → 直接返回。
- 未命中 → 内核分配物理页、读入磁盘数据并加入缓存树。
这样:
- 不关心该页对应哪些物理磁盘块,也不关心是否连续
- 同一页唯一标识是 page index
- 查找效率高、逻辑简单
小结:address_space 引入“逻辑页”抽象,将“不连续的磁盘块问题”转化为“连续页索引问题”。
3. 解决了“物理与虚拟映射混乱”的问题:从“多份副本”到“一份缓存,多处映射”
核心矛盾:同一个文件可能被多个进程、以多种方式(如 read、mmap)同时访问。如果没有统一管理,每个映射可能在内存中持有自己独立的缓存副本,导致数据不一致和内存浪费。
address_space 的解决之道
address_space 是 物理缓存页的唯一管理者,对应一个文件仅有 一个。
但同一个文件可以被多个虚拟地址空间映射,即多个 vm_area_struct。它们都引用这同一份物理缓存页。
如何实现:
address_space 结构中的 i_mmap 字段(一个优先搜索树)记录了所有映射到这些物理页的虚拟内存区域。当数据被修改时: 所有通过不同虚拟地址访问此文件的进程,看到的都是同一份、最新的数据。 写回磁盘时,也只需要处理唯一的物理页副本。
4. 读写操作如何与 address_space 协作?
理解结构后,再看内核如何利用它进行读写。
读流程:
- 查找:内核收到读取文件某段数据的请求。它计算出对应的文件页偏移量,然后调用
find_get_page(address_space, 偏移量),在page_tree基数树中查找。 - 命中:如果页在缓存中,直接返回数据,完成高速访问。
- 未命中:如果页不在缓存中,内核会:
- 分配一个新的物理页 (
page_cache_alloc)。 - 将其插入到
address_space的基数树中 (add_to_page_cache_lru)。 - 调用
a_ops->readpage(),从磁盘读入数据到该页。 - 将数据返回给用户。
- 分配一个新的物理页 (
写流程:
- 内核同样会先在页高速缓存中寻找目标页。
- 将数据从用户空间拷贝到内核的缓存页中。
- 将页标记为“脏”(
SetPageDirty(page)),但通常不会立即写回磁盘。 - 在后台,内核会在适当时机(如周期性回写、内存压力等)调用
a_ops->writepage(),将脏页的内容写回磁盘。
留下评论