进程生命周期管理:从创建到终止的完整逻辑

发布于:2025-08-05 ⋅ 阅读:(9) ⋅ 点赞:(0)

前言

在操作系统的世界里,进程就像一个个忙碌的 “工作单元”,从被创建到完成任务后终止,始终遵循着一套严谨的生命周期规则。理解进程的生命周期管理,是揭开操作系统多任务调度神秘面纱的关键 —— 而这其中,进程的创建、终止与等待机制,构成了整个生命周期的核心骨架。

本文将沿着 “创建→终止→等待” 的脉络,系统解析进程管理的底层逻辑:从fork函数如何 “复制” 出一个新进程,到写时拷贝技术如何优化内存效率;从进程正常退出与异常终止的不同场景,到退出码背后的状态传递逻辑;更会深入探讨为何父进程必须等待子进程,以及waitwaitpid函数在阻塞 / 非阻塞模式下的实现细节。无论你是想搞懂 “父子进程为何能共享代码却互不干扰”,还是想理解 “如何安全回收子进程资源”,这些知识点都将为你构建起进程管理的完整知识体系。

目录

1. 进程创建 

1.1 fork函数初始

1.2 fork函数返回值

1.3 写时拷贝

1.4 fork常规用法

1.5 fork调用失败原因

2. 进程终止

2.1 进程退出场景

2.2 进程常见退出方法

2.3 退出码

3. 进程等待

3.1 进程等待必要性

3.2 进程等待函数

wait

waitpid

3.3 获取子进程status

3.4 阻塞与非阻塞等待


1. 进程创建 

1.1 fork函数初始

在linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

#include <unistd.h>
pid_t fork(void);
返回值:⾃进程中返回0,⽗进程返回⼦进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
分配新的内存块和内核数据结构给子进程
将父进程部分数据结构内容拷贝至子进程
添加子进程到系统进程列表当中
fork返回,开始调度器调度

当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序。

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;
}

 

这里看到了三行输出,一行before,两行after。进程2466154先打印before消息,然后它有打印after。
另一个after消息有2466155打印的。注意到进程2466155没有打印before,为什么呢?如下图所示

 

所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。

1.2 fork函数返回值

父进程返回子进程的pid,子进程返回0

1.3 写时拷贝

通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
因为有写时拷贝技术的存在,所以父子进程得以彻底分离!完成了进程独立性的技术保证!
写时拷贝,是一种延时申请技术,可以提高整机内存的使用率

写时拷贝减少创建时间  减少内存浪费 

1.4 fork常规用法

一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,
生成子进程来处理请求。
一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

1.5 fork调用失败原因

1. 系统中有太多的进程  2. 实际用户的进程数超过了限制
本质是内存不足了

2. 进程终止

进程终止的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。

2.1 进程退出场景

代码运行完毕,结果正确

代码运行完毕,结果不正确

代码异常终止

在以前的语言代码中,都有main函数,main函数的返回值,通常表明的程序的执行情况.

代码运行完毕,结果正确,return 0;

代码运行完毕,结果不正确,return !0;不同的非零值表示不同的出错原因。

2.2 进程常见退出方法

正常终止:
1. 从main返回          2. 调用exit          3. _exit
异常退出:
ctrl + c,信号终止
#include <unistd.h>
void exit(int status);
main函数结束,表示进程结束,其他函数return,只表示自己函数调用完成,返回。

任何地方调用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;
}    

exit(C)  vs _exit(系统)
进程如果exit退出的时候,exit(),进程退出的时候,会进行缓冲区的刷新。
进程如果exit退出的时候,_exit(),进程退出的时候,不会进行缓冲区的刷新。

int main()
 {
 printf("main");
 sleep(2);
 exit(1);
}

 exit变成_exit后

所以也可以得出我们之前谈论的缓冲区一定不是操作系统内部的缓冲区。 

 前者是库函数,后者是系统调用,exit最后会调用_exit,

2.3 退出码

退出码(退出状态)可以告诉我们最后一次执行的命令的状态。在命令结束以后,我们可以知道命令是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码 0 时表示执行成功,没有问题。 代码 1 或 0 以外的任何代码都被视为不成功。
main函数的返回值是进程的退出码。也能自己设置return值
代码异常终止,退出码无意义,进程一旦出现异常,一般是进程收到了信号。

 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;
}
    

终端打印部分常见退出码:

Linux Shell 中的主要退出码:

return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。

3. 进程等待

3.1 进程等待必要性

之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

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;
}
    

 

如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
如果不存在该子进程,则立即出错返回。

3.3 获取子进程status

wait和waitpid,都有一个status参数,该参数是⼀个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息。
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16
⽐特位):

 

如果代码没有异常,低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函数则解决了 “僵尸进程” 的资源泄漏问题,确保系统资源的有序回收。

理解这些机制,不仅能帮助我们写出更健壮的多进程程序(如避免僵尸进程、正确处理子进程状态),更能让我们体会到操作系统设计的精妙 —— 每一个函数接口的背后,都是对 “效率” 与 “安全” 的平衡,每一种机制的实现,都服务于 “让多任务协作更可靠” 的终极目标。

进程生命周期的故事远不止于此,它与进程调度、信号处理等机制紧密相连,共同构成了操作系统的核心能力。希望本文能成为你探索进程管理的起点,让你在编写或调试多进程程序时,多一份对底层逻辑的清晰认知,少一份面对 “僵尸进程”“孤儿进程” 时的困惑。


网站公告

今日签到

点亮在社区的每一天
去签到