内核为块设备提供了两种通用的缓存方案。
页缓存(page cache)
块缓存(buffer cache)
页缓存的结构
在页缓存中搜索一页所花费的时间必须最小化,以确保缓存失效的代价尽可能低廉,因为在缓存失效时,进行搜索的计算时间实际上被浪费了。因而,页缓存设计的一个关键的方面就是,对缓存的页进行高效的组织。
管理和查找缓存的页
对此用途而言,树数据结构是非常流行的,Linux也采用了这种结构来管理页缓存中包含的页,称为基数树(radix tree)
基数树也是不平衡的,换句话说,在树的不同分支之间,可能有任意数目的高度差。树本身由两种不同的数据结构组成,还需要另一种数据结构来表示叶,其中包含了有用的数据。因为页缓存组织的是内存页,因而基数树的叶子是 page 结构的实例,该事实并不会影响到树的实现。
树的根由一个简单的数据结构表示,其中包含了树的高度(所包含结点的最大层次数目)和一个指针,指向组成树的的第一个结点的数据结构。
结点本质上是数组。
树的各结点通过一个唯一的键来访问,键是一个整数。
树结点的增删涉及的工作量都很少,因此缓存管理操作所涉及的时间开销可以降低到最低限度。
树的结点具备两种搜索标记(search tag)。二者用于指定给定页当前是否是脏的,或该页是否正在向底层块设备回写。重要的是,标记不仅对叶结点设置,还一直向上设置到根结点。这使得内核可以判断,在某个范围内是否有一页或多页设置了某个标记位。
回写修改的数据
几个专门的内核守护进程在后台运行,称为 pdflush ,它们将周期性激活,而不考虑页缓存中当前的情况。这些守护进程扫描缓存中的页,将超出一定时间没有与底层块设备同步的页写回。
pdflush 的第二种运作模式是:如果缓存中修改的数据项数目在短期内显著增加,则由内核激活 pdflush 。
提供了相关的系统调用,可由用户或应用程序通知内核写回所有未同步的数据。最著名的是sync 调用,因为还有一个同名的用户空间工具,是基于该调用的。
为管理可以按整页处理和缓存的各种不同对象,内核使用了“地址空间”抽象,将内层中的页与特定的块设备(或任何其他系统单元,或系统单元的一部分)关联起来。
最初,我们只对一个方面感兴趣。每个地址空间都有一个“宿主”,作为其数据来源。大多数情况下,宿主都是表示一个文件的inode。
因为所有现存的inode都关联到其超级块,内核只需要扫描所有超级块的链表,并跟随相关的inode,即可获得被缓存页的列表。
通常,修改文件或其他按页缓存的对象时,只会修改页的一部分,而非全部。这在数据同步时引起了一个问题。将整页写回到块设备是没有意义的,因为内存中该页的大部分数据仍然与块设备是同步的。为节省时间,内核在写操作期间,将缓存中的每一页划分为较小的单位,称为缓冲区。在同步数据时,内核可以将回写操作限制于那些实际发生了修改的较小的单位上。因而,页缓存的思想没有受到危害。
块缓存的结构
与内存页相比,块不仅比较小(大多数情况下),而且长度是可变的,依赖于使用的块设备。
随着日渐倾向于使用基于页操作实现的通用文件存取方法,块缓存作为中枢系统缓存的重要性已经逐渐失去,主要的缓存任务现在由页缓存承担。
另外,基于块的I/O的标准数据结构,现在已经不再是缓冲区,而是第6章讨论的 struct bio 。
块缓存在结构上由两个部分组成:
缓冲头(buffer head)包含了与缓冲区状态相关的所有管理数据,包括块号、块长度、访问计数器等,将在下文讨论。这些数据不是直接存储在缓冲头之后,而是存储在物理内存的一个独立区域中,由缓冲头结构中一个对应的指针表示。
有用数据保存在专门分配的页中,这些页也可能同时存在于页缓存中。这进一步细分了页缓存
当然,有些应用程序在访问块设备时,使用的是块而不是页,读取文件系统的超级块,就是一个实例。一个独立的块缓存用于加速此类访问。该块缓存的运作独立于页缓存,而不是在其上建立的。为此,缓冲头数据结构(对块缓存和页缓存是相同的)群集在一个长度恒定的数组中,各个数组项按LRU(least recently used,最近最少使用)方式管理。在一个数组项用过之后,将其置于索引位置0,其他数组项相应下移。这意味着最常使用的数组项位于数组的开头,而不常用的数组项将被后推,如果很长时间不用,则会“掉出”数组。
因为数组的长度,或者说LRU列表中的项数,是一个固定值,在内核运行期间不改变,内核无须运行独立的线程来将缓存长度修整为合理值。相反,内核只需要在一项“掉出”数组时,将相关的缓冲区从缓存删除,以释放内存,用于其他目的。
地址空间
内存中的页分配到每个地址空间。这些页的内容可以由用户进程或内核本身使用各式各样的方法操作。
后备存储器指定了填充地址空间中页的数据的来源。地址空间关联到处理器的虚拟地址空间,是由处理器在虚拟内存中管理的一个区域到源设备(使用块设备)上对应位置之间的一个映射。
数据结构
<linux/fs.h>
struct address_space {
struct inode *host; /* owner: inode, block_device */
struct radix_tree_root page_tree; /* radix tree of all pages */
rwlock_t tree_lock; /* and rwlock protecting it */
unsigned int i_mmap_writable;/* count VM_SHARED mappings */
struct prio_tree_root i_mmap; /* tree of private and shared mappings */
struct list_head i_mmap_nonlinear;/*list VM_NONLINEAR mappings */
spinlock_t i_mmap_lock; /* protect tree, count, list */
unsigned int truncate_count; /* Cover race condition with truncate */
unsigned long nrpages; /* number of total pages */
pgoff_t writeback_index;/* writeback starts here */
const struct address_space_operations *a_ops; /* methods */
unsigned long flags; /* error bits/gfp mask */
struct backing_dev_info *backing_dev_info; /* device readahead, etc */
spinlock_t private_lock; /* for use by the address_space */
struct list_head private_list; /* ditto */
struct address_space *assoc_mapping; /* ditto */
} __attribute__((aligned(sizeof(long))));
与地址空间所管理的区域之间的关联。是通过两个字段建立的。inode指向了后备存储器,一个基树的根列出了地址空间中所有的物理内存页。
缓存页的总数保存在 nrpages 计数器变量中。
address_space_operations 是一个指向结构的指针,该结构包含了一组函数指针,指向用于处理地址空间的特定操作。
i_mmap 是一棵树的根结点,该树包含了与该inode相关的所有普通内存映射。该树的任务在于,支持查找包含了给定区间中至少一页的所有内存区域,而辅助宏 vma_prio_tree_foreach就 用于该目的。所有页都可以在树中找到,而且树的结构很容易操作,就足够了。(优先查找树(priority search tree)用于建立文件中的一个区域与该区域映射到的所有虚拟地址空间之间的关联。)
i_mmap_writeable 统计了所有用 VM_SHARED 属性创建的映射,它们可以由几个用户同时共享。 i_mmap_nonlinear 用于建立一个链表,包括所有包含在非线性映射中的页
backing_dev_info 是一个指针,指向另一个结构,其中包含了与地址空间相关的后备存储器的有关信息。
后备存储器是指与地址空间相关的外部设备,用作地址空间中信息的来源。它通常是块设备:
<backing-dev.h>
struct backing_dev_info {
unsigned long ra_pages; /* max readahead in PAGE_CACHE_SIZE units *///预读的最大数目
unsigned long state; /* Always use atomic bitops on this *///状态
unsigned int capabilities; /* Device capabilities *///BDI_CAP_NO_WRITEBACK ,那么不需要数据同步;否则,需要进行同步。
...
}
private_list 用于将包含文件系统元数据(通常是间接块)的 buffer_head 实例彼此连接起来。assoc_mapping 是一个指向相关的地址空间的指针。
flags 中的标志集主要用于保存映射页所来自的GFP内存区的有关信息。它也可以保存异步输入输出期间发生的错误信息,在异步I/O期间错误无法之间传递给调用者。 AS_EIO 代表一般性的I/O错误, AS_ENOSPC 表示没有足够的空间来完成一个异步写操作。
页树
内核使用了基数树来管理与一个地址空间相关的所有页。
radix_tree_root 结构是每个基数树的的根结点:
<linux/radix-tree.h>
struct radix_tree_root {
unsigned int height;
gfp_t gfp_mask;
struct radix_tree_node *rnode;
};
height 指定了树的高度,即根结点之下结点的层次数目。根据该信息和每个结点的项数,内核可以快速计算给定树中数据项的最大数目。
gfp_mask 指定了从哪个内存域分配内存。
rnode 是一个指针,指向树的第一个结点。
实现
基数树的结点基本上由以下数据结构表示:
<lib/radix-tree.c>
#ifdef __KERNEL__
#define RADIX_TREE_MAP_SHIFT (CONFIG_BASE_SMALL ? 4 : 6)
#else
#define RADIX_TREE_MAP_SHIFT 3 /* For more stressful testing */
#endif
#define RADIX_TREE_MAP_SIZE (1UL << RADIX_TREE_MAP_SHIFT)
#define RADIX_TREE_MAP_MASK (RADIX_TREE_MAP_SIZE-1)
#define RADIX_TREE_TAG_LONGS \
((RADIX_TREE_MAP_SIZE + BITS_PER_LONG - 1) / BITS_PER_LONG)
struct radix_tree_node {
unsigned int height; /* Height from the bottom */
unsigned int count;
struct rcu_head rcu_head;
void *slots[RADIX_TREE_MAP_SIZE];
unsigned long tags[RADIX_TREE_MAX_TAGS][RADIX_TREE_TAG_LONGS];
};
slots 是一个 void 指针的数组,根据结点所在的层次,指向数据或其他结点。
count 保存了该结点中已经使用的数组项的数目。
每个树结点都可以进一步指向64个结点(或叶子)。
标记
基数树的每个结点都包含了额外的标记信息,用于指定结点中的每个页是否具有标记中指定的属性。
当