Linux内存地址空间[三]
原文作者兰新宇, 原文地址, 本文仅在原文的基础上进行了部分格式调整,对部分自己感觉文字较多的地方配上图片,以便于自己后续能够更好的理解。既然已经有人有写得好的文章了,那么自己也就懒得从头写了。要站在巨人的肩膀上(其实是因为我懒)。
一、mm_struct
上文vm_area_struct中还有一个vm_mm没讲到,而这个vm_mm,则是联系vm_area_struct和它所属进程的关键纽带。它指向的是负责管理内存的mm_struct结构体,而这个mm_struct又可以从task_struct这个几乎记录了一个进程所有信息的结构体中获取。
来看下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占有的虚拟地址资源。
二、参考文档
understanding the linux kernel
professional linux kernel architecture
understanding the linux virtual memory manager