0%

《趣谈linux操作系统》小结(十七) - 虚拟内存管理

虚拟内存管理

整个虚拟内存空间要一分为二,一部分是用户态地址空间,一部分是内核态地址空间,那这两部分的分界线在哪里呢?这就要 task_size 来定义。

图片替换文本

在内核任务结构 struct task_struct里面有一个成员保存内存管理信息的

1
struct mm_struct    *mm;

用户态布局

用户态虚拟空间里面有几类数据,例如代码、全局变量、堆、栈、内存映射区等。在 struct mm_struct 里面,有下面这些变量定义了这些区域的统计信息和位置。

1
2
3
4
5
6
7
8
9
10
11
12

unsigned long mmap_base; /* base of mmap area */
unsigned long total_vm; /* Total pages mapped */
unsigned long locked_vm; /* Pages that have PG_mlocked set */
unsigned long pinned_vm; /* Refcount permanently increased */
unsigned long data_vm; /* VM_WRITE & ~VM_SHARED & ~VM_STACK */
unsigned long exec_vm; /* VM_EXEC & ~VM_WRITE & ~VM_STACK */
unsigned long stack_vm; /* VM_STACK */
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;

total_vm 是总共映射的页的数目。我们知道,这么大的虚拟地址空间,不可能都有真实内存对应,所以这里是映射的数目。当内存吃紧的时候,有些页可以换出到硬盘上,有的页因为比较重要,不能换出。locked_vm 就是被锁定不能换出,pinned_vm 是不能换出,也不能移动。

data_vm 是存放数据的页的数目,exec_vm 是存放可执行文件的页的数目,stack_vm 是栈所占的页的数目。

start_code 和 end_code 表示可执行代码的开始和结束位置,start_data 和 end_data 表示已初始化数据的开始位置和结束位置。

start_brk 是堆的起始位置,brk 是堆当前的结束位置。

start_stack 是栈的起始位置,栈的结束位置在寄存器的栈顶指针中。

arg_start 和 arg_end 是参数列表的位置, env_start 和 env_end 是环境变量的位置。它们都位于栈中最高地址的地方。

mmap_base 表示虚拟地址空间中用于内存映射的起始地址。

图片替换文本

除了位置信息之外,struct mm_struct 里面还专门有一个结构 vm_area_struct,来描述这些区域的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

struct vm_area_struct {
/* The first cache line has the info for VMA tree walking. */
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address within vm_mm. */
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next, *vm_prev;
struct rb_node vm_rb;
struct mm_struct *vm_mm; /* The address space we belong to. */
struct list_head anon_vma_chain; /* Serialized by mmap_sem &
* page_table_lock */
struct anon_vma *anon_vma; /* Serialized by page_table_lock */
/* Function pointers to deal with this struct. */
const struct vm_operations_struct *vm_ops;
struct file * vm_file; /* File we map to (can be NULL). */
void * vm_private_data; /* was vm_pte (shared mem) */
} __randomize_layout;

vm_start 和 vm_end 指定了该区域在用户空间中的起始和结束地址。vm_next 和 vm_prev 将这个区域串在链表上。vm_rb 将这个区域放在红黑树上。vm_ops 里面是对这个内存区域可以做的操作的定义。

虚拟内存区域可以映射到物理内存,也可以映射到文件,映射到物理内存的时候称为匿名映射,anon_vma 中,anoy 就是 anonymous,匿名的意思,映射到文件就需要有 vm_file 指定被映射的文件。

内存的映射建立是由函数load_elf_binary来完成的。

图片替换文本

执行 brk 处理内存申请的操作,实现的入口是 sys_brk 函数。sys_brk 函数的参数 brk 是新的堆顶位置,而当前的 mm->brk 是原来堆顶的位置。

将原来的堆顶和现在的堆顶,都按照页对齐地址,然后比较大小。如果两者相同,说明这次增加的堆的量很小,还在一个页里面,不需要另行分配页,直接跳到 set_brk 那里,设置 mm->brk 为新的 brk 就可以了。如果发现新旧堆顶不在一个页里面,麻烦了,这下要跨页了。

如果发现新堆顶小于旧堆顶,这说明不是新分配内存了,而是释放内存了,释放的还不小,至少释放了一页,于是调用 do_munmap 将这一页的内存映射去掉。

如果堆将要扩大,就要调用 find_vma。如果打开这个函数,看到的是对红黑树的查找,找到的是原堆顶所在的 vm_area_struct 的下一个 vm_area_struct,看当前的堆顶和下一个 vm_area_struct 之间还能不能分配一个完整的页。如果不能,没办法只好直接退出返回,内存空间都被占满了。如果还有空间,就调用 do_brk 进一步分配堆空间,从旧堆顶开始,分配计算出的新旧堆顶之间的页数。

在 do_brk 中,调用 find_vma_links 找到将来的 vm_area_struct 节点在红黑树的位置,找到它的父节点、前序节点。接下来调用 vma_merge,看这个新节点是否能够和现有树中的节点合并。如果地址是连着的,能够合并,则不用创建新的 vm_area_struct 了,直接跳到 out,更新统计值即可;如果不能合并,则创建新的 vm_area_struct,既加到 anon_vma_chain 链表中,也加到红黑树中。

内核态布局

内核态的虚拟空间和某一个进程没有关系,所有进程通过系统调用进入到内核之后,看到的虚拟地址空间都是一样的。内核态空间只有一份,共享的。

图片替换文本

32 位的内核态虚拟地址空间一共就 1G,占绝大部分的前 896M,我们称为直接映射区。所谓的直接映射区,就是这一块空间是连续的,和物理内存是非常简单的映射关系,其实就是虚拟内存地址减去 3G,就得到物理内存的位置。

这 896M 还需要仔细分解。在系统启动的时候,物理内存的前 1M 已经被占用了,从 1M 开始加载内核代码段,然后就是内核的全局变量、BSS 等,也是 ELF 里面涵盖的。这样内核的代码段,全局变量,BSS 也就会被映射到 3G 后的虚拟地址空间里面。具体的物理内存布局可以查看 /proc/iomem。

创建进程的系统调用会创建一个task_struct的实例, 这个实例就是保存在内核的直接映射区域内 即896MB的内存中。

内核态地址:

在 896M 到 VMALLOC_START 之间有 8M 的空间。VMALLOC_START 到 VMALLOC_END 之间称为内核动态映射空间,也即内核想像用户态进程一样 malloc 申请内存,在内核里面可以使用 vmalloc。假设物理内存里面,896M 到 1.5G 之间已经被用户态进程占用了,并且映射关系放在了进程的页表中,内核 vmalloc 的时候,只能从分配物理内存 1.5G 开始,就需要使用这一段的虚拟地址进行映射,映射关系放在专门给内核自己用的页表里面。

PKMAP_BASE 到 FIXADDR_START 的空间称为持久内核映射。使用 alloc_pages() 函数的时候,在物理内存的高端内存得到 struct page 结构,可以调用 kmap 将其映射到这个区域。

FIXADDR_START 到 FIXADDR_TOP(0xFFFF F000) 的空间,称为固定映射区域,主要用于满足特殊需求。

在最后一个区域可以通过 kmap_atomic 实现临时内核映射。如果要把文件内容写入物理内存,这件事情要内核来干了,这就只好通过 kmap_atomic 做一个临时映射,写入物理内存完毕后,再 kunmap_atomic 来解映射即可。

64位系统的内核布局也是同样的道理,不过是大小不一样,设计的功能区不一样。

图片替换文本

进程虚拟内存的布局和物理内存的布局, 分别通过查看proc/$PID/maps 和 /proc/iomem。

行动,才不会被动!

欢迎关注个人公众号 微信 -> 搜索 -> fishmwei,沟通交流。