前言
内核除了需要管理自己使用的物理内存,还要管理用户空间中进程使用的内存。这部分内存称为进程地址空间。由于使用虚拟内存,所以每个进程都认为自己拥有整个物理内存。
进程的地址空间
进程的内存地址空间由可寻址的虚拟内存组成,根据架构不同有32位或64位的独立连续的地址空间(flat)。两个内存可能有相同的内存地址,但其实互不相干,这种称为线程。
尽管进程可以访问2^32GB 或者 2^64 GB 的虚拟内存,但是不代表进程有权限访问所有虚拟地址,一个进程可以访问的合法地址空间称为memory areas 内存区域.进程可以动态的增加或减少自己的内存区域。
如果进程访问了有效内存区域外的内存地址,内核会终止进程并报“Segmentation Fault”。
进程的内存区域包含一下内存对象:
- A memory map of the executable file’s code, called the text section.
- A memory map of the executable file’s initialized global variables, called the data section.
- A memory map of the zero page (a page consisting of all zeros, used for purposes such as this) containing uninitialized global variables, called the bss section.1
- A memory map of the zero page used for the process’s user-space stack. (Do not confuse this with the process’s kernel stack, which is separate and maintained and used by the kernel.)
- An additional text, data, and bss section for each shared library, such as the C library and dynamic linker, loaded into the process’s address space.
- Any memory mapped files.
- Any shared memory segments.
- Any anonymous memory mappings, such as those associated with malloc().
内存描述符 mm_struct
mm_struct 结构体用来存放进程地址空间的所有信息。通常每个进程都有唯一的 mm_struct.所有的 mm_struct 结构体通过 mmlist 双向链表链接,链表的首元素是 init_mm 内存描述符,代表init 进程的地址空间,操作该链表需要使用 mmlist_lock 锁防止并发访问。
mm_struct 源码
1 | struct mm_struct { |
内存描述符的分配
task_struct 中 mm 域中存放该进程的内存描述符。fork()函数调用 copy_mm()复制父进程的内存描述符。子进程实际是通过kernel/fork.c 文件的 allocate_mm() 函数 从 mm_cachep slab cache 中分配。
如果父进程希望和子进程共享地址空间,在调用 clone()时设置 CLONE_VM 标志,这样的进程就是线程,是否共享地址空间,也是进程和线程唯一的区别。
指定 CLONE_VM 后,不再调用allocate_mm()
,
1 | if (clone_flags & CLONE_VM) { |
内存描述符的撤销
进程退出 调用 kernel/exit.c 中的 exit_mm()
,其中 mmput()
-1 mm_user
计数,到0后,mmdrop()
-1 mm_count 计数。也为0后代表无人使用。free_mm()
宏通过kmem_cache_free()
将 mm_struct
结构体归还到 mm_cachep
slab cache.
内核线程
内核线程没有进程地址空间,也没有自己的 mm_struct
。内核线程的 mm域 为空。这也是内核线程的真实含义——没有用户上下文。
内核线程使用调度前一个进程的内存描述符,内核发现 mm域 为 NULL 时,保留前一个进程地址空间,内核线程不访问用户空间内存,只使用地址空间和内核内存相关的信息,那部分对于所有进程都是一样的。
虚拟内存区域(Virtual Memory Areas)
VMA 由结构体vm_area_struct
表示,描述一个指定地址空间的内连续的独立的内存范围。VMA 中 vm_mm
指向对应的 mm_struct
,VMA 对于指向的mm_struct
是独一无二的。
每个 VMA 作为一个单独的内存对象管理,有一致的属性。每一个 VMA 可以代表不同类型的内存区域,比如内存映射文件或进程用户空间栈。
1 | struct vm_area_struct { |
VMA 操作函数
上面的结构体中 vm_ops
指向 VMA 结构体一些操作函数,和 VFS 一样,不同类型的 vma 实现特定的实例方法。
1 |
|
VMA 的树形结构和链表结构
VMA 通过 mmap
和 mm_rb
来访问内存区域,mmap
通过链表的形式存放 VMA 结构体,主要用于遍历,按照地址增长排序,mmap
执行链表第一个内存区域。mm_rb
通过红黑树来存放 VMA 结构体,mm_rb
指向红黑树根节点,红黑数主要用于定位特定内存区域。两种结构中存放完全相同的 vm_area_struct
结构体,只是数据结构不同。
查看实际进程内存区域
使用 /proc/PID/maps 或者 pmap 命令可以查看给定进程使用的内存空间以及使用内存的程序/库等。
1 | # pmap -x 5371 |
可以看到代码段是 rx 权限,数据段和 bss 有 rw 权限,堆栈可能有 rwx 的权限。
上述 C 库所占有的内存是共享的不可写的,实际属于这个进程的私有物理进程很少,这样可以减少大量内存。
操作内存的函数
find_vma()
find_vma()
用于找到给定的内存地址属于哪一个内存区域,返回一个vm_area_struct
结构体
1 | /* Look up the first VMA which satisfies addr < vm_end, NULL if none. */ |
mmap() and do_mmap() 创建地址空间
do_mmap() 函数创建一个新的线性地址空间,但并不一定创建新的 VMA ,如果创建的 VMA 和已存在的地址区间相邻且相同权限,会合并为一个。否则创建新的 VMA,从 vm_area_cachep slab cache 中分配一个vm_area_struct
,然后通过vma_link()
加入到链表和红黑树中,更新total_vm
,最后返回新的地址区间的初始地址。
1 | unsigned long do_mmap(struct file *file, unsigned long addr, unsigned long len, unsigned long prot,unsigned long flag, unsigned long offset) |
在用户空间通过mmap()
系统调用获取内核函数do_mmap()
的功能。
do_munmap()
do_munmap() 删除地址空间,从 start 开始删除 len 长。1
int do_munmap(struct mm_struct *mm, unsigned long start, size_t len)
页表
虽然程序使用的是虚拟内存,但是 cpu 需要操作物理内存,虚拟内存到物理内存的映射使用页表,Linux 使用三级页表,可以节省页表所占用的空间。
顶级页表也是一级页表(PGD)指向二级页表(PMD)指向最后的页表(PTE)指向物理页面。
多数体系结构,查找页表是硬件完成的,每个进程的内存描述符中 pgd 指向一级页表。页表对应的结构体依赖体系结构。
TLB :虚拟地址到物理地址映射的硬件缓存