第6章 进程
进程号和父进程号
#include unistd.h>
pid_t getpid(void);
// 返回值:总是成功返回进程号
Linux内核限制进程号需小于等于32767(由PID_MAX定义,可通过/proc/sys/kernel/pid_max文件进行调整),新进程创建时,内核会按顺序将下一个可用的进程号分配给其使用,每当进程号达到32767时,内核将重置进程号计数器(重置为300,而不是1,小于此值得进程号为系统进程或守护进程长期占有),以便从最小整数开始分配
#include unistd.h>
pid_t getppid(void);
// 返回值:总是成功返回父进程号
每个进程都有父进程,以此类推,直到回溯到1号进程-init进程,即所有进程的始祖,如果子进程的父进程终止,子进程就会变成“孤儿”,init进程随即收养该进程,变成其父进程
进程内存布局
图中的读写段包括:初始化数据段和非初始化数据段
- 文本段包含了进程运行的程序机器语言指令
- 初始化数据段包含显式初始化的全局变量和静态变量
- 未初始化数据段包含未进行显式初始化的全局变量和静态变量
- 栈是动态增减的段,由栈帧组成,栈帧中保存着函数的局部变量、实参和返回值
- 堆是可在运行时进行内存分配的一块区域,顶端称为program break
size命令可以显示二进制可执行文件的文本段、初始化数据段、非初始化数据段的大小
3个全局符号etext、edata和end分别表示文本段、初始化数据段和非初始化数据段结尾处下一个字节的地址
虚拟内存管理
Linux像多数现代内核一样,采用虚拟内存管理技术,该技术利用大多数程序的特征,即访问局部性以求高效使用CPU和RAM
- 空间局部性:程序倾向于访问最近访问过的内存地址附近的内存
- 时间局部性:程序倾向于在不久再次访问刚刚访问过的内存
进程的有效虚拟地址范围在其生命周期内可以发生变化,如:
- 由于栈向下增长超出之前到达的位置
- 堆中分配或释放内存时,通过brk、sbrk或malloc函数族提升program break的位置
- 调用shmat连接System V共享内存区时,或当调用shmdt脱离共享内存区时
- 调用mmap创建内存映射或调用munmap解除内存映射时
虚拟内存的实现需要硬件中分页内存管理单元(PMMU)的支持,虚拟地址空间与物理地址空间隔离,有很多好处:
- 进程与进程,进程与内核相互隔离,所以一个进程不能读取或修改另一个进程或内核的内存
- 适当情况下,两个或更多进程能够共享内存,由于内核可以使不同进程的页表条目指向相同的RAM页
- 便于实现内存保护机制
- 程序员和编译器、链接器之类的工具无需关注程序在RAM的物理布局
需要驻留到内存的只是程序的一部分,程序的加载和运行都很快,一个进程所占用的内存(虚拟内存)能够超出RAM容量
栈和栈帧
内核栈是每个进程保留在内核内存中的内存区域,在执行系统调用的过程中供内部函数调用使用
用户栈包括如下信息:函数实参和局部变量
- 函数调用的链接信息,如CPU寄存器
命令行参数
要从程序的任意位置访问argc、argv,有两个方法,但是会破坏移植性:
- 通过Linux专有的/proc/PID/cmdline文件可以读取任意进程的命令行参数,每个参数以null字节终止,可以通过/proc/self/cmdline文件访问自己进程的命令行参数
GNU C语言提供了两个全局变量,可在程序任意位置获取调用该程序的程序名称,第一个是program_invocation_name,提取程序完整路径名,program_invocation_short_name提取不含目录的程序名称
环境列表
新进程在创建之时,会继承父进程的环境副本,这是一种原始的进程间通信方式,却很有用,这种信息传递是单向的、一次性的,子进程创建后,父子进程都可更改各自的环境变量,这些变更对方都不再可见;可以通过设置环境变量无需修改代码来改变一些库函数的行为
大多数shell使用export向环境中添加环境变量:SHELL=/bin/bash export SHELL
但是这样会把这个值永久的添加到shell环境中,此后这个shell创建的所有子进程都继承此环境,可以使用unset命令撤销一个环境变量
可以使用如下命令将一个变量添加到程序的环境中,而不影响其父shell:NAME=value program
env命令在运行程序时使用了一份经过修改的shell环境列表副本,可同时为shell环境列表副本增加和移除环境变量定义,以修改此环境列表,printenv命令显示当前的环境列表
Linux专有的/proc/PID/environ文件可查看任意进程的环境列表,每个“NAME=value”都以空字节结束
从程序中访问环境有两个方法:使用全局变量char **environ访问环境列表
通过声明main函数的第三个参数来访问环境列表,要避免使用,因为作用域仅在main函数
int main(int argc, cha *argv[], char *envp[]);
getenv可以从进程环境中检索单个值:
#include <stdlib.h> char *getenv(const char *name); // 返回值:若成功,返回value字符串,若出错,返回NULL // 如name是SHELL,则返回/bin/bash // 程序不应该修改函数返回的字符串,且实现使用静态分配的缓冲区执行返回结果,后续getenv、setenv、putenv、unsetenv等都可以重写该缓冲区
修改进程环境变量主要用途:
对该进程后续创建的所有子进程都可见
- 对将要载入进程内存的新程序可见
setenv可以代替putenv,向环境添加一个变量:int putenv(char *string); // 返回值:若成功,返回0,若出错,返回非0 // string是name=value形式的字符串,string参数不应该为自动变量,即在栈中分配的字符数组
unsetenv用来移除由name标识的变量:int setenv(const char *name, const char *value, int override); // 返回值:若成功,返回0,若出错,返回-1 // 不要在name的结尾或value的开始添加=字符 // 若name存在,且override为0,则不改变环境,若override为非0,则改变环境 // setenv会复制参数到环境中,所以name和value使用自动变量不会有任何问题
有时需要清除整个环境:int unsetenv(const char *name); // 返回值:若成功,返回0,若出错,返回-1 // 参数name不需要添加=字符
int clearenv(void); // 返回值:若成功,返回0,若出错,返回非0 // 也可通过将environ变量赋值为NULL来清除环境
非局部跳转:setjmp和longjmp
C语言中,所有函数作用域的层级相同,即不支持嵌套函数,所以goto语句不能从当前函数跳转到另一个函数,偶尔需要此功能的场景是:在一个深度嵌套的函数调用中发生了错误,需要放弃当前任务,并在较高层级的函数中继续执行 ```include
int setjmp(jmp_buf env); // 返回值:初始调用若成功,返回0 // 为后续调用longjmp确立跳转目标,该目标即setjmp调用的地方 // 把当前进程环境信息保存到env中,调用longjmp时必须指定相同的env变量,env应该定义为全局变量 // env还保存了程序计数寄存器和栈指针寄存器,这是longjmp能够调用完成的关键 限制: setjmp只能在一些特殊的语境中调用如if、switch、while等,这是因为作为常规函数的setjmp实现无法保证拥有足够信息来保存所有寄存器和封闭表达式中用到的临时栈位置,以便于在longjmp调用后此类信息能得以正确恢复,因此,仅允许在足够简单且无需临时存储的表达式中调用 void longjmp(jmp_buf env, int val); 限制: 如果将env定义为全局变量,执行如下操作 - 调用x(),其中使用setjmp保存env
- 从x()返回
- 调用y(),使用env调用longjmp 这是一个严重错误,因为longjmp不能跳转到一个已经返回的函数中,多线程中应谨防类似的滥用,如在线程1中调用setjmp,却在线程2中调用longjmp ``` 某些程序二进制接口实现的语义要求longjmp恢复先前setjmp的调用所保存的CPU寄存器副本,这意味着longjmp操作会导致经过优化的变量被赋予错误值,具备良好移植性的程序应该在调用setjmp的函数中,将相关局部变量声明为volentile