目录
[Part1. fork失败的两个原因](#Part1. fork失败的两个原因)
[Part2. 进程退出](#Part2. 进程退出)
[Part2.1. 进程退出的场景](#Part2.1. 进程退出的场景)
[Part2.2. return底层原理](#Part2.2. return底层原理)
[Part2.3. 获取退出码](#Part2.3. 获取退出码)
[Part2.4. 僵尸进程](#Part2.4. 僵尸进程)
[Part3. 进程等待 wait、waitpid](#Part3. 进程等待 wait、waitpid)
[Part3.1. wait函数](#Part3.1. wait函数)
[2. waitpid函数](#2. waitpid函数)
[Part4. status状态位图、解析宏](#Part4. status状态位图、解析宏)
[Part5. 进程程序替换、exec系列函数](#Part5. 进程程序替换、exec系列函数)
[Part5.1. 程序替换介绍](#Part5.1. 程序替换介绍)
[Part5.2. exec系列函数](#Part5.2. exec系列函数)
[Part6. 整体总结](#Part6. 整体总结)
[Part7. 结语](#Part7. 结语)
前言
Linux系统中,执行任务的基本是进程,对于他的理解是非常重要的,接下来来跟随小编的视角来看看吧。
let's go!!!!!!!!
Part1. fork失败的两个原因
1. 系统内进程数量达到上限,无法创建新进程。
2. 内存资源不足,子进程需要复制页表、虚拟地址空间,内存不够时fork失败。
shell小知识点:
bash内置命令(cd、echo、export)不会创建子进程,直接在当前bash进程执行;
外部命令(ls、cat)会fork创建子进程,在子进程中执行程序。
Part2. 进程退出
Part2.1. 进程退出的场景
进程退出意味着:释放占用内存、回收各类内核资源。
三种退出场景:
1. 正常退出:代码执行完毕。
◦ main函数return;
◦ 调用库函数exit();
◦ 系统调用_exit()。
2. 异常退出1:收到信号,程序崩溃(段错误、kill杀死进程)。
3. 异常退出2:代码逻辑出错,执行异常。
main的返回值代表进程执行状态:
• 返回0:程序执行成功;
• 返回非0:程序执行出错。
Part2.2. return底层原理
• 返回值数值比较小:把返回值放到寄存器,直接给内核;
• 返回值数值大:走内存拷贝,把返回值传递给内核。
Part2.3. 获取退出码
1. shell中:echo $?,查看上一个进程的退出码。
2. 程序内:
◦ strerror(x):传入错误码,返回对应错误描述字符串,头文件<string.h>。
◦ errno:系统调用失败时会设置全局错误码,头文件<errno.h>。
注意:如果进程是被信号杀死崩溃,返回的数字不是普通return退出码,代表收到的信号编号。
Part2.4. 僵尸进程
子进程退出之后,内核不会立刻释放子进程的task_struct。
• 子进程的退出状态、退出码保存在task_struct;
• 必须等待父进程调用wait / waitpid读取这个退出信息;
• 如果父进程一直不回收,子进程就变成僵尸进程。
僵尸进程本身不占用内存,但是task_struct内核结构体残留;大量僵尸会占用pid资源,导致无法新建进程。
Part3. 进程等待 wait、waitpid
为什么需要进程等待
1. 回收子进程资源,避免产生僵尸进程。
2. 获取子进程退出信息:判断子进程是正常跑完,还是异常崩溃。
Part3.1. wait函数
cpp#include <sys/wait.h> pid_t wait(int *status);• 功能:阻塞等待任意一个子进程退出。
• 返回值:
0:成功,返回退出的子进程pid;
-1:调用失败(没有子进程)。• 参数status:输出型参数,把子进程退出状态带回父进程。传NULL代表不关心退出状态。
缺点:wait是阻塞调用,如果子进程没有退出,父进程就卡住,不能做别的工作;只能等待任意子进程,无法指定某一个子进程。
2. waitpid函数
cpp#include <sys/wait.h> pid_t waitpid(pid_t pid, int *status, int options);参数解析
1. pid_t pid:指定要等待哪个子进程
◦ pid > 0:等待pid等于该值的指定子进程;
◦ pid = -1:等待任意一个子进程,等价于wait;
◦ pid = 0:等待和调用者同组的所有子进程;
◦ pid < -1:等待该进程组内任意子进程。
2. *int status:输出参数,保存子进程退出信息。status不是简单的退出码,是一个位图,需要宏函数解析。
3. int options:选项,最常用WNOHANG。
◦ WNOHANG:非阻塞等待。如果子进程还没有退出,waitpid立刻返回0,父进程不会卡住,可以继续做其他业务;轮询检测子进程状态。
waitpid返回值
• > 0:有子进程退出,返回退出子进程pid;
• = 0:设置WNOHANG,子进程还没有退出;
• -1:出错,没有子进程。
Part4. status状态位图、解析宏
status是int,内核把多种退出信息打包到这个整数,不能直接打印status当做退出码,要用系统提供的宏解析。
核心宏
1. WIFEXITED(status)
判断进程是否正常退出(return / exit)。返回true代表正常结束。2. WEXITSTATUS(status)
如果WIFEXITED为true,用它拿到子进程的退出码,等价main return的值。3. WIFSIGNALED(status)
判断进程是否被信号杀死(异常崩溃),返回true代表死于信号。4. WTERMSIG(status)
如果WIFSIGNALED为true,获取杀死进程的信号编号。
位图底层原理:
int共32bit,低7bit用来保存终止信号;次8bit保存进程退出码。
取出退出码:(status >> 8) & 0xFF。
取出信号编号:status & 0x7F。
非阻塞轮询示例代码模板
cpppid_t id = waitpid(pid, &status, WNOHANG); if(id > 0) { //子进程退出,处理回收逻辑 } else if(id == 0) { //子进程还没退出,父进程做自己的业务,过一会再来waitpid } else { //出错 }
wait底层原理:
子进程退出,内核释放绝大部分资源,task_struct保留;父进程调用wait/waitpid,内核把保存在task_struct里面的退出状态拷贝给父进程,之后彻底释放task_struct。
Part5. 进程程序替换、exec系列函数
Part5.1. 程序替换介绍
fork创建子进程之后,子进程默认复制父进程的代码和数据。
很多场景我们希望子进程跑完全不同的程序,就使用exec族函数做程序替换。注意:程序替换不会创建新进程!
pid不变,虚拟地址空间不变,task_struct不变;
仅仅把磁盘上新程序的代码段、数据段加载到当前进程的虚拟地址空间,覆盖原来的代码数据。堆、栈会重新初始化。
Part5.2. exec系列函数
cpp#include <unistd.h> //示例:执行ls -l -a execl("/usr/bin/ls", "ls", "-l", "-a", NULL);函数名后缀含义:
• l:list,可变参数列表传参,以NULL结尾;
• p:path,自动从环境变量PATH寻找可执行文件,不用写完整路径;
• v:vector,参数放到字符串数组传入;
• e:environment,可以自己传入自定义环境变量。
常用函数:execl、execlp、execv、execvp、execve(系统调用,其他都是库函数封装)。
exec函数返回值
exec调用成功没有返回值,直接跳转到新程序入口运行;
只有调用失败才返回-1,继续执行原来代码。
经典模型:
1. fork创建子进程;
2. 子进程调用exec做程序替换,跑新业务;
3. 父进程调用wait/waitpid等待子进程回收资源。
Part6. 整体总结
1. fork失败两大原因:系统进程数上限、内存不足。shell内置命令不创建子进程。
2. 进程退出:正常退出(return/exit)、信号异常崩溃。退出码保存在内核task_struct。
3. 僵尸进程:子进程退出,父进程没有wait回收,残留task_struct。
4. wait:阻塞等待任意子进程;waitpid可以指定子进程,支持WNOHANG非阻塞轮询。status位图,通过WIFEXITED、WEXITSTATUS解析退出信息。
5. exec程序替换:不产生新进程,覆盖当前进程代码数据;fork+exec是Linux创建执行业务进程的标准范式。
Part7. 结语
这篇文章我们认识并知道了Linux进程,接下来,小编还会带来更多Linux相关的知识,敬请期待~
最后,祝大家可以:春风得意马蹄疾,一日看尽长安花!最后的最后,要是觉得本文还可以的话,可以点点赞,关注小编一波,谢谢大家!~
