一、进程退出

之前我们学习了如何创建进程 —— 通过 fork 等机制为程序分配 CPU、内存等资源,让程序能以 “多任务协作” 的方式运行起来,去处理计算、IO 等各类任务。而进程运行时会持续占用系统资源,若没有 “退出”,完成任务的进程会一直 “霸占” 资源,导致系统资源逐渐枯竭,新任务无法获得资源支持;同时fork创建的子进程若出现本身错误, 如内存越界等原因也要告知我们父进程自己最终状态, 是任务成功了,还是哪里出了问题。这样系统才能及时回收资源、根据错误状态开展后序任务。
说白了,进程退出就像 “用完工具要归位、做完事要反馈结果”,是保障系统资源循环利用、程序间有序协作的关键环节。

1.1进程退出场景

  • 代码运行完毕,结果正确: 程序按预期执行完所有逻辑(如计算任务完成、文件处理结束),是最理想的退出状态。
  • 代码运行完毕,结果不正确: 程序执行流程完整,但因逻辑错误(如算法 Bug、输入处理不当)导致结果不符合预期。
  • 代码异常终止: 程序执行中遭遇意外(如段错误、被信号强制终止),未完成正常流程就被迫停止。

1.2 退出码

从 main 函数返回:main 执行 return n; 时,进程会以状态码 n 退出,状态码0, 我们认为是正常退出
在这里插入图片描述
通过 echo $? 可以查看上一个进程的退出码

echo $?

其中,main 函数的返回值, 本质表示,进程运行完成时是否是正确的结果, 可以用不同的数字表示不同的出错原因(0代表正常退出, 非0代表程序运行错误) —— 退出码, 让我们的父进程接收到错误原因
知道了错误码, 我们还可以通过strerror了解对应的错误码对应的描述, 如下
在这里插入图片描述
通过调用strerror接口显示错误码描述
在这里插入图片描述
例如, 当我们通过ls 指令进入一个不存在的文件时, 系统会显示文件不存在, 即对应我们的错误码 2, 同样我们可以查看上一个进程(ls)的退出码来查看验证
在这里插入图片描述

通常在程序内部我们可以通过errno查看(显示上一条错误信息,当存在多个错误, 会将上一条错误码进行覆盖), 当函数执行失败时(如文件打开失败、网络连接超时),不会直接返回详细错误描述,而是通过 errno 存储一个整数型错误码,我们则通过该错误码进一步判断失败原因。在早期 C 标准中,errno 是一个全局变量 (定义于 <errno.h> 头文件)
错误演示

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>

int main() {
    int res = 0;

    // 尝试打开一个不存在的文件(故意失败)
    FILE* file = fopen("xxx.txt", "r");
    if (file == NULL) {
        printf("file fopen fall: %d : %s\n", errno, strerror(errno));
        res = errno;
    } else {
		printf("fopen success!\n");
    }

    return res;
}

运行结果
在这里插入图片描述

1.3 异常退出

在之前通过ajx命令查看进程属性时, 我们通过kill - 9命令可以杀死一个进程, 实际上是向该进程发送所谓的"终止信息", 来结束当前进程.我们通过kill -l 查看信号
在这里插入图片描述
程序内部发生错误时终端则显示对应信号信息, 例如: 除0错误,运行程序后,终端会输出 Floating point exception (core dumped)(浮点异常),进程因非法运算被操作系统终止,属于异常退出。
程序演示
在这里插入图片描述
常见触发异常退出的场景:
外部信号强制终止:操作系统或其他进程向当前进程发送 “终止信号”,且进程未捕获或无法处理该信号。

  • 用户按下Ctrl+C,系统发送SIGINT信号(中断信号);
  • 进程占用资源过久,被管理员用kill命令发送SIGKILL信号(强制终止,无法捕获);

程序内部致命错误:进程执行时出现无法恢复的错误,被操作系统直接终止。

  • 访问非法内存(如空指针解引用、数组越界);
  • 执行非法指令(如 CPU 不支持的操作码);
    浮点运算异常(如除以 0)。

1.4 exit 和 _exit

在 C 语言中,exit 和 _exit 都是用于终止进程的函数,但它们的核心区别在于 退出前是否执行 “清理操作”,这直接影响程序的资源释放、数据完整性等行为(稍后测试)

// 通过man 3号手册查看参数信息
//标准库函数
#include <stdlib.h>
void exit(int status);

//系统调用封装
#include <unistd.h>
void _exit(int status);

我们通过一个简单的例子观察两者最直接区别

// exit部分
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    while (1) {
        printf("测试exit和_exit区别");
        exit(0);
    }
    return 0;
}
// _exit部分
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    while (1) {
        printf("测试exit和_exit区别");
        _exit(0);
    }
    return 0;
}

分别运行两者代码, 发现exit()退出程序成功打印测试代码, 而_exit()调用部分什么也没打印
在这里插入图片描述
在之前所学习中C 语言的标准输出(如 printf)默认使用 “缓冲机制”:输出内容会先存到缓冲区,只有当缓冲区满、遇到换行符(\n)或程序正常退出时,才会真正打印到终端。
所以我们得出结论:
exit 会强制刷新所有缓冲区,确保数据不丢失;先清理,再终止

适合 “正常退出” 场景当进程需要完整释放资源、确保数据写入(如文件、终端)、执行收尾逻辑时使用。
程序正常完成任务后退出;
处理完输入输出后主动终止。

_exit 不刷新缓冲区,未输出的内容会直接丢弃。 直接终止,不清理

适合 “无需清理” 或 “子进程退出” 场景当进程不需要(或不应该)执行清理操作时使用,典型场景是 子进程退出: 子进程若用 exit,可能会清理父进程共享的资源(如文件流缓冲区),导致父进程数据异常; 用 _exit 可避免干扰父进程,仅终止自身。

简单说:exit 是 “体面退出”(做好收尾再走),_exit 是 “紧急撤离”(立刻终止,不带走一片云彩)。
在这里插入图片描述

二、进程等待

  • 进程等待必要性
    子进程退出后,若父进程未处理,子进程会进入僵尸状态 Z (详细请看) ,保留 PCB 等内核资源,导致内存泄漏。
    父进程通过等待可以获取子进程的退出状态(如正常退出的返回值、异常终止的信号等),从而判断子进程的执行结果。

2.1 wait 和 waitpid

  1. wait() 是最简单的进程等待函数,功能是阻塞 等待任意一个子进程退出,并回收其资源。
//函数原型
#include <sys/wait.h>
pid_t wait(int *status);

其中status:输出型参数,用于存储子进程的退出状态(若为NULL,表示不关心退出状态)。
返回值
成功:返回等待的子进程的 PID
失败:返回 -1(如无子进程时)
我们将之前演示僵尸进程代码改造一下,观察wait()回收子进程场景

#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
using namespace std;
int main () {
    pid_t id = fork();
    pid_t res;
    if (id == 0) {
        // 子进程
        cout << "子进程pid " << getpid() << " ppid "$
        // 子进程先退出
        sleep(2);
        exit(0);
    } else {
	cout << "父进程pid " << getpid() << " ppid "$
       sleep(5); // 回收前睡眠5秒钟, 此时为Z状态
        res = wait(NULL);
        sleep(30);
    }
    return res;
}

循环调用监控进程状态

while :; do ps ajx | head -1 ;  ps ajx | grep waitpro | grep -v grep; sleep 1; echo "   "; done

在这里插入图片描述
特殊的若子进程一直不退出, 父进程在wait等待时, 会一直处于阻塞状态.

  1. waitpid()
pid_t waitpid(pid_t pid, int *status, int options);

返回值:

  • 若子进程已退出,返回该子进程的 PID
    若options选项为WNOHANG且子进程未退出,返回0
    失败则返回 -1(如无符合条件的子进程)

参数:

  • pid:指定等待的子进程 PID,当前只考虑pid取 > 0 和 pid = -1
    pid > 0:等待 PID 为pid的子进程。
    pid = -1:等待任意子进程(与wait()功能相同)
  • status:同wait(),用于存储子进程退出状态
    status是一个输出型参数,用于存储子进程的退出状态信息。它本身是一个 int 类型的整数,但并非直接表示子进程的退出码(如 exit(n) 中的 n),而是通过不同的位存储了子进程的退出细节(比如 “是否正常退出”“退出码是多少”“是否被信号终止”“终止信号是什么” 等)。
    为什么 status 不能直接使用?
    status 的内部结构是 “位级别的分段存储”
    低 7 位:通常存储 “终止信号” 相关信息(若子进程被信号杀死)。
    第 8 位: core dump信号(信号部分后文在补充, 在此不讨论)
    高 8 位:通常存储 “正常退出的退出码”(若子进程通过 exit() 或 return 正常退出)。在这里插入图片描述

其中终止信号在之前我们从kill -l 得出 信号码 1 ~ 64, 所以若低七位为0, 则代表进程没有异常, 我们只需知道该进程的退出码为多少
在这里插入图片描述
可以通过打印函数, 以及位运算观察status结果

// 在末尾添加该语句, 开头代码子进程exit(0) 改为 exit(13), 退出码为13
printf("exit sucess! exit sig: %d, exit: %d, res: %d\n", status&0x7f, (status >> 8) & 0xff, res);

示例: 符合预期
在这里插入图片描述
在实际应用场景中, 我们一般不会使用位运算方式获取以上信息, 而是通过宏解析

功能
WIFEXITED(status)进程正常退出返回非0
WIFSIGNALED(status)进程被信号终止返回非0
WEXITSTATUS(status)进程正常退出情况下, 返回退出码
WTERMSIG(status)存在异常情况下, 返回异常信号值
  • options选项
    0: 默认阻塞等待, 子进程未退出时,父进程挂起(一直等待)
    WNOHANG:非阻塞轮询等待(若子进程未退出,立即返回0)

    非阻塞轮询必要性
    默认情况下,waitpid(pid, &status, 0) 是阻塞等待:父进程会暂停执行(进入阻塞状态),直到子进程退出后才继续运行。但在实际开发中,父进程可能需要同时处理其他任务(如监听网络请求、处理用户输入等),不能长时间阻塞。此时 WNOHANG 就非常有用 —— 它让父进程可以 “轮询” 检查子进程是否退出,同时不耽误自身工作。且在进程退出时始终保持父进程最后一个退出
    简单来说在检查子进程退出之前, 父进程等待时还干其他的事

下面代码可以简单演示父进程在轮询时干其他的事情

#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>

using namespace std;
int main () {
    pid_t pid = fork();
    pid_t res;
    int status;

    if (pid == 0) {
        printf("子进PID %d 10秒后退出...\n", getpid());
        sleep(10);
        exit(0);
    } else { // 父进程:非阻塞等待,同时做自己的事

        while (1) {
            res = waitpid(pid, &status, WNOHANG);
            //sleep(1);
            if (res == 0) {
                printf("父进程:子进程还在运行,继续工作...\n");
                sleep(1); // 模拟其他工作
            } else if (res == pid) {
                printf("父进程:子进程PID: %d已退出,回收完成\n", res);
                break; // 退出循环
            } else {
                perror("waitpid failed");
                exit(1);
            }
	}
    }
    return res;
}

在这里插入图片描述

三、进程替换

在操作系统中,进程替换是指用一个全新的程序替换当前进程的代码、数据、堆、栈等内存区域(原程序的执行逻辑彻底消失),同时保持进程的 pid 、父进程 id、用户 id、文件描述符等核心属性不变的操作。
为什么需要进程替换?
当一个进程(通常是子进程)完成了初始化工作后,可能不需要继续执行原程序的逻辑,而是需要执行另一个全新的程序。此时无需创建新进程(避免 pid 变化),而是采取用新进程替换原来的子进程

3.1 替换函数

下面介绍exec系列5种进程替换函数

1. execl

#include <unistd.h>
int execl(const char *path, const char *arg, ... /* (char  *) NULL */);
  • path:新程序的绝对路径或相对路径 必须准确指定 (execl 不会从环境变量 PATH 中搜索程序)
  • arg: 传递给新程序的命令行参数,是一个可变参数列表。
    第一个参数 arg 通常是新程序的名称,对应新程序 main 函数的 argv[0]。后续参数是具体的命令选项参数
  • 最后一个参数必须以 (char*)NULL 结尾,标记参数结束。
  • 返回值:
    若成功,execl 不会返回(因为当前进程已被新程序替换)失败,返回 -1(此时原进程的执行逻辑可继续,可通过 perror() 查看错误原因)。
    单进程代码示例
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main() {
    printf("当前进程 PID: %d, PPID: %d\n", getpid(), getppid());
    
    execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
    // 程序替换后, 下面行不会执行
    printf("当前进程 PID: %d, PPID: %d\n", getpid(), getppid());
    return 0;
}

现象:
在这里插入图片描述
多进程时替换

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main() {
    pid_t id = fork();

    if (id == 0) {
        printf("当前进程 PID: %d, PPID: %d\n", getpid(), getppid());

        execl("/usr/bin/ls", "ls", "-a", "-l", NULL);

        printf("当前进程 PID: %d, PPID: %d\n", getpid(), getppid());
        exit(0);
    } else {
	pid_t res = waitpid(id, NULL , 0);
        sleep(5);
        printf("父进程正常执行, 父进程 PID:%d, 子进程 PID: %d\n", getpid(), res);
    }
    return 0;
}

在这里插入图片描述

2.execlp

#include <unistd.h>
int execlp(const char *file, const char *arg, ... /* (char  *) NULL */);

第一个参数 file 会自动从环境变量 PATH 中搜索新程序的路径, 若使用绝对或相对路径, 则直接使用该路径,其他参数与execl保持一致
在这里插入图片描述

3.execv

#include <unistd.h>
int execv(const char *path, char *const argv[]);
  • 参数
    path: 新程序的绝对路径或相对路径(必须显式指定完整路径,execv 不会从环境变量 PATH 中搜索程序)
    argv: 指向命令行参数数组的指针,数组格式要求:
    argv[0] 通常是新程序的名称, argv[1], argv[2], … 是具体的命令选项或参数并且数组必须以 NULL 结尾标记参数列表结束。

函数返回值与execl保持一致
用法

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>


int main() {
    pid_t id = fork();

    if (id == 0) {
        printf("当前进程 PID: %d, PPID: %d\n", getpid(), getppid());

        //execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
        //execlp("ls", "ls", "-a", "-l", NULL);
        char* const argv[] = {"ls", "-a", "-l", NULL};

        execv("/usr/bin/ls", argv);
        printf("当前进程 PID: %d, PPID: %d\n", getpid(), getppid());
    } else {
	pid_t res = waitpid(id, NULL, 0);
        printf("父进程PID : %d, 子进程PID: %d\n", getpid(), res);
    }
    return 0;
}

在这里插入图片描述

4.execvp

#include <unistd.h>
int execvp(const char *file, char *const argv[]);

同样的第一个参数 file会自动从环境变量 PATH 中搜索新程序的路径, 若使用绝对或相对路径, 则直接使用该路径 返回值保持一致
示例

	char* const argv[] = {"ls", "-a", "-l", NULL};
    //execv("/usr/bin/ls", argv);
    execvp("ls", argv);

5.execle

#include <unistd.h>
int execle(const char *path, const char *arg, ... , char *const envp[]);

在execl函数基础上, 新增允许自定义环境变量数组(而非继承其父进程的环境变量)
envp:自定义的环境变量数组
数组元素是字符串,格式为 NAME=VALUE
数组必须以 NULL 结尾(即 envp[n] = NULL)。
新程序的环境变量将完全由 envp 指定,不再继承原进程的环境变量。
我们通过将替换程序改为改为 printenv, 来实现打印全部自定义环境变量信息

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        char* envp[] = {
            "USER=zhangsan",
            "HOME=11111",
            "LANG=66666",
             NULL
        };

	execle(
            "/usr/bin/printenv",
            "printenv",
            NULL,
            envp
        );
    } else {
	wait(NULL);
        printf("父进程:子进程已打印所有环境变量\n");
    }
    return 0;
}

在这里插入图片描述

记忆口诀:
带 l (list) 使用参数列表
带 v(vector) 参数用数组
带 p(path) 自动搜索当前环境变量
带 e(env) 使用自己维护的环境变量

	char *const argv[] = {"ps", "-ef", NULL};
    char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};    	
    execl("/bin/ps", "ps", "-ef", NULL);
    // 带p的,可以使用环境变量PATH,无需写全路径
    execlp("ps", "ps", "-ef", NULL);
    // 带e的,需要自己组装环境变量
    execle("/bin/ps", "ps", "-ef", NULL, envp);
    // 带v的,使用指针数组
    execv("/bin/ps", argv);
    // 带p的,可以使用环境变量PATH,无需写全路径
    execvp("ps", argv);
    // 带e的,需要自己组装环境变量
    execve("/bin/ps", argv, envp);
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐