linux内存地址空间[三]

linux内存地址空间[三]

Posted by Albert on August 17, 2020

Linux内存地址空间[三]

​ 原文作者兰新宇, 原文地址, 本文仅在原文的基础上进行了部分格式调整,对部分自己感觉文字较多的地方配上图片,以便于自己后续能够更好的理解。既然已经有人有写得好的文章了,那么自己也就懒得从头写了。要站在巨人的肩膀上(其实是因为我懒)。

一、mm_struct

上文vm_area_struct中还有一个vm_mm没讲到,而这个vm_mm,则是联系vm_area_struct和它所属进程的关键纽带。它指向的是负责管理内存的mm_struct结构体,而这个mm_struct又可以从task_struct这个几乎记录了一个进程所有信息的结构体中获取。

img

​ 来看下struct mm_struct中与vma相关的元素有哪些:

struct mm_struct
{          
    struct vm_area_struct * mmap;                 
    struct rb_root mm_rb;    
    int map_count;   
    unsigned long total_vm;    
    struct vm_area_struct * mmap_cache;   
    unsigned long (*)()get_unmapped_area; 
    ...  
} 

​ 其中mmap指向vma链表的头节点,mm_rb指向vma红黑树的根节点。map_count是vma的总个数,total_vm是进程地址空间的总大小(以page为单位)。mmap_cache保存了上一次找到的vma,根据局部性原理,下一次要用到的vma正好是上次使用的vma的可能性是比较大的,因此使用find_vma()函数查找vma时,会首先从mmap_cache中找,找到了就直接返回。

vma = mm->mmap_cache;    
if (vma && vma->vm_end > addr && vma->vm_start <= addr)
    return vma;

​ 没找到再去红黑树里面找:

rb_node = mm->mm_rb.rb_node;    
vma = NULL;
while (rb_node) {        
    vma_tmp = rb_entry(rb_node, struct vm_area_struct, vm_rb);        
    if (vma_tmp->vm_end > addr) {            
        vma = vma_tmp;            
        if (vma_tmp->vm_start <= addr)                
            break;            
        rb_node = rb_node->rb_left;        
    } else            
        rb_node = rb_node->rb_right;    
}
if (vma)        
        mm->mmap_cache = vma;    
return vma;

​ 这种mmap_cache的命中率通常只有35%-50%,之后内核开发者又在此基础上,设计了新的VMA cache方案

​ 对vma的操作除了查找,还有增加和删除。加载一个动态链接库或者通过mmap创建映射时,都需要在进程地址空间中增加一个vma。具体过程是首先通过get_unmapped_area()找到虚拟地址空间中一块空闲且大小满足要求的区域(根据你上报的你家的人数,给你街道中一个住的下你家所有人的空房子),分配给新vma并设置其flag属性(限制你家对这个房子的使用,比如只能住,不能私自改建),返回该vma起始处的虚拟地址(告诉你这个房子的门牌号)。当然,你出于某种目的,也可以指定就街道上的某间房子(调用mmap()时指定参数addr),如果这间房子正好是空的,就可以分配给你。

if (addr) {
    addr = PAGE_ALIGN(addr);                 
    vma = find_vma(mm, addr);                  
    if (TASK_SIZE -len >= addr &&                      
        (!vma || addr + len <= vma->vm_start))                         
            return addr;              
}

​ 这里的房子有点特殊,街道上房间的总数是固定的,每个房间的大小是4平方米(页面大小4 KB),只要是相邻的空房间,就可以组成一个空房子。房间总数也是有限的(3 GB内存的话差不多是75万个房间),你来晚了,或者你狮子大开口,要一个50万房间的空房子(比如通过malloc(2G)),那就有可能出现分配不到的情况(可用虚拟地址空间不足)。

​ 如果新建的vma和它地址上紧挨着的vma有相同的属性,且基于相同的映射对象(比如是同一个文件),则还会产生vma的合并(上下两层楼打通,做成一个跃层)。减少vma的数量有利于减轻内核的管理工作量,降低系统开销。如果没有发生合并,则需要调用insert_vm_struct( )在vma链表和vma红黑树中分别插入代表新vma的节点(给你家的房子被街道办事处登记,方便日后管理)。

​ 要注意的是,房子的分配是按照你上报的人数,但具体给你几个房间的钥匙(分配几个物理页面),取决于你家实际住进来的人数,比如你申请的是10个房间,但只住进来3个人,就只有3个房间的钥匙,剩下的钥匙等真正有人搬进来再给,房间资源有限,占着不住不是浪费么。分配的vma只是这段虚拟地址的使用权,而不是物理地址的使用权。

​ 那是不是我申请成功10个房间,就可以保证10个人都能住进来呢?这个嘛,街道(进程)最开始也是这样以为的,后来出现了房间申请成功,结果拿钥匙开不了门的情况,街道就向上级管理者(内核)反映啊,这才被告知了一个残酷的现实:除了本条街道,还有很多条其他街道,大家处在一个平行空间中(虚拟地址空间都是0~3 GB),这70多万个房间,其实是被所有街道共享的,谁先拿到一个房间的钥匙(使用物理页面),谁才真正拥有这个房间。

​ 一切都是假象……然后街道问上级:那你为啥一直允许我们街道上的人一口气申请那么多房间呢?上级若有所思的说:我当时设计这个制度啊,主要是考虑到很多人可能申请的多,但实际用不了那么多(比如malloc(2G),但实际只用了1 M),我也不知道实际谁会用的多一点,为了让资源(这些房间)得到最充分的利用,我只能先允许他们申请着。以后这种事情多了,大家也渐渐明白,别一下申请那么多,不合理的需求,到了上级那里是通不过的,这种申请超过实际可用物理内存的现象,被称为memory overcommit

​ 通过munmap()解除映射时,则需要在进程地址空间中删除对应的vma,并释放该vma占有的虚拟地址资源。

二、参考文档

理解LINUX的MEMORY OVERCOMMIT

understanding the linux kernel

professional linux kernel architecture

understanding the linux virtual memory manager