Linux系统编程——进程控制
进程创建
fork 函数
通过调用操作系统提供的系统调用接口,从原已存在 父进程 中,创建一个新的 子进程,详细介绍见 之前这篇 blog
Linux系统编程——进程-CSDN博客
一些补充:
一旦创建了子进程,父子进程的页表中,可读可写的数据的属性也会暂时被修改为 只读,
如果 父子进程有一方要对历史上 本来 可写的区域 进行写入时,操作系统会 将这个异常 处理为:执行写时拷贝,修改父子进程的 页表 中的数据部分的 映射关系、并修改 页表中该数据项的属性为 可写。
那么问题来了,操作系统是怎么知道 “这个区域是 历史上 本来 可写的”?
在 fork 时,操作系统就会把 “这页原来可写” 这一信息偷偷塞进页表项的保留位里,之后的写保护异常处理程序只需检查这些位即可。
fork 常规应用
pid_t id = fork();
一个父进程希望复制自己,使 父子进程同时执行不同代码段:父进程等待客户端请求,生成子进程来处理请求;
或者
让子进程从 fork 返回后调用 exec 程序替换函数 等等......
fork 调用失败
系统中进程过多,内存不足
实际用户的进程数超过限制
进程终止
进程终止的本质:释放系统资源、释放该进程申请的相关内核数据结构和对应的数据和代码
进程的退出场景:
1、代码运行完毕,结果正确
2、代码运行完毕,结果不正确
3、代码异常终止
exit & _exit
void exit (int status);
exit(0); // 库函数void _exit (int status);
_exit(0); // 系统调用
在任何代码处调用 exit、_exit,直接退出该进程,'0' 为该进程的退出码;退出码表示了进程最后一次执行的命令的状态,通过退出码可以知道该进程最后一次的命令是否成功执行;
和 return 的区别:exit 和 _exit 是直接退出进程,return 表示当前函数返回(main 函数的 return 等同于执行 exit)。
status 参数 是 int,4个字节,但是仅有 低 8 位可以被父进程获取(在后面的进程等待会讲到)。
exit 是库函数,_exit 是系统调用接口;
exit 在底层也会调用 _exit,但在调用 _exit 终止进程前,还会:
1、执行用户通过 atexit 或 on_exit 定义的清理函数
2、关闭所有打开的流,写入所有的缓存数据
3、调用 _exit
进程等待
what
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息;
解决了“僵尸进程”的问题,避免内存泄漏。
how
wait / waitpid
通过系统调用接口 wait / waitpid ,对子进程进行 状态检测、回收、释放资源、获取子进程退出信息、回收僵尸进程
wait:一次可以等待任意一个子进程
waitpid:可以等待指定 pid 的子进程,并设置等待方式(阻塞等待或者非阻塞轮询)
注意:父进程只能等待自己的直系子进程
wait / waitpid 参数
pid_t pid
:指定等待某个进程
int* stat_loc
:输出型参数,将子进程的退出码带出来,供父进程处理,如果传参了 NULL,说明父进程不关心该 子进程的退出状态
因为返回值返回的是被等待成功子进程的 pid,那么子进程的退出码需要以输出型参数的方式带回,供父进程/用户 处理:
但是 int 类型 32bit 的 status,被划分为几个部分使用,在进程等待中我们只使用 后 16 位:
所以可以通过 对 status 进行 位运算,来获取其后7位(被信号所杀,看是哪种信号)、或 右移8位之后 的 后8位(计算退出码)
但是系统也提供了宏,对 status 进行判断:
WIFEXITED(status) (查看进程是否正常退出)若为正常终止子进程返回的状态,则为真
WEXITSTATUS(status) (查看进程退出码)若WIFEXITED(status)为真,提取退出码
int options
:父进程执行等待的方式(默认 0 —— 阻塞等待,或者传入 WNOHANG —— 非阻塞轮询)
阻塞等待
类似于硬件或者软件没有就绪,父进程必须要等待子进程执行完毕,获取子进程执行结果后,才能接着执行 自己的后续代码;
那么父进程在等待时,子进程的 PCB 中会维护一个 阻塞等待队列,把父进程 投递到 该子进程的 阻塞等待队列中;当子进程执行完毕时,内核会依次将该子进程的 等待队列中的 进程进行调度(唤醒);如果子进程没有结束,父进程会一直等待
非阻塞轮询
传入 WNOHANG 参数后,父进程进行非阻塞等待,如果此时 子进程并未终止,该系统调用 wait / waitpid 会直接返回:
返回值 pid_t > 0:等待成功,返回被等待成功的子进程 pid
返回值 pid_t < 0:等待失败
返回值 pid_t = 0:子进程并未终止
要实现轮询,需要加上循环:
int status;
while(1)
{pid_t ret = waitpid(-1, &status, WNOHANG);if(ret > 0){// 等待成功}if(ret < 0){// 等待失败}if(ret = 0){// 子进程未终止// 如果子进程未终止,在这段代码中,可以让父进程做一些// 轻量化的任务,而且必须要能返回到这段代码中// 因为主要任务是 等待子进程,回收子进程}
}
why
1、进程自行终止 或者 被 wait / waitpid 强制终止,解决 僵尸进程 问题,避免内存泄漏;这是进程等待的系统调用接口 wait / waitpid 一定会做的
2、父进程在等待完成后,获取 子进程任务的完成情况;如果子进程返回了完成情况,wait / waitpid 进行接收,如果子进程没有返回完成情况,wait / waitpid 就不接收 —— 非必须
小结
父进程想要知道 子进程的状态,想要获取其退出码或者 其他数据,为什么必须使用 wait / waitpid 等系统调用?
因为 进程间 具有独立性,父进程是拿不到 子进程的数据的
复习:僵尸进程是如何形成的?
子进程先结束,而父进程既不退出,也不调用 wait / waitpid 去收尸,内核就只能把子进程的“尸体”(PCB 等少量资源)留在进程表里,于是它变成了僵尸进程(Zombie)
通过 进程等待 解决僵尸进程的 过程
1、父进程通过在代码中调用 wait / waitpid
2、wait / waitpid 通过访问子进程的 PCB 来获取其执行完毕的状态信息
3、写入 输出型参数 status
4、修改子进程的 僵尸状态 为 x(dead) 状态
进程程序替换
使用 fork 创建了子进程之后,尽管后续可能有 if 语句对父子进程的分流,但子进程 往往要调用 exec 系列的函数 来替换掉 该进程的 所有代码和数据,发生写时拷贝,然后执行新程序,原程序的所有代码与数据都不再执行;
另外,调用 exec 是执行替换工作,并不会创建新的进程,所以该进程的 pid 不会改变。
上面是库中提供的接口,功能类似,只是传参略有差异:
所有接口都要说明:我要替换的可执行程序文件,文件名?在哪里(一般用绝对路径)?可变参数?环境变量(如果传入,将覆盖掉原环境变量)?
带有 l 的接口,表示在传入 可变的 命令行参数 时,需要以 字符串 的形式逐个传入
带有 p 的接口,表示 默认在 PATH 环境变量中查找该文件
带有 e 的接口,表示需要自己传入环境变量,当然也可以传入 该头文件中定义的 extern char **environ 是 内核 / 运行时以“全局变量”形式导出的当前进程的完整环境变量表
带有 v 的接口,表示在传入 可变的 命令行参数 时,以指针数组的形式传入,所以与 带有 l 的接口分为 两派
所有 exec 系列的进程程序替换接口,除了 execve 是系统调用,其余都是包装了 execve 的 库函数。
小结及零碎知识点
程序替换 不会 创建新进程,只进行 代码和数据的 替换工作
如果替换失败 —— 替换成功不会返回,替换失败会有返回值
进程程序替换时,新程序从磁盘上加载进内存,那么 CPU 如何得知程序的入口地址?
Linux 下的可执行文件格式为 ELF。程序在编译时,链接器在生成 ELF 文件时把程序入口地址写入 ELF header(表头) 的 e_entry 字段;当 exec 系列函数 进行程序替换时,内核的 ELF 装载器解析该字段,并在返回用户态前把 CPU 的程序计数器设为该地址,新程序就从该入口开始运行。
一般 linux 中,所有进程都是 bash 的子进程,bash 通过 execve 系统调用执行我们输入的命令。
exce 系列的库函数及系统调用接口 就类似于 加载器。
“内建命令”本质上是 shell 进程自己内部的一段 C 函数,它直接在当前 shell 进程里执行,既不会再 fork 出子进程,也不会调用任何新的可执行文件,因此当然也不是内核提供的系统调用或系统函数,就是 shell 源码里写好的函数。
如果是在 shell / bash 中执行普通命令,那么 bash 会创建子进程,通过 exce 系统调用接口,进行进程程序替换,在子进程中执行,父进程进行等待 —— 以获取进程退出码。
shell 本身环境变量从哪里来?
如果是使用服务器,用户目录下的 .bash_profile 导入
shell 的环境变量 = 从内核 / init 一路继承下来的“祖传环境” + 登录/非登录启动脚本(.bash_profile、.bashrc 等)按需修改/追加的结果。
.bash_profile 只是登录 shell 会读取的众多脚本之一,并非环境变量的“唯一来源”。