我最近在学 Linux 进程控制:fork、退出码与进程等待
最近在学 Linux 系统编程/操作系统,刚接触 fork、waitpid 这些的时候真的有点绕。尤其是"为什么一个函数会有两个返回值"这个问题,我一开始想了半天。这篇就当我的学习记录,也顺便把几个容易混的点整理一下,希望能帮到同样在学的同学。
1. 子进程为什么返回 0,父进程为什么返回子进程 pid?
我一开始最不理解的就是:为什么 fork 之后,子进程拿到 0,父进程拿到子进程的 pid?后来终于搞懂了,其实这是为了区分父子进程,并方便父进程管理子进程。
-
子进程返回 0 :一个父进程可以创建多个子进程,但是子进程有且只有一个父进程 。子进程返回 0,代表自己是子进程,可以用
getppid()获取父进程 id。 -
父进程返回子进程 pid :父进程可能创建很多子进程,需要拿到子进程 pid,用来区分不同子进程,后续可以
wait()等待子进程回收资源。
约定:0 代表子进程;>0 是子进程 pid,在父进程中;返回 -1 代表 fork 失败。
2. 为什么 fork 一个函数会有两个返回值?
这个问题我踩过一个思维坑:我总以为"同一个函数返回了两次"。其实不是。
fork 调用时,内核创建新子进程,复制父进程的地址空间(页表浅拷贝 + COW)。
- 原来的父进程:继续执行,拿到返回值(子进程 pid)
- 新创建出来的子进程:从 fork 返回处继续执行,拿到返回值 0
本质:产生了两个独立进程,两个进程各自拿到一份返回值,不是同一个函数在单个进程里返回两次。
3. 为什么同一个 id,既等于 0,又大于 0?
看这行代码的时候,我一度怀疑变量是不是精神分裂了:
text
pid_t id = fork();
其实不是同一个变量同时有两个值。
- 父进程 :
id > 0(值为子进程 pid) - 子进程 :
id = 0
fork 之后变成两个独立进程,各自有独立的 id 变量(虚拟地址相同,但物理内存后面会 COW 分离),两个进程中变量的值不一样。
进程终止:退出码和信号
我们都知道,当进程创建时是先创建地址空间这种数据结构,后在使用时再加载到物理空间中。那进程终止的时候,情况其实分好几种。
代码跑完,结果正确
比如 main 函数 return 0。这里要注意,main 函数的 return 0 是进程的退出码,并不是"返回值"给谁。可以用:
bash
echo $? #可以查看上一个进程的退出码
代码跑完,结果不正确
比如 main 函数 return 非 0,或者程序内部发现错误后返回错误码。
c
#include<stdio.h>
#include<string.h>
int mian(){
for(int i=0;i<200;i++){
printf("%d->%s\n",strerror(i));
}
}
c
#include<stdio.h>
#include<string.h>
#include<errno.h>
int mian(){
FILE* fp=fopen("1.txt","r");//失败
if(fp==NULL){
return errno;
//printf("%d:%s\n",errno,strerror(errno));
}
}
上面这段代码里 mian 是手误,应该是 main,不过代码块我按原笔记保留了。意思就是 fopen 失败后,把 errno 作为退出码返回。
代码没跑完,结果不正确
这种情况通常是被信号打断了。整理一下就是:
- 代码跑完(运行期间没有收到信号):signal number = 0,并且
return 0-> signumber:0 && 退出码:0 - 跑完但不正确:signumber:0 && 退出码 != 0
- 未跑完报错:signumber != 0 && 退出码无意义
当一个进程退出的时候,OS 会把退出的信息保存在 task_struct 中,所以进程退出需要僵尸进程维持退出状态,等父进程来读取。
进程退出:return、exit 和 _exit
进程退出的方式主要有:
main函数return- 在任意地方调用
exit - 非
main函数return:只是函数结束,不是进程结束 - 非
main函数exit:进程结束
这里有个我踩过的坑:exit 终止进程会强制刷新缓冲区,_exit 不会。exit 是对系统调用 _exit 的封装。这也说明缓冲区和刷新缓冲区的操作一定不在内核中,而是语言层负责的。
进程等待:waitpid 与 WNOHANG
进程不等待会导致内存泄漏------更准确地说,是子进程退出后变成僵尸进程,占着内核里的进程表项不释放。而且父进程也拿不到子进程的退出信息,所以进程等待除了回收资源,也允许父进程获取子进程信息。
c
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *wstatus, int options);
/*
参数说明
1. pid
pid > 0:等待指定PID的子进程
pid = -1:等待任意子进程,等价于wait()
pid = 0:等待同进程组内任意子进程
pid < -1:等待abs(pid)进程组中的任意子进程
2. wstatus:输出型参数,保存子进程退出信息;传NULL代表不关心退出状态
宏解析wstatus:
WIFEXITED(wstatus):非0,子进程正常退出
WEXITSTATUS(wstatus):获取子进程退出码,仅正常退出有效
WIFSIGNALED(wstatus):非0,子进程被信号杀死
WTERMSIG(wstatus):获取杀死进程的信号编号
3. options
0:阻塞等待,子进程未退出时父进程阻塞卡住
WNOHANG:非阻塞等待,子进程没退出立刻返回0,父进程不阻塞,可以轮询检测
返回值
>0:成功,返回退出子进程pid
0:使用WNOHANG,子进程仍然运行,未退出
-1:出错(无对应子进程、参数错误)
*/
/* wait 和 waitpid对比
wait:只能等待任意子进程,仅阻塞等待
waitpid:可指定等待对象,支持阻塞/WNOHANG非阻塞
*/
// WNOHANG非阻塞示例
#include <stdio.h>
#include <unistd.h>
int main()
{
int wstatus;
pid_t ret = waitpid(-1, &wstatus, WNOHANG);
if(ret > 0)
{
printf("子进程%d退出\n", ret);
}
else if(ret == 0)
{
printf("子进程还在运行\n");
}
else
{
perror("waitpid");
}
return 0;
}
/*
一句话总结
waitpid用来等待、回收子进程,防止僵尸进程;可以指定等待哪个子进程,支持WNOHANG非阻塞轮询。
重点考点:WNOHANG返回0不是失败,代表子进程仍然存活!
*/
这里我一定要单独提醒:WNOHANG 返回 0 不是失败! 我一开始看到返回 0 就以为出错了,其实 0 代表子进程还在运行,父进程可以继续干别的事,过会儿再来轮询。
wstatus 里到底存了什么?
图片里展示的就是 wstatus 的位布局:

每一个十六进制字符,单独换成 4 个 bit,前面的 0 不能省略!
c
int waitpid1=0;
waitpid(pid,&waitpid1,0);
int exit_code=(waitpid1>>8)&0xff;
int signal_code=waitpid1 & 0x7f;
int coredump=(waitpid1>>7)&0x1;
通过这种方式就可以获取退出码信号变化、符号位等信息。比如低 7 位是终止信号,第 7 位表示是否 core dump,高 8 位是退出码。
总结一下
fork后父子进程各自拿到一份返回值:子进程是 0,父进程是子进程 pid。- 进程退出状态主要看 signal number 和退出码:正常退出 signal 为 0,退出码才有意义。
exit会刷新用户态缓冲区,_exit不会,所以缓冲区刷新不是内核干的事。- 父进程要用
waitpid回收子进程,避免僵尸进程。 WNOHANG返回 0 不是失败,只是子进程还在跑。wstatus可以通过位运算拆出退出码、信号编号和 coredump 标志。
差不多就是这些。进程控制这块一开始真的容易晕,尤其是 fork 的返回值和 waitpid 的状态解析,但多写几遍、画一画位图,就慢慢清楚了。如果哪里理解错了,欢迎大佬指正~