前言
rmap笔记。
一、kernel 2.6.0版本rmap(第一版rmap)
2.6.0版本的rmap的整体结构图如下所示。
2.6.0的rmap的主要函数是:page_add_rmap和try_to_unmap_one
page_add_rmap:其主要功能是将存储pte表项的虚拟地址放在struct pte_chain的ptes数组中,存放ptep时按照n-1、n-2、…、1、0这种次序存放,next_and_idx则存放下一个struct pte_chain的首地址,并记录其可用的数组下标(NRPTE-1)。
struct pte_chain {
unsigned long next_and_idx;
pte_addr_t ptes[NRPTE];
} ____cacheline_aligned;
该函数在什么时候会调用page_add_rmap:
例如在COW时:do_page_fault->__do_page_fault->handle_mm_fault->handle_pte_fault->do_wp_page->page_add_rmap函数,首先需要alloc_page申请一个new page给write者使用。page_table = pte_offset_map(pmd, address)返回的是一个虚拟地址,表示存储pte表项的page frame的虚拟地址。再调page_add_rmap将new_page和page_table关联起来存储在pte_chain数组上,这个关系在try_to_unmap_one里面需要使用。
static inline pte_t *pmd_page_kernel(pmd_t pmd)
{
unsigned long ptr;
把页目录项中存的页表地址先去掉低PTRS_PER_PTE * PAGE_SIZE位属性,然后通过__va转换成线性地址
ptr = pmd_val(pmd) & ~(PTRS_PER_PTE * sizeof(void *) - 1);
ptr += PTRS_PER_PTE * sizeof(void *);
return __va(ptr);页表的虚拟地址,因为内核的物理地址转虚拟地址只需要加上PAGE_OFFSET即可
}
#define pte_offset_map(dir,addr) (pmd_page_kernel(*(dir)) + __pte_index(addr))
static int do_wp_page(struct mm_struct *mm, struct vm_area_struct * vma,
unsigned long address, pte_t *page_table, pmd_t *pmd, pte_t pte)
{
……
/*
* Re-check the pte - we dropped the lock
*/
spin_lock(&mm->page_table_lock);
page_table = pte_offset_map(pmd, address);#这个page_table是一个虚拟地址
if (pte_same(*page_table, pte)) {
if (PageReserved(old_page))
++mm->rss;
page_remove_rmap(old_page, page_table);
break_cow(vma, new_page, address, page_table);
pte_chain = page_add_rmap(new_page, page_table, pte_chain);##page_table是一个虚拟地址
lru_cache_add_active(new_page);
/* Free the old page.. */
new_page = old_page;
}
……
}
pgtable_add_rmap:主要功能是将用户态虚拟address和mm存存储在pte页表的struct page结构体里面。
其中进程的mm_struct直接记录在page->mapping,而address的高bit则记录在page->index(通过ptep_to_address反向计算出address)。
pgtable_add_rmap:存储pte表项的struct page的page->index = address & ~((PTRS_PER_PTE * PAGE_SIZE) - 1);
ptep_to_address:根据pte表项的struct page存储address的高bits,以及ptep(存储pte表项的地址)可以计算出address & (~4095)
首先根据ptep则((unsigned long)ptep & ~PAGE_MASK) =ptep & 0xFFFFF000就得到了pte表项在pmd的偏移,再乘以PTRS_PER_PTE就是low bits也就得到了address & ( ~4095 )。通过address就能知道其属于哪个vma了进而能接触pte映射。
#define PTRS_PER_PTE 512
#define PTRS_PER_PMD 1
#define PTRS_PER_PGD 2048
#存储page->index
#参数struct page* page指的是pte的page frame
#参数struct mm_struct* mm是进程的mm
#参数unsigned long address是进程需要do_page_fault的向下PAGE_SIZE对齐的虚拟地址
static inline void pgtable_add_rmap(struct page * page, struct mm_struct * mm, unsigned long address)
{
#ifdef BROKEN_PPC_PTE_ALLOC_ONE
/* OK, so PPC calls pte_alloc() before mem_map[] is setup ... ;( */
extern int mem_init_done;
if (!mem_init_done)
return;
#endif
page->mapping = (void *)mm;
page->index = address & ~((PTRS_PER_PTE * PAGE_SIZE) - 1);虚拟地址address的高bit位存在对应pte页表的struct
page
inc_page_state(nr_page_table_pages);
}
#define PAGE_SHIFT 12
#define PAGE_SIZE (1UL << PAGE_SHIFT)
#define PAGE_MASK (~(PAGE_SIZE-1))
#获取page->index
#参数pte_t* ptep在arm32上是物理地址
static inline unsigned long ptep_to_address(pte_t * ptep)
{
struct page * page = kmap_atomic_to_page(ptep);
unsigned long low_bits;
low_bits = ((unsigned long)ptep & ~PAGE_MASK) * PTRS_PER_PTE;#将ptep的低12bit清除,这是因为此时得到的pfn<<12, 再乘以PTRS_PER_PTE
return page->index + low_bits;
}
try_to_unmap_one:解除page和所有pte表项的映射关系。
在try_to_unmap_one时,由于已经知道ptep,那么怎么获取其映射的address呢?
由于ptep是指向pte表项的,那么pte表项也是由一个page里面分出来,那么pte也有起始位置。
static int try_to_unmap_one(struct page * page, pte_addr_t paddr)
{
pte_t *ptep = rmap_ptep_map(paddr);/*在page_add_rmap时会将ptep转为paddr,故而这里转回来,对arm实际就是空的*/
unsigned long address = ptep_to_address(ptep);/*根据ptep可得到其映射的虚拟地址*/
struct mm_struct * mm = ptep_to_mm(ptep);/*page->mapping 表示就是struct mm_struct*/
struct vm_area_struct * vma;
pte_t pte;
int ret;
if (!mm)
BUG();
/*
* We need the page_table_lock to protect us from page faults,
* munmap, fork, etc...
*/
if (!spin_trylock(&mm->page_table_lock)) {锁住这个page_table,担心其他路径会修改ptep表项
rmap_ptep_unmap(ptep);
return SWAP_AGAIN;
}
/* During mremap, it's possible pages are not in a VMA. */
vma = find_vma(mm, address);
if (!vma) {
ret = SWAP_FAIL;
goto out_unlock;
}
/* The page is mlock()d, we cannot swap it out. */
if (vma->vm_flags & VM_LOCKED) {
ret = SWAP_FAIL;
goto out_unlock;
}
/* Nuke the page table entry. */
flush_cache_page(vma, address);
pte = ptep_get_and_clear(ptep);/*清除pte表项*/
flush_tlb_page(vma, address);/*刷新tlb表项*/
if (PageSwapCache(page)) {
/*
* Store the swap location in the pte.
* See handle_pte_fault() ...
*/
swp_entry_t entry = { .val = page->index };
swap_duplicate(entry);
set_pte(ptep, swp_entry_to_pte(entry));
BUG_ON(pte_file(*ptep));
} else {
unsigned long pgidx;
/*If a nonlinear mapping then store the file page offset in the pte.*/
pgidx = (address - vma->vm_start) >> PAGE_SHIFT;
pgidx += vma->vm_pgoff;
pgidx >>= PAGE_CACHE_SHIFT - PAGE_SHIFT;
if (page->index != pgidx) {
set_pte(ptep, pgoff_to_pte(page->index));
BUG_ON(!pte_file(*ptep));
}
}
/* Move the dirty bit to the physical page now the pte is gone. */
if (pte_dirty(pte))
set_page_dirty(page);
mm->rss--;
page_cache_release(page);
ret = SWAP_SUCCESS;
out_unlock:
rmap_ptep_unmap(ptep);
spin_unlock(&mm->page_table_lock);
return ret;
}
二、kernel 2.6.33 版本rmap(第二版rmap)
由于kernel 2.6.0通过在struct page增加一个union的方式来实现,这样导致所有struct page都增加了一个union的消耗,对于内存消耗过多。另外由于每个pte_chain都是一个数组链表,也有不小内存消耗,在fork的时候需要copy这么一份数据,这些动作导致fork出现了性能问题。于是乎有新的一版rmap,消除掉pte_chain和增加struct page成员的方式,新版本在2.6.7就有了,本笔记采用2.6.33版本来学习。
kernel 2.6.33
do_anonymous_page
->anon_vma_prepare 申请一个anon_vma结构体,将vma挂入anon_vma的双向链表
->page_add_new_anon_rmap
1,SetPageSwapBacked,设置page可以swap交换出去。
2,增加_mapcount引用计数。
3,__page_set_anon_rmap让anon_vma指针加1,表示是匿名映射,设置page->mapping为anon_vma,
page->index = vma->vm_pgoff + (address - vma->vm_start)>> PAGE_SHIFT;
page->mapping:
1,等于NULL:表示该page frame不再内存中,而是被swap out到磁盘去了。
2,不等于NULL:
当bit[0]值为1时,表示page->mapping是指向struct anon_vma,含义是匿名映射
当bit[0]值为0时,表示page->mapping是指向struct address_space,含义是文件映射
page->index:表示address在vma的第几个page(linear_page_index函数计算)
其中vma->vm_pgoff有几种含义,
1,对于文件映射表示该vma相对于文件起始地址的偏移量(单位page)。
2,对于匿名MAP_SHARED映射表示0。
3,对于匿名MAP_PRIVATE映射表示vm_addr/PAGE_SIZE(vm_pgoff只有在匿名页面的生命周期中只有RMAP会使用)。
/**
* page_add_new_anon_rmap - add pte mapping to a new anonymous page
* @page: the page to add the mapping to
* @vma: the vm area in which the mapping is added
* @address: the user virtual address mapped
*
* Same as page_add_anon_rmap but must only be called on *new* pages.
* This means the inc-and-test can be bypassed.
* Page does not have to be locked.
*/
void page_add_new_anon_rmap(struct page *page,
struct vm_area_struct *vma, unsigned long address)
{
VM_BUG_ON(address < vma->vm_start || address >= vma->vm_end);
SetPageSwapBacked(page);设置这个page可以交换到磁盘
atomic_set(&page->_mapcount, 0); /* increment count (starts at -1) 增加_mapcount计数,又有一个进程用了该page frame*/
__inc_zone_page_state(page, NR_ANON_PAGES);增加zone和vm_stat匿名页的计数
__page_set_anon_rmap(page, vma, address);
if (page_evictable(page, vma))
lru_cache_add_lru(page, LRU_ACTIVE_ANON);
else
add_page_to_unevictable_list(page);
}
static void __page_set_anon_rmap(struct page *page,
struct vm_area_struct *vma, unsigned long address)
{
struct anon_vma *anon_vma = vma->anon_vma;
BUG_ON(!anon_vma);
anon_vma = (void *) anon_vma + PAGE_MAPPING_ANON;
page->mapping = (struct address_space *) anon_vma;
page->index = linear_page_index(vma, address);计算page->index,即address相对于vma的偏移量(单位是PAGE_SIZE)
}
在页面回收或页面迁移时,例如在shrik_page_list->try_to_unmap->try_to_unmap_anon
try_to_unmap_anon
:
根据struct page的mapping得到anon_vma结构体,这个anon_vma上挂入所有映射到该page的vma。之后就是遍历所有的vma。
vma_address
:
根据page->index和vma->vm_pgoff,vma->vm_start可以得到address地址。由于page->index表示address相对于vma->vm_start偏移多少
个page_size,那么address = (page->index - vma->vm_pgoff) << PAGE_SHIFT + vma->vm_start。得到了address那么就可以得到pte表
项,并且可以取消pte表项的映射,具体取消映射是在try_to_unmap_one。
static int try_to_unmap_anon(struct page *page, enum ttu_flags flags)
{
struct anon_vma *anon_vma;
struct vm_area_struct *vma;
int ret = SWAP_AGAIN;
anon_vma = page_lock_anon_vma(page);
if (!anon_vma)
return ret;
list_for_each_entry(vma, &anon_vma->head, anon_vma_node) {
unsigned long address = vma_address(page, vma);
if (address == -EFAULT)
continue;
ret = try_to_unmap_one(page, vma, address, flags);
if (ret != SWAP_AGAIN || !page_mapped(page))
break;
}
page_unlock_anon_vma(anon_vma);
return ret;
}
/*
* At what user virtual address is page expected in @vma?
* Returns virtual address or -EFAULT if page's index/offset is not
* within the range mapped the @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);
unsigned long address;
address = vma->vm_start + ((pgoff - vma->vm_pgoff) << PAGE_SHIFT);
if (unlikely(address < vma->vm_start || address >= vma->vm_end)) {
/* page should be within @vma mapping range */
return -EFAULT;
}
return address;
三、kernel 5.0 版本rmap(第三版rmap)
对于第二版本的rmap,由于vma->anon_vma和struct page的mapping均直接指向anon_vma结构体,anon_vma结构体有一个spin_lock,那么在进程进行fork时,会导致父进程和子进程的vma,struct page均指向同一个anon_vma,也就是不管进程有多少个,vma有多少个,整个系统同一个祖宗分支的进程大家共享只有一个anon_vma,那么这样就会导致一个问题。
假若有1000个子进程,每个子进程有1个vma映射了1000个pages,那么就有100万个page的page->mapping同时指向anon_vma,而anon_vma有一个spinlock_t lock用于Serialize access to vma list 。那么就会造成非常激烈的锁竞争。
第三版本rmap为了解决锁竞争激烈的问题,如何解决,让每个进程都有一份anon_vma,这样就能保证抢锁是进程内部的动作,而不是整个系统的。
rmap应用于文件映射、匿名映射、KSM映射,我们这里仅讨论匿名映射。
引用
https://blog.51cto.com/u_15015138/2557286
https://blog.csdn.net/u010923083/article/details/116456497