资讯中心

【Linux系统篇】从 fork 到 WNOHANG:进程创建与等待机制详解

📅 2026/7/23 21:03:52
【Linux系统篇】从 fork 到 WNOHANG:进程创建与等待机制详解
个人主页爱和冰阔乐专栏传送门《数据结构与算法》 、C学习方向C方向学习爱好者⭐人生格言得知坦然 失之淡然博主简介文章目录前言一、进程创建1.1 fork函数初识1.2 写时拷贝1.3 fork创建子进程的用途1.4 fork失败的原因二、进程终止2.1 进程退出的三种场景2.2 main函数的返回值2.3 strerror和errno2.4 异常退出要看信号2.5 return、exit与_exit三、进程等待3.1 为什么需要进程等待3.2 wait等待子进程3.3 waitpid等待指定子进程四、status如何保存退出信息4.1 为什么exit(1)打印成2564.2 不要在业务代码里手写位解析4.3 异常退出看终止信号4.4 使用系统宏读取退出结果五、非阻塞等待5.1 WNOHANG的返回值5.2 非阻塞等待代码5.3 等待期间处理其他任务总结前言进程控制这一部分主要讲三件事怎么创建子进程、进程怎么退出、父进程怎么回收子进程。fork创建子进程以后父子进程会各自执行。子进程完成任务后可能正常返回也可能因为异常收到信号。父进程需要通过wait或waitpid获取结果并回收资源。本文仍按学习时的顺序展开从fork、写时拷贝讲到退出码、进程等待和非阻塞等待。一、进程创建1.1 fork函数初识Linux中使用fork从当前进程创建一个子进程#includeunistd.hpid_tfork(void);fork的返回值要结合执行位置判断执行位置返回值用途子进程0进入子进程分支父进程子进程PID记录并管理新建的子进程创建失败-1根据错误信息处理失败父进程返回子进程PID是为了后续能够指定等待和管理这个子进程子进程返回0则可以直接作为分支判断标记。一个函数看起来返回了两次是因为fork之后出现了父子两个执行流。fork之前只有父进程执行fork之后父子进程都从调用位置继续向后运行谁先执行由调度器决定。进入内核后OS需要为子进程分配内核数据结构、复制父进程的部分进程信息、把子进程加入进程列表最后再让调度器选择执行。1.2 写时拷贝创建子进程时父子进程可以共享只读代码。数据需要保持独立但操作系统不会在fork时立刻复制全部数据页。刚创建完成时父子页表先指向同一批物理数据页并把相关页面设置成写时拷贝状态。某一方真正写入时CPU触发缺页异常。OS确认虚拟地址合法、页表映射正常而且这次访问属于写时拷贝后再申请新的物理页、复制数据并修改页表映射。因此准确说法是写时拷贝不是“写实拷贝”。它不是普通程序错误而是OS利用页权限实现延迟复制。为什么要写时拷贝如果fork时把父进程数据完整复制一份数据越多创建子进程越慢。子进程通常只修改少量数据全部复制会让大量未修改内容在内存中重复保存。写时拷贝把“创建时全部复制”改成“真正修改时再复制”减少了创建时间也提高了物理内存利用率。结论fork时不立即复制全部数据真正写入时才复制对应物理页。1.3 fork创建子进程的用途子进程被创建出来一定是为了完成某个任务。常见用途有两类子进程继续执行当前程序通过if/else分流让父子进程完成不同任务子进程调用exec系列接口执行全新的程序。Shell运行外部命令时通常就是先创建子进程再让子进程替换成目标程序。1.4 fork失败的原因fork失败时返回-1。常见原因是系统资源不足例如无法继续分配内核数据结构、达到进程数量限制或内存压力过大。失败后不能继续把返回值当成父进程分支处理。二、进程终止进程终止的本质是OS释放该进程占用的内核数据结构、代码和数据等资源。2.1 进程退出的三种场景代码运行完毕结果正确代码运行完毕但结果不正确代码异常终止例如除零、野指针等情况。判断顺序正常退出看退出码异常退出看终止信号。子进程也是进程执行结果同样分为这三种情况。子进程被创建是为了完成任务父进程需要知道任务是否完成、结果是否正确以及有没有异常退出。2.2 main函数的返回值写C/C代码时我们经常在main末尾写return 0。这个返回值给谁它又表示什么main是程序入口。return 0通常表示程序执行成功非0表示任务失败。不同非0值可以对应不同错误原因具体含义由程序自己约定。只有main有“执行到函数末尾等价于返回0”的特殊规则。普通的非void函数如果没有返回值不能简单理解成默认返回0。子进程没有打印内容时bash怎么判断它的执行情况先看下面的例子#includestdio.hintmain(){// printf(hello world\n);FILE*fpfopen(log.txt,r);if(fpNULL)return1;fclose(fp);return0;}这里使用echo $?查看最近一个前台进程的退出码。进程退出后内核会保留退出状态等待父进程读取。2.3 strerror和errno怎样把错误编号转换成文字描述C标准库提供了strerror可以先查看man 3 strerror。#includestdio.h#includestring.hintmain(){inti0;//并不清楚多少个随机写的200for(;i200;i){printf(%d-%s\n,i,strerror(i));}}打印结果能看到当前系统支持的错误描述。具体数量与系统环境有关不能把某台机器的结果当成所有Linux系统的固定值。系统调用失败时通常会设置errno。程序可以根据自己的约定返回对应值但Shell退出状态通常只保留低8位不适合直接承载任意大的错误编号。退出码由程序自己约定不要求必须对应C标准库错误码。例如程序可以使用13表示某个自定义错误。退出码只能描述正常结束后的执行结果。若进程异常终止还要继续看终止信号。2.4 异常退出要看信号下面先看一个异常退出场景信号部分后续再详细介绍#includestdio.hintmain(){inta10;a/0;return89;}进程在执行return 89之前已经异常终止所以89没有机会成为退出码。异常退出通常表示进程收到了信号。2.5 return、exit与_exit进程退出常见有下面几种方式在main中执行return主函数结束随后进程退出。普通函数中的return只结束当前函数在代码任意位置调用exit(code)整个进程直接结束。// 在任意函数中结束整个进程exit(23);#includestdio.h#includestring.h#includeerrno.h#includestdlib.hvoidfun(){printf(fun begin!\n);exit(40);printf(fun end!\n);}intmain(){fun();printf(man!\n);}任何位置调用exit都会结束整个进程这与普通函数中的return不同。退出状态由内核保存父进程通过等待接口读取。_exit()直接终止调用它的进程。exit是C标准库接口_exit是系统调用层接口。下面比较两者的差异。调用exit时若printf没有换行内容可能先留在stdio缓冲区。进程退出前exit会刷新缓冲区因此最终仍能看到输出。若字符串带换行在终端环境下通常会提前刷新。#includestdio.h#includeunistd.h#includestdlib.hintmain(){printf(man\n);sleep(2);exit(23);}把同样代码改成_exit后带换行时可能已经触发终端行缓冲刷新不带换行时内容仍留在用户态缓冲区_exit不负责刷新所以看不到对应输出。exit会执行C库清理并刷新stdio缓冲区_exit不会执行这些用户态清理直接进入进程终止流程。exit在进入_exit之前通常还会调用通过atexit注册的清理函数并关闭标准I/O流。因此这里观察到的是C库缓冲区差异。若缓冲区完全由内核维护调用_exit时就不应该出现这种现象。方式作用范围是否执行C库清理return结束当前函数在main中会结束进程由main返回时正常完成exit(code)结束整个进程是会刷新stdio缓冲区_exit(code)直接终止整个进程否不刷新stdio缓冲区三、进程等待3.1 为什么需要进程等待子进程退出后父进程如果一直不处理就可能形成僵尸进程。僵尸进程已经结束kill -9也无法再次终止它父进程需要知道子进程任务是否完成、结果是否正确父进程通过等待回收子进程资源并按需取得退出信息。3.2 wait等待子进程父进程创建子进程后需要通过wait或waitpid等接口等待子进程。这个过程就是进程等待。下面就是僵尸进程的代码#includestdio.h#includeerrno.h#includestring.h#includestdlib.h#includeunistd.h#includesys/types.hintmain(){pid_tidfork();if(id0){//子intcnt5;while(cnt){printf(我是一个子进程pid:%d,ppid:%d\n,getpid(),getppid());sleep(1);cnt--;}exit(0);}//父进程sleep(100);return0;}pid_t wait(int *status)等待任意一个子进程。成功时返回被回收子进程的PID并把退出信息写入status失败时返回-1。不关心退出信息时可以传NULL。#includestdio.h#includeerrno.h#includestring.h#includestdlib.h#includeunistd.h#includesys/types.h#includesys/wait.hintmain(){pid_tidfork();if(id0){//子intcnt5;while(cnt){printf(我是一个子进程pid:%d,ppid:%d\n,getpid(),getppid());sleep(1);cnt--;}exit(0);}//父进程// sleep(100);// 情况// 1.子进程退出wait回收解决僵尸进程// 2.五秒以内子进程没有退出那么wait在干嘛sleep(10);pid_tridwait(NULL);if(rid0)printf(wait success,rid:%d\n,rid);//rid子进程pidsleep(10);return0;}可以使用下面的Shell循环持续监测。父进程执行wait后退出的子进程不会继续保持僵尸状态while:;dopsajx|head-1psajx|grep[c]odesleep1done现象如果等待子进程子进程没有退出父进程会阻塞在wait调用处这里与scanf场景类似这个例子可以按时间看前5秒父子进程同时运行子进程退出后在父进程调用wait前短暂进入僵尸状态wait返回后子进程被回收。3.3 waitpid等待指定子进程waitpid同样用于等待子进程。成功时返回子进程PIDstatus是输出型参数options控制阻塞方式。传入指定PID时父进程只等待对应子进程。// 等待任意子进程pid_tridwaitpid(-1,NULL,0);// 等待指定子进程pid_tridwaitpid(id,NULL,0);父进程没有子进程、传入PID不属于当前进程或参数错误时waitpid会失败。四、status如何保存退出信息4.1 为什么exit(1)打印成256父进程通过status取得子进程退出信息。它不只保存main的返回值还要同时表示进程是否正常退出、终止信号等状态。不要直接把原始status当退出码。先判断退出类型再提取对应信息。intstatus0;pid_tridwaitpid(id,status,0);if(rid0){printf(raw status: %d\n,status);}这里设置的退出码是1但原始status打印成256这是为什么status不是单纯的退出码它同时编码正常退出、退出码、终止信号和core dump等信息。常见实现中退出码位于较高的8位所以exit(1)对应的原始值会表现为1 8也就是256。4.2 不要在业务代码里手写位解析右移8位可以帮助理解常见布局但正式代码应优先使用sys/wait.h提供的宏。if(WIFEXITED(status)){printf(exit code: %d\n,WEXITSTATUS(status));}elseif(WIFSIGNALED(status)){printf(signal: %d\n,WTERMSIG(status));}这时就能解释bash为什么能拿到退出码。父子进程地址空间彼此独立子进程修改全局变量会发生写时拷贝父进程不能靠普通变量直接读取结果只能通过OS提供的等待接口取得。4.3 异常退出看终止信号Linux中有多种信号可以使用kill -l查看。前面使用过kill -9。列表左侧是信号编号右侧是信号名称这些名称在头文件中以宏定义形式存在。grep#define SIGKILL/usr/include/*-Rvim/usr/include/asm/signa信号列表中没有0号终止信号。进程异常终止时status会记录对应的终止信号。若进程正常退出终止信号部分为0若进程被信号终止退出码就没有业务意义。手工执行status 0x7F可以帮助理解但正式代码应使用WIFSIGNALED和WTERMSIG。由于进程具有独立性父进程必须通过系统调用取得子进程退出信息。wait和waitpid从哪里取得信息子进程退出后会释放大部分资源但内核仍保留必要的进程结构和退出状态等待父进程读取。4.4 使用系统宏读取退出结果OS不希望用户在业务代码里反复手写位运算因此提供了对应宏WEXITSTATUS获取正常退出码WIFEXITED判断进程是否正常退出WIFSIGNALED判断进程是否被信号终止WTERMSIG获取终止信号。五、非阻塞等待5.1 WNOHANG的返回值waitpid的最后一个参数有多个选项。这里重点看WNOHANG子进程尚未结束时立即返回不阻塞父进程。非阻塞等待需要配合轮询。等待方可以在子进程运行期间继续处理自己的任务。waitpid使用WNOHANG时可以按返回值区分三种结果返回值含义 0已回收对应子进程返回值就是子进程PID 0子进程仍在运行本次立即返回 0等待失败需要检查参数和错误信息5.2 非阻塞等待代码下面看非阻塞等待的完整判断while(1){intstatus0;pid_tridwaitpid(id,status,WNOHANG);if(rid0){if(WIFEXITED(status)){printf(exit code: %d\n,WEXITSTATUS(status));}break;}if(rid0){printf(子进程还在运行\n);sleep(1);continue;}perror(waitpid);break;}5.3 等待期间处理其他任务父进程在waitpid返回0时可以周期性执行自己的任务if(rid0){Download();Log();sleep(1);}总结fork通过写时拷贝降低创建子进程的成本return、exit和_exit决定进程如何结束wait与waitpid负责获取退出结果并回收子进程。正常退出先看退出码异常退出先看终止信号。解析status时优先使用系统提供的宏不要在业务代码里依赖手写位运算。使用WNOHANG后父进程可以在等待期间继续处理任务但轮询需要控制频率不能写成一直占用CPU的空转循环。资源分享【Linux进程】程序地址空间详解虚拟地址、页表、写时拷贝与mm_struct【Linux排障实战】Docker容器启动失败怎么查端口、日志、权限与网络【Linux系统编程】环境变量深度解析——从 fork 继承到 export 内建命令两张表打通进程上下文