物理内存管理
物理内存管理是内核非常重要的核心模块。主要包括memblock初期分配器、伙伴系统以及SLAB分配器等三种技术手段。
在Linux内核启动初期使用memblock初期内存分配器对探测到的可用内存地址范围进行管理。这种内存分配器仅仅只为了满足系统启动时间内对内存页的简单管理,管理粒度较粗。
在系统启动起来后正常运行时采用的复杂一些但能高效管理4KB粒度页面的伙伴系统,是运行时的主要物理页内存分配器,对外通过alloc_pages作为申请内存的接口。伙伴系统实现了11个空闲内存块链表,分别是4KB、8KB、…、4MB。当内核需要申请内存的时候,伙伴系统会在自己数据结构中合适的空闲链表里快速找到可用的内存出来。
但即使伙伴系统管理粒度比memblock内存分配器要高很多,但最小粒度4KB对于可能频繁使用各种几十字节小对象的内核程序来说还是有点太大了。为了更高效地管理各种不同尺寸的内核对象的内存分配和释放。内核还在伙伴系统的基础上建了个SLAB内存分配器。该分配器只用于内存,不对用户程序开放。
为什么
free -m命令展示的总内存比dmidecode中输出的要少,少了的这些内存跑哪里去了?Linux并不会把全部的物理内存都提供给我们使用。Linux为了维护自身的运行,会需要消耗一些内存。在本文中我们介绍了kdump机制对内存的消耗,也介绍了内存的页管理机制对内存的占用。但实际上还以一些其他的消耗,例如NUMA机制中的node、zone的管理等等也都需要内存。 另外在本文中,我们也给大家介绍了memblock内存分配器。内核在启动检测到内存的地址布局后,会用这个布局来初始化memblock内存分配器。后面内核的kdump机制、页管理机制、NUMA初始化等再需要使用内存的时候,都是向memblock分配器来申请内存的。其中kdump机制大约需要几百MB,页管理机制中
struct page的开销大约也需要总内存的大约1.5%左右。 如果你通过free -m查看到可用内存比实际的物理内存小也丝毫不必感到奇怪。
内核是怎么知道某个内存地址范围是属于哪个NUMA node节点的呢?
同样还是需要依赖高级配置和电源接口之ACPI规范。在这个接口规范中的第17章中描述了NUMA相关的内容。在ACPI中定义了两个表,分别是:
- SRAT(System Resource Affinity Table):在这个表中表示的是CPU核和内存的关系图。包括有几个node,每个node里面有那几个CPU逻辑核,有哪些内存。
- SLIT(System Locality Information Table):在这个表中记录的是各个结点之间的距离。 有了这个规范,CPU读取这两个表就可以获得NUMA系统的CPU及物理内存分布信息。 最后我们再扩展提一点,NUMA特性对性能影响是较大的。在不少公司中,都对运行的服务进行了NUMA绑定。但也有不同的声音,认为NUMA可能在全局内存并未用尽的情况下出现内存分配错误,导致系统抖动。
第二章 进程如何使用内存
进程的运行中脱离不开对内存的分配和使用。进程在启动过程中对代码段、数据段的加载、栈的初始化都涉及对内存的申请和使用。另外我们开发者在程序运行中在堆中申请的各种变量也都依赖内存的分配。所以内存是进程的核心资源。
正因为内存的如此之重要,所以今天来深入地了解一下进程所使用的内存的底层的工作原理。同样,开篇我们还是以几个问题作为引入:
- 申请内存申请到的真的是物理内存吗?
- 对虚拟内存的申请如何转化为对物理内存的访问的?
top命令输出进程的内存指标中VIRT和RES分别是什么含义?- 堆栈的大小限制是多大?当堆栈发生溢出后应用程序会发生什么?
- 堆栈发生溢出后应用程序会发生什么?(注:原文重复,保留)
- 进程栈和线程栈是相同的东西吗?
- 你知道malloc大概是怎样工作的吗?
在学习完本章内容后,相信你会对以上问题有更深入的理解。好了,我们开始!
2.1 虚拟内存和物理页
在内存的使用中,一个非常重要的概念就是虚拟内存和物理内存的关系,理解清楚这二者的联系与区别非常的重要。因此我把它作为本章的第一个小节。我们先介绍下虚拟内存的管理,再介绍下它是如何和物理内存联系起来的。
2.1.1 虚拟地址空间
虚拟内存的管理是以进程为单位的,每个进程都有一个虚拟地址空间。在实现上,每个进程的task_struct都有一个核心对象 - mm_struct类型的mm。它代表的就是进程的虚拟地址空间。
在这个虚拟地址空间中,每一段已经分配出去的地址范围都是通过一个个虚拟内存区域 VMA(Virtual Memory Area)来表示,在内核中对应的结构体是vm_area_struct。其vm_start和vm_end表示启用的虚拟地址范围的开始和结束。
当进程运行一段时间后,可能会分配出去许多段地址范围。那就会有许多vm_area_struct对象的存在。
许多个vm_struct对象各自所指明已经分配出去的地址范围,加起来就形成了对整个虚拟地址空间的占用情况。当然了,内核会保证各个vm_area_struct对象之间的范围不存在交叉的情况出现。
// file:include/linux/sched.h
struct task_struct {
...
struct mm_struct *mm;
}
// file:include/linux/mm_types.h
struct vm_area_struct {
unsigned long vm_start;
unsigned long vm_end;
...
}进程运行过程中不断地分配和释放vm_area_struct,运行一段时间后就会有很多的vm_area_struct对象。而且在内存访问的过程中,也需要经常查找虚拟地址和某个vm_area_struct的对应关系。所以所有的vm_area_struct对象需要使用合适的数据结构高效管理起来,以便高性能地遍历或查询。
在Linux 6.1版本之前,数据结构上一直使用的是红黑树来管理所有的vm_area_struct,以便支持高效的查询。但是红黑树虽然查询、插入、删除可能做到O(log(n))的复杂度,效率比较高,但是遍历性能却比较低下。所以除了红黑树外,还额外使用了双向链表,专门用来加速遍历过程。
我们平时很多时候在思考某种应用场景的时候,往往想的是采用哪一种数据更合适,在一种数据结构的死胡同里想来想去。确实,如果任何一种数据结构都不能满足所有需求,同时采用两种数据结构来管理也是个很好的选择。
其中列表和红黑树中的元素都指向的是vm_area_struct对象。所有的vm_area_struct通过红黑树有序地组织了起来。
(此处可能配图:红黑树+双向链表管理VMA)
这种红黑树+双向链表的搭配提供了不错的性能,一直延续用了非常长的时间。但它仍然存在缺陷。最明显的缺陷是随着近些年来服务器上的核数越来越多,应用程序的线程也越来越多,多线程情况下锁争抢的问题开始浮现了出来。2019年的LFSMM(Linux Storage, Filesystem, and Memory-Management Summit)峰会上多次讨论了这个问题。需要加锁的原因是红黑树由于需要平衡操作,可能会影响多个红黑树的节点。还有就是修改需要同步到双向链表。因为这两个原因,基于红黑树+双向链表的数据结构就必须得加锁才可以。前面在mm_struct的源码下的mmap_sem就是锁定义。
所以在Linux 6.1版本里,对VMA的管理被替换成了mapple tree。这种数据结构一开始就是按照无锁的方式来设计的,使用Linux中的现成安全 - read-copy-update (RCU) 无锁编程方式实现。减少了锁的开销。参见作者博客 https://blogs.oracle.com/linux/post/the-maple-tree-a-modern-data-structure-for-a-complex-problem
// file:linux-5.4.56:include/linux/mm_types.h
struct mm_struct {
...
// 双向链表
struct vm_area_struct *mmap;
// 红黑树
struct rb_root mm_rb;
// 锁
struct rw_semaphore mmap_sem;
}2.1.2 缺页中断
用户进程中在申请内存的时候,其实申请到的只是一个vm_area_struct而已,仅仅只是一段地址范围。物理内存并不会立即就分配,具体的分配等到实际访问的时候。当进程在运行的过程中在栈上开始分配和访问变量的时候,如果物理页还没有分配,会触发缺页中断。在缺页中断中来真正地分配物理内存。
为了避免篇幅过长,触发缺页中断的过程就先不展开了。我们直接看一下用户态内存缺页中断的核心处理入口do_user_addr_fault,它位于arch/x86/mm/fault.c文件下。
凡是用户地址空间的地址,都调用do_user_addr_fault进行缺页中断的处理。在这个函数中调用find_vma根据变量地址address,通过遍历管理所有vm_area_struct的双向链表,找到其所在的VMA对象。
在6.1以前的版本中,find_vma的实现上是通过遍历VMA双向链表来实现的。
// file:include/linux/mm_types.h
struct mm_struct {
struct {
struct maple_tree mm_mt;
...
}
}
// file:include/linux/maple_tree.h
struct maple_tree {
...
void __rcu *ma_root;
unsigned int ma_flags;
}// file:arch/x86/mm/fault.c
static inline void do_user_addr_fault(..., unsigned long address)
{
...
// 根据新的 address 查找对应的 vma
vma = find_vma(mm, address);
...
good_area:
// 调用handle_mm_fault来完成真正的内存申请
fault = handle_mm_fault(mm, vma, address, flags);
}
// file:linux-5.4.56:mm/nommu.c
struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr)
{
// 缓存查找逻辑
...
// 没有缓存就遍历双向链表
for (vma = mm->mmap; vma; vma = vma->vm_next) {
if (vma->vm_start > addr)
return NULL;
if (vma->vm_end > addr) {
vmacache_update(addr, vma);
return vma;
}
}
return NULL;
}在6.1版本中,因为管理VMA的数据结构由红黑树+双向链表替换成了maple tree,所以find_vma也就是在maple tree的查找函数mas_walk来查询了。
// file:mm/nommu.c
struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr)
{
MA_STATE(mas, &mm->mm_mt, addr, addr);
return mas_walk(&mas);
}
// file:include/linux/maple_tree.h
#define MA_STATE(name, mt, first, end) \
struct ma_state name = { \
.tree = mt, \
.index = first, \
.last = end, \
.node = MAS_START, \
.min = 0, \
.max = ULONG_MAX, \
.alloc = NULL, \
}
// file:lib/maple_tree.c
void *mas_walk(struct ma_state *mas)
{
retry:
entry = mas_state_walk(mas);
...
return entry;
}其中MA_STATE宏的作用是构造一个查找用的参数对象出来,把要用的maple tree的地址,还有要查找的地址都放到一个变量里。然后把构造出来的参数对象传递到mas_walk函数中进行真正的查询。
找到正确的vma(要访问的变量地址 address 处于其vm_start和vm_end之间)后,那么缺页中断函数do_user_addr_fault会依次调用 handle_mm_fault → __handle_mm_fault 来完成真正物理内存的申请。
在__handle_mm_fault中,又是将各种参数都统一整合到了一个参数对象vm_fault中,包括发生缺页的内存地址address,也包括中间的各级页表项。
随着Linux源码的越来越复杂,新版本函数中需要的参数也越来越多。但是函数的参数列表过长的话非常影响源码的可理解性,是一种代码的坏味道。这在《重构:改善既有代码的设计》一书中有介绍。所以在6.1版本的内核中,很多函数都把比较长的参数列表通过以一个参数对象的形式整合了起来,这样代码看起来更清晰容易理解。
Linux 是用四级页表来管理虚拟地址空间到物理内存之间的映射管理的。所以在实际申请物理页面之前,需要先check一遍需要的各级页表项是否存在,不存在的话需要申请。
为了好区分,Linux 还给每一级页表都起了一个名字。
- 一级页表:Page Global Dir,简称 pgd
- 二级页表:Page Upper Dir,简称 pud
- 三级页表:Page Mid Dir,简称 pmd
- 四级页表:Page Table,简称 pte
// file:include/linux/mm.h
struct vm_fault {
const struct {
struct vm_area_struct *vma; // 缺页VMA
unsigned long address; //缺页地址
...
};
pmd_t *pmd; // 二级页```c
pmd_t *pmd; // 二级页表项
pud_t *pud; // 三级页表项
pte_t *pte; // 四级页表项
...
}
// file:mm/memory.c
static vm_fault_t __handle_mm_fault(struct vm_area_struct *vma,
unsigned long address, unsigned int flags)
{
struct vm_fault vmf = {
.vma = vma,
.address = address & PAGE_MASK,
.real_address = address,
...
};
...
// 依次查看或申请每一级页表项
pgd = pgd_offset(mm, address);
p4d = p4d_alloc(mm, pgd, address);
vmf.pud = pud_alloc(mm, p4d, address);
...
vmf.pmd = pmd_alloc(mm, vmf.pud, address);
...
return handle_pte_fault(&vmf);
}看下面这个图就比较好理解了:
(此处可能配图:四级页表映射示意图)
在检查或申请好所需要的各级页表项后,进入do_anonymous_page进行处理.
在handle_pte_fault会处理很多种的内存缺页处理,比如文件映射缺页处理、swap缺页处理、写时复制缺页处理、匿名映射页处理等几种情况.我们开发者申请的变量内存对应的是匿名映射页处理,会进入到do_anonymous_page函数中.
在do_anonymous_page调用alloc_zeroed_user_highpage_movable分配一个可移动的匿名物理页出来.在底层会调用到伙伴系统的alloc_pages进行实际物理页面的分配.
内核是用伙伴系统来管理所有的物理内存页的.其它模块需要物理页的时候都会调用伙伴系统对外提供的函数来申请物理内存.
// file:mm/memory.c
static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
{
vmf->pte = pte_offset_map(vmf->pmd, vmf->address);
...
// 匿名映射页处理
return do_anonymous_page(vmf);
// 其它处理
...
}
// file:mm/memory.c
static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
...
// 分配可移动的匿名页面,底层通过alloc_page支持
page = alloc_zeroed_user_highpage_movable(vma, vmf->address);
...
}到了这里,开篇的问题“堆栈的物理内存是什么时候分配的?”其实就有答案了.进程在加载的时候只是会给新进程的栈内存分配一段地址空间范围.而真正的物理内存是等到访问的时候触发缺页中断,再调用alloc_pages从伙伴系统中申请的.
2.2 虚拟内存使用方式
整个进程运行过程中几乎都是在围绕着对虚拟内存的分配和使用而进行的.具体的使用方式大概可以概括成这么几种.第一类是操作系统加载程序时在加载逻辑里对新进程的虚拟内存的设置和使用,具体包括:
- 程序启动时,加载程序会将程序代码段、数据段通过mmap映射到虚拟地址空间中,
- 对新进程初始化栈区和堆区
另外就是程序运行期间动态地对所存储各种数据进行申请和释放.这涉及到一类是栈,进程线程运行时函数调用、存储局部变量都使用的是栈.另一类是堆,各种开发语言运行时通过new、malloc等函数就是从堆中分配内存的.这类内存申请和释放需要依赖操作系统提供的关系虚拟地址空间相关的mmap、brk等系统调用来实现.接下来先总体上看下进程的虚拟内存,然后再了解mmap和brk系统调用.
2.2.1 进程启动时对虚拟内存的使用
其中程序加载过程我们在第三章中介绍过.如3.4节所述,在进程加载过程中:
- 在解析完ELF文件信息后为进程创建新的地址空间,同时给其准备一个默认大小4KB的栈
- 将可执行文件以及它所依赖的各种动态链接so库通过
elf_map函数映射到虚拟地址空间中 - 另外还会对进程的堆区进行初始化
在程序加载启动成功以后,在进程的地址空间中的代码段、数据段就都设置完毕、栈、堆也都初始化好了.
在底层实现上,无论是代码段、数据段,还是栈内存、堆内存在底层都是对应一个个的vm_area_struct对象.每一个vm_area_struct对象都表示这段虚拟内存地址空间已经分配和使用了.
具体对于每一种使用类型,在底层都是申请vm_area_struct来实现的.
- 对于栈:是在
execve依次调用do_execve_common、bprm_mm_init,最后在__bprm_mm_init中申请的vm_area_struct对象. - 对于可执行文件以及进程所依赖的各种so动态链接库:是
execve时依次调用do_execve_common、search_binary_handler、load_elf_binary、elf_map,调用mmap_region申请vm_area_struct对象,最终将可执行文件中的代码段、数据段等映射到内存中的. - 对于堆内存:是在
load_elf_binary的最后set_brk初始化堆时,依次调用vm_brk_flags、do_brk_flags,最后申请了vm_area_struct对象.
// file:fs/exec.c
static int __bprm_mm_init(struct linux_binprm *bprm)
{
struct mm_struct *mm = bprm->mm;
// 申请占用一段地址范围
bprm->vma = vma = vm_area_alloc(mm);
vma->vm_end = STACK_TOP_MAX;
vma->vm_start = vma->vm_end - PAGE_SIZE;
...
}
// file:mm/mmap.c
unsigned long mmap_region(struct file *file, unsigned long addr,
unsigned long len, vm_flags_t vm_flags, unsigned long pgoff,
struct list_head *uf)
{
...
vma = vm_area_alloc(mm);
vma->vm_start = addr;
vma->vm_end = addr + len;
...
return addr;
}
// file:mm/mmap.c
static int do_brk_flags(struct ma_state *mas, struct vm_area_struct *vma,
unsigned long addr, unsigned long len, unsigned long flags)
{
...
// 申请虚拟地址空间范围对象
vma = vm_area_alloc(mm);
// 对齐进行初始化
vma_set_anonymous(vma);
vma->vm_start = addr;
vma->vm_end = addr + len;
vma->vm_pgoff = pgoff;
vma->vm_flags = flags;
...
}可见,操作系统除了在加载程序的时候,给进程初始化好了内存地址空间,并也设置了各个使用的区间范围.在书的配套源码中,打开chapter-04/test-02的源代码,编译运行一下.
# gcc main.c -o main
# ./main
请另起一个命令行查看虚拟地址空间状态,命令是 cat /proc/2299718/maps
然后按任意键退出程序...根据命令提示再打开一个控制台输入,cat /proc/2299718/maps可以查看进程的虚拟地址空间内容概要.要注意的是2299718是进程pid,在你的机器上肯定会不一样,注意替换.
55e7bd72d000-55e7bd72e000 r--p 00000000 08:10 26220233
/.../work_my/tests/test005/main
55e7bd72e000-55e7bd72f000 r-xp 00001000 08:10 26220233
/.../work_my/tests/test005/main
...
从实验的结果中可以看到,可执行程序、依赖的动态链接库都被加载到进程的地址空间中了.还有就是进程的 [heap]、[stack]也都初始化好了.
另外内核还提供了各种系统调用,让用户进程有机会额外再申请地址空间来使用.相关的系统调用包括 mmap、sbrk/brk等.我们在接下来的小节中分别展开讨论它们.
2.2.2 mmap
在虚拟内存管理相关的系统调用中,提供的最接近底层实现,而且也最常用的就算是mmap了.在各种运行时库,比如glibc、golang运行时中都能经常看见对它的使用.这个系统调用可以用于文件映射和匿名映射.我们在此处忽略文件映射,只看匿名映射.
匿名映射的名字解释
匿名映射这个名字起的有点绕,其本意是文件映射有文件,而匿名映射没有对应的物理文件.但我觉得直接叫普通内存地址空间更容易理解.
55e7bd72f000-55e7bd730000 r--p 00002000 08:10 26220233
/.../work_my/tests/test005/main
55e7bd730000-55e7bd731000 r--p 00002000 08:10 26220233
/.../work_my/tests/test005/main
55e7bd731000-55e7bd732000 rw-p 00003000 08:10 26220233
/.../work_my/tests/test005/main
55e7beed3000-55e7beef4000 rw-p 00000000 00:00 0 [heap]
7f8dbb03f000-7f8dbb061000 r--p 00000000 08:01 17553 /usr/lib/x86_64-linux-gnu/libc-2.28.so
7f8dbb061000-7f8dbb1a8000 r-xp 00022000 08:01 17553 /usr/lib/x86_64-linux-gnu/libc-2.28.so
7f8dbb1a8000-7f8dbb1f4000 r--p 00169000 08:01 17553 /usr/lib/x86_64-linux-gnu/libc-2.28.so
7f8dbb1f4000-7f8dbb1f5000 ---p 001b5000 08:01 17553 /usr/lib/x86_64-linux-gnu/libc-2.28.so
7f8dbb1f5000-7f8dbb1f9000 r--p 001b5000 08:01 17553 /usr/lib/x86_64-linux-gnu/libc-2.28.so
7f8dbb1f9000-7f8dbb1fb000 rw-p 001b9000 08:01 17553 /usr/lib/x86_64-linux-gnu/libc-2.28.so
7f8dbb1fb000-7f8dbb201000 rw-p 00000000 00:00 0
7f8dbb20b000-7f8dbb20c000 r--p 00000000 08:01 17548 /usr/lib/x86_64-linux-gnu/ld-2.28.so
7f8dbb20c000-7f8dbb22a000 r-xp 00001000 08:01 17548 /usr/lib/x86_64-linux-gnu/ld-2.28.so
7f8dbb22a000-7f8dbb232000 r--p 0001f000 08:01 17548 /usr/lib/x86_64-linux-gnu/ld-2.28.so
7f8dbb232000-7f8dbb233000 r--p 00026000 08:01 17548 /usr/lib/x86_64-linux-gnu/ld-2.28.so
7f8dbb233000-7f8dbb234000 rw-p 00027000 08:01 17548 /usr/lib/x86_64-linux-gnu/ld-2.28.so
7f8dbb234000-7f8dbb235000 rw-p 00000000 00:00 0
7ffde1847000-7ffde1868000 rw-p 00000000 00:00 0 [stack]
7ffde1906000-7ffde1909000 r--p 00000000 00:00 0 [vvar]
7ffde1909000-7ffde190a000 r-xp 00000000 00:00 0 [vdso]
匿名映射过程其实就是在向内核申请一段可用的内存地址范围而已,非常的简单.当mmap被调用后,内核就会多申请一个vm_area_struct出来.表明这段内存可用,然后返回给用户.
理解底层原理的好处
说句题外话,我以前在不了解内核实现的时候,学习mmap死活都理解不了它是干啥的.在理解了底层实现中的vm_area_struct等概念的时候,一下子就把mmap给理解透了.这就是理解了底层实现原理的重要的好处.
mmap系统调用的入口位于 arch/x86/kernel/sys_x86_64.c.
接下来的实现调用逻辑比较深,ksys_mmap_pgoff => vm_mmap_pgoff => do_mmap_pgoff => do_mmap => mmap_region.具体调用过程我们不过多展开,直接看 mmap_region.
在 mmap_region 中调用 vm_area_alloc 申请了一个新的 vm_area_struct 对象出来,对其进行初始化.这样用户申请的虚拟内存就分配好了.返回后用户就可以使用了.
测试代码
为了帮助大家理解使用mmap后地址空间长啥样子,我写了一个测试代码在本书配套源码的
chapter-04/test-03中,简单编译执行即可.
// file:arch/x86/kernel/sys_x86_64.c
SYSCALL_DEFINE6(mmap, unsigned long, addr, unsigned long, len,
unsigned long, prot, unsigned long, flags,
unsigned long, fd, unsigned long, off)
{
...
return ksys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT);
}
// file:mm/mmap.c
unsigned long mmap_region(struct file *file, unsigned long addr,
unsigned long len, vm_flags_t vm_flags, unsigned long pgoff,
struct list_head *uf)
{
...
// 申请新 vm_area_struct
vma = vm_area_alloc(mm);
// 对其进行初始化
vma->vm_start = addr;
vma->vm_end = addr + len;
...
return addr;
}这是测试代码的全部输出,按照代码提示另起一个控制台,使用 cat /proc/{$pid}/maps 可以看到在mmap调用后,进程的地址空间中多了一段地址范围出来.
2.2.3 sbrk和brk
在进程启动完后,exec系统调用会给进程初始化好当前虚拟地址空间中的堆区,也设置好了 start_brk 和 brk 等指针.
接下来的 sbrk 和 brk 系统调用就是在上面这个基础上进行工作.这二者的工作也是非常的简单.sbrk 就是返回 mm_struct->brk 指针的值.brk 系统调用就是尝试着修改 mm_struct->brk,往大了加就是要扩大堆区.将其往小了改就是在缩减堆区的大小.
相关源码
相关源码位于
mm/mmap.c,操作的具体函数是do_brk_flags.
# gcc main.c -o main
# ./main
这是一个mmap匿名映射的例子!
请另起一个命令行查看虚拟地址空间状态,命令是 cat /proc/2288406/maps
然后按任意键继续...
mmap私有映射成功,再次查看虚拟地址空间状态,观察有什么变化
然后按任意键继续...
接触mmap私有映射成功,再次查看虚拟地址空间状态,观察有什么变化
然后按任意键退出程序...
// file:mm/mmap.c
static int do_brk_flags(struct ma_state *mas, struct vm_area_struct *vma,
unsigned long addr, unsigned long len, unsigned long flags)
{
struct mm_struct * mm = current->mm;
...
// 首先尝试在现有的vma上进行扩展,扩展成功则就退出了
if (vma && vma->vm_end == addr && !vma_policy(vma) &&
can_vma_merge_after(vma, ...)){
mas_set_range(mas, vma->vm_start, addr + len - 1);
vma->vm_end = addr + len;
vma->vm_flags |= VM_SOFTDIRTY;
goto out;
}
// 否则申请新的VMA
vma = vm_area_alloc(mm);
vma->vm_start = addr;