1.简介

进程是一个运行中程序的实例,每个程序都运行在某个进程的上下文中,这使得操作系统既可以为程序提供唯一运行的假象,也可以并发地运行程序。

上下文指的是程序正确运行所需的状态集合,包括

  • 内存中的代码和数据
  • 通用目的寄存器
  • 程序计数器PC
  • 打开的文件描述符
  • etc.

2.进程内存布局

进程为每个程序提供一种独立使用系统地址空间的假象,这是由虚拟内存提供的。其内存布局如下:

  • 文本段:包含了进程运行的程序机器语言指令。文本段具有只读属性,以防止进程通过错误指针意外修改自身指令。因为多个进程可同时运行同一程序,所以又将文本段设为可共享,这样,一份程序代码的拷贝可以映射到所有这些进程的虚拟地址空间中。
  • 初始化数据段:包含显式初始化的全局变量和静态变量。当程序加载到内存时,从可执行文件中读取这些变量的值。
  • 未初始化数据段:包含了未进行显式初始化的全局变量和静态变量。程序启动之前,系统将本段内所有内存初始化为0。将经过初始化的全局变量和静态变量与未初始化的全局变量和静态变量分开存放,其主要原因在于程序在磁盘上存储时,没有必要为未经初始化的变量分配存储空间。相反,可执行文件只需记录未初始化数据段的位置及所需大小,直到运行时再由程序加载器来分配空间。
  • 栈(stack):是一个动态增长和收缩的段,有栈帧(stack frames)组成。系统会为每个当前调用的函数分配一个栈帧。栈帧中存储了函数的局部变量(所谓自动变量)、实参和返回值。
  • 堆(heap):是可在运行时(为变量)动态进行内存分配的一块区域。堆顶端称为program break。

进程、内存布局与虚拟内存 - 图1

3.虚拟内存

虚拟内存为进程提供了一致的地址空间,因此简化了内存管理。每个进程虽然可能会看到一样的虚拟地址,但是该虚拟地址实际会被MMU翻译成主存中的物理地址,从而为进程提供假象。

虚拟内存被组织成存放在磁盘上的N个连续的字节大小的单元组成的数组,每一个字节都有一个唯一的虚拟地址,作为数组的索引。磁盘中数组的内容被缓存在主存中。

为了知道虚拟页是否有被缓存到主存中,进程需要维护一个常驻内存的页表,从而将虚拟页映射到物理页中。

进程、内存布局与虚拟内存 - 图2

若内存中已经缓存了该页,则称之为页命中,直接从内存中读取该页。

若内存中没有缓存该页,则称之为缺页。此时,进程会调用内核的缺页异常处理程序,该程序会将所需的页缓存到内存中,并重新启动导致缺页的指令,获取该页。

Linux进程的虚拟内存布局如下。

进程、内存布局与虚拟内存 - 图3

4.内存映射

操作系统通过将虚拟内存区域和磁盘中的对象关联起来,以初始化这个虚拟内存的区域,称之为内存映射。

这种方式为进程之间共享对象提供了极大的便利,因为如果我们期望进程之间拥有同一个对象,那么重复生成这些对象就是时间和空间的双重浪费。由于虚拟内存的本质就是对物理地址的映射,这提醒我们可以对同一个对象的物理地址进行虚拟内存的共同映射,从而节省重复新建对象的开销。

内存映射有两种常用的方式。

  • 共享对象:如果你希望多个进程可以共同看到对该对象的修改,或者说想要通过同一对象进行进程间的通信,那么就可以将该对象作为共享对象。
  • 私有对象:如果你只是希望对这个对象进行简单的复制,想要为每个进程保存一个该对象的独立副本,那么就可以将该对象作为私有对象。

私有对象是如何实现的?改区域结构会被标记为私有的写时复制,只要没有进程对自己的私有区域进行修改,则会持续保持共享。若进程想要对私有区域进行修改的时候,该写操作就会出发写时复制的机制,即会在物理内存中创建一个该对象的新副本,指向新副本并恢复写权限,然后继续对新副本执行写操作。
进程、内存布局与虚拟内存 - 图4