在本章中,我们将介绍 Linux 2.4 的页面缓存。顾名思义,页面缓存是物理页面的缓存。在 UNIX 世界中,页面缓存的概念随着 SVR4 UNIX 的引入而普及,它在数据 I/O 操作中取代了缓冲区缓存。
SVR4 页面缓存仅用于文件系统数据缓存,因此使用 `struct vnode` 和文件偏移量作为哈希参数。相比之下,Linux 页面缓存被设计得更加通用,因此使用 `struct address_space`(如下所述)作为第一个参数。由于 Linux 页面缓存与地址空间的概念紧密耦合,因此您至少需要基本了解地址空间才能理解页面缓存的工作方式。地址空间是某种软件 MMU,它将一个对象(例如 inode)的所有页面映射到另一个并发单元(通常是物理磁盘块)。`struct address_space` 的定义在 include/linux/fs.h
中,如下所示:
struct address_space {
struct list_head clean_pages;
struct list_head dirty_pages;
struct list_head locked_pages;
unsigned long nrpages;
struct address_space_operations *a_ops;
struct inode *host;
struct vm_area_struct *i_mmap;
struct vm_area_struct *i_mmap_shared;
spinlock_t i_shared_lock;
};
为了理解地址空间的工作方式,我们只需要关注其中的几个字段:`clean_pages`、`dirty_pages` 和 `locked_pages` 是双向链表,分别链接着属于该地址空间的所有干净页、脏页和锁定页;`nrpages` 是该地址空间中页面的总数。`a_ops` 定义了该对象的方法,`host` 是指向该地址空间所属 inode 的指针——它也可能为 NULL,例如在交换器地址空间的情况下(mm/swap_state.c
)。
`clean_pages`、`dirty_pages`、`locked_pages` 和 `nrpages` 的用途显而易见,因此我们将更仔细地查看 `address_space_operations` 结构体,它也在同一个头文件中定义:
struct address_space_operations {
int (*writepage)(struct page *);
int (*readpage)(struct file *, struct page *);
int (*sync_page)(struct page *);
int (*prepare_write)(struct file *, struct page *, unsigned, unsigned);
int (*commit_write)(struct file *, struct page *, unsigned, unsigned);
int (*bmap)(struct address_space *, long);
};
为了基本了解地址空间(和页面缓存)的原理,我们需要查看 ->`writepage` 和 ->`readpage`,但在实践中,我们也需要查看 ->`prepare_write` 和 ->`commit_write`。
您可能仅凭名称就能猜到 `address_space_operations` 方法的作用;尽管如此,它们确实需要一些解释。它们在文件系统数据 I/O 中的使用,是页面缓存最常见的路径,为理解它们提供了一个很好的方法。与大多数其他类 UNIX 操作系统不同,Linux 具有通用的文件操作(SYSVish vnode 操作的子集),用于通过页面缓存进行数据 I/O。这意味着数据不会在读/写/mmap 时直接与文件系统交互,而是在可能的情况下从/向页面缓存读取/写入。如果用户想要读取尚未在内存中的页面,或者在内存不足时将数据写入磁盘,页面缓存必须从实际的底层文件系统中获取数据。
在读取路径中,通用方法将首先尝试查找与所需 inode/索引元组匹配的页面。
hash = page_hash(inode->i_mapping, index);
然后我们测试页面是否实际存在。
hash = page_hash(inode->i_mapping, index); page = __find_page_nolock(inode->i_mapping, index, *hash);
当页面不存在时,我们分配一个新的空闲页面,并将其添加到页面缓存哈希表中。
page = page_cache_alloc(); __add_to_page_cache(page, mapping, index, hash);
页面哈希后,我们使用 ->`readpage` 地址空间操作来实际填充页面数据。(file 是 inode 的打开实例)。
error = mapping->a_ops->readpage(file, page);
最后,我们可以将数据复制到用户空间。
对于写入文件系统,存在两条路径:一条用于可写映射 (mmap),另一条用于 write(2) 系列系统调用。mmap 的情况非常简单,因此将首先讨论它。当用户修改映射时,VM 子系统会将页面标记为脏页。
SetPageDirty(page);
bdflush 内核线程尝试释放页面,无论是作为后台活动还是因为内存不足,都将尝试对显式标记为脏页的页面调用 ->`writepage`。 ->`writepage` 方法现在必须将页面内容写回磁盘并释放页面。
第二条写入路径 _要_ 复杂得多。对于用户写入的每个页面,我们基本上执行以下操作:(完整代码请参见 mm/filemap.c:generic_file_write()
)。
page = __grab_cache_page(mapping, index, &cached_page); mapping->a_ops->prepare_write(file, page, offset, offset+bytes); copy_from_user(kaddr+offset, buf, bytes); mapping->a_ops->commit_write(file, page, offset, offset+bytes);
因此,首先我们尝试查找哈希页面或分配新页面,然后我们调用 ->`prepare_write` 地址空间方法,将用户缓冲区复制到内核内存,最后调用 ->`commit_write` 方法。正如您可能已经看到的,->`prepare_write` 和 ->`commit_write` 与 ->`readpage` 和 ->`writepage` 从根本上不同,因为它们不仅在实际需要物理 I/O 时调用,而且在用户每次修改文件时都会调用。处理这种情况有两种(或更多?)方法,第一种方法使用 Linux 缓冲区缓存来延迟物理 I/O,通过使用 buffer_heads 填充 `page->buffers` 指针,这些 buffer_heads 将在 try_to_free_buffers (fs/buffers.c
) 中使用,以便在内存不足时请求 I/O,这在当前内核中非常普遍。另一种方法只是将页面标记为脏页,并依赖 ->`writepage` 来完成所有工作。由于 `struct page` 中缺少有效性位图,因此这不适用于粒度小于 `PAGE_SIZE` 的文件系统。