【Linux 进程控制】—— 进程亦生生不息:起于鸿蒙,守若空谷,归于太虚

进程创建再识fork函数

linux中 fork 函数是非常重要的函数,它从已存在进程中创建⼀个新进程。创建出来的新进程叫做子进程,而原进程则称为父进程。

linux参考手册中,fork函数的原型如下:(man 2 fork 指令查看)

代码语言:JavaScript代码运行次数:0运行复制

NAME       fork - create a child processSYNOPSIS       #include <sys/types.h>       #include <unistd.h>       pid_t fork(void);

如上不难看出:

fork 函数的功能是创建一个子进程头文件有 参数为 void ,返回值为 pid_t (实际上是Linux内核中typedef出来的一个类型)


进程调用 fork,当控制转移到内核中的 fork 代码后,内核做如下几件事:

分配新的内存块和内核数据结构给子进程将父进程部分数据结构内容拷贝至子进程添加子进程到系统进程列表当中fork返回,开始调度器调度

【Linux 进程控制】—— 进程亦生生不息:起于鸿蒙,守若空谷,归于太虚

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

代码语言:javascript代码运行次数:0运行复制

int main(void){    pid_t pid;    printf("Before: pid is %dn", getpid());    if ((pid = fork()) == -1)        pError("fork()"), exit(1);    printf("After:pid is %d, fork return %dn", getpid(), pid);    sleep(1);    return 0;}

输出:

代码语言:javascript代码运行次数:0运行复制

Before: pid is 40176After:pid is 40176, fork return 40177After:pid is 40177, fork return 0

如下图所示:

【Linux 进程控制】—— 进程亦生生不息:起于鸿蒙,守若空谷,归于太虚

所以,fork之前父进程独立执行,fork之后,父子进程两个执行流分别执行之后的代码。值得注意的是,fork之后,谁先执行完全由调度器决定,并没有明确的先后关系!


fork函数返回值

fork创建成功:

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

为什么给父进程返回子进程的pid,这个问题我们之前已经讨论过:

为什么子进程返回0


fork创建失败:

返回 -1并设置错误码:

当系统资源不足(如进程数超限、内存耗尽)时,fork() 失败。

错误码:

需检查 errno 确定具体原因代码语言:javascript代码运行次数:0运行复制

if (pid == -1) {    perror("fork failed"); // 输出类似 "fork failed: Resource temporarily unavailable"}

常见错误码:

EAGAIN:进程数超过限制(RLIMIT_NPROC)或内存不足。ENOMEM:内核无法分配必要数据结构所需内存。


写时拷贝 copy-On-Write

为什么需要写时拷贝?

在传统的进程创建方式中,fork() 会直接复制父进程的所有内存空间给子进程。这种方式存在明显问题:

内存浪费:如果父进程占用 1GB 内存,子进程即使不修改任何数据,也会立即消耗额外 1GB 内存。性能低下:复制大量内存需要时间,尤其是对大型进程而言,fork() 会显著延迟程序运行。

COW 的解决思路:

推迟实际的内存复制,直到父子进程中某一方尝试修改内存页时,才进行真正的拷贝。在此之前,父子进程共享同一份物理内存。

具体见下图:

【Linux 进程控制】—— 进程亦生生不息:起于鸿蒙,守若空谷,归于太虚

因为有写时拷贝技术的存在,所以父子进程得以彻底分离!完成了进程独立性的技术保证! 写时拷贝,是⼀种延时申请技术,可以提高整机内存的使用率。

写时拷贝的工作流程

1、 fork() 调用时

共享内存页:内核仅为子进程创建虚拟内存结构(页表),但物理内存页仍与父进程共享。标记为只读:内核将共享的物理内存页标记为只读(即使父进程原本可写)。

2、进程尝试写入内存

触发页错误:当父进程或子进程尝试修改某个共享内存页时,由于页被标记为只读,CPU 会触发页错误(Page Fault)。

内核介入处理:操作系统会由用户态陷入内核态处理异常

分配新的物理内存页,复制原页内容到新页。修改触发写入的进程的页表,使其指向新页。将新页标记为可写,恢复进程执行。

3、后续操作

修改后的进程独享新内存页,另一进程仍使用原页。未修改的内存页继续共享,不做复制,操作系统不做任何无意义的事情。


进程等待

之前我们在讲进程概念的时候讲过,如果父进程创建出子进程后,如果子进程已经退出,父进程却没有对子进程回收,那么就子进程就会变成 “僵尸进程” ,造成内存泄露等问题。

进程等待的必要性

僵尸进程问题:

子进程终止后,其退出状态会保留在进程表中,直到父进程读取。若父进程未处理,子进程将保持僵尸状态(Zombie),占用系统资源。状态收集:父进程需知晓子进程的执行结果(成功、错误代码、信号终止等)。资源回收:内核释放子进程占用的内存、文件描述符等资源。进程等待的方法wait代码语言:javascript代码运行次数:0运行复制

#include<sys/types.h>#include<sys/wait.h>pid_t wait(int* status);

具体功能:

阻塞父进程,直到等待到任意一个子进程终止。

参数:

status:输出型参数,用来存储子进程退出状态的指针(可为 NULL,表示不关心状态)。

返回值:

成功:返回终止的子进程PID。失败:返回-1(如无子进程)。


waitpid代码语言:javascript代码运行次数:0运行复制

#include <sys/wait.h>pid_t waitpid(pid_t pid, int *status, int options);

功能:更灵活的等待方式,可指定子进程或非阻塞等待模式。

参数:

pid:

>0:等待指定 PID 的子进程。-1:等待任意子进程(等效于 wait)。0:等待同一进程组的子进程。

status:同 wait,输出型参数,表明子进程的退出状态。

options: 默认为0,表示阻塞等待

WNOHANG:非阻塞模式,无子进程终止时立即返回 0。WUNTRACED:报告已停止的子进程(如被信号暂停)。

返回值:

成功:返回子进程PID。WNOHANG 且无子进程终止:返回0。失败:返回-1。

做个总结:

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

【Linux 进程控制】—— 进程亦生生不息:起于鸿蒙,守若空谷,归于太虚

获取子进程 status

wait 和 waitpid,都有⼀个 status 参数,该参数是⼀个输出型参数,由操作系统填充。

如果传递 NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。

status 不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图 (只研究 status 低16比特位):

【Linux 进程控制】—— 进程亦生生不息:起于鸿蒙,守若空谷,归于太虚

如何理解呢? 子进程的退出分为两种情况:

正常终止

高 8 位(第 8 ~ 15 位):保存子进程的退出状态(退出码)(即 exit(code) 或 return code 中的 code 值)。

第 7 位:通常为 0,表示正常终止。

示例:

若子进程调用 exit(5),表明子进程是正常退出,则 status 的高 8 位为 00000101(即十进制 5)。

被信号所杀导致终止

低 7 位(第 0 ~ 6 位):保存导致子进程终止的信号编号。

第 7 位:若为 1,表示子进程在终止时生成了 core dump 文件(用于调试)。有关 core dump 文件,后面会讲,大家这里先了解一下即可。

第 8 ~ 15 位:未使用(通常为 0)。

示例:

若子进程因 SIGKILL(信号编号 9)终止,则 status 的低 7 位为 0001001(即十进制 9)。

做个小总结:代码语言:javascript代码运行次数:0运行复制

低 16 位结构:| 15 14 13 12 11 10 9 8 | 7 | 6 5 4 3 2 1 0 |---------------------------------------------正常终止 → [ 退出状态(高8位) ]  0  [ 未使用 ]被信号终止 → [   未使用(全0)   ] c  [ 信号编号 ]

如何解析 status?

使用宏定义检查 status 的值:

功能

WIFEXITED(status)

若子进程正常终止(exit 或 return)返回真。

WEXITSTATUS(status)

若 WIFEXITED 为真,返回子进程的退出码(exit 的参数或 return 的值)。

WIFSIGNALED(status)

若子进程因信号终止返回真。

WTERMSIG(status)

若 WIFSIGNALED 为真,返回导致终止的信号编号。

WCOREDUMP(status)

若子进程生成了核心转储文件返回真。

常用的两个宏:

WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是 否是正常退出)WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的 退出码)

示例一:子进程正常退出

代码语言:javascript代码运行次数:0运行复制

int main(){    pid_t pid = fork();    if (pid == 0)    { // 子进程        printf("子进程运行中... PID=%dn", getpid());        // 1. 正常退出:调用 exit(42)        exit(42);    }    else    { // 父进程        int status;        waitpid(pid, &status, 0); // 等待子进程结束        if (WIFEXITED(status))        { // 正常退出            printf("子进程正常退出,退出码: %dn", WEXITSTATUS(status));        }        else if (WIFSIGNALED(status))        { // 被信号终止            printf("子进程被信号终止,信号编号: %dn", WTERMSIG(status));        }    }    return 0;}

输出:

代码语言:javascript代码运行次数:0运行复制

子进程运行中... PID=56153子进程正常退出,退出码: 42

示例二:子进程被信号终止

代码语言:javascript代码运行次数:0运行复制

int main(){    pid_t pid = fork();    if (pid == 0)    { // 子进程        printf("子进程运行中... PID=%dn", getpid());        int *p = NULL;        *p = 100;  // 对空指针解引用,触发 SIGSEGV 被信号终止    }    else    { // 父进程        int status;        waitpid(pid, &status, 0); // 等待子进程结束        if (WIFEXITED(status))        { // 正常退出            printf("子进程正常退出,退出码: %dn", WEXITSTATUS(status));        }        else if (WIFSIGNALED(status))        { // 被信号终止            printf("子进程被信号终止,信号编号: %dn", WTERMSIG(status));        }    }    return 0;}

输出:

代码语言:javascript代码运行次数:0运行复制

子进程运行中... PID=56203子进程被信号终止,信号编号: 11

阻塞等待与非阻塞等待阻塞等待(Blocking Wait)代码语言:javascript代码运行次数:0运行复制

pid_t waitpid(pid_t pid, int *status, 0);  // options 参数为 0

示例:

代码语言:javascript代码运行次数:0运行复制

int main(){    int status;    pid_t child_pid = fork();    if (child_pid == 0)    {        // 子进程执行任务        exit(10);    }    else    {        // 父进程阻塞等待子进程结束        waitpid(child_pid, &status, 0);        if (WIFEXITED(status))        {            printf("子进程退出码: %dn", WEXITSTATUS(status));        }    }}

非阻塞等待(Non-blocking Wait)

关键选项:宏 WNOHANG(定义在 中)。

代码语言:javascript代码运行次数:0运行复制

pid_t waitpid(pid_t pid, int *status, WNOHANG);

示例:非阻塞轮询方式

代码语言:javascript代码运行次数:0运行复制

int main(){    int status;    pid_t child_pid = fork();    if (child_pid == 0)    {        sleep(3); // 子进程休眠 3 秒后退出        exit(10);    }    else    {        while (1)        {            pid_t ret = waitpid(child_pid, &status, WNOHANG);            if (ret == -1)            {                perror("waitpid");                break;            }            else if (ret == 0)            {                printf("子进程未结束,父进程继续工作...n");                sleep(1); // 避免频繁轮询消耗 CPU            }            else            {                if (WIFEXITED(status))                {                    printf("子进程退出码: %dn", WEXITSTATUS(status));                }                break;            }        }    }}

阻塞等待和非阻塞等待的对比:

场景

阻塞等待

非阻塞等待

父进程任务优先级

必须立即处理子进程结果

需同时处理其他任务

子进程执行时间

较短或确定

较长或不确定

资源消耗

CPU 空闲,无额外开销

需轮询,可能占用更多 CPU

典型应用

简单脚本、单任务场景

多进程管理、事件驱动程序


进程终止

进程= 内核数据结构 + 进程自己的代码和数据

进程退出场景代码运行完毕,结果正确代码运行完毕,结果不正确代码异常终止

如何理解这三种进程退出的场景呢?举个例子

代码运行完毕,结果正确

程序完整执行了所有逻辑,未触发任何错误或异常。输出结果与预期完全一致,符合功能需求或算法目标。代码语言:javascript代码运行次数:0运行复制

int sum(int a, int b){    return a + b;}int main(){    int result = sum(3, 5);    printf("Result: %dn", result); // 输出 8,结果正确    return 0;}

输出:

代码语言:javascript代码运行次数:0运行复制

Result: 8

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

程序正常结束(无崩溃或异常),但输出结果与预期不符。通常由逻辑错误、算法错误或数据处理错误导致。

例如:

代码语言:javascript代码运行次数:0运行复制

// 错误实现:本应计算阶乘,但初始值错误int factorial(int n){    int result = 0; // 错误!应为 result = 1    for (int i = 1; i <= n; i++)    {        result *= i;    }    return result;}int main(){    printf("5! = %dn", factorial(5)); // 输出 0,结果错误    return 0;}

代码未执行完毕,异常终止

程序未执行完毕就中途崩溃或被强制终止。通常由运行时错误、资源限制或外部信号触发。比如除零错误,对空指针解引用等异常

例如:

代码语言:javascript代码运行次数:0运行复制

int main(){    int *ptr = NULL;    *ptr = 42;  // 对空指针解引用,触发段错误    printf("Value: %dn", *ptr);    return 0;}

段错误:

代码语言:javascript代码运行次数:0运行复制

Segmentation fault

再比如:

代码语言:javascript代码运行次数:0运行复制

int main(){    int a = 10;    int b = a / 0; // 程序除零异常    printf("Value: %dn", b);    return 0;}

浮点数异常:

代码语言:javascript代码运行次数:0运行复制

Floating point exception

进程常见退出方法

正常终止(可以通过 echo $? 查看进程退出码)

从main返回调用exit_exit

异常退出:

ctrl + c,信号终止


进程退出码

退出码是一个 8 位无符号整数(8-bit unsigned Integer),因此取值范围为 2^8=256 个值。

Linux Shell 中的常见退出码:

【Linux 进程控制】—— 进程亦生生不息:起于鸿蒙,守若空谷,归于太虚

退出码 0 表示命令执行有误,这是完成命令的理想状态。退出码 1 我们也可以将其解释为 “不被允许的操作”。例如在没有 sudo 权限的情况下使用 yum;130 ( SIGINT 或 ^C )和 143 ( SIGTERM )等终止信号是非常典型的,它们属于 128+n 信号,其中 n 代表信号编号。


这里需要补充一点:

进程退出码和错误码是两个完全不同的概念,不要混为一谈!

错误码

要理解错误码,首先要认识全局变量 error

例如:fork函数调用失败后,会立刻返回-1,并设置全局变量 error

【Linux 进程控制】—— 进程亦生生不息:起于鸿蒙,守若空谷,归于太虚

定义:errno 是一个线程安全的整型变量,用于存储最近一次系统调用或库函数调用失败的错误码。

特性:

成功调用不会重置 errno,因此必须在调用后立即检查其值。每个线程有独立的 errno 副本(多线程安全)。

头文件:

代码语言:javascript代码运行次数:0运行复制

#include <errno.h>

与之对应的是 strerror 函数,该函数可以将对应的错误码转化成字符串描述的错误信息打印出来,方便程序员调试代码。

实际上,我们可以通过 for 循环来打印查看Linux系统下所有的错误码以及其错误信息:

代码语言:javascript代码运行次数:0运行复制

int main(){    for (int i = 0; i < 135; ++i)    {        printf("%d-> %sn", i, strerror(i));    }    return 0;}

不难看出,在Linux系统下,一共有 0 ~ 133 总共134个错误码,其中 0 表示 success ,即程序运行成功, 1 ~ 133 则分别对应一个错误信息。

代码语言:javascript代码运行次数:0运行复制

0-> Success1-> Operation not permitted2-> No such file or directory3-> No such process4-> Interrupted system call5-> Input/output error6-> No such device or address7-> Argument list too long8-> Exec format error9-> Bad file descriptor10-> No child processes11-> Resource temporarily unavailable12-> Cannot allocate memory13-> Permission denied14-> Bad address15-> Block device required16-> Device or resource busy17-> File exists18-> Invalid cross-device link19-> No such device20-> Not a directory21-> Is a directory22-> Invalid argument23-> Too many open files in system24-> Too many open files25-> Inappropriate ioctl for device26-> Text file busy27-> File too large28-> No space left on device29-> Illegal seek30-> Read-only file system31-> Too many links32-> Broken pipe33-> Numerical argument out of domain34-> Numerical result out of range35-> Resource deadlock avoided36-> File name too long37-> No locks available38-> Function not implemented39-> Directory not empty40-> Too many levels of symbolic links41-> Unknown error 4142-> No message of desired type43-> Identifier removed44-> Channel number out of range45-> Level 2 not synchronized46-> Level 3 halted47-> Level 3 reset48-> Link number out of range49-> Protocol driver not attached50-> No CSI structure available51-> Level 2 halted52-> Invalid exchange53-> Invalid request descriptor54-> Exchange full55-> No anode56-> Invalid request code57-> Invalid slot58-> Unknown error 5859-> Bad font file format60-> Device not a stream61-> No data available62-> Timer expired63-> Out of streams resources64-> Machine is not on the network65-> Package not installed66-> Object is remote67-> Link has been severed68-> Advertise error69-> Srmount error70-> Communication error on send71-> Protocol error72-> Multihop attempted73-> RFS specific error74-> Bad message75-> Value too large for defined data type76-> Name not unique on network77-> File descriptor in bad state78-> Remote address changed79-> Can not Access a needed shared library80-> Accessing a corrupted shared library81-> .lib section in a.out corrupted82-> Attempting to link in too many shared libraries83-> Cannot exec a shared library directly84-> Invalid or incomplete multibyte or wide character85-> Interrupted system call should be restarted86-> Streams pipe error87-> Too many users88-> Socket operation on non-socket89-> Destination address required90-> Message too long91-> Protocol wrong type for socket92-> Protocol not available93-> Protocol not supported94-> Socket type not supported95-> Operation not supported96-> Protocol family not supported97-> Address family not supported by protocol98-> Address already in use99-> Cannot assign requested address100-> Network is down101-> Network is unreachable102-> Network dropped connection on reset103-> Software caused connection abort104-> Connection reset by peer105-> No buffer space available106-> Transport endpoint is already connected107-> Transport endpoint is not connected108-> Cannot send after transport endpoint shutdown109-> Too many references: cannot splice110-> Connection timed out111-> Connection refused112-> Host is down113-> No route to host114-> Operation already in progress115-> Operation now in progress116-> Stale file handle117-> Structure needs cleaning118-> Not a XENIX named type file119-> No XENIX semaphores available120-> Is a named type file121-> Remote I/O error122-> Disk quota exceeded123-> No medium found124-> Wrong medium type125-> Operation canceled126-> Required key not available127-> Key has expired128-> Key has been revoked129-> Key was rejected by service130-> Owner died131-> State not recoverable132-> Operation not possible due to RF-kill133-> Memory page has hardware error134-> Unknown error 134

错误码的应用:

代码语言:javascript代码运行次数:0运行复制

int main(){    FILE *fp = fopen("invalid.txt", "r");//以只读方式打开不存在的文件会出错    if (fp == NULL)    {        // 使用 strerror 获取错误描述        printf("%d->%sn", errno,strerror(errno));                    return 1; //退出码设为1    }    return 0;}

输出:

代码语言:javascript代码运行次数:0运行复制

2->No such file or directory

使用错误码和对应的错误信息可以帮助程序员快速定位错误模块,调试程序,掌握错误码的使用与调试技巧,是提升 Linux 编程效率和系统可靠性的关键。


_exit函数和exit函数

_exit函数

代码语言:javascript代码运行次数:0运行复制

#include <unistd.h>void _exit(int status);

参数 status:进程的退出状态码,范围是 0~255。父进程可以通过 wait() 或 waitpid() 获取该状态码。返回值:无(进程直接终止,不会返回调用者)。

当前进程调用 _exit() 后,操作系统会立即介入,会从用户态陷入内核态,执行以下操作:

关闭所有文件描述符:内核会关闭进程打开的文件、套接字、管道等资源,但不会刷新标准 I/O 库(如 stdio)的缓冲区。释放用户空间内存:回收进程的代码段、数据段、等内存资源。发送 SIGCHLD 信号: 通知父进程子进程已终止,并传递退出状态码 status。终止进程:进程的状态变为 ZOMBIE(僵尸进程),直到父进程通过 wait() 回收其资源。

本质上,_exit() 最终会调用 Linux 内核的 exit_group 系统调用(sys_exit_group),终止整个进程及其所有线程。其内核处理流程如下:

释放进程资源:

关闭所有文件描述符。释放内存映射(mmap)和虚拟内存区域。解除信号处理程序绑定。

更新进程状态:

将进程状态设为 TASK_DEAD向父进程发送 SIGCHLD 信号。

调度器介入:

从运行队列中移除进程。切换到下一个进程执行。


exit函数

代码语言:javascript代码运行次数:0运行复制

#include <stdlib.h>void exit(int status);  // C #include <cstdlib>void exit(int status);  // c++ 

参数 status:进程的退出状态码,范围 0~255(0 通常表示成功,非零表示异常)。返回值:无(进程终止,不会返回调用者)。

进程调用 exit 时,按以下顺序执行操作:

调用 atexit 注册的函数:按注册的逆序执行所有通过 atexit 或 at_quick_exit(若使用quick_exit)注册的函数。刷新所有标准 I/O 缓冲区:清空 stdout、stderr 等流的缓冲区。 注意: stderr 默认无缓冲,stdout 在交互式设备上是行缓冲。关闭所有打开的文件流:调用 fclose 关闭所有通过 fopen 打开的文件。 注意:不会关闭底层文件描述符(需手动 close)。删除临时文件:删除由 tmpfile 创建的临时文件。终止进程:向操作系统返回状态码 status。父进程可通过 wait 或 waitpid 获取该状态码。


其实本质上,exit 是一个标准库函数,最后也会调用_exit,但是在这之前,exit还做了其他的清理工作:

【Linux 进程控制】—— 进程亦生生不息:起于鸿蒙,守若空谷,归于太虚

我们举个例子,帮大家直观的感受一下这两者的区别

示例一:使用 exit 函数

代码语言:javascript代码运行次数:0运行复制

int main(){    printf("hello");    exit(0);}

输出:

代码语言:javascript代码运行次数:0运行复制

[root@localhost linux]# ./a.outhello[root@localhost linux]#

示例二:使用 _exit 函数

代码语言:javascript代码运行次数:0运行复制

int main(){    printf("hello");    _exit(0);}

输出:

代码语言:javascript代码运行次数:0运行复制

[root@localhost linux]# ./a.out[root@localhost linux]#

return 退出

状态码传递:

main函数中的 return 语句返回一个整数值(通常称为退出状态码),表示程序的执行结果:

0:表示程序成功执行。非0:表示程序异常终止(具体数值由程序员定义)。

return与exit()的关系

隐式调用exit():

在 main 函数中使用 return 时,C/C++运行时会自动调用 exit() 函数,并将返回值作为参数传递给它。代码语言:javascript代码运行次数:0运行复制

int main(){    return 42;  // 等价于 exit(42);}

return的执行流程

当在main函数中执行return时,程序会做以下几件事:

返回值传递:将返回值传递给运行时环境。

清理操作:

调用局部对象析构函数(按照创建顺序的逆序)。调用全局对象的析构函数(同样逆序)。

调用exit():运行时调用exit(),执行以下操作:

刷新所有I/O缓冲区(如 std::cout)。关闭通过 fopen 打开的文件流。执行通过 atexit() 注册的函数。

终止进程:将控制权交还给操作系统。

值得注意的一点是:在非main函数的其他函数中使用 return 仅退出当前函数,返回到调用者,不会终止进程。


_exit、exit 和 return 对比

以下是一个详细的表格供大家理解参考

特性

_exit() (系统调用)

exit() (标准库函数)

return (在 main 中)

所属标准

POSIX 系统调用

C/C++ 标准库函数

C/C++ 语言关键字

头文件

(C)、(C++)

无(语言内置)

执行流程

立即终止进程,不执行任何用户空间清理。

1. 调用 atexit 注册的函数2. 刷新 I/O 缓冲区3. 关闭文件流

1. 调用 C++ 局部对象析构函数2. 隐式调用 exit() 完成后续清理

清理操作

内核自动回收进程资源(内存、文件描述符),不刷新缓冲区、不调用析构函数

清理标准库资源(刷新缓冲区、关闭文件流),但不调用 C++ 局部对象析构函数

调用 C++ 局部和全局对象析构函数,并触发 exit() 的清理逻辑

多线程行为

立即终止所有线程,可能导致资源泄漏

终止整个进程,但可能跳过部分线程资源释放(如线程局部存储)

同 exit(),但在 C++ 中会正确析构主线程的局部对象

C++ 析构函数调用

❌ 不调用任何对象的析构函数(包括全局对象)

❌ 不调用局部对象析构函数✅ 调用全局对象析构函数(C++)

✅ 调用局部和全局对象析构函数(C++)

缓冲区处理

❌ 不刷新 stdio 缓冲区(如 printf 的输出可能丢失)

✅ 刷新所有 stdio 缓冲区

✅ 通过隐式调用 exit() 刷新缓冲区

适用场景

1. 子进程退出(避免重复刷新缓冲区)2. 需要立即终止进程(绕过清理逻辑)

1. 非 main 函数的程序终止2. 需要执行注册的清理函数(如日志收尾)

1. 在 main 函数中正常退出2. 需要确保 C++ 对象析构(RAII 资源管理)

错误处理

直接传递状态码给操作系统,无错误反馈机制

可通过 atexit 注册错误处理函数,但无法捕获局部对象析构异常

可通过 C++ 异常机制处理错误(需在 main 中捕获)

信号安全

✅ 可在信号处理函数中安全调用(如 SIGINT)

❌ 不可在信号处理函数中调用(可能死锁)

❌ 不可在信号处理函数中使用(仅限 main 函数流程)

资源泄漏风险

高(临时文件、未释放的手动内存等需内核回收)

中(未关闭的文件描述符、手动内存需提前处理)

低(依赖 RAII 自动释放资源)

底层实现

直接调用内核的 exit_group 系统调用

调用 C 标准库的清理逻辑后,最终调用 _exit()

编译器生成代码调用析构函数,并跳转到 main 结尾触发 exit()

最后总结下:

_exit():最底层的终止方式,适合需要绕过所有用户空间清理的场景(如子进程退出)。exit():平衡安全与效率,适合非 main 函数的程序终止,但需注意 C++ 对象析构问题。return:C++ 中最安全的退出方式,优先在 main 函数中使用,确保资源自动释放。

以上就是【Linux 进程控制】—— 进程亦生生不息:起于

© 版权声明
THE END
喜欢就支持一下吧
点赞13 分享