进程与虚拟内存

进程内存布局

从内核角度看,进程由用户内存空间和一系列的内核数据结构组成,其中用户内存空间包含了程序代码以及代码所使用的变量,而内核数据结构则用于维护进程状态信息。
每个进程所分配的内存由很多部分组成,通常称之为“段”,其内容如下:

  • 文本段
    包含了进程运行的程序机器语言指令,文本段具有只读属性,以防止进程通过错误指针修改自身指令。因为多个进程可以同时运行同一程序,所以又将文本段设置为可共享,这样,一份程序代码的拷贝可以映射到所有这些进程的虚拟地址空间中。
  • 初始化数据段
    包含了显式初始化的全局变量和静态变量。当程序加载到内存时,从可执行文件中读取这些变量的值。
  • 未初始化数据段
    包含未进行显式初始化的全局变量和静态变量。程序启动前,系统将本段内所有内存初始化为0.将初始化了的变量和未初始化的变量分开存放的原因在于:程序在磁盘上存储时,没有必要为未经初始化的变量分配存储空间。相反,可执行文件只需记录未初始化数据段的位置以及所需大小,直到运行时再由程序加载器来分配这一空间。

  • 栈是一个动态增长和收缩的段,由栈帧组成,系统会为每个当前调用的函数分配一个栈帧。栈帧中存储了函数的局部变量(自动变量),实参和返回值。

  • 堆是在程序运行时为变量动态进行内存分配的一块区域。

看一个实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdio.h>
#include<stdlib.h>
char globBuf[65535]; //存于未初始化数据段
int primes[] = {1,2,3,4}; //存于初始化数据段
static void square(int x){ //分配一个栈帧
int result; //存于该函数的栈帧中
result = x*x;
}
int main(int argc,char *argv[]){ //分配一个栈帧
static int key = 9973; //存于初始化数据段
static char mbuff[10000000]; //存于未初始化数据段
char *p; //存于main的栈帧中
p = malloc(1024); //指针指向该进程内存中的堆段
square(key);
return 0;
}


在大多数UNIX实现中C语言编程环境提供了3个全局符号:etext,edata,end,可在程序中使用这些符号(extern char etext,edata,end;)以获取相应程序文本段,初始化数据段,和非初始化数据段结尾的下一个字节的地址。

虚拟内存管理

虚拟内存的规划之一是将每个程序使用的内存切割成小型的、固定大小的“页”单元,相应的将RAM划分成一系列与虚存页尺寸相同的页帧。任一时刻,每个程序仅有部分页需要驻留在物理内存页帧中,这些页构成了所谓的驻留集,程序未使用的页拷贝保存在交换区——这是磁盘空间中的保留区域,作为计算机RAM的补充——仅在需要时才会载入物理内存。如果进程欲访问的页面目前并未驻留在物理内存中,将会发生页面错误,内核挂起进程,同时从磁盘中将该页面载入内存。有的情况下,多个进程可以共享内存,这是由于内核可以使不同的进程的页表条目指向相同的RAM页。

虚拟内存的实现需要硬件中分页内存管理单元(PMMU)的支持,PMMU把要访问的每个虚拟内存地址转换为相应的物理内存地址,当特定虚拟内存地址所对应的页面没有在物理内存中找到时,将以页面错误通知内核。

栈和栈帧

栈驻留在内存的高地址端向下增长(堆的方向),专用寄存器——栈指针跟踪当前栈顶,每次调用函数时,会在栈上新分配一帧,函数返回时,再从栈上将此帧移去。
栈分为内核栈用户栈

  • 内核栈是每个进程保留在内核内存中的内存区域,在执行系统调用的过程中供内核内部函数使用
  • 用户栈中包含函数实参、局部变量和函数调用链接信息。在调用函数时,函数实参、局部变量自动创建(故又称为自动变量),被调用函数中保存函数调用者的寄存器的副本,函数返回时,栈帧释放,这些变量自动销毁,调用者寄存器恢复原状

参考书籍:《linux/UNIX系统编程手册》