【TLPI读书笔记】 六、进程
进程和程序
- 进程是一个可执行程序的实例。
程序是包含了一系列信息的文件,这些信息描述了如何在运行时创建一个进程,所包含的内容如下所示:
- 二进制格式标识
- 机器语言指令
- 程序入口地址
- 数据
- 符号表及重定位表
- 共享库和动态链接信息
- 其他信息
- 可以用一个程序来创建许多进程,或者反过来说,许多程序运行的可以是同一程序
- 进程是由内核定义的抽象的实体,并为该实体分配用以执行程序的各项系统资源
- 从内核角度看,进程由用户内存空间和一系列内核数据结构组成,其中用户内存空间包含了程序代码及代码所使用的变量,而内核数据结构则用于维护进程状态信息。
进程号和父进程号
每个进程都有一个进程号(PID
),进程号是一个正数,用以唯一标识系统中的某个进程。
系统调用getpid()
返回调用进程的进程号
#include <unistd.h>
pid_t getpid(void);
// Always successfully returns process ID of caller
返回值
- 成功:返回进程ID
每个进程都有一个创建自己的父进程。使用系统调用getppid()
可以检索到父进程的进程号
#include <unistd.h>
pid_t getppid(void);
// Always successfully returns process ID of caller
返回值
- 成功:返回进程ID
如果子进程的父进程终止,则子进程就会变成“孤儿”,init进程随即将收养该进程,子进程后续对getppid()
的调用将返回进程号1
。
进程内存布局
每个进程所分配的内存由很多部分组成,通常称之为“段(segment)”。如下所示:
- 文本段
- 初始化数据段
- 未初始化数据段(bss)
- 栈(stack)
- 堆(heap)
size
命令可以显示二进制可执行文件的文本段、初始化数据段、非初始化数据段(bss)的段大小
进程内存布局如下图所示:
虚拟内存管理
上述关于进程内存布局的这一布局存在于虚拟内存中。
虚拟内存管理技术,该技术利用了大多数程序的一个典型特征,即访问局部性,以求高效实用CPU和RAM(物理内存)资源。大多程序都展现了两种类型的局部性:
- 空间局部性:是指程序倾向于访问在最近访问过的内存地址附近的内存
- 时间局部性:是指程序倾向于在不久的将来再次访问最近刚访问过的内存地址
正是由于访问局部性特征,使得程序即便仅有部分地址空间存在于RAM中,依然可能得以执行。
虚拟内存的规划:
- 将每个程序使用的内存切割成小型的、固定大小的“页(
page
)”单元 - 将RAM划分成一系列与虚拟页尺寸相同的页帧
- 任一时刻,每个程序仅有部分页需要驻留在物理内存页帧中。这些页构成了所谓驻留集(
resident set
) - 程序未使用的页拷贝保存在交换区(
swap area
)内————这是磁盘空间中的保留区域,作为计算机RAM的补充。仅在需要时才会载入物理内存 - 若进程欲访问的页面目前并未驻留在物理内存中,将会发生页面错误(
page fault
),内核即刻挂起进程的执行,同时从磁盘中将该页面载入内存 - 为支持这一组织方式,内核需要为每个进程维护一张页表(
page table
)。该页表描述了每页在进程虚拟地址空间中的位置。页表中的每个条目要么指出一个虚拟页面在RAM中的所在位置,要么表明其当前驻留在磁盘上
虚拟内存管理使进程的虚拟地址空间与RAM物理地址空间隔离开来,这带来许多优点:
- 进程与进程、进程与内核相互隔离
- 适当情况下,两个或者更多进程能够共享内存
- 便于实现内存保护机制
- 程序员和编译器、链接器之类的工具无需关注程序在RAM中的物理布局
- 因为需要驻留在内存中的仅是程序的一部分,所以程序的加载和运行都很快
栈和栈帧
函数的调用和返回使栈的增长和收缩呈线性。栈驻留在内存的高端并向下增长(朝堆的方向)。专用寄存器————栈指针(stack pointer
),用于跟踪当前栈顶。每次调用函数时,会在栈上新分配一帧,每当函数返回时,再从栈上将此帧移去。
有时会用用户栈(user stack
)来表示此处所讨论的栈,以便与内核栈区分开来。内核栈是每个进程保留在内核内存中的内存区域,在执行系统调用过程中供内部函数调用使用。
每个(用户)栈帧包括如下信息:
- 函数实参和局部变量
- (函数)调用的链接信息
因为函数能够嵌套调用,所以栈中可能有多个栈帧。
命令行参数(argc
,argv
)
每个C语言程序都必须有一个称为main()
的函数,作为程序启动的起点。
命令行参数(由shell逐一解析)通过两个入参提供给main()
函数。参数如下所示:
int argc
:表示命令行参数的个数char *argv[]
:是一个指向命令行参数的指针数组,每一个参数又都是以空字符(null
)结尾的字符串。argv[0]
指向的通常是该程序的名称
argc/argv
参数机制的局限之一在于这些变量仅对main()
函数可用。要为其他函数所用,必须把参数进行传递或设置一个指向该参数的全局变量。
许多程序使用getopt()
库函数解析命令行选项(即以“-”符号开头的参数)。
环境列表
每一个进程都有与其相关的称之为环境列表的字符串数组,或简称为环境。其中每个字符串都以 名称=值 形式定义。常将列表中的名称称为环境变量。
新进程在创建之时,会继承其父进程的环境副本。这是一种原始的进程间通信方式,却颇为常用。
从程序中访问环境
- 可以使用全局变量
char **environ
(字符串数组)访问环境列表 可以通过声明
main()
函数中的第三个参数来访问环境列表:int main(int argc, char *argv[], char *envp[])
getenv()
函数能够从进程环境中检索单个值#include <stdlib.h> char *getenv(const char *name); // Returns pointer to (value)string, or NULL if no such variable
参数
name
:环境变量名称
返回值
成功:返回环境变量值
失败:返回NULL
修改环境
putenv()
函数向调用进程的环境列表中添加一个新变量,或者修改一个已存在的变量值
#include <stdlib.h>
char *putenv(char *string);
// Returns 0 on success, or nonzero on error
参数
string
:环境变量的“名称=值”对形式的字符串。该字符串不能为自动变量,因为环境列表会直接指向该字符地址而不是拷贝一个副本
返回值
成功:返回0
失败:返回非0
值
setenv()
函数可以替代putenv()
函数,向环境变量添加一个变量
#include <stdlib.h>
char *setenv(const char *name, const char *value, int overwrite);
// Returns 0 on success, or -1 on error
参数
name
:环境变量的名称value
:环境变量的值overwrite
:如果存在,是否覆盖
返回值
成功:返回0
失败:返回-1
unsetenv()
函数从环境中移除由name
参数标识的变量
#include <stdlib.h>
char *unsetenv(const char *name);
// Returns 0 on success, or -1 on error
参数
name
:环境变量的名称
返回值
成功:返回0
失败:返回-1
clearenv()
函数用于清除整个环境
#include <stdlib.h>
int clearenv(void);
// Returns 0 on success, or nonzero on error
返回值
成功:返回0
失败:返回非0
值
执行非局部跳转:setjmp()
和longjmp()
使用库函数setjmp()
和longjmp()
可执行非局部跳转。术语“非局部”是指跳转的目标为当前执行函数之外的某个位置。
C语言包含goto
语句,但是存在一个限制————不能从当前函数跳转到另一函数。setjmp()
和longjmp()
就弥补了这点。
#include <setjmp.h>
int setjmp(jmp_buf env);
// Returns 0 on initial call, nonzero on return via longjmp()
void longjmp(jmp_buf env, int val);
setjmp()
调用为后续由longjmp()
调用执行的跳转确立了跳转目标。该目标正是程序发起setjmp()
调用的位置。从编程角度看来,调用longjmp()
函数后,看起来就和从第二次调用setjmp()
返回时完全一样。通过查看setjmp()
返回的整数值可以区分是初始返回还是第二次返回。
参数
env
:环境,setjmp()
将当前进程环境的各种信息保存到env参数中,该变量应是全局变量val
:指定跳转后setjmp()
返回的值,以此区分是否是初始返回。初始返回0
返回值(setjmp()
)
成功:初始化调用返回0
,跳转后的二次返回返回非0
总结
本章的核心内容在进程布局,这涉及到虚拟内存、栈和栈帧等概念,其次介绍了进程的其他内容:参数和环境。