进程是内存的使用者,进程(process)是一个程序(program)的执行实例,
程序包含的信息有:
- 二进制格式标识:每个程序文件都包含用于描述可执行文件格式的元信息(metainformation)。
- 机器语言指令:对程序算法进行编码。
- 程序入口地址:标识程序开始执行时的起始指令位置。
- 数据:程序文件包含的变量初始值和程序使用的字面常量值(比如字符串)。
- 符号表及重定位表:描述程序中函数和变量的位置及名称。这些表格有多种用途,其中包含调试和运行时的符号解析(动态链接)。
- 共享库和动态链接信息:程序文件所包含的一些字段,列出了程序运行时需要使用的共享库,以及加载共享库的动态链接器的路径名。
- 其它信息:程序文件还包含许多其它信息,用以描述如何创建进程。
对应进程的使用的内存, 它有多个部分(段)组成, 如下图
- 文本段(text):包含了进程运行的程序机器语言指令。文本段具有只读属性,以防止进程通过错误指针意外修改自身指令。因为多个进程可同时运行同一程序,所以又将文本段设为可共享,这样,一份程序代码的拷贝可以映射到所有这些进程的虚拟地址空间中。包含const全局变量
- 初始化数据段(data):包含显示初始化的全局变量和静态变量。当程序加载到内存时,从可执行文件中读取这些变量的值。
- 未初始化数据段(bss):包含了未进行显示初始化的全局变量和静态变量。程序启动之前,系统将本段内所有内存初始化为0。出于历史原因,此段常被称为BSS段,这源于老版本的汇编语言助记符“block started by symbol”。将经过初始化的全局变量和静态变量与未初始化的全局变量和静态变量分开存放,其主要原因在于程序在磁盘上存储时,没有必要为未经初始化的变量分配存储空间。相反,可执行文件只需记录未初始化数据段的位置及所需大小,直到运行时再由程序加载器来分配空间。
- 堆(heap):是可在运行时(为变量)动态进行内存分配的一块区域。堆顶端称为program break。进程申请内存从堆分配的
- 栈(stack):是一个动态增长和收缩的段,有栈帧(stack frames)组成。系统会为每个当前调用的函数分配一个栈帧。栈帧中存储了函数的局部变量(所谓自动变量)、实参和返回值。
注意: register变量是放在寄存器上的,用完释放, 变量默认都是auto类型的
虚拟内存和物理内存按统一的页大小管理, 这样方便映射,通常一页大小是4KB,如果使用了hugepage, 支持2M, 1G的页。
任一时刻,每个程序仅有部分页需要驻留在物理内存页帧中,内核需要为每个进程维护一张页表(page table)。该页表描述了每页在进程虚拟地址空间(virtual address space)中的位置(可为进程所用的所有虚拟内存页面的集合)。页表中的每个条目要么指出一个虚拟页面在RAM中的所在位置,要么表明其当前驻留在磁盘上。如果申请内存或者要使用的内存不在 页表映射的时候, 会触发SIGSEGV或者页面错误,内核挂起进程,映射物理内存,或者冲sawp磁盘中将页面载入到内存
对于4k的page, 使用了4级页表来节省页表空间(如果使用1级页表,页面大小为4KB,整个虚拟地址空间为4GB,则每个进程需要包含1M个页表项), 2M的大页内存需要3级页表, 如下图是4级页表
图中CR3保存着进程页目录PGD的地址,不同的进程有不同的页目录地址。进程切换时,操作系统负责把页目录地址装入CR3寄存器。
地址翻译过程如下
- 对于给定的线性地址,根据线性地址的bit22~bit31作为页目录项索引值,在CR3所指向的页目录中找到一个页目录项。(1024项)
- 找到的页目录项对应着页表,根据线性地址的bit12~bit21作为页表项索引值,在页表中找到一个页表项。
- 找到的页表项中包含着一个页面的地址,线性地址的bit0~bit11作为页内偏移值和找到的页确定线性地址对应的物理地址。
TLB
CPU的Memory management unit(MMU)cache了最近使用的页面映射。我们称之为translation lookaside buffer(TLB)。TLB是一个组相连的cache。当一个虚拟地址需要转换成物理地址时,首先搜索TLB。如果发现了匹配(TLB命中),那么直接返回物理地址并访问。然而,如果没有匹配项(TLB miss),那么就要从页表中查找匹配项,如果存在也要把结果写回 TLB
虚拟内存管理是使进程的虚拟地址空间与RAM物理地址空间隔离开来,这带来许多优点:
- 进程与进程、进程与内核相互隔离,所以一个进程不能读取或修改另一个进程或内核的内存。这是因为每个进程的页表条目指向RAM(或交换区)中截然不同的物理页面集合。
- 适当情况下,两个或更多进程能够共享内存。这是由于内核可以使不同进程的页表条目指向相同的RAM页。内存共享常发生于如下两种场景:
- 执行同一程序的多个进程,可共享一份(只读的)程序代码副本。当多个程序执行相同的程序文件(或加载相同的共享库)时,会隐式地实现这一类型的共享。
- 进程可以使用shmget()和mmap()系统调用显示地请求与其他进程共享内存区。这么做是出于进程间通信的目的。
- 便于实现内存保护机制:也就是说,可以对页表条目进行标记,以表示相关页面内容是可读、可写、可执行亦或是这些保护措施的组合。多个进程共享RAM页面时,允许每个进程对内存采取不同的保护措施。例如:一个进程可能以只读方式访问某页面,而另一进程则以读写方式访问同一页面。
- 程序员和编译器、链接器之类的工具无需关注程序在RAM中的物理布局。
- 因为需要驻留在内存中的仅是程序的一部分,所以程序的加载和运行都很快。而且,一个进程所占用的内存(即虚拟内存大小)能够超出RAM的容量。
参考资料