我最近在学 Linux 进程控制:fork、退出码与进程等待

我最近在学 Linux 进程控制:fork、退出码与进程等待

最近在学 Linux 系统编程/操作系统,刚接触 forkwaitpid 这些的时候真的有点绕。尤其是"为什么一个函数会有两个返回值"这个问题,我一开始想了半天。这篇就当我的学习记录,也顺便把几个容易混的点整理一下,希望能帮到同样在学的同学。

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 的状态解析,但多写几遍、画一画位图,就慢慢清楚了。如果哪里理解错了,欢迎大佬指正~

相关推荐
企业数字化笔记1 小时前
固定资产Excel批量导入怎么防重复?资产编码、重复检查和错误回执
java·后端·excel
SamDeepThinking1 小时前
Java 17内存屏障:为什么需要,怎么用
java·后端·面试
azhou的代码园2 小时前
景区游船预约服务系统
人工智能·spring boot·后端
小灰灰搞电子2 小时前
Rust Attribute(属性标记)完整整理
开发语言·后端·rust
tedcloud1233 小时前
God‘s Eye View 怎么搭建?在云服务器上部署一个实时 3D 地球可视化平台
linux·服务器·开发语言·后端·rust
深入云栈3 小时前
Netty 4.2.x 源码深度解析 (十三):Epoll 传输 —— Linux 高性能 IO 的 Netty 实现
java·后端
Lyra_Infra3 小时前
从一段错误 JSON 说起:Policy、Role 与 IAM
后端·json·aigc
Charlie_Byte3 小时前
在用户目录安装多版本 JDK,并用环境变量切换
后端
黑马程序员毕设4 小时前
基于微信小程序的节目活动报名与管理系统设计与实现
java·开发语言·spring boot·后端·电脑