页框回收中的反向映射
PFRA的目的之一就是能释放共享页框,为了达到这个目的Linux2.6内核能够快速定位指向同一页框的所有页表项,这个过程就叫做反向映射。
反向映射简单来说就是在页描述符中加入附加字段,这样就把某个页描述符它所确定的页框对应的全部页表项联接起来,但是这样的话一旦对该链表更新会有很大的开销。所有就有一种成熟的技术出现,Linux2.6就有叫做面向对象的反向映射的技术。实际上对于任何可以回收的用户态页面,内核保留系统中该页所在所有线性区(对象)的反向链接,每个线性区描述符存放一个指针指向一个内存描述符,这个内存描述符又包含一个指针指向一个页全局目录。这样的话,这些反向链接使得PFRA能够检索引用某页的所有页表项。因为线性区描述符比页描述符少所以更新共享页的反向链接就比较节省时间。
首先PFRA需要判断这个页面是共享的还是非共享的,以及是匿名的还是非匿名的。需要查看页描述符的两个字段_mapcount
和mapping
。
struct page {
...
struct address_space *mapping;
atomic_t _mapcount;
...
} _struct_page_alignment;
_mapcount:存放引用页框的页表项数目,初始值位-1,表示没有页表项引用页框;为0表示该页框是非共享的,大于0表示该页框是共享的。page_count函数接受页描述符地址,返回值为1+_mapcount,这样当返回值为1就表示这个页框是非共享的,即某个进程的用户态地址空间中存放着一个非共享的页。
mapping:表示这个页是匿名的还是非匿名的。
- 如果mapping为空,表示这个页属于交换高速缓存。
- 如果mapping非空,且mapping最低位为1,表示这个页是一个匿名页。同时这个mapping字段存放的是anon_vma描述符的指针
- 如果mapping非空,且mapping最低位为0,表示这个页是一个非匿名页。这个mapping字段指向对应文件的address_space对象
try_to_unmap
try_to_unmap() 参数为页描述符指针,它尝试清空所有引用该页描述符对应页框的页表项。
如果从页表项中成功清除所有对该页框的引用,函数返回 SWAP_SUCCESS(0);否则返回 SWAP_AGAIN(1);出错返回 SWAP_FAIL(2)。
/*
* SWAP_SUCCESS - we succeeded in removing all mappings
* SWAP_AGAIN - we missed a mapping, try again later
* SWAP_FAIL - the page is unswappable
*/
int try_to_unmap(struct page *page)
{
int ret;
BUG_ON(PageReserved(page));
BUG_ON(!PageLocked(page));
if (PageAnon(page)) //判断这个页是不是匿名的
ret = try_to_unmap_anon(page);//匿名的处理
else
ret = try_to_unmap_file(page);//非匿名的处理
if (!page_mapped(page))//page_mapped函数,如果此页映射到pagetables,则返回true。这里如果页没有映射到pagetables,返回一个SWAP_SUCCESS,删除了所有映射
ret = SWAP_SUCCESS;
return ret;
}
匿名页的反向映射
匿名页经常由几个进程共享。最常见的情形:创建新进程,父进程的所有页框,包括匿名页,同时也分配给子进程。另外(不常见),进程创建线性区时使用两个标志 MAP_ANONYMOUS 和 MAP_SHARED,表明这个区域内的也将由该进程后面的子进程共享。
将引用同一页框的所有匿名页链接起来的策略:将该页框所在的匿名线性区存放在一个双向循环链表中。需要注意的是,即使一个匿名线性区存有不同的页,也始终只有一个反向映射链表用于该区域中的所有页框。
当为一个匿名线性区分配第一页时,内核创建一个新的 anon_vma 数据结构
struct anon_vma {
spinlock_t lock; /* 竞争条件下保护链表的自旋锁 */
struct list_head head; /*线性区描述符双向循环链表的头部 */
};
然后,内核将匿名线性区的 vm_area_struct 描述符插入 anon_vma 链表。vm_area_struct 中对应链表的两个字段
struct vm_area_struct {
struct list_head anon_vma_node; /*存放指向链表中前一个和后一个元素的指针 */
struct anon_vma *anon_vma; /* 指向 anon_vma 数据结构*/
};
最后,内核将 anon_vma 数据结构的地址存放在匿名页描述符的 mapping 字段。当已被一个进程引用的页框插入另一个进程的页表项时(如调用 fork() 时),内核只是将第二个进程的匿名线性区插入 anon_vma 数据结构的双向循环链表,而第一个进程线性区的 anon_vma 字段指向该 anon_vma 数据结构。因此,每个 anon_vma 链表通常包含不同进程的线性区。
从上图可以看到每一个内存描述符,通过他的页全局目录pgd和匿名页的起始地址vm_start就可以找到对应页表项,该线性地址可以由线性区描述符及页描述符的 index 字段得到。
如此一来通过anon_vma 链表,内核可快速定位引用同一匿名页框的所有页表项
try_to_unmap_anon()
当回收匿名页框时,PFRA 必须扫描 anon_vma 链表中的所有线性区,检查是否每个区域都存有一个匿名页,而其对应的页框就是目标页框。 该工作通过 try_to_unmap_anon() 实现,参数为目标页框描述符
static int try_to_unmap_anon(struct page *page)
{
struct anon_vma *anon_vma;
struct vm_area_struct *vma;
int ret = SWAP_AGAIN;
anon_vma = page_lock_anon_vma(page);//获得 anon_vma 数据结构的自旋锁,页描述符的 mapping 字段指向该数据结构。
if (!anon_vma)//防止anon_vma链表为空,这里做个判断
return ret;
//遍历anon_vma 链表
list_for_each_entry(vma, &anon_vma->head