好了,在了解了页高速缓存相关的数据结构以后,我们来介绍一下基本的页高速缓存处理函数:
对页高速缓存操作的基本高级函数有查找、增加和删除页。在以上函数的基础上还有另一个函数确保高速缓存包含指定页的最新版本。
函数find_get_page()接收的参数为指向address_space对象的指针和偏移量。它获取地址空间的自旋锁,并调用radix_tree_lookup()函数搜索拥有指定偏移量的基树的叶子节点:
struct page * find_get_page(struct address_space *mapping, unsigned long offset)
{
struct page *page;
read_lock_irq(&mapping->tree_lock);
page = radix_tree_lookup(&mapping->page_tree, offset);
if (page)
page_cache_get(page);
read_unlock_irq(&mapping->tree_lock);
return page;
}
函数radix_tree_lookup根据偏移量值中的位依次从树根开始并向下搜索,如上节所述。如果遇到空指针,函数返回NULL;否则,返回叶子节点的地址,也就是所需要的页描述符指针。如果找到了所需要的页,find_get_page()函数就增加该页的使用计数器,释放自旋锁,并返回该页的地址;否则,函数就释放自旋锁并返回NULL:
void *radix_tree_lookup(struct radix_tree_root *root, unsigned long index)
{
void **slot;
slot = __lookup_slot(root, index);
return slot != NULL ? *slot : NULL;
}
static inline void **__lookup_slot(struct radix_tree_root *root,
unsigned long index)
{
unsigned int height, shift;
struct radix_tree_node **slot;
height = root->height;
if (index > radix_tree_maxindex(height))
return NULL;
if (height == 0 && root->rnode)
return (void **)&root->rnode;
shift = (height-1) * RADIX_TREE_MAP_SHIFT;
slot = &root->rnode;
while (height > 0) {
if (*slot == NULL)
return NULL;
slot = (struct radix_tree_node **)
((*slot)->slots +
((index >> shift) & RADIX_TREE_MAP_MASK));
shift -= RADIX_TREE_MAP_SHIFT;
height--;
}
return (void **)slot;
}
函数find_get_pages()与find_get_page()类似,但它实现在高速缓存中查找一组具有相邻索引的页。它接收的参数是:指向address_space对象的指针、地址空间中相对于搜索起始位置的偏移量、所检索到页的最大数量、指向由该函数赋值的页描述符数组的指针。find_get_pages()依赖radix_tree_gang_lookup()函数实现查找操作,radix_tree_gang_lookup()函数为指针数组赋值并返回找到的页数。尽管由于一些页可能不在页高速缓存中而会出现空缺的页索引,但所返回的页还是递增的索引值。
还有另外几个函数实现页高速缓存上的查找操作。例如,find_lock_page()函数与find_get_page()类似,但它增加返回页的使用记数器,并调用lock_page()设置PG_locked标志,从而当函数返回时调用者能够以互斥的方式访问返回的页。随后,如果页已经被加锁,lock_Page()函数就阻塞当前进程。最后,它在PG_locked位置位时调用__wait_on_bit_lock()函数。后面的函数把当前进程置为TASK_UNINTERRUPTIBLE状态,把进程描述符存入等待队列,执行address_space对象的sync_page方法以取消文件所在块设备的请求队列,最后调用schedule()函数来挂起进程,直到把PG_locked标志清0。内核使用unlock_page()函数对页进行解锁,并唤醒在等待队列上睡眠的进程。
函数find_trylock_page()与find_lock_page()类似,仅有一点不同,就是find_trylock_page()从不阻塞:如果被请求的页已经上锁,函数就返回错误码。最后要说明的是,函数find_or_create_page()执行find_lock_page();不过,如果找不到所请求的页,就分配一个新页并把它插人页高速缓存。
函数add_to_page_cache()把一个新页的描述符插入到页高速缓存。它接收的参数有:页描述符的地址page, address_space对象的地址mapping, 表示在地址空间内的页索引的值offset和为基树分配新节点时所使用的内存分配标志gfp_mask。函数执行以下操作:
int add_to_page_cache(struct page *page, struct address_space *mapping,
pgoff_t offset, gfp_t gfp_mask)
{
int error = radix_tree_preload(gfp_mask & ~__GFP_HIGHMEM);
if (error == 0) {
write_lock_irq(&mapping->tree_lock);
error = radix_tree_insert(&mapping->page_tree, offset, page);
if (!error) {
page_cache_get(page);
SetPageLocked(page);
page->mapping = mapping;
page->index = offset;
mapping->nrpages++;
trace_add_to_page_cache(mapping, offset);
__inc_zone_page_state(page, NR_FILE_PAGES);
}
write_unlock_irq(&mapping->tree_lock);
radix_tree_preload_end();
}
return error;
}
add_to_page_cache首先调用radix_tree_preload()函数,它禁用内核抢占,并把一些空的radix_tree_node结构赋给每CPU变量radix_tree_preloads。radix_tree_node结构的分配由slab分配器高速缓存radix_tree_node_cachep来完成。如果radix_tree_preload()预分配radix_tree_node结构不成功,函数add_to_page_cache()就终止并返回错误码-ENOMEM。否则,如果radix_tree_preload()成功地完成预分配,add_to_page_cache()函数肯定不会因为缺乏空闲内存或因为文件的大小达到了64GB而无法完成新页描述符的插入:
int radix_tree_preload(gfp_t gfp_mask)
{
struct radix_tree_preload *rtp;
struct radix_tree_node *node;
int ret = -ENOMEM;
preempt_disable();
rtp = &__get_cpu_var(radix_tree_preloads);
while (rtp->nr < ARRAY_SIZE(rtp->nodes)) {
preempt_enable();
node = kmem_cache_alloc(radix_tree_node_cachep, gfp_mask);
if (node == NULL)
goto out;
preempt_disable();
rtp = &__get_cpu_var(radix_tree_preloads);
if (rtp->nr < ARRAY_SIZE(rtp->nodes))
rtp->nodes[rtp->nr++] = node;
else
kmem_cache_free(radix_tree_node_cachep, node);
}
ret = 0;
out:
return ret;
}
add_to_page_cache随后获取mapping->tree_lock自旋锁——注意,radix_tree_preload()函数已经禁用了内核抢占。
调用radix_tree_insert()在树中插入新节点,该函数执行下述操作:
int radix_tree_insert(struct radix_tree_root *root,
unsigned long index, void *item)
{
struct radix_tree_node *node = NULL, *slot;
unsigned int height, shift;
int offset;
int error;
/* Make sure the tree is high enough. */
if (index > radix_tree_maxindex(root->height)) {
error = radix_tree_extend(root, index);
if (error)
return error;
}
slot = root->rnode;
height = root->height;
shift = (height-1) * RADIX_TREE_MAP_SHIFT;
offset = 0; /* uninitialised var warning */
while (height > 0) {
if (slot == NULL) {
/* Have to add a child node. */
if (!(slot = radix_tree_node_alloc(root)))
return -ENOMEM;
if (node) {
node->slots[offset] = slot;
node->count++;
} else
root->rnode = slot;
}
/* Go a level down */
offset = (index >> shift) & RADIX_TREE_MAP_MASK;
node = slot;
slot = node->slots[offset];
shift -= RADIX_TREE_MAP_SHIFT;
height--;
}
if (slot != NULL)
return -EEXIST;
if (node) {
node->count++;
node->slots[offset] = item;
BUG_ON(tag_get(node, 0, offset));
BUG_ON(tag_get(node, 1, offset));
} else {
root->rnode = item;
BUG_ON(root_tag_get(root, 0));
BUG_ON(root_tag_get(root, 1));
}
return 0;
}
注意,radix_tree_insert首先调用radix_tree_maxindex()获得最大索引,该索引可能被插人具有当前深度的基树;如果新页的索引不能用当前深度表示,就调用radix_tree_extend()通过增加适当数量的节点来增加树的深度:
static int radix_tree_extend(struct radix_tree_root *root, unsigned long index)
{
struct radix_tree_node *node;
unsigned int height;
int tag;
/* Figure out what the height should be. */
height = root->height + 1;
while (index > radix_tree_maxindex(height))
height++;
if (root->rnode == NULL) {
root->height = height;
goto out;
}
do {
if (!(node = radix_tree_node_alloc(root)))
return -ENOMEM;
/* Increase the height. */
node->slots[0] = root->rnode;
/* Propagate the aggregated tag info into the new root */
for (tag = 0; tag < RADIX_TREE_MAX_TAGS; tag++) {
if (root_tag_get(root, tag))
tag_set(node, tag, 0);
}
node->count = 1;
root->rnode = node;
root->height++;
} while (height > root->height);
out:
return 0;
}
分配新节点是通过执行radix_tree_node_alloc()函数实现的,该函数试图从slab分配器高速缓存获得radix_tree_node结构,如果分配失败,就从存放在radix_tree_preloads中的预分配的结构池中获得radix_tree_node结构:
static struct radix_tree_node *
radix_tree_node_alloc(struct radix_tree_root *root)
{
struct radix_tree_node *ret;
gfp_t gfp_mask = root_gfp_mask(root);
ret = kmem_cache_alloc(radix_tree_node_cachep, gfp_mask);
if (ret == NULL && !(gfp_mask & __GFP_WAIT)) {
struct radix_tree_preload *rtp;
rtp = &__get_cpu_var(radix_tree_preloads);
if (rtp->nr) {
ret = rtp->nodes[rtp->nr - 1];
rtp->nodes[rtp->nr - 1] = NULL;
rtp->nr--;
}
}
return ret;
}
radix_tree_insert然后根据页索引的偏移量,从根节点(mapping->page_tree)开始遍历树,直到叶子节点。如果需要,就调用radix_tree_node_alloc()分配新的中间节点。
radix_tree_insert最后把页描述符地址存放在对基树所遍历的最后节点的适当位置,并返回0:
node->count++;
node->slots[offset] = item;
回到函数add_to_page_cache(),在分配好页之后,增加页描述符的使用计数器page->count。
由于页是新的,所以其内容无效:函数设置页框的PG_locked标志,以阻止其他的内核路径并发访问该页。
用mapping和offset参数初始化page->mapping和page->index。(重点!!!!)
add_to_page_cache()函数最后递增在地址空间所缓存页的计数器(mapping->nrpages);释放地址空间的自旋锁;并且调用radix_tree_preload_end()重新启用内核抢占,返回0(成功)。
函数remove_from_page_cache()通过下述步骤从页高速缓存中删除页描述符:
1. 获取自旋锁page->mapping->tree_lock并关中断。
2. 调用radix_tree_delete()函数从树中删除节点。该函数接收树根的地址(page->mapping->page_tree)和要删除的页索引作为参数,并执行下述步骤:
a) 如上节所述,根据页索引从根节点开始遍历树,直到到达叶子节点。遍历时,建立radix_tree_path结构的数组,描述从根到与要删除的页相应的叶子节点的路径构成。
b) 从最后一个节点(包含指向页描述符的指针)开始,对路径数组中的节点开始循环操作。对每个节点,把指向下一个节点(或页描述符)位置数组的元素置为NULL,并递减count字段。如果count变为0,就从树中删除节点并把radix_tree_node结构释放给slab分配器高速缓存。然后继续循环处理路径数组中的节点。否则,如果count不等于0,继续执行下一步。
c) 返回已经从树中删除的页描述符指针。
3. 把page->mapping字段置为NULL。
4. 把所缓存页的page->mapping->nrpages计数器的值减1。
5. 释放自旋锁page->mapping->tree_lock,打开中断,函数终止。
函数read_cache_page()确保高速缓存中包括最新版本的指定页。它的参数是指向address_space对象的指针mapping,表示所请求页的偏移量的值index,指向从磁盘读页数据的函数的指针filler(通常是实现地址空间readpage方法的函数)以及传递给filler函数的指针data(通常为NULL),下面是对这个函数的简单说明:
1. 调用函数find_get_page()检查页是否已经在页高速缓存中。
2. 如果页不在页高速缓存中,则执行下述子步骤:
a) 调用alloc_pages()分配一个新页框。
b) 调用add_to_page_cache()在页高速缓存中插人相应的页描述符。
c) 调用lru_cache_add()把页插人该管理区的非活动LRU链表中。
3. 此时,所请求的页已经在页高速缓存中了。调用mark_page_accessed()函数记录页已经被访问过的事实。
4. 如果页不是最新的(PG_uptodate标志为0),就调用filler函数从磁盘读该页。
5. 返回页描述符的地址。
前面我们曾强调,页高速缓存不仅允许内核快速获得含有块设备中指定数据的页,还允许内核从高速缓存中快速获得给定状态的页。
例如,我们假设内核必须从高速缓存获得属于指定所有者的所有页和脏页(即其内容还没有写回磁盘)。存放在页描述符中的PG_dirty标志表示页是否是脏的,但是,如果绝大多数页都不是脏页,遍历整个基树以顺序访问所有叶子节点(页描述符)的操作就太慢了。
相反,为了能快速搜索脏页,基树中的每个中间节点都包含一个针对每个孩子节点(或叶子节点)的脏标记,当有且只有至少有一个孩子节点的脏标记被置位时这个标记被设置。最底层节点的脏标记通常是页描述符的PG_dirty标志的副本。通过这种方式,当内核遍历基树搜索脏页时,就可以跳过脏标记为0的中间结点的所有子树:中间结点的脏标记为0说明其子树中的所有页描述符都不是脏的。
同样的想法应用到了PG_writeback标志,该标志表示页正在被写回磁盘。这样,为基树的每个结点引入两个页描述符的标志:PG_dirty和PG writeback。每个结点的tags字段中有两个64位的数组来存放这两个标志。tags[0](PAGECACHE TAG DIRTY)数组是脏标记,而tags[l] (PAGECACHE TAG WRITEBACK)数组是写回标记。
设置页高速缓存中页的PG_dirty或PG_writeback标志时调用函数radix_tree_tag_set(),它作用于三个参数:基树的根、页的索引以及要设置的标记的类型(PAGECACHE TAG DIRTY或PAGECACHE TAG WRITEBACK)。函数从树根开始并向下搜索到与指定索引对应的叶子结点;对于从根通往叶子路径上的每一个节点,函数利用指向路径中下一个结点的指针设置标记。然后,函数返回页描述符的地址。结果是,从根结点到叶子结点的路径中的所有结点都以适当的方式被加上了标记。
清除页高速缓存中页的PG_dirty或PG_writeback标志时调用函数radix_tree_tag_clear(),它的参数与函数radix_tree_tag_set()的参数相同。函数从树根开始并向下到叶子结点,建立描述路径的radix_tree_path结构的数组。然后,函数从叶子结点到根结点向后进行操作:清除底层结点的标记,然后检查是否结点数组中所有标记都被清0,如果是,函数把上层父结点的相应标记清0,并如此继续上述操作。最后,函数返回页描述符的地址。
从基树删除页描述符时,必须更新从根结点到叶子结点的路径中结点的相应标记。函数radix_tree_delete()可以正确地完成这个工作(尽管我们在上一节没有提到这一点)。而函数radix_tree_insert()不更新标记,因为插入基树的所有页描述符的PG_dirty和PG_writeback标志都被认为是清零的。如果需要,内核可以随后调用函数radix_tree_tag_set()。
函数radix_tree_tagged()利用树的所有结点的标志数组来测试基树是否至少包括一个指定状态的页。函数通过执行下面的代码轻松地完成这一任务(root是指向基树radix_tree_root结构的指针,tag是要测试的标记):
for (idx = 0; idx < 2; idx++) {
if (root->rnode->tags[tag][idx])
return 1;
}
return 0;
因为可能假设基树所有结点的标记都正确地更新过,所以radix_tree_tagged()函数只需要检查第一层的标记。使用该函数的一个例子是:确定一个包含脏页的索引节点是否要写回磁盘。注意,函数在每次循环时要测试在无符号长整型的32个标志中,是否有被设置的标志。
函数find_get_pages_tag()和find_get_pages()类似,只有一点不同,就是前者返回的只是那些用tag参数标记的页。该函数对快速找到一个索引节点的所有脏页是非常关键的。