【知识讲解】 Linux进程的回收相关接口了解


目录

前言

[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()会让父进程挂起,休眠,直到任意一个子进程退出,内核唤醒父进程,完成回收。


cpp 复制代码
pid_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) // 获取杀死进程的信号编号

示例:

cpp 复制代码
int 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:出错,比如没有子进程。


非阻塞轮询回收僵尸

cpp 复制代码
while(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相关的知识,敬请期待~
最后,祝大家可以:春风得意马蹄疾,一日看尽长安花!

最后的最后,要是觉得本文还可以的话,可以点点赞,关注小编一波,谢谢大家!~

相关推荐
新时代牛马1 小时前
PCI与PCIe 完整篇:硬件拓扑→ 报文协议→配置/BAR → Linux 驱动(一条主线讲透)
linux·运维·网络
筝筝ba1 小时前
如何跳过FFDC
xml·linux·运维·服务器·网络
赴生-2 小时前
Liunx 操作系统 进程概念(下)
linux
风静如云2 小时前
VMWare:Ubuntu(26.04)共享后看不到
linux
为思念酝酿的痛2 小时前
传输层协议UDP
linux·网络·udp
Elnaij2 小时前
Linux系统与系统编程(16)——日志、线程池的实现与线程安全
linux
杨云龙UP2 小时前
DB2 HADR 主备架构活动日志参数优化操作手册
linux·运维·服务器·数据库·db2·db2 hadr·日志参数调整
csdn_aspnet3 小时前
如何从电脑主机拷东西到VWmare虚拟机
linux·运维·服务器·windows·vmware·虚拟机
Vcaker3 小时前
Linux学习28-Kubernetes service
linux·运维·学习