目录
一、地址空间
二、内存描述符
三、虚拟内存区域
四、操作内存区域
find_vma()
mmap() 和 do_mmap():创建地址区间
五、页表
一、地址空间
进程地址空间由进程可寻址并且允许进程使用的虚拟内存组成, 每个进程都有一个 32 位或 64 位的平坦(flat)地址空间,空间的具体大小取决于体系结构。术语 “平坦(flat)” 指的是地址空间范围是一个独立的连续空间。每个进程都有唯一的这种平坦地址空间,一个进程的地址空间与另一个进程的地址空间即使有相同的内存地址,实际上也彼此互不相干。相对地,能与其他进程共享它们的地址空间的进程,被称为线程。
进程只能访问有效内存区域内的内存地址,每个内存区域也具有相关权限,如果一个进程访问了不在有效范围中的内存区域,或以不正确的方式访问了有效地址,那么内核就会终止该进程,并返回 “段错误(Segmentation Fault)” 信息。
内存区域可以有如下类型的内容:
- 代码段(text section),可执行文件代码的内存映射。
- 数据段(data section),可执行文件的已初始化全局变量的内存映射。
- bss 段(bss section),可执行文件的未初始化全局变量的内存映射。
- 进程用户空间栈。
- 内存映射文件。
- 共享内存段。
- 匿名的内存映射,比如由 malloc() 分配的内存。
二、内存描述符
内核使用内存描述符结构体表示进程的地址空间,该结构包含了和进程地址空间有关的全部信息。内存描述符由 mm_struct 结构体表示,定义在 <linux/sched.h> 中:
mm_users 域记录正在使用该地址的进程数目。比如,两个线程共享该地址空间,那么 mm_users 的值便等于 2;
mmap 和 mm_rb 这两个不同数据结构体描述的对象是相同的:该地址空间中的全部内存区域。但是前者以链表形式存放而后者以红黑树形式存放。mmap 结构体作为链表,利于简单、高效地遍历所有元素;而 mm_rb 作为红黑树,更适合搜索指定元素。
在进程描述符 task_struct 中的 mm 域就存放着该进程使用的内存描述符。fork() 函数利用 copy_mm() 函数复制父进程的内存描述符给子进程。通常,每个进程都有唯一的 mm_struct 结构体,即唯一的进程地址空间。
当进程退出时,内核会调用 exit_mm() 函数,内存描述符中的 mm_users 用户计数会减一,如果用户计数等于 0,则减少 mm_count 使用计数,如果该计数也为 0,则释放该结构体。
内核线程没有进程地址空间,也没有相关的内存描述符。所以内核线程对应的进程描述符中 mm 域为空。所以内核线程没有用户上下文。
三、虚拟内存区域
虚拟内存区域(virtual memoryAreas,VMAs),由 vm_area_struct 结构体描述,定义在 <linux/mm_types.h> 中。该结构体描述了指定地址空间内连续区间上的一个独立内存范围。
vm_start 是内存区间的开始地址,而 vm_end 是内存区间的结束地址。
VMA 标志是一种位标志,定义于 <linux/mm.h>。包含在 vm_flags 域内,标志了内存区域所包含的页面的行为和信息。
vm_area_struct 结构体中的 vm_ops 域指向与指定内存区域相关的操作函数表,内核使用表中的方法操作 VMA。操作函数表由 vm_operations_struct 结构体表示,定义在文件 <linux/mm.h> 中:
四、操作内存区域
find_vma()
find_vma() 函数能识别给定的内存地址属于哪一个内存区域,定义在 <mm/mmap.c> 中:
struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr);
该函数在指定的地址空间中搜索第一个 vm_end 大于 addr 的内存区域。如果没找到这种区域,则函数返回 NULL,否则返回指向匹配的内存区域的 vm_area_struct 结构体指针。
mmap() 和 do_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)
该函数映射由 file 指定的文件,具体映射的是文件中从偏移 offset 处开始,长度为 len 字节的范围内的数据。如果 file 参数是 NULL 并且 offset 参数也是 0,那么就代表这次映射没有和文件相关,被称为匿名映射(anonymous mapping)。如果指定了文件名和偏移量,那么该映射就被称为文件映射(file-backed mapping)。
在用户空间可以通过 mmap() 系统调用获取内核函数 do_mmap() 的功能。定义如下:
void *mmap2(void start, size_t length, int prot, int flags, int fd, off_t pgoff)
该系统调用是 mmap() 调用的第二种变种,最原始的 mmap() 调用中最后一个参数是字节偏移量,而 mmap2() 使用页面偏移作为最后一个参数。使用页面偏移量可以映射更大的文件和更大的偏移位置。
五、页表
虽然应用程序操作的对象是映射到物理内存之上的虚拟内存,但是处理器直接操作的却是物理内存。所以当应用程序访问一个虚拟地址时,首先必须将虚拟地址转化成物理地址,然后处理器才能解析地址访问请求。地址的转换工作需要通过查询页表才能完成,地址转换需要将虚拟地址分段,使每段虚拟地址都作为一个索引指向页表,而页表项则指向下一级别的页表或者指向最终的物理页面。
Linux 中使用三级页表完成地址转换。利用多级页表能够节约地址转换需要占用的存放空间。
- 顶级页表是页全局目录(PGD),PGD中的表项指向二级页目录中的表项:PMD。
- 二级页表是中间页目录(PMD),其中表项指向 PTE 中的表项。
- 最后一级简称页表,该页表直接指向物理页面。
多数体系结构中,搜索页表的工作是由硬件完成的。
为了加快查询速度,多数体系结构实现了一种高速缓存(translate lookaside buffer,TLB)。TLB 作为一个将虚拟地址直接映射到物理地址的硬件缓存,当请求访问一个虚拟地址时,处理器会首先检查 TLB 中是否缓存了该虚拟地址到物理地址的映射,如果在缓存中则直接命中,立即返回物理地址;否则,就要再通过页表搜索需要的物理地址。