下一页 上一页 目录

4. Linux 页面缓存

在本章中,我们将介绍 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` 的文件系统。


下一页 上一页 目录