linux 逆向映射机制浅析教程
闲话不多说,之前一个问题是想要根据物理页框号得到映射的虚拟地址,一时间不知道如何下手了,在群里和一个朋友讨论了一番,记得之前看swap机制的交换缓存时,记载说系统当要换出一个页面时,可以很容易找到使用该页面的所有进程,然后撤销映射。这一点也就成了我的突破口。经过对源码的一番研究结合相关书籍,便有了今天这篇文章。重点就是逆向映射机制。
顾名思义,有一个虚拟地址经过页面转换得到物理地址的过程为正向映射,那么根据物理地址推导虚拟地址呢?自然成了逆向映射。众所周知,Linux下每个物理页面对应一个page结构,物理页框号可以很容易的转化到page结构,不妨看下内核是怎么转化的。
#define __pfn_to_page(pfn) (mem_map + ((pfn) - ARCH_PFN_OFFSET))
#define __page_to_pfn(page) ((unsigned long)((page) - mem_map)+ ARCH_PFN_OFFSET)
这里有点像windows 的pfn数据库了,mem\_map是一个page指针,作为pfn数据库(实际上是一个大的数组的起始),ARCH\_PFN\_OFFSET是物理起始地址的pfn。所以差值实际就是有效pfn。通过page转化成pfn也是同样的思路。那么这和逆向映射什么关系呢?下面要说的就是至关重要的page结构,该结构比较庞大,我们只说和逆向映射有关系的部分。
page结构中有两个字段:
struct page{
struct address_space *mapping;
union {
pgoff_t index; /* Our offset within mapping. */
void *freelist; /* slub/slob first free object */
bool pfmemalloc; /* If set by the page allocator,
* ALLOC_NO_WATERMARKS was set
* and the low watermark was not
* met implying that the system
* is under some pressure. The
* caller should try ensure
* this page is only used to
* free other pages.
*/
};
struct {
union {
/*
* Count of ptes mapped in
* mms, to show when page is
* mapped & limit reverse map
* searches.
*
* Used also for tail pages
* refcounting instead of
* _count. Tail pages cannot
* be mapped and keeping the
* tail page _count zero at
* all times guarantees
* get_page_unless_zero() will
* never succeed on tail
* pages.
*/
atomic_t _mapcount;
struct { /* SLUB */
unsigned inuse:16;
unsigned objects:15;
unsigned frozen:1;
};
int units; /* SLOB */
};
atomic_t _count; /* Usage count, see below. */
};
};
};
}
其实这里想说的就三个字段,mapping,在映射匿名页面的时候指向一个anon\_vma结构,在映射文件页面的时候指向inode节点的address-space;index,表示对应的虚拟页面在vma中的线性索引;\_mapcount,共享该页面的进程的数目;注意该值默认是-1,当有一个进程使用时为0,所以其值表明除了当前进程还有多少进程在使用,便于撤销。了解了这三个字段,接下来就好解释多了。通过一个函数page\_referenced来解释。
int page_referenced(struct page *page, int is_locked,struct mem_cgroup *memcg, unsigned long *vm_flags)
原版解释如下:Quick test\_and\_clear\_referenced for all mappings to a page,returns the number of ptes which referenced the page.就是快速的检查并清除一个页面的所有引用(不同页表当中),返回引用这个page页面的pte数量。简单走一下流程
int page_referenced(struct page *page,
int is_locked,
struct mem_cgroup *memcg,
unsigned long *vm_flags)
{
int referenced = 0;
int we_locked = 0;
*vm_flags = 0;
if (page_mapped(page) && page_rmapping(page)) {
if (!is_locked && (!PageAnon(page) || PageKsm(page))) {
we_locked = trylock_page(page);
if (!we_locked) {
referenced++;
goto out;
}
}
if (unlikely(PageKsm(page)))
referenced += page_referenced_ksm(page, memcg,
vm_flags);
else if (PageAnon(page))
referenced += page_referenced_anon(page, memcg,
vm_flags);
else if (page->mapping)
referenced += page_referenced_file(page, memcg,
vm_flags);
if (we_locked)
unlock_page(page);
if (page_test_and_clear_young(page_to_pfn(page)))
referenced++;
}
out:
return referenced;
}
首先检查正向和逆向映射是否都存在,如果没有锁定该页面并且页面是KSM 页面或者文件映射页面,则需要trylock,如果加锁失败,则直接out.接下来就是对不同情况的处理。如果是KSM页面走page\_referenced\_ksm。如果是匿名映射页,走page\_referenced\_anon,如果是文件映射页,走page\_referenced\_file。KSM是内核页面共享的一种机制,主要用在KVM中,但是其他地方也可以引用,由于其需要计算页面是否相同,所以在重复率不高的场合,大部分选择关掉KSM,关于KSM在另一篇文章已经介绍。
如果是匿名映射页面,进入page\_referenced\_anonstatic int page\_referenced\_anon(struct page *page,struct mem\_cgroup *memcg,unsigned long *vm\_flags)函数
static int page_referenced_anon(struct page *page,
struct mem_cgroup *memcg,
unsigned long *vm_flags)
{
unsigned int mapcount;
struct anon_vma *anon_vma;
pgoff_t pgoff;
struct anon_vma_chain *avc;
int referenced = 0;
anon_vma = page_lock_anon_vma_read(page);
if (!anon_vma)
return referenced;
mapcount = page_mapcount(page);
pgoff = page->index << (PAGE_CACHE_SHIFT - PAGE_SHIFT);
anon_vma_interval_tree_foreach(avc, &anon_vma->rb_root, pgoff, pgoff) {
struct vm_area_struct *vma = avc->vma;
unsigned long address = vma_address(page, vma);
/*
* If we are reclaiming on behalf of a cgroup, skip
* counting on behalf of references from different
* cgroups
*/
if (memcg && !mm_match_cgroup(vma->vm_mm, memcg))
continue;
referenced += page_referenced_one(page, vma, address,
&mapcount, vm_flags);
if (!mapcount)
break;
}
page_unlock_anon_vma_read(anon_vma);
return referenced;
}
要查看页面的访问情况,肯定要定位到具体的PTE,而PTE只能根据虚拟地址查找页表获得,所以当务之急还是找到虚拟地址和页表。这里首先获得page对应的anon\_vma,前面提到,在匿名映射情况下,page->mapping指向anon\_vma结构。然后获取了page的共享计数mapcount,获取page对应的虚拟页框在vma中对应的线性索引index,接下来就开始遍历interval-tree了。每个anon\_vma\_chain关联一个进程的vma,通过vma\_address(page, vma)便可以获取在当前vma对应的进程的虚拟地址。暂且忽略cgroup相关的内容。接下来调用page\_referenced\_one解除映射。前面已经提到,目前已经有了虚拟地址,有了vma,根据vma可以获取对应的mm\_struct,进而获取页基址,OK,流程走通了。该函数就不在列举了,函数中有两种情况,如果是大页面(2M页面),需要获得是pmd;如果是普通页面,需要获取pte;之后检查\_PAGE\_ACCESSED位。如果被设置,则清除,然后++引用计数器,否则,不变。所以经常访问的页面,引用计数器高,就更容易被定义成活跃页面,常驻活跃LRU链表,就不容易被换出。
回顾下最初的问题,通过物理地址找到虚拟地址,在获取了vma和index后,一个函数就解决问题,但是笔者这里有一个疑问,代码显示这里根据page结构中的index对所有的vma进行索引,这点令我很困惑,理论上将不能保证page映射的虚拟页框在所有的vma中都是同样的偏移吧?如果有知道的老师,还请告知!!
static inline unsigned long
__vma_address(struct page *page, struct vm_area_struct *vma)
{
pgoff_t pgoff = page->index << (PAGE_CACHE_SHIFT - PAGE_SHIFT);
if (unlikely(is_vm_hugetlb_page(vma)))
pgoff = page->index << huge_page_order(page_hstate(page));
return vma->vm_start + ((pgoff - vma->vm_pgoff) << PAGE_SHIFT);
}
代码到这里就不需要多解释了吧,关于anon\_vma结构的组织,以后凑空在分析;
感谢主!
参考:
linux 3.10.1源码
《深入linux内核架构》