前言
在操作系统的世界里,进程就像一个个忙碌的 “工作单元”,从被创建到完成任务后终止,始终遵循着一套严谨的生命周期规则。理解进程的生命周期管理,是揭开操作系统多任务调度神秘面纱的关键 —— 而这其中,进程的创建、终止与等待机制,构成了整个生命周期的核心骨架。
本文将沿着 “创建→终止→等待” 的脉络,系统解析进程管理的底层逻辑:从
fork
函数如何 “复制” 出一个新进程,到写时拷贝技术如何优化内存效率;从进程正常退出与异常终止的不同场景,到退出码背后的状态传递逻辑;更会深入探讨为何父进程必须等待子进程,以及wait
与waitpid
函数在阻塞 / 非阻塞模式下的实现细节。无论你是想搞懂 “父子进程为何能共享代码却互不干扰”,还是想理解 “如何安全回收子进程资源”,这些知识点都将为你构建起进程管理的完整知识体系。
目录
1. 进程创建
1.1 fork函数初始
在linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h>
pid_t fork(void);
返回值:⾃进程中返回0,⽗进程返回⼦进程id,出错返回-1
int main()
{
pid_t pid;
printf("Before: pid is %d\n", getpid());
if ( (pid=fork()) == -1 )perror("fork()"),exit(1);
printf("After:pid is %d, fork return %d\n", getpid(), pid);
sleep(1);
return 0;
}
1.2 fork函数返回值
父进程返回子进程的pid,子进程返回0
1.3 写时拷贝

写时拷贝减少创建时间 减少内存浪费
1.4 fork常规用法
1.5 fork调用失败原因
2. 进程终止
2.1 进程退出场景
代码运行完毕,结果正确
代码运行完毕,结果不正确
代码异常终止
在以前的语言代码中,都有main函数,main函数的返回值,通常表明的程序的执行情况.
代码运行完毕,结果正确,return 0;
代码运行完毕,结果不正确,return !0;不同的非零值表示不同的出错原因。
2.2 进程常见退出方法
#include <unistd.h>
void exit(int status);
任何地方调用exit,表示进程结束。并返回给父进程,子进程的退出码。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void func(){
printf("I am func.\n");
exit(1);
}
int main(){
func();
printf("main\n");
return 0;
}
int main()
{
printf("main");
sleep(2);
exit(1);
}

exit变成_exit后
所以也可以得出我们之前谈论的缓冲区一定不是操作系统内部的缓冲区。
前者是库函数,后者是系统调用,exit最后会调用_exit,
2.3 退出码
echo $? 查看最近一个程序(进程)退出时的退出码,进程的退出码是写到了task_struct内部的。
#include <stdio.h>
#include <stdlib.h>
int main()
{
// 注意:文件打开模式需要用双引号括起来
FILE *fp = fopen("Hello.txt", "r");
// 检查文件是否成功打开
if (fp == NULL) {
return 1;
}
// C语言中关闭文件使用fclose()函数,而不是C++的成员函数形式
fclose(fp);
return 0;
}
终端打印部分常见退出码:
3. 进程等待
3.1 进程等待必要性
3.2 进程等待函数
wait
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取⼦进程退出状态,不关⼼则可以设置成为NULL
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if (id < 0) {
perror("fork"); // 错误处理:打印fork失败原因
return 1;
}
else if (id == 0) { // 子进程逻辑
int cnt = 3;
while (cnt--) {
// 增加换行符刷新缓冲区,避免输出混乱
printf("I am child, pid : %d\n", getpid());
sleep(1); // 子进程每次打印后休眠1秒,方便观察
}
// 子进程退出前显式说明
printf("Child process exit\n");
exit(0); // 子进程正常退出
}
// 父进程逻辑
pid_t ret = wait(NULL); // 回收子进程资源,不关心退出状态
if (ret > 0) { // 等待成功的判断(ret为回收的子进程PID)
printf("wait success, child pid=%d\n", ret);
} else {
perror("wait"); // 处理wait可能的错误
return 1;
}
sleep(2);
return 0;
}
如果等待子进程,子进程没有退出,父进程会阻塞在wait处。
当子进程结束若等待个几秒观察僵尸的情况。
观察图片明显解决了僵尸的问题
waitpid
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的⼦进程的进程ID;
如果设置了选项WNOHANG,⽽调⽤中waitpid发现没有已退出的⼦进程可收集,则返回0;
如果调⽤中出错,则返回-1,这时errno会被设置成相应的值以指⽰错误所在;
参数:
pid:
Pid=-1,等待任⼀个⼦进程。与wait等效。
Pid>0.等待其进程ID与pid相等的⼦进程。
status: 输出型参数
WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真。(查看进程是
否是正常退出)
WEXITSTATUS(status): 若WIFEXITED⾮零,提取⼦进程退出码。(查看进程的
退出码)
options:默认为0,表⽰阻塞等待
WNOHANG: 若pid指定的⼦进程没有结束,则waitpid()函数返回0,不予以等
待。若正常结束,则返回该⼦进程的ID。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if (id < 0) {
perror("fork"); // 错误处理:打印fork失败原因
return 1;
}
else if (id == 0) { // 子进程逻辑
int cnt = 3;
while (cnt--) {
// 增加换行符刷新缓冲区,避免输出混乱
printf("I am child, pid : %d\n", getpid());
sleep(1); // 子进程每次打印后休眠1秒,方便观察
}
// 子进程退出前显式说明
printf("Child process exit\n");
exit(0); // 子进程正常退出
}
// 父进程逻辑
pid_t ret = wait(id,NULL,0); // 回收子进程资源,不关心退出状态
if (ret > 0) { // 等待成功的判断(ret为回收的子进程PID)
printf("wait success, child pid=%d\n", ret);
} else {
perror("wait"); // 处理wait可能的错误
return 1;
}
sleep(2);
return 0;
}
3.3 获取子进程status
如果代码没有异常,低7个比特位为0,一旦低7个比特位!=0,异常退出的,退出码无意义。
退出异常,低7位保存异常时对应的信号编号
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
int main() {
pid_t id = fork();
if (id < 0) { // 增加fork失败的错误处理
perror("fork failed");
return 1;
} else if (id == 0) { // 子进程逻辑
int cnt = 3;
while (cnt--) {
printf("我是子进程,pid:%d, ppid:%d\n", getpid(), getppid());
sleep(1);
}
exit(10); // 子进程退出,返回状态码10
}
// 父进程逻辑
int status = 0;
// 等待指定子进程(id),阻塞式等待(WNOHANG=0)
pid_t rid = waitpid(id, &status, 0);
if (rid > 0) {
// 正确解析退出状态:先判断是否正常退出
if (WIFEXITED(status)) { // 宏判断是否正常退出
printf("wait success, 回收的子进程pid:%d, 退出码:%d\n",
rid, WEXITSTATUS(status)); // 宏获取退出码
} else if (WIFSIGNALED(status)) { // 宏判断是否被信号终止
printf("子进程被信号终止,信号编号:%d\n", WTERMSIG(status));
}
} else {
printf("wait failed: %d:%s\n", errno, strerror(errno));
}
return 0;
}
#include <errno.h>
#include <string.h>
#include <unistd.h>
int main(){
pid_t id=fork();
if(id==0){
int cnt=3;
while(cnt--){
printf("我是子进程,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
exit(10);
}
int status=0;
pid_t rid=waitpid(id,&status,0);
if(rid>0){
printf("wait success,rid:%d,exit code:%d,exit signal :%d\n",rid,(status>>8)&0xFF,status&0x7F);
}
else{
printf("wait failed:%d:%s\n",errno,strerror(errno));
}
return 0;
}
代码中有除0操作 ,退出异常:
子进程的退出信息只能放在task_struct.
3.4 阻塞与非阻塞等待
张三在寝室楼下打电话找李四吃饭,李四在复习,说等一会,过了一会张三又打了一次电话。李四说还有一点,于是张三隔一段时间就给李四打电话直到李四下楼。
张三是父进程,李四是子进程,打电话就是一次调用,以上就是非阻塞轮询。
轮询就是通过循环完成的。
第二次张三同样找李四吃饭,李四同样在复习,这次打电话张三叫李四不要挂电话,就一直开着,知道李四下楼,就打一次电话。
这就是阻塞调用。
非阻塞调用,pid_t waitpid,返回值大于0,等待结束;等于0,调用结束,但是子进程没有退出;小于0,等待失败。
非阻塞调用可以让等待方做做自己的事。
阻塞等待只有大于和小于。
非阻塞调用示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <vector> // C++容器,需用g++编译
#include <errno.h>
// 函数指针类型定义
typedef void(*handler)();
// 全局函数回调列表(C++标准库容器,需加std::)
std::vector<handler> func;
void Load(){
printf("登录!\n");
}
void Exit(){
printf("退出!\n");
}
// 初始化回调函数列表(只需要初始化一次)
void work(){
// 避免重复添加(如果已经有函数则不再添加)
if(func.empty()){
func.push_back(Load);
func.push_back(Exit);
}
}
// 执行所有回调函数
void handle(){
if(func.empty()){
work(); // 若未初始化则先初始化
}
for(auto e : func){ // auto是C++11特性,需用g++编译
e();
}
}
int main(){
pid_t id = fork();
if(id < 0){
printf("fork error! errno:%d\n", errno);
return 1;
}
else if(id == 0){ // 子进程逻辑
printf("我是子进程, pid:%d\n", getpid());
sleep(3); // 子进程休眠3秒后退出
exit(1); // 退出码为1
}
else{ // 父进程逻辑
int status = 0;
// 非阻塞等待:WNOHANG表示若子进程未结束则立即返回0
pid_t ret = waitpid(id, &status, WNOHANG);
// 循环等待子进程结束(每次检查都需要重新调用waitpid)
while(ret == 0){
printf("child is running!\n");
handle(); // 执行回调函数(登录、退出)
sleep(1); // 休眠1秒再检查,避免CPU空转
ret = waitpid(id, &status, WNOHANG); // 重新获取子进程状态
printf("本轮调用结束!\n");
}
// 检查等待结果(ret应为子进程PID,即id)
if(ret == id){
// 判断子进程是否正常退出
if(WIFEXITED(status)){
printf("wait child success, child return code is :%d.\n",
WEXITSTATUS(status));
}
} else {
printf("wait child failed, ret:%d, errno:%d\n", ret, errno);
return 1;
}
}
return 0;
}
结束语
进程的创建、终止与等待,看似是三个独立的操作,实则是操作系统 “资源管理” 与 “程序协作” 理念的集中体现:
fork
通过写时拷贝实现高效的进程复制,既保证了进程独立性,又避免了不必要的内存浪费;进程终止机制通过退出码传递状态,让程序的结束有迹可循;而wait
/waitpid
函数则解决了 “僵尸进程” 的资源泄漏问题,确保系统资源的有序回收。理解这些机制,不仅能帮助我们写出更健壮的多进程程序(如避免僵尸进程、正确处理子进程状态),更能让我们体会到操作系统设计的精妙 —— 每一个函数接口的背后,都是对 “效率” 与 “安全” 的平衡,每一种机制的实现,都服务于 “让多任务协作更可靠” 的终极目标。
进程生命周期的故事远不止于此,它与进程调度、信号处理等机制紧密相连,共同构成了操作系统的核心能力。希望本文能成为你探索进程管理的起点,让你在编写或调试多进程程序时,多一份对底层逻辑的清晰认知,少一份面对 “僵尸进程”“孤儿进程” 时的困惑。