一、进程和程序
1.进程(process)是一个可执行程序(program)的实例。
2.程序是包含了一系列信息的文件, 这些信息描述了如何在运行时创建一个进程, 所包括的内容如下所示:
-
二进制格式标识
。每 个 程 序 文件 都 包 含 用于 描 述 可 执行 文 件 格 式的 元 信 息(metainformation)。内核(kernel)利用此信息来解释文件中的其他信息。 -
程序入口地址
:标识程序开始执行时的起始指令位置。 -
机器语言指令
:对程序算法进行编码。 -
数据
:程序文件包含的变量初始值和程序使用的字面常量(literal constant)值(比如字符串)。 -
符号表及重定位表
:描述程序中函数和变量的位置及名称。这些表格有多种用途,其中包括调试和运行时的符号解析(动态链接)。 -
共享库和动态链接信息
:程序文件所包含的一些字段,列出了程序运行时需要使用的共享库,以及加载共享库的动态链接器的路径名。 -
其他信息
:程序文件还包含许多其他信息,用以描述如何创建进程。
3.从内核角度看,进程由用户内存空间(user-space memory)和一系列内核数据结构组成,其中用户内存空间包含了程序代码及代码所使用的变量, 而内核数据结构则用于维护进程状态信息。
记录在内核数据结构中的信息包括许多与进程相关的标识号(IDs)、虚拟内存表、打开文件的描述符表、信号传递及处理的有关信息、进程资源使用及限制、当前工作目录和大量的其他信息。
二、进程号和父进程号
1.每个进程都有一个进程号(PID),进程号是一个正数,用以唯一标识系统中的某个进程。
2.Linux 内核限制进程号需小于等于 32767。新进程创建时,内核会按顺序将下一个可用的进程号分配给其使用。每当进程号达到 32767 的限制时,内核将重置进程号计数器,以便从小整数开始分配。
旦进程号达到 32767,会将进程号计数器重置为 300,而不是 1。之所以如此,是因为低数值的进程号为系统进程和守护进程所长期占用,在此范围内搜索尚未使用的进程号只会是浪费时间。
在 Linux2.4 版本及更早版本中,进程号的上限 32767,由内核常量 PID_MAX 所定义。
在 Linux 2.6 版本中,情况有所改变。尽管进程号的默认上限仍是 32767,但可以通过 Linux系统特有的/proc/sys/kernel/pid_max 文件来进行调整(其值=最大进程号+1)。在 32 位平台中, pid_max 文件的最大值为 32768, 但在 64 位平台中, 该文件的最大值可以高达到 222(约400 万),系统可能容纳的进程数量会非常庞大。
3.每一个进程都有一个父进程,以此类推,1号进程(init)是所有进程的始祖。如果子进程的父进程终止,则子进程就会变成“孤儿”, init 进程随即将收养该进程。
4.系统调用 getpid()返回调用进程的进程号
#include <unsistd.h>
pid_t getpid(void);
5.系统调用 getppid()返回调用进程的父进程号
#include <unsistd.h>
pid_t getppid(void);
三、进程内存布局
每个进程所分配的内存由很多部分组成,通常称之为“段(segment)”。
文本段包含了进程运行的程序机器语言指令
。文本段具有只读属性
,以防止进程通过错误指针意外修改自身指令。因为多个进程可同时运行同一程序,所以又将文本段设为可共享
,这样,一份程序代码的拷贝可以映射到所有这些进程的虚拟地址空间中。初始化数据段包含显式初始化的全局变量和静态变量
。当程序加载到内存时,从可执行文件中读取这些变量的值。未初始化数据段包含了未进行显式初始化的全局变量和静态变量
。程序启动之前,系统将本段内所有内存初始化为 0
(出于历史原因,此段常被称为 BSS 段,这源于老版本的汇编语言助记符“block started by symbol”)。将经过初始化的全局变量和静态变量与未经初始化的全局变量和静态变量分开存放,其主要原因
在于程序在磁盘上存储时,没有必要为未经初始化的变量分配存储空间。相反,可执行文件只需记录未初始化数据段的位置及所需大小,直到运行时再由程序加载器来分配这一空间。栈(stack)是一个动态增长和收缩的段,由栈帧(stack frames)组成。
系统会为每个当前调用的函数分配一个栈帧。栈帧中存储了函数的局部变量(所谓自动变量)、实参和返回值。堆(heap)是可在运行时(为变量)动态进行内存分配的一块区域。
堆顶端称作 program break。
下面的代码展示了不同类型的 C 语言变量,并以注释说明每种变量分属于哪个段。
这些说明正确的前提是假定使用了非优化的编译器,且在应用程序二进制接口(ABI)中,是通过栈来传递所有参数的。实际上,优化编译器会将频繁使用的变量分配于寄存器中,或者索性将变量彻底剔除。此外,一些 ABI 需要通过寄存器,而不是栈,来传递函数实参和结果。
下图展示了各种内存段在 x86-32 体系结构中的布局,该图的顶部标记为 argv、 environ的空间用来存储程序命令行实参(通过 C 语言中 main()函数的 argv 参数获得)和进程环境列表,图中十六进制的地址会因内核配置和程序链接选项差异而有所不同。图中标灰的区域表示这些范围在进程虚拟地址空间中不可用,也就是说,没有为这些区域创建页(page table)。
四、虚拟内存管理
1.程序的局部性原理
进程的内存布局存在于虚拟内存之中。Linux和多数现代内核一样,采用虚拟内存管理技术。该技术基于程序的时间局部性、空间局部性原理。
空间局部性(Spatiallocality):是指程序倾向于访问在最近访问过的内存地址附近的内存(由于指令是顺序执行的,且有时会按顺序处理数据结构)。
时间局部性(Temporal locality):是指程序倾向于在不久的将来再次访问最近刚访问过的内存地址(由于循环)。
2.页和页帧,驻留集和交换区,页表
虚拟内存将每个进程使用的内存划分为若干个小型的、固定大小的"页"(page)
。相应地,RAM(物理内存)划分为若干个和页大小相同的"页帧"
,用来存放虚拟内存的"页"。
任意时刻,程序仅有部分页需要驻留在物理内存页帧中,这些页构成了所谓的驻留集(resident set)
;程序暂未使用的页拷贝保存到交换区
(swap area,磁盘中的保留区域,作为计算机RAM的补充)中,仅在需要时才会载入物理内存。
如果进程访问的页面不在物理内存中,将发生页面错误,内核将挂起该进程,同时从磁盘中将该页面载入内存。
为了支持虚拟内存管理,内核需要为每个进程维护一张页表
。
该页表描述了虚拟内存到物理内存的映射。页表中的每个条目要么指出一个虚拟页面在 RAM 中的所在位置,要么表明其当前驻留在磁盘上。
同时还有大量的虚拟内存并未映射到物理内存(页表中没有相应记录),当进程试图访问这种虚拟内存时,进程将收到一个 SIGSEGV 信号。
3.虚拟内存变化的时机
由于内核能够为进程分配和释放页(和页表条目),所以进程的有效虚拟地址范围在其生命周期中可以发生变化。这可能会发生于如下场景:
- 由于栈向下增长超出之前曾达到的位置。
- 当在堆中分配或释放内存时,通过调用 brk()、 sbrk()或 malloc 函数族来提升 program break (堆顶)的位置。
- 当调用 shmat()连接 System V 共享内存区时,或者当调用 shmdt()脱离共享内存区时
- 当调用 mmap()创建内存映射时,或者当调用 munmap()解除内存映射时
4.虚拟内存的优点
虚拟内存管理使进程的虚拟地址空间与 RAM 物理地址空间隔离开来,这带来许多优点:
- 进程与进程、进程与内核相互隔离,所以一个进程不能读取或修改另一进程或内核的内存。这是因为每个进程的页表条目指向RAM(或交换区)中截然不同的物理页面集合。
- 适当情况下,两个或者更多进程能够共享内存。这是由于内核可以使不同进程的页表条目指向相同的 RAM 页。
- 便于实现内存保护机制。多个进程共享 RAM 页面时,允许每个进程对内存采取不同的保护措施。
- 程序员和编译器、链接器之类的工具无需关注程序在 RAM 中的物理布局。
- 因为需要驻留在内存中的仅是程序的一部分,所以程序的加载和运行都很快。
- 提高CPU的利用率。每个进程使用的 RAM 减少了, RAM 中同时可以
容纳的进程数量就增多了,增加了CPU使用的概率。
五、栈和栈帧
X86-32 体系架构之上的 Linux(和多数其他 Linux 和 UNIX 实现),栈驻留在内存的高端并向下增长(朝堆的方向)。专用寄存器—栈指针(stack pointer),用于跟踪当前栈顶。每次调用函数时,会在栈上新分配一帧,每当函数返回时,再从栈上将此帧移去。
六、环境列表
每一个进程都有与其相关的称之为环境列表(environment list)的字符串数组,或简称为环境(environment)。其中每个字符串都以名称=值(name=value)形式定义。常将列表中的名称称为环境变量
(environment variables)。
新进程在创建之时,会继承其父进程的环境副本。