内存管理(1)---匿名页面

一、匿名页面
这些内存页面保存了一些通常所说的机动性最强的内容,或者可以认为是银行的活期存款,这些内存可以随时被使用,随时被归还。例如用户通过malloc-->>mmap申请的内存,或者通过brk/sbrk扩大的堆空间。相对于mmap文件、文件系统元数据之类的内容,这些空间对用户来说最为顺手,也最为常见。但是管理起来也比较复杂,因为这里涉及到这些页面如果被同时使用了很多,系统内存负载将会变的很重,此时系统的虚拟内存就要起作用,将这些页面换到慢速二级存储设备,例如硬盘上。但是此时同样会涉及到一个问题,因为一个页面可以被几个进程共享,例如fork出的子进程和父进程可能会共享相同的页面,此时将一个页面swap到二级存储设备上之后,此时每个进程的pte项都需要做相应的一次性修改。这些pte项指向的页面即将被周转做其它用处,而他们指向的真正数据内容将会被虚拟到二级存储设备上。
这里有两个需要直面的基本问题:
第一个问题就、是如何找到一个页面是被哪些(所有的)进程的pte指向。找到这些之后,需要将这些pte逐一修改,并让它们指向换出的二级存储设备上。
第二个问题是将哪些页面换出去?系统中可能有很多的匿名页面,可能有些正在镁光灯下呼风唤雨,有些可能在角落里被人默默遗忘。比如说当前用户正在打WAR3,那么可能这个程序使用的大量内存资源都会在一段时间内别频繁使用(一局比赛正常来说10--40mins);相反的,一个周期性运行的后台任务可能只有每隔一段时间才运行一段时间,例如某些客户端的后台更新任务。此时一个优秀的系统应该能够准确的将长时间不用的页面置换出去,最近长被使用的保留在内存中,这就是Latest Recently Used算法(最近最少被使用),也就是内核中最为常见的LRU缩写的来源。
二、匿名空间的申请
和IP地址空间一样,一个进程的地址空间也是一个重要的资源。最近据说IPV4地址已经告罄,就像之前大家觉得IPV4地址足够使用一样,在没有使用大型软件之前,可能也是觉得4G逻辑地址空间是足够使用的,但是我还是有幸遇到了地址空间被用完而导致的分配失败问题,作为无意义的测试,可以尝试不断mmap,直到自己地址被使用完。
废话了一段,大致的意思是想说,地址作为一种资源,你如果想使用的话,就必须先申请。你申请到了也可以不用,但是系统不能再将这些地址分配其它人,除非你通过munmap告诉系统这个地址空间已经使用完毕,此时才可以再次分配给其它人。虽然说大部分用户态编程都是使用malloc来分配,但是malloc的底层在linux下正是通过mmap来实现,通过strace可以明显的看到对于mmap的系统调用:
[root@Harry malloc]# cat malloc.c 
#include <stdlib.h>
int main()
{
    printf("Hello ");这里添加两个打印是为了在strace中起到定界的作用,从而可以知道malloc使用的系统调用
     malloc(0x100000);
    printf("world ");
}
[root@Harry malloc]# strace ./a.out 
write(1, "Hello ", 6Hello
)                  = 6
mmap2(NULL, 1052672, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb77af000
write(1, "world ", 6world
)                  = 6
exit_group(6)                           = ?
内核中该系统调用的实现位于linux-2.6.21archi386kernelsys_i386.c
sys_mmap2-->>do_mmap_pgoff
……
    addr = get_unmapped_area(file, addr, len, pgoff, flags);这里是逻辑地址分配,也就是从4GB(当然用户态通常只能使用3G,内核使用1G)地址空间申请长度为len的地址空间。如果内存压力已经很大,这里也可能返回失败。
……
    vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);开始分配vma结构,也就是内核中表示某个内存区间已经被占用的控制结构,所有的这种结构表示了一个进程的逻辑地址空间的占用情况。
……

    if (file) {这个分支对于匿名页面映射来说是不会走到的,但是对于其他的文件mmap来说非常之重要,而且从该流程中可以看看到如何确定一个页面是在一个匿名映射地址空间中,例如通过mmalloc分配的地址空间。
        error = -EINVAL;
        if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
            goto free_vma;
        if (vm_flags & VM_DENYWRITE) {
            error = deny_write_access(file);
            if (error)
                goto free_vma;
            correct_wcount = 1;
        }
        vma->vm_file = file;
        get_file(file);
        error = file->f_op->mmap(file, vma);这里调用文件使用的mmap函数指针,对于ext2文件系统,其mmap操作为generic_file_mmap函数,其中最为重要的操作就是安装vma->vm_ops成员,这个将会在按需调页中使用,ext2文件系统中该值初始化为generic_file_vm_ops。
        if (error)
            goto unmap_and_free_vma;
    } else if (vm_flags & VM_SHARED) {
三、匿名页面的分配
可以看到,mmap操作执行的实质性操作非常少,我们甚至认为它和文件系统中的open系统调用类似,它只是进行一些最为必要的基础性初始化工作,而真正的实质性操作都是通过read/write来执行的,只是内存的read/write内核执行是不需要用户态关系的,只要用户态访问或者写入了区间内容,那么这个按需调页就会被触发。
正如刚才所说,mmap只是完成了初始化工作,页面没有分配,而且这片内存使用的PTE也没有初始化,当之后真正使用这片地址的时候,将会触发访问异常,这些需要CPU硬件支持,对于常见的386系统,这个功能应该是在386时开始支持。
对于386系统,它的缺页异常处理函数在linux-2.6.21archi386kernel raps.c
set_intr_gate(14,&page_fault)设置访问异常的处理函数为page_fault

linux-2.6.21archi386kernelentry.S
KPROBE_ENTRY(page_fault)
    RING0_EC_FRAME
    pushl $do_page_fault
linux-2.6.21archi386mmfault.c
fastcall void __kprobes do_page_fault(struct pt_regs *regs, unsigned long error_code)
从这个函数开始,执行的相关调用链为
handle_mm_fault--->>>__handle_mm_fault--->>>handle_pte_fault,在该函数中
            if (vma->vm_ops) {由于内存页面中没有初始化vma的这个成员,所以不会走这个流程。
                if (vma->vm_ops->nopage)
                    return do_no_page(mm, vma, address,
                              pte, pmd,
                              write_access);
                if (unlikely(vma->vm_ops->nopfn))
                    return do_no_pfn(mm, vma, address, pte,
                             pmd, write_access);
            }
            return do_anonymous_page(mm, vma, address,对于malloc申请的页面将会进入这个分支。
                         pte, pmd, write_access);
**********************
static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,
        unsigned long address, pte_t *page_table, pmd_t *pmd,
        int write_access)
    if (write_access) {这里如果是写操作,此时就需要分配新的页面。
        /* Allocate our own private page. */
        pte_unmap(page_table);

        if (unlikely(anon_vma_prepare(vma)))该函数中可能会动态分配一个struct anon_vma结构,并将其地址赋值给struct vm_area_struct结构的struct anon_vma *anon_vma成员,明显地,这个结构是一个队列头结构,它引导一个vm_area_struct结构链表(通过vm_area_struct的struct list_head anon_vma_node;成员连接在一起)。至于什么样的vm_area_struct连接在一起,它们连接在一起有什么作用,将在之后说明。
            goto oom;
        page = alloc_zeroed_user_highpage(vma, address);这里开始分配真正的页面,也就是触发了按需页面分配的真正分配动作。
        if (!page)
            goto oom;

        entry = mk_pte(page, vma->vm_page_prot);
        entry = maybe_mkwrite(pte_mkdirty(entry), vma);

        page_table = pte_offset_map_lock(mm, pmd, address, &ptl);
        if (!pte_none(*page_table))
            goto release;
        inc_mm_counter(mm, anon_rss);
        lru_cache_add_active(page);将新分配的页面添加到cache链表中,这个可以为之后的页面LRU提供参考,当系统物理页面周转紧张而需要将一些页面swap到二级存储设备上的时候,可以参考这个链表中的内容。
        page_add_new_anon_rmap(page, vma, address);这里函数表示这个页面是被匿名映射了,并且初始化其映射个数为0(-1表示未映射),并且将页面结构中的page->mapping成员初始化为anon_vma_prepare函数中分配的anon_vma结构的地址在偏移一个字节。这样充分使用了地址的低两个bits,因为内核中结构的大小是4字节对齐的,所以正常情况下一个struct page中的mapping指向的地址应该是4字节对齐,它的低两个bit值为零。在判断一个page时候是匿名映射的时候,可以判断其struct page结构中的mapping最低一个bit是否为1,如果为1,则是匿名映射,并且将这个数值减一就可以得到所有映射了这个页面的vm_area_struct结构链表。当需要将这个页面swap出去的时候,就可以遍历这个链表中所有的vm_area_struct结构,并更新它们的pte结构。
四、匿名页面的换出
假设说同一个页面被不同进程的PTE指向,而此时又需要将这个页面swap到二级缓存中,此时就需要逐一修改所有进程指向该页面的pte,让他们更新到新的正确位置。但是既然是匿名页面,此时其它的进程如何能访问到呢?最为直观的就是当一个进程执行了fork的时候,也就是执行fork的时候,对应的函数调用结构为
do_fork--->>copy_process--->>>copy_mm--->>dup_mm--->>>dup_mmap--->>>anon_vma_link(tmp);
void anon_vma_link(struct vm_area_struct *vma)
{
    struct anon_vma *anon_vma = vma->anon_vma;在dup_mmap函数中可以看到,tmp被赋值为父进程的vm_area_struct,所以新的tmp结构的anon_vma和父进程指向相同的anon_vma结构。

    if (anon_vma) {
        spin_lock(&anon_vma->lock);
        list_add_tail(&vma->anon_vma_node, &anon_vma->head);这里通过vm_area_struct结构中嵌入的anon_vma_node结构将新的vm_area_struct结构添加到父进程anon_vma引导的链表中。
        validate_anon_vma(vma);
        spin_unlock(&anon_vma->lock);
    }
}
当需要把这个匿名页面swap出去,或者其它原因释放这个页面的时候,此时需要将所有映射入该页面的vm_area_struct的pte做相应的更新。
shrink_page_list--->>>try_to_unmap---->>>try_to_unmap_anon--->>>try_to_unmap_one
在该函数中,将会完成各个vm_area_struct结构中引用了该页面的PTE更新,
if (PageAnon(page)) {
        swp_entry_t entry = { .val = page_private(page) };
……
        set_pte_at(mm, address, pte, swp_entry_to_pte(entry));
至于其中的page_private成员,应该是在shrink_page_list函数中通过add_to_swap--->>>__add_to_swap_cache--->>>set_page_private(page, entry.val)函数设置的。对于所有使用了该页面的PTE的遍历则在try_to_unmap_anon函数中通过下面的循环完成
    list_for_each_entry(vma, &anon_vma->head, anon_vma_node) {
        ret = try_to_unmap_one(page, vma, migration);
        if (ret == SWAP_FAIL || !page_mapped(page))
            break;
    }
五、页面老化
对于选择什么样的页面换出的问题,在do_anonymous_page中通过lru_cache_add_active函数表示了自己愿意被老化,所以就添加到了系统中该页面所属的zone的active链表中:
lru_cache_add_active--->>>__pagevec_lru_add_active--->>>add_page_to_active_list(zone, page);
static inline void
add_page_to_active_list(struct zone *zone, struct page *page)
{
    list_add(&page->lru, &zone->active_list);将页面添加到一个区的活动页面链表中,之后页面回收或者swap的时候将从这个链表中进行操作。
    __inc_zone_state(zone, NR_ACTIVE);
}
在shrink_active_list
    while (!list_empty(&l_hold)) {
        cond_resched();
        page = lru_to_page(&l_hold);
……
        list_add(&page->lru, &l_inactive);这里将会把页面从活跃链表移动到非活跃链表中。
    }
在shrink_inactive_list--->>>shrink_page_list--->>try_to_unmap/pageout
来完成将非活跃链表中的页面从内存中移除,可能是放入swap区(对于那些使用匿名映射并且配置了swap),对于那些是文件内容在内存映射的页面,可以将修改内容写入( mapping->a_ops->writepage(page, &wbc))。

原文地址:https://www.cnblogs.com/tsecer/p/10486052.html