1. 地址空间
进程地址空间由进程可寻址的虚拟内存组成,它是平坦的。
每个进程的内存地址空间互不相干。
一个进程的地址空间的所有虚拟地址不一定有权限访问,若访问非法内存,则会报“段错误”。
内存区域是可访问的合法地址空间,包含:
- 代码段
- 数据段(已初始化的全局变量)
- bss段(未初始化的全局变量)
- 用户栈
- 动态链接库的代码段、数据段和bss段
- 内存映射文件
- 共享内存段
- 匿名的内存映射(如
malloc()
分配的内存)
2. 内存描述符
用于描述进程的地址空间,由struct mm_struct
表示,定义在<linux/sched.h>
中,task_struct
中的mm
域就是内存描述符。
它维护2个引用计数:
mm_count
:主引用计数mm_users
:使用该地址空间的进程数,只有它为0时,mm_count
才能递减,当它为0时才能被回收
内存区域由2种数据结构表示,它们的内容是一样的:
mmap
:vm_area_struct
链表mm_rb
:rb_root
红黑树
前者时候遍历,后者适合查找。
2.1. 分配内存描述符
fork()
时,通过copy_mm()
(写时)复制父进程的内存描述符;而子进程通过allocate_mm()
从slab缓存中分配到内存描述符。
若
clone()
时指定CLONE_VM
则allocate_mm()
不会被调用。
2.2. 撤销内存描述符
进程退出时,内核调用exit_mm()
(定义在kernel/exit.c
中)撤销。
它减少mm_users
,若减到0,则调用mmdrop()
让mm_count
递减,若它也为0,则会调用free_mm()
将它归还到slab缓存中。
2.3. 内核线程
内核线程没有进程地址空间,因此task_struct->mm
域为空。
当一个进程被调度时,该进程的mm
域指向的地址空间会被装载到内存,若mm
为空(如内核线程),则保留前一个进程的地址空间。
3. 虚拟内存区域(VMA)
由struct vm_area_struct
描述,定义在<linux/mm_types.h>
中。
它描述了某个地址空间内一个独立的连续内存范围。每个VMA可代表不同类型的内存区域,不同类型的区域不能重叠。
3.1. VMA标志
由vm_flags
定义,标记了该区域所包含的页面的行为和信息,如可写、可读、可执行、可共享等等。
不同于物理访问权限,它反映的是内核处理要遵守的行为标准
3.2. VMA操作
由vm_ops
域指向一个vm_operations_struct
结构体表示(与VFS设计很类似)。
操作包括:
- 将指定内存区域加入一个地址空间/从一个地址空间删除
- 物理内存中,对应页面不存在(页错误)时,处理页错误
- …
3.3. 实际使用中的内存区域
可使用/proc/<pid>/maps
或者pmap
工具查看某个进程的内存空间和包含的内存区域。
4. 操作内存区域
4.1. find_vma()
struct vm_area_struct * find_vma(strcut mm_struct *mm, unsigned long addr);
查找第一个vm_end
大于addr
的VMA,找不到则返回空。结果会被缓存。
4.2. find_vma_prev()
struct vm_area_struct * find_vma_prev(strcut mm_struct *mm, unsigned long addr, struct vm_area_struct **pprev);
查找第一个小于addr
的VMA,pprev
存放先于addr
的VMA指针。
4.3. find_vma_intersection()
struct vm_area_struct* find_vma_intersection(struct mm_struct *mm, unsigned long start_addr, unsigned long end_addr);
返回第一个和指定区间相交的VMA。
5. mmap()
和do_mmap()
:创建地址区间
unsigned long do_mmap(struct file *file, unsigned long addr, unsigned long len, unsigned long prot, unsigned long flag, unsigned long offset);
它可创建一个新的线性地址区间(VMA),但是若创建的区间和某个区间相邻,且权限相同,则会被合并。
函数中,file
指定文件,映射从offset
开始,长度为len
。若file
为空且offset
为0,则为“匿名映射”,否则是”文件映射”。
addr
可选,指定空闲区域的起始位置;prot
指定区域中页面的访问权限;flag
指定VMA标志。
mmap()
与mmap2()
是do_mmap()
的包装。
6. mummap()
和do_mummap()
:删除地址区间
int do_mummap(struct mm_struct *mm, unsigned long start, size_t len)
从特定进程地址空间中删除指定的地址区间(VMA),删除位置从start
开始,长度为len
。
mummap()
是对do_mummap()
的简单包装。
7. 页表
Linux中采用三级页表实现,之后也有四级页表的实现。以4KB页为例:
对于三级页表:
- 顶层是页全局目录PGD,指向PMD,包含
pgd_t
数组(31~30) - 二级页表是PMD,指向页目录,包含
pmd_t
数组(29~21) - 最后一级是PTE(20~12),指向页表,配合偏移(11~0)转化成物理内存地址
对于四级页表,类似:
- PML4,指向一个PGD(47~39)
- PGD,指向一个PMD(38~30)
- PMD,指向一个页目录(29~21)
- PTE(20~12),指向页表,配合偏移(11~0)转换为物理地址
为了提高效率,引入TLB,在查询物理地址时首先在TLB中查询,找不到时采取内存中查找页表。