目录
[Part1. fork调用失败的原因](#Part1. fork调用失败的原因)
[Part1.1. 进程数目达到上限](#Part1.1. 进程数目达到上限)
[Part1.2. 内存资源不足](#Part1.2. 内存资源不足)
[Part2. Shell内置命令的特殊执行](#Part2. Shell内置命令的特殊执行)
[Part3. 进程退出](#Part3. 进程退出)
[Part4. 僵尸进程的产生](#Part4. 僵尸进程的产生)
[Part5. 进程回收](#Part5. 进程回收)
[Part6. waitpid()](#Part6. waitpid())
[Part7. 知识体系总结](#Part7. 知识体系总结)
[Part8. 结语](#Part8. 结语)
前言
Linux系统中,进程的回收就相当于C语言的free,为了避免资源的浪费,对于他的理解是非常重要的,接下来来跟随小编的视角来看看吧。
let's go!!!!!!!!
Part1. fork调用失败的原因
fork()用来创建子进程,但不是每次调用都一定成功。主要有两类典型失败场景:
Part1.1. 进程数目达到上限
Linux有参数nproc,限制单个用户能够同时拥有的最大进程数量。如果我们循环fork()大量创建进程,就会触发限制,fork()返回-1。
p.s.不是整个系统所有进程总数,是当前用户的配额。
Part1.2. 内存资源不足
创建子进程时,内核需要为新进程分配task_struct、页表等内核数据结构。虽然有写时拷贝COW,不需要立刻复制全部用户内存,但内核本身的开销无法省略。如果系统内存已经耗尽,fork()会失败 。
Part2. Shell内置命令的特殊执行
在bash里,命令分为内置命令(built‑in) 和外部命令
• 内置命令:cd、echo、export、pwd等。
不需要创建子进程,直接在bash进程内部执行函数逻辑。
举个例子:cd修改的是当前Shell进程自己的工作目录。如果cd是外部程序,在子进程里修改目录,父Shell的工作目录不会发生任何变化,cd命令就彻底失效了。
• 外部命令:ls、gcc、top等磁盘上的可执行文件。
Shell执行流程:fork()创建子进程 → exec程序替换 → 子进程运行程序。
Part3. 进程退出
进程退出,意味着进程生命周期走到终点。
进程退出 ≠ 所有资源立刻全部消失用户空间的内存(堆、栈、代码段)会被操作系统回收;
但是task_struct(进程控制块PCB)不会立刻释放!PCB里保存着进程的退出状态、退出原因,要留给父进程读取。只要PCB没释放,进程在系统进程表里还有条目,它就是僵尸进程(zombie)。
进程退出一共有四种方式,分为正常终止、异常终止:
正常终止(3种)
1. main函数return;
2. 任意位置调用C库函数 exit(int exit_code);
3. 调用系统调用 _exit(int exit_code)。
exit() 和 _exit() 的区别:
• exit():C库函数,会刷新C标准IO缓冲区,调用注册的atexit()退出处理函数,最后调用_exit();
• _exit():直接系统调用,跳过缓冲区刷新、跳过钩子函数,直接通知内核终止进程。
异常终止(1种)
进程收到未捕获、未忽略的终止信号,比如段错误SIGSEGV(11)、kill -9发送的SIGKILL(9)。
程序没有机会执行return或者exit,由内核直接杀死进程。
main函数return返回值的含义
main的返回值叫做进程退出码(exit code),是一个int类型。约定俗成:
• return 0:代表进程正常执行完毕,运行结果符合预期;
• return 非0:代表进程运行结束,但是出现了某种问题。
Linux Shell可以拿到上一条命令的退出码
echo ? echo ?会打印最近一个前台进程的退出码。退出码会被内核保存在子进程的task_struct中,等待父进程调用wait/waitpid读取。只要父进程没有读取,这块信息就会保留,进程就处于僵尸状态。
拓展:return返回大对象的原理
如果main返回一个很小的值(int),编译器直接放到寄存器;
如果返回一个很大的自定义对象,编译器会悄悄在调用方栈上开辟一块空间,传一个隐藏指针,函数内部把对象拷贝过去,避免寄存器放不下。
错误码相关函数:errno、strerror
1. errno(头文件 <errno.h>)
errno是一个线程局部全局变量。当系统调用(open、fork、pipe)失败的时候,操作系统会把一个整数错误编号写入errno。
p.s. :只有调用失败的时候errno才有意义!调用成功不会清零errno,不能拿errno!=0判断出错。2. strerror(int errnum)(头文件 <string.h>)
输入错误编号,返回人类可读的错误描述字符串。
std::cout << strerror(errno) << std::endl;
perror()等价于:打印自定义字符串 + strerror(errno)。
一个重要区分:
• 如果进程正常退出:退出码就是return x或者exit(x)传入的值;
• 如果进程被信号杀死:exit code没有意义!进程根本没有机会设置退出码,终止原因由信号编号记录。
Part4. 僵尸进程的产生
定义:子进程已经退出,但是父进程还没有调用wait() / waitpid()读取它的退出状态,子进程的PCB保留在系统中,这个进程就是僵尸进程(Zombie,状态标记Z)。
用户空间内存已经全部释放,只占极小的内核内存(task_struct),但是进程ID(PID)依然被占用。
如果一个程序不停创建子进程,父进程永远不回收,系统PID资源会慢慢耗尽,最后fork()失败。
常见误区:
用kill -9杀僵尸进程 ------ 完全无效
僵尸进程已经死了!kill只能给活着的进程发信号。
唯一解法:
1. 父进程调用wait() / waitpid()主动回收;
2. 杀死父进程,子僵尸变成孤儿进程,被init(systemd,PID=1)收养,systemd会自动回收僵尸。
Part5. 进程回收
函数原型:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
• 返回值:成功返回被回收的子进程pid;失败返回-1(没有子进程);• 参数status:输出型参数!传入一个int变量的地址,内核会把子进程的退出状态数据写到这个整数里;
• 如果传 NULL:代表不关心退出状态,只回收僵尸释放PCB。
wait()默认是阻塞调用:
如果当前没有已经退出的子进程,wait()会让父进程挂起,休眠,直到任意一个子进程退出,内核唤醒父进程,完成回收。
cpppid_t rid = wait(nullptr); if(rid > 0){ std::cout << "成功回收子进程pid = " << rid << std::endl; }status这个int不是简单等于exit_code
Linux把大量信息打包编码到一个int的各个二进制位中,用位域、掩码区分不同信息。
在32位int下:
• 低7位:如果进程被信号终止,这里存放终止信号编号;
• 第8位:core dump标记,为1代表程序崩溃生成了转储文件;
• 次低8位(8~15位):只有进程正常退出的时候有效,保存退出码(exit code)。
举个例子:
如果子进程 return 5,正常退出:
status中 (status >> 8) & 0xFF = 5,低7位 = 0
如果子进程被信号11(段错误)杀死:
status低7位=11,(status >>8)&0xFF的值无意义。
POSIX标准提供了一套宏来安全解析status:
WIFEXITED(status) // bool,true:子进程正常退出
WEXITSTATUS(status) // 获取正常退出的退出码,只在WIFEXITED为true时有意义
WIFSIGNALED(status) // bool,true:子进程被信号杀死
WTERMSIG(status) // 获取杀死进程的信号编号示例:
cppint status; pid_t rid = wait(&status); if(rid>0){ if(WIFEXITED(status)){ std::cout << "正常退出,退出码:" << WEXITSTATUS(status) << std::endl; }else if(WIFSIGNALED(status)){ std::cout << "被信号" << WTERMSIG(status) << "杀死\n"; } }
Part6. waitpid()
wait()只能阻塞等待任意一个子进程退出,功能有限。waitpid()可以精确控制回收行为,进程池、服务器代码几乎都用waitpid。
原型:
pid_t waitpid(pid_t pid, int *status, int options);参数1:pid,指定要等待哪个子进程
• pid > 0:只等待PID等于pid的那个子进程;
• pid = -1:等待任意子进程,等价于wait();
• pid = 0:等待和调用进程同一个进程组的任意子进程;
• pid < -1:等待进程组ID等于abs(pid)的所有子进程。
参数3:options,选项,可以按位或组合
最重要的选项:WNOHANG
WNOHANG → wait no hang,非阻塞!
如果没有已经退出的子进程,waitpid不会阻塞休眠,立刻返回0。
返回值区分:
1. >0:成功回收一个僵尸,返回被回收子进程PID;
2. ==0:调用成功,但是没有子进程退出(只有WNOHANG的时候才会出现);
3. -1:出错,比如没有子进程。
非阻塞轮询回收僵尸
cppwhile(true){ int status; pid_t rid = waitpid(-1, &status, WNOHANG); if(rid > 0){ // 成功回收一个僵尸进程 std::cout << "回收pid:" << rid << std::endl; }else if(rid ==0){ // 当前没有僵尸,可以去做别的业务逻辑 break; }else{ // rid == -1,没有子进程了 break; } }这种模式叫轮询(polling),父进程不用阻塞卡在回收逻辑,可以一边处理自己的任务,一边定期检查有没有子进程变成僵尸。
Part7. 知识体系总结
1. fork失败两大原因:用户进程上限、内存不足;返回-1,无异常;
2. 进程正常退出:return / exit() / _exit();异常退出:收到终止信号;
3. 退出码保存在PCB,父进程不读取 → 僵尸进程;僵尸只能回收,不能kill;
4. wait()阻塞等待任意子进程;waitpid()可以指定pid,可以非阻塞(WNOHANG);
5. status不能直接读,必须用WIFEXITED、WEXITSTATUS等宏解析;
Part8. 结语
这篇文章我们认识并知道了Linux进程的回收,接下来,小编还会带来更多Linux相关的知识,敬请期待~
最后,祝大家可以:春风得意马蹄疾,一日看尽长安花!最后的最后,要是觉得本文还可以的话,可以点点赞,关注小编一波,谢谢大家!~
