第六章 进程
本章关注进程虚拟内存的布局和内容。
进程和程序
进程是一个可执行程序的实例。
程序是包含了一系列信息的文件,这些信息描述了如何在运行时创建一个进程,内容如下:
- 二进制格式标识:用于描述可执行文件格式的元信息。
- 机器语言指令:对程序算法进行编码。
- 程序入口地址:标识程序开始执行指令的起始位置。
- 数据:变量初始值和字面常量。
- 符号表和重定位表:描述函数和变量的位置和名称。
- 共享库和动态链接信息。
一个程序可以创建许多进程。进程是由内核定义的抽象的实体,并为该实体分配用以执行程序的系统资源。
从内核角度看,进程由用户内存空间和一系列内核数据结构组成,其中用户内存空间包含了程序代码和使用的变量,内核数据结构用于维护进程状态信息。内核数据结构中的信息包括许多与进程相关的标示号、虚拟内存表、打开文件的描符表、信号传递及处理的信息、进程资源及使用限制、当前工作目录等。
进程号和父进程
每个进程都有一个进程号,用来唯一标识某个进程。系统调用getpid()返回调用进程的进程号。
Linux内核限制进程号需小于等于32767,新进程创建时内核会按顺序将下一个可用的进程号分配给其使用,一旦进程号达到32767,会将进程号计数器重置为300,而不是1,因为低数值的进程号为系统进程和守护进程所长期占用,在此范围内搜索尚未使用的进程号是浪费时间。在Linux 2.6中可以通过修改/proc/sys/kernel/pid_max文件来调整进程号上限。
系统调用getppid()可得到父进程进程好。
所用进程的始祖--1号进程init。使用pstree命令可以查看系统当前的进程家族树。
如果子进程的父进程终止,子进程将变成“孤儿”,init将收养该进程。该子进程随后对getpid()的调用将返回1.
进程虚拟内存布局
每个进程分配的内存由许多部分组成,称之为段(segment):
- 文本段:程序的机器语言指令,具有只读属性,可以被运行同一程序的所有进程共享。
- 初始化数据段:显式初始化的全局变量和静态变量,当程序加载到内存时从可执行文件中读取这些变量的值。
- 未初始化数据段(BSS段):未进行显式初始化的全局变量和静态变量,程序启动之前系统会将本段内的所有内存初始化为0。
- 栈:是一个动态增长和收缩的段,由栈帧组成,系统会为每个当前调用的函数分配一个栈帧,其中存储了函数的局部变量、实参、返回值。
- 堆:在运行时动态进行内存分配的一块区域;(堆的顶端称为program break)。
大多数程序都展现了两种类型的局部性:
- 空间局部性:程序倾向于访问在最近访问过的内存地址附近的内存;(指令是顺序执行的,且有时会按顺序处理数据结构)
- 时间局部性:程序倾向于在不久的将来再次访问最近刚访问过的内存地址;(循环)
虚拟内存将每个程序使用的内存切割为小型的、固定大小的页单元,相应地将RAM划分为一系列与虚拟内存页尺寸相同的页帧。任何时刻每个程序仅有部分页需要驻留于物理内存页帧中,这些页构成了所谓的驻留集,程序未使用的页拷贝保存在交换区中(磁盘空间中的保留区域),仅在需要时才会载入物理内存。
内核为每个进程维护一张页表,描述了每页在进程虚拟地址空间中的位置,页表中每个条目要么指出一个虚拟页面再RAM中的位置,要么表明其当前驻留在磁盘上。虚拟内存的实现需要硬件中的分页内存管理单元(PMMU)的支持,PMMU把要访问的每个虚拟内存地址转换为相应的物理内存地址,当特定的虚拟内存地址所对应的页没有驻留于RAM中时,将以页面错误通知内核。
虚拟内存管理使得进程的虚拟地址空间与RAM物理地址空间隔离开来,这带来许多优点:
- 进程与进程、进程与内核相互隔离,一个进程不能读取或修改另一进程或内核的内存。
- 适当情况下不同的进程能够共享内存,比如指向同一程序的不同进程或者使用mmap()显式进行内存共享(内核可以使不同进程的页表条目指向相同的RAM页)。
- 便于实现内存保护机制:可以对页表条目进行标记以表示相关页面内容是可读、可写、可执行的;多个进程共享RAM页面时,允许每个进程对内存采取不同的保护措施。
- 程序员和编译器、链接器之类的工具无需关注程序在RAM中的物理布局。
- 因为驻留在内存中的仅是程序的一部分,所以程序的加载和运行都很快,而且一个进程所占用的内存(即虚拟内存大小)能够超出RAM的容量。
- 由于每个进程使用的RAM减少了,RAM中同时可以容纳的进程数量就增多了,这增大了如下事件的概率:任何时刻CPU都至少可以执行一个进程,从而提高CPU的利用率。
专用寄存器“栈指针”用于跟踪当前的栈顶,每次调用函数时会在栈上新分配一帧,每当函数返回时再从栈上将此栈帧移去。
内核栈不同于用户栈(即用户进程虚拟内存空间中的栈),是每个进程保留在内核内存中的内存区域,在执行系统调用的过程中供内核的内部函数调用使用。
每个用户栈帧主要包含如下信息:
- 函数的实参、局部变量,函数在返回时会自动销毁这些变量。
- 函数调用的链接信息:每个函数调用另一个函数时,会在被调用函数的栈帧中保存寄存器的副本,以便函数返回时能够为函数调用者将寄存器恢复原样;
新进程在创建时会继承其父进程的环境副本,这是一种原始的进程间通信的方式。常见的用途是在shell中通过在自身环境中放置变量值,shell可以确保把这些值传递给其所创建的进程,并以此来执行用户命令。此后这个shell所创建的所有子进程都将继承此环境。
可以通过声明main函数的第三个参数来访问环境列表:
int main(int argc, char *argv[], char *envp[])
使用库函数setjmp()和longjmp()可以执行非局部跳转,即跳转的目标为当前执行函数之外的某个位置。(C语言的goto语句不能从当前函数跳到另一个函数,因为编译器无法知道当调用Y时,X函数的栈帧是否在栈上)
优化编译器会重组程序的指令执行顺序,并在CPU的寄存器中,而非RAM中存储某些变量。将变量申明为volatile是告诉优化器不要对其进行优化。
应该尽可能避免使用setjmp()和longjmp()函数。