物理页面描述符struct page中与反向映射有关的两个关键成员是mapping和__mapcount:
struct page {
……
……
atomic_t _mapcount;
union {
……
struct {
……
struct address_space *mapping;
};
……
……
};
文件页和匿名页采用了不同的数据结构和机制处理与页面相关的虚拟内存域。下面主要介绍匿名页和文件页反向映射相关的数据结构和实现机制,还会简单介绍一下虚拟内存技术中KSM的机制和KSM页的反向映射原理。
匿名页的反向映射有3个关键的数据结构:
struct vm_area_struct(简称VMA)在进程虚拟地址空间有过介绍,描述进程一段的虚拟地址空间,该结构体中与反向映射下关的成员如下所示:
///include/linux/mm_types.h
/*
*用于描绘进程地址空间中的一段区域
*/
struct vm_area_struct {
...
...
/*
* A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
* list, after a COW of one of the file pages. A MAP_SHARED vma
* can only be in the i_mmap tree. An anonymous MAP_PRIVATE, stack
* or brk vma (with NULL file) can only be in an anon_vma list.
*/
struct list_head anon_vma_chain; /* Serialized by mmap_sem &
* page_table_lock */
struct anon_vma *anon_vma; /* Serialized by page_table_lock */
...
...
}
struct ano_vma(简称AV)管理匿名页面映射的所有VMA,匿名页面的struct page的mapping成员指向该结构体。该结构体设计出来就是为了反向映射服务
// /include/linux/rmap.h
/*
*(1)对于一个页框,若该页为匿名页,则其struct page中的mapping指向AV(anon_vma的简称)
*(2)AV结构体用于管理匿名页对应的所有VMAs.可以找到页对应的AV,然后再遍历AV的rb_root查询到该页框所有的
* VMA。
*/
struct anon_vma {
struct anon_vma *root; /* Root of this anon_vma tree */
struct rw_semaphore rwsem; /* W: modification, R: walking the list */
/*
* The refcount is taken on an anon_vma when there is no
* guarantee that the vma of page tables will exist for
* the duration of the operation. A caller that takes
* the reference is responsible for clearing up the
* anon_vma if they are the last user on release
*/
atomic_t refcount;
/*
* Count of child anon_vmas and VMAs which points to this anon_vma.
*
* This counter is used for making decision about reusing anon_vma
* instead of forking new one. See comments in function anon_vma_clone.
*/
unsigned degree;
struct anon_vma *parent; /* Parent of this anon_vma */
/*
* NOTE: the LSB of the rb_root.rb_node is set by
* mm_take_all_locks() _after_ taking the above lock. So the
* rb_root must only be read/written after taking the above lock
* to be sure to see a valid next pointer. The LSB bit itself
* is serialized by a system wide lock only visible to
* mm_take_all_locks() (mm_all_locks_mutex).
*/
/* Interval tree of private "related" vmas */
struct rb_root_cached rb_root;
};
struct anon_vma_chain检查(AVC),该结构体也是为反向映射服务的,目的链接VMA和AV,缓解反向映射遇到了效率和锁竞争激烈问题。
// /include/linux/rmap.h
//AVC是链接VMA和AV间的桥梁
struct anon_vma_chain {
struct vm_area_struct *vma;
struct anon_vma *anon_vma;
//通过same_vma链表节点,将anon_vma_chain添加到vma->anon_vma_chain链表中;
struct list_head same_vma; /* locked by mmap_sem & page_table_lock */
//通过rb红黑树节点,将anon_vma_chain添加到anon_vma->rb_root的红黑树中;
//该rb为anon_vma->rb_root中的一个节点,该节点存储自己的avc信息
struct rb_node rb; /* locked by anon_vma->rwsem */
unsigned long rb_subtree_last;
#ifdef CONFIG_DEBUG_VM_RB
unsigned long cached_vma_start, cached_vma_last;
#endif
};
ps:
上述整个流程如图1所示。
由于linux进程具有写时复制技术,子进程在创建时共享其父进程的匿名页;只有当子进程对匿名页进行写操作时,子进程才会新分配一个物理页,并将原先匿名页的数据copy到新创建的物理页中,最后子进程通过虚拟地址向新创建的物理页进行写操作。
下面模拟linux中一个进程创建,子进程创建,孙进程创建这些场景中匿名页反向映射相关结构体的变化过程:
进程A被创建,该进程的匿名映射VMA0通过page fault分配第一个物理页。此时进程A中VMA0,AV0和AVC0这3个数据结构的关系图如图2所示:
图2中进程A新分配的物理页pi会映射到进程A中VMA0某一段对应的虚拟地址上。因为该页为匿名页为了维护反向映射的机制,物理页pi的描述符
struct page中的mapping成员指向AV0,struct page的index成员是pi物理页对应虚拟地址在VMA0中的偏移。
为了维护反向映射的机制,进程A在建立虚拟地址和pi物理地址的过程中会创建AV0和AVC0。对于AV0,他是进程A中的一个strutc anon_vma,因为
AV0此处是一个顶级结构所以stuct anon_vma的root和parent成员指向它自身。AV0该数据结构通过AVC0的中转来管理与pi物理页有关的VMA结构(此时只
有VMA0与pi有关)。因为AVC0是一个struct anon_vma_chain结构体,他的anon_vam和vma分别指向AV0和VMA0,所以AVC0是链接AV0和VMA0的桥梁.此外
AVC0插入到AV0的b_root成员指向的红黑树中,同时也插入到VMA0的anon_vma_chain成员指向的链表中。
进程A执行fork创建一个子进程B(VMA1是为子进程B创建的虚拟地址空间段),此时进程A,B中VMA,AVC和AVC3者间的关系如图3所示:
进程A通过fork创建子进程B,则子进程B会将父进程的VMA0复制到子进程中记为VMA1,这时子进程会为自己的VMA1分配一个自己的AV记为AV1。接下来就是为进程A,B相关的AVC,AV和VMA建立连接关系:
通过上面建立的关系可以达到如下目的:
父进程A还可以创建其他的子进程,新建进程与父进程间的关系和进程B基本一样。只需要注意父进程A每创建一个新的子进程,A进程的AV0的rb_root指向的红黑树就必定会插入一个起桥梁作用的AVC,用该AVC来将新建子进程的VMA和AV0连接起来(该新建子进程的VMA是VMA0的一个拷贝)
若子进程B再通过fork创建一个进程C,则父进程A,子进程B和孙子进程C够成的3层AV和VMA的关系结构如图4所示。
子进程B创建孙子进程C,在执行fork时相关操作同2基本一致,只是需要多建立一层爷孙关系,具体步骤如下:
通常linux用户态进程用malloc给自己分配一段内存时(匿名页),会先给进程分配一个新的VMA结构体用于管理虚拟段地址,此时并不会立即给新分配的VMA关联物理内存,只有当进程需要操作新分配的虚拟地址时,cpu才会触发"page fault"机制,该机制通过调用do_anonymous_page函数给新VMA分配一段物理内存并将VMA和新分配的物理内存建立映射关系。函数源码如 下所示。
static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pte_t *page_table, pmd_t *pmd,
unsigned int flags)
{
...
...
...
/* Allocate our own private page. */
//为进程地址空间准备struct anon_vma数据结构和struct anon_vma_chain链表。
if (unlikely(anon_vma_prepare(vma)))
goto oom;
//从HIGHMEM区域分配一个zeroed页面
page = alloc_zeroed_user_highpage_movable(vma, address);
if (!page)
goto oom;
...
...
...
inc_mm_counter_fast(mm, MM_ANONPAGES);
page_add_new_anon_rmap(page, vma, address);
mem_cgroup_commit_charge(page, memcg, false);
lru_cache_add_active_or_unevictable(page, vma);
...
...
...
}
page fault在建立VMA和物理内存页的映射关系时会用到调用一个名为anon_vma_prepare函数,用此函数来给进程虚拟地址空间中的VMA分配AV,以及利用AVC结构以红黑树和链表的 方式将VMA和AV关联起来。函数调用流程如图5所示。
anon_vma_prepare函数具体代码实现如下:
int anon_vma_prepare(struct vm_area_struct *vma)
{
struct anon_vma *anon_vma = vma->anon_vma;
struct anon_vma_chain *avc;
might_sleep();
if (unlikely(!anon_vma)) {
struct mm_struct *mm = vma->vm_mm;
struct anon_vma *allocated;
//分配一个struct anon_vma_chain结构。
avc = anon_vma_chain_alloc(GFP_KERNEL);
if (!avc)
goto out_enomem;
//是否可以和前后vma合并
anon_vma = find_mergeable_anon_vma(vma);
allocated = NULL;
//如果无法合并,则重新分配一个结构体
if (!anon_vma) {
anon_vma = anon_vma_alloc();
if (unlikely(!anon_vma))
goto out_enomem_free_avc;
allocated = anon_vma;
}
anon_vma_lock_write(anon_vma);
/* page_table_lock to protect against threads */
spin_lock(&mm->page_table_lock);
if (likely(!vma->anon_vma)) {
//建立struct vm_area_struct和struct anon_vma关联
vma->anon_vma = anon_vma;
//建立struct anon_vma_chain和其他结构体的关系。
anon_vma_chain_link(vma, avc, anon_vma);
/* vma reference or self-parent link for new root */
anon_vma->degree++;
allocated = NULL;
avc = NULL;
}
spin_unlock(&mm->page_table_lock);
anon_vma_unlock_write(anon_vma);
if (unlikely(allocated))
put_anon_vma(allocated);
if (unlikely(avc))
anon_vma_chain_free(avc);
}
return 0;
out_enomem_free_avc:
anon_vma_chain_free(avc);
out_enomem:
return -ENOMEM;
}
当通过AVC建立了VMA和AV的关联后,还需要将物理页描述符struct page和其对应的AV关联起来,这样才能打通整个反向映射流程。执行完page_add_new_anon_rmap函数后,我们就可以通过物理页描述符struct page的mapping成员找到对应的AV,进而找到AV红黑树下的所有VMA。(每个VMA可以通过相关方式获取到物理页对应进程pid号和进程对应的虚拟地址)。
page_add_new_anon_rmap函数源码:
void page_add_new_anon_rmap(struct page *page,
struct vm_area_struct *vma, unsigned long address)
{
VM_BUG_ON_VMA(address < vma->vm_start || address >= vma->vm_end, vma);
//设置PG_SwapBacked表示这个页面可以swap到磁盘。
SetPageSwapBacked(page);
//设置_mapcount引用计数为0
atomic_set(&page->_mapcount, 0); /* increment count (starts at -1) */
if (PageTransHuge(page))
__inc_zone_page_state(page, NR_ANON_TRANSPARENT_HUGEPAGES);
__mod_zone_page_state(page_zone(page), NR_ANON_PAGES,
hpage_nr_pages(page));
//将page->mapping指向vma对应的AV
__page_set_anon_rmap(page, vma, address, 1);
}
__page_set_anon_rmap函数源码
static void __page_set_anon_rmap(struct page *page,
struct vm_area_struct *vma, unsigned long address, int exclusive)
{
struct anon_vma *anon_vma = vma->anon_vma;
BUG_ON(!anon_vma);
//当前页是否有为匿名页
if (PageAnon(page))
return;
/*
* If the page isn't exclusively mapped into this vma,
* we must use the _oldest_ possible anon_vma for the
* page mapping!
*/
if (!exclusive)
anon_vma = anon_vma->root;
anon_vma = (void *) anon_vma + PAGE_MAPPING_ANON;
//mapping指定页面所在的地址空间,这里指向匿名页面的地址空间数据结构struct anon_vma
page->mapping = (struct address_space *) anon_vma;
page->index = linear_page_index(vma, address);
}
通过page_add_new_anon_rmap函数来关联匿名页描述符struct page和AV,最终得到struct page和AV的映射关系图如图6所示。
**ps:**anon_vma_prepare函数会在内存管理机制中的很多流程中使用,本小节只对缺页异常处理中匿名页这一块的流程做了分析。下面列出该函数重要的应用。
通过面的介绍我们了解了反向映射的基本原理,以及父子进程间VMA,VAC和VA间得连接关系。下面我们讲解如何利用反向映射相关结构体来实现子进程创建时匿名页的写时复制机制。
一个进程A有一个匿名VMA1,当进程A访问该VMA1时,出现page fault.于是进入内核态为该VMA1分配一段物理内存记为pi,并建立VMA1与pi的映射。关系图如下:
进程A通过fork创建进程B时,linux不会为子进程B创建新的物理内存,而是公用父进程的物理内存。此时A,B进程共享的物理内存被设定为只读权限。如图6所示B进程和A进程共享pi物理内存,在进程B中VMA1虚拟内存段指定的虚拟地址映射到物理内存页pi,VMA1通过 AVC_0-1与进程A的AV0建立连接关系。此时物理页pi在进程B中的反向映射链为:
AV0—>AVC_0-1—>VMA1。虽然进程B此时对于VMA1有自身的AV结构AV1,但此时AV1未与任何物理内存中的页建立连接关系。
若进程A或进程B对物理页pi进行写操作时,因为B进程对物理页pi只有读权限,写操作会使CPU会触发 “page fault” 的错误,从而调用do_page_fault()函数执行do_wp_page()操作.do_wp_page()函数会先进行一些安全监测操作,然后调用__do_wp_page()函数做复制操作具体步骤如下:
上述3步阐述了linux子进程创建如何利用反向映射相关结构体实现写时复制(COW)技术,最终父子进程中与反向映射相关结构体间的连接关系如图7所示。
同匿名页一样,可能会有多个进程的VMA同时共享一个文件映射页。而进程文件页的反向映射是通过一个与文件相关的结构address_space来进行维护的。
若某个物理页是文件页,则该页的结构描述符struct page的mapping成员指向一个address_space结构体。了解page cache的都知道address_space结构中的page_tree成员指向的基数树用于维护并存储该文件特定区域的文件缓存页。而在文件页的反向映射中address_space结构体的i_mmap成员指向的的是一个rbtree(老版本是priority search tree,由于实现复杂被rbtree替代),该rbtree存储着共享该文件页的所有进程的VMA。所以文件页的反向映射流程如图8所示。
由图8可知文件页的结构描述符struct page的mapping成员指向adress_space,struct address_space的i_mmap成员指向一个rbtree的树根,因为一个共享文件页会被映射到多个进程的VMA中,因此所有的这些VMA都会被插入到上述rbtree树中。最后只需将struct page的index成员和红黑树中每个节点存储的VMA数据相结合,os就能获取到所有映射了该文件页的进程pid和文件页在对应进程中的虚拟地址。到此文件页反向映射流程全部打通。
ps:
文件页类型:
如何定位某文件页首地址在对应文件的偏移量和在某一进程中映射的的虚拟地址?(计算流程如图9所示)
在文件中的偏移量为struct page->index
某一进程中映射的的虚拟地址记为viraddress,通过逆向映射计算出该文件页在该进程中对应的虚拟地址段结构描述符记为VMA。则
viraddress = VMA->vm_start + page->index - VMA->vm_pgoff
KSM机制是linux内核将内容相同的匿名页面进行合并,将映射到这个页面的页表项标记为只读,然后释放原来的页表,来达到节省大量内存的目的。KSM机制中合并的只读页面被称为KSM页,根据KSM页的特性可知该页属于匿名页(KSM页是匿名页的子集,在linux内核中KSM页的页描述符struct page中的mapping成员第0位和第1位都被置位)。linux中KSM实现详细介绍可参考https://www.cnblogs.com/arnoldlu/p/8335541.html#scan_get_next_rmap_item或https://zhuanlan.zhihu.com/p/102469328。
在KSM机制中会维护两颗红黑树,分别为stable tree和unstable tree.其中stable tree中的每个节点stable_node是用来维护的KSM机制中的KSM页(简称kpage).对于每个kpage都有一个rmap_item结构体来描述它的反向映射,其中rmap_item结构中的anon_vma成员是一个struct anon_vma结构体.拿到kpage对应的ano_vma数据,我们就可以向匿名页反向映射原理一样,从anon_vma的rb_root成员指向的红黑树中找到映射该kpage物理页所有的vma,即找到所有映射该kpage页的进程和进程中对应的虚拟地址。
struct rmap_item {
struct rmap_item *rmap_list
union {
//当rmap_item加入stable树时,指向VMA的anon_vma数据结构。
struct anon_vma *anon_vma; /* when stable */
#ifdef CONFIG_NUMA
int nid; /* when node of unstable tree */
#endif
};
//进程的struct mm_struct数据结构
struct mm_struct *mm;
unsigned long address; /* + low bits used for flags below */
unsigned int oldchecksum; /* when unstable */
union {
struct rb_node node; /* when node of unstable tree */
struct { /* when listed from stable tree */
struct stable_node *head;
struct hlist_node hlist;
};
};
};
ps:一个kpage会存储在一个stable tree的。该树的每个节点由struct stable_node结构体来描述
struct stable_node {
union {
struct rb_node node; /* when node of stable tree */
struct { /* when listed for migration */
struct list_head *head;
struct list_head list;
};
};
/*hlist是一个双向链表,用于存储所有与kpage相关联的rmap_item数据。因为kpage可能是多个用户或多个进程共享的物理
*页面,因此在反向映射过程中kpage在不同的进程的VMA中虚拟地址偏移是不一样的。kpage对每个不同进程的反向映射都会
*维护一个特定的rmap_item结构。而kpage在指定进程中的vma的虚拟地址偏移量是该指定进程对应的
*rmap_item->address(在非KSM匿名页中此偏移量是page->index).
*/
struct hlist_head hlist;
unsigned long kpfn;
#ifdef CONFIG_NUMA
int nid;
#endif
};
在linux内核中如果一个page被多个虚拟地址(虚拟页)映射,我们可以通过Rmap Walk机制对该page的所有vma进行遍历。该机制通过struct rmap_walk_control来进行控制,需要
//mm/rmap.h
/*
* rmap_walk_control: To control rmap traversing for specific needs
*
* arg: passed to rmap_one() and invalid_vma()
* rmap_one: executed on each vma where page is mapped
* done: for checking traversing termination condition
* anon_lock: for getting anon_lock by optimized way rather than default
* invalid_vma: for skipping uninterested vma
*/
struct rmap_walk_control {
void *arg;
int (*rmap_one)(struct page *page, struct vm_area_struct *vma,
unsigned long addr, void *arg);
int (*done)(struct page *page);
struct anon_vma *(*anon_lock)(struct page *page);
bool (*invalid_vma)(struct vm_area_struct *vma, void *arg);
};
Rmap Walk机制实现函数
//mm/rmap.c
int rmap_walk(struct page *page, struct rmap_walk_control *rwc)
{
if (unlikely(PageKsm(page)))
return rmap_walk_ksm(page, rwc);
else if (PageAnon(page))
return rmap_walk_anon(page, rwc, false);
else
return rmap_walk_file(page, rwc, false);
}
Rmap Walk机制实现流程图(匿名页):
知识来源:
https://www.dazhuanlan.com/2019/11/14/5dcd1e7420eb8/
https://www.cnblogs.com/LoyenWang/p/12164683.html
https://www.sohu.com/a/294105390_467784
https://www.cnblogs.com/linhaostudy/p/10350326.html