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

欢迎来到一整颗红豆的博客✨,一个关于探索技术的角落,记录学习的点滴📖,分享实用的技巧🛠️,偶尔还有一些奇思妙想💡

本文由一整颗红豆原创✍️,感谢支持❤️!请尊重原创📩!欢迎评论区留言交流🌟

个人主页 👉 一整颗红豆

本文专栏➡️Linux驾驭之道 掌控操作系统的艺术与哲学

生生不息:起于鸿蒙,守若空谷,归于太虚

进程创建

再识fork函数

之前在 Linux进程状态 这篇文章中,我们已经为大家介绍过Linux系统中一个非常重要的系统调用 --- fork ,今天我们在来重谈 fork 函数,让大家对这个系统调用有更深刻的理解。

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

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

bash 复制代码
NAME
       fork - create a child process

SYNOPSIS
       #include <sys/types.h>
       #include <unistd.h>

       pid_t fork(void);

如上不难看出:

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

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

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

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

c 复制代码
int main(void)
{
    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;
}

输出:

bash 复制代码
Before: pid is 40176
After:pid is 40176, fork return 40177
After:pid is 40177, fork return 0

这里看到了三行输出,⼀行before,两行after。其中 40176就是父进程啦,40177就是子进程。进程40176先打印before消息,然后它有打印after。另⼀个after消息是进程40177打印的。注意到进程40177没有打印before,为什么呢?

如下图所示:

当父进程执行到fork创建出子进程时,已经执行了上面的before代码,而创建出子进程后,子进程不会去执行父进程已经执行过的代码,而是和父进程一同执行fork之后的代码。这就是为什么子进程没有打印before的原因

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


fork函数返回值

类型定义:fork() 返回 pid_t 类型(通常为 int 通过 typedef 定义),用于表示进程ID(PID)。

fork创建成功:

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

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

一个父进程可以创建一个或者多个子进程,父进程需要通过返回值获得新创建的子进程的唯一标识符(正整数),从而可以管理创建的多个子进程(如发送信号、等待终止等)

为什么子进程返回0

子进程返回0,标识自己为子进程,子进程通过返回值 0 确认自己的身份。子进程无需知晓父进程的PID(实际上可以通过 getppid() 获取)


fork创建失败:

返回 -1并设置错误码:

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

错误码:

  • 需检查 errno 确定具体原因
c 复制代码
if (pid == -1) {
    perror("fork failed"); // 输出类似 "fork failed: Resource temporarily unavailable"
}

常见错误码:

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

写时拷贝 Copy-On-Write

写时拷贝(COW)是 Linux 中 fork() 系统调用的核心优化机制,它使得进程创建变得高效且资源友好,通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自⼀份副本。

为什么需要写时拷贝?

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

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

COW 的解决思路:

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

具体见下图:

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

写时拷贝的工作流程

1、 fork() 调用时

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

2、进程尝试写入内存

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

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

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

3、后续操作

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

进程等待

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

在Linux系统中,进程等待是父进程通过系统调用等待子进程终止并获取其退出状态的过程,主要目的是避免僵尸进程并回收子进程资源。

进程等待的必要性

僵尸进程问题:

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

进程等待的方法

wait

cpp 复制代码
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);

具体功能:

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

参数:

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

返回值:

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

waitpid

cpp 复制代码
#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,子进程存在且正常运行,则进程可能阻塞。 如果不存在该子进程,则立即出错返回。

获取子进程 status

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

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

status 不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图

(只研究 status 低16比特位):


如何理解呢?

子进程的退出分为两种情况:

  1. 正常终止

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

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

示例:

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

  1. 被信号所杀导致终止

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

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

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

示例:

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

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

如何解析 status?

难道真的需要我们将 status 当作位图,使用位操作来提取子进程的退出信息吗?

这么做对我们程序员来说当然小菜一碟,不过有点多余了,没必要。Linux系统为我们定义了多种宏用来提取 status,方便且专业。

使用宏定义检查 status 的值:

功能
WIFEXITED(status) 若子进程正常终止(exit 或 return)返回真。
WEXITSTATUS(status) 若 WIFEXITED 为真,返回子进程的退出码(exit 的参数或 return 的值)。
WIFSIGNALED(status) 若子进程因信号终止返回真。
WTERMSIG(status) 若 WIFSIGNALED 为真,返回导致终止的信号编号。
WCOREDUMP(status) 若子进程生成了核心转储文件返回真。

常用的两个宏:

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

示例一:子进程正常退出

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

输出:

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

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

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

输出:

bash 复制代码
子进程运行中... PID=56203
子进程被信号终止,信号编号: 11

阻塞等待与非阻塞等待

在 Unix/Linux 中,父进程通过 wait 或 waitpid 函数等待子进程结束。它们的核心区别在于是否允许父进程在等待子进程时继续执行其他任务。

阻塞等待(Blocking Wait

父进程调用 waitpid 后,会一直挂起(阻塞),直到目标子进程终止。在阻塞期间,父进程无法执行其他操作,直到子进程退出。

cpp 复制代码
pid_t waitpid(pid_t pid, int *status, 0);  // options 参数为 0

示例:

cpp 复制代码
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("子进程退出码: %d\n", WEXITSTATUS(status));
        }
    }
}

非阻塞等待(Non-blocking Wait

父进程调用 waitpid 时,若子进程未结束,则父进程立即返回,而不是挂起。父进程可以继续执行其他任务,同时定期检查子进程状态。需结合循环实现非阻塞式轮询(polling)。

关键选项:宏 WNOHANG (定义在 <sys/wait.h> 中)。

cpp 复制代码
pid_t waitpid(pid_t pid, int *status, WNOHANG);

示例:非阻塞轮询方式

cpp 复制代码
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("子进程退出码: %d\n", WEXITSTATUS(status));
                }
                break;
            }
        }
    }
}

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

场景 阻塞等待 非阻塞等待
父进程任务优先级 必须立即处理子进程结果 需同时处理其他任务
子进程执行时间 较短或确定 较长或不确定
资源消耗 CPU 空闲,无额外开销 需轮询,可能占用更多 CPU
典型应用 简单脚本、单任务场景 多进程管理、事件驱动程序

进程终止

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

进程终止是进程生命周期的最后一个阶段,涉及资源释放、状态通知及父进程回收等关键步骤。进程终止的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的代码和数据。

进程退出场景

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止

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

代码运行完毕,结果正确

  • 程序完整执行了所有逻辑,未触发任何错误或异常。
  • 输出结果与预期完全一致,符合功能需求或算法目标。
cpp 复制代码
int sum(int a, int b)
{
    return a + b;
}

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

输出:

c 复制代码
Result: 8

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

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

例如:

cpp 复制代码
// 错误实现:本应计算阶乘,但初始值错误
int factorial(int n)
{
    int result = 0; // 错误!应为 result = 1
    for (int i = 1; i <= n; i++)
    {
        result *= i;
    }
    return result;
}

int main()
{
    printf("5! = %d\n", factorial(5)); // 输出 0,结果错误
    return 0;
}

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

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

例如

cpp 复制代码
int main()
{
    int *ptr = NULL;
    *ptr = 42;  // 对空指针解引用,触发段错误
    printf("Value: %d\n", *ptr);
    return 0;
}

段错误:

bash 复制代码
Segmentation fault

再比如:

cpp 复制代码
int main()
{
    int a = 10;
    int b = a / 0; // 程序除零异常
    printf("Value: %d\n", b);
    return 0;
}

浮点数异常:

cpp 复制代码
Floating point exception

进程常见退出方法

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

  1. 从main返回
  2. 调用exit
  3. _exit

异常退出:

  1. ctrl + c,信号终止

进程退出码

进程退出码(退出状态)可以告诉我们最后⼀次执行的命令的状态。在命令结束以后,我们可以知道命令是成功完成的还是以错误结束的。通常是你程序中mian函数的返回值,其基本思想是,程序返回退出代码 0 时表示执行成功,没有问题。 0 以外的任何代码都被视为不成功。

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

Linux Shell 中的常见退出码:

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

这里需要补充一点:

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

错误码

在 Linux 系统中,错误码(Error Codes )是操作系统用于标识程序运行中遇到的各类问题的核心机制。这些错误码通过全局变量 errno (定义在 <errno.h> 头文件中)传递,帮助开发者快速定位和调试问题。

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

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

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

特性:

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

头文件:

cpp 复制代码
#include <errno.h>

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

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

cpp 复制代码
int main()
{
    for (int i = 0; i < 135; ++i)
    {
        printf("%d-> %s\n", i, strerror(i));
    }
    return 0;
}

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

cpp 复制代码
0-> Success
1-> Operation not permitted
2-> No such file or directory
3-> No such process
4-> Interrupted system call
5-> Input/output error
6-> No such device or address
7-> Argument list too long
8-> Exec format error
9-> Bad file descriptor
10-> No child processes
11-> Resource temporarily unavailable
12-> Cannot allocate memory
13-> Permission denied
14-> Bad address
15-> Block device required
16-> Device or resource busy
17-> File exists
18-> Invalid cross-device link
19-> No such device
20-> Not a directory
21-> Is a directory
22-> Invalid argument
23-> Too many open files in system
24-> Too many open files
25-> Inappropriate ioctl for device
26-> Text file busy
27-> File too large
28-> No space left on device
29-> Illegal seek
30-> Read-only file system
31-> Too many links
32-> Broken pipe
33-> Numerical argument out of domain
34-> Numerical result out of range
35-> Resource deadlock avoided
36-> File name too long
37-> No locks available
38-> Function not implemented
39-> Directory not empty
40-> Too many levels of symbolic links
41-> Unknown error 41
42-> No message of desired type
43-> Identifier removed
44-> Channel number out of range
45-> Level 2 not synchronized
46-> Level 3 halted
47-> Level 3 reset
48-> Link number out of range
49-> Protocol driver not attached
50-> No CSI structure available
51-> Level 2 halted
52-> Invalid exchange
53-> Invalid request descriptor
54-> Exchange full
55-> No anode
56-> Invalid request code
57-> Invalid slot
58-> Unknown error 58
59-> Bad font file format
60-> Device not a stream
61-> No data available
62-> Timer expired
63-> Out of streams resources
64-> Machine is not on the network
65-> Package not installed
66-> Object is remote
67-> Link has been severed
68-> Advertise error
69-> Srmount error
70-> Communication error on send
71-> Protocol error
72-> Multihop attempted
73-> RFS specific error
74-> Bad message
75-> Value too large for defined data type
76-> Name not unique on network
77-> File descriptor in bad state
78-> Remote address changed
79-> Can not access a needed shared library
80-> Accessing a corrupted shared library
81-> .lib section in a.out corrupted
82-> Attempting to link in too many shared libraries
83-> Cannot exec a shared library directly
84-> Invalid or incomplete multibyte or wide character
85-> Interrupted system call should be restarted
86-> Streams pipe error
87-> Too many users
88-> Socket operation on non-socket
89-> Destination address required
90-> Message too long
91-> Protocol wrong type for socket
92-> Protocol not available
93-> Protocol not supported
94-> Socket type not supported
95-> Operation not supported
96-> Protocol family not supported
97-> Address family not supported by protocol
98-> Address already in use
99-> Cannot assign requested address
100-> Network is down
101-> Network is unreachable
102-> Network dropped connection on reset
103-> Software caused connection abort
104-> Connection reset by peer
105-> No buffer space available
106-> Transport endpoint is already connected
107-> Transport endpoint is not connected
108-> Cannot send after transport endpoint shutdown
109-> Too many references: cannot splice
110-> Connection timed out
111-> Connection refused
112-> Host is down
113-> No route to host
114-> Operation already in progress
115-> Operation now in progress
116-> Stale file handle
117-> Structure needs cleaning
118-> Not a XENIX named type file
119-> No XENIX semaphores available
120-> Is a named type file
121-> Remote I/O error
122-> Disk quota exceeded
123-> No medium found
124-> Wrong medium type
125-> Operation canceled
126-> Required key not available
127-> Key has expired
128-> Key has been revoked
129-> Key was rejected by service
130-> Owner died
131-> State not recoverable
132-> Operation not possible due to RF-kill
133-> Memory page has hardware error
134-> Unknown error 134

错误码的应用:

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

输出:

bash 复制代码
2->No such file or directory

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


_exit函数和exit函数

_exit函数

在 Linux 系统中,_exit() 是一个直接终止进程的系统调用,它会立即终止当前进程,并通知操作系统回收资源,但不执行任何用户空间的清理操作。

bash 复制代码
#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函数

在 C/C++ 语言中,exit 是一个用于正常终止程序执行的标准库函数。它会执行一系列清理操作后终止进程,并将控制权交还给操作系统。

cpp 复制代码
#include <stdlib.h>
void exit(int status);  // C 

#include <cstdlib>
void exit(int status);  // C++ 
  • 参数 status :进程的退出状态码,范围 0~255(0 通常表示成功,非零表示异常)。
  • 返回值:无(进程终止,不会返回调用者)。

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

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

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

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

示例一:使用 exit 函数

cpp 复制代码
int main()
{
    printf("hello");
    exit(0);
}

输出:

bash 复制代码
[root@localhost linux]# ./a.out
hello[root@localhost linux]#

示例二:使用 _exit 函数

cpp 复制代码
int main()
{
    printf("hello");
    _exit(0);
}

输出:

bash 复制代码
[root@localhost linux]# ./a.out
[root@localhost linux]#

聪明的同学很快就知道了,我们通过 printf 打印 "hello" 并没有加上换行符,所以"hello"

在缓冲区内没有被立即刷新,所以当我们使用exit终止进程时,exit会帮我们做相应的清理工作,包括刷新I/O缓冲区。而调用_exit时则不会刷新,进程直接退出。

return 退出

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

状态码传递:

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

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

return与exit()的关系

隐式调用exit():

  • 在 main 函数中使用 return 时,C/C++运行时会自动调用 exit() 函数,并将返回值作为参数传递给它。
cpp 复制代码
int main()
{
    return 42;  // 等价于 exit(42);
}

return的执行流程

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

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

清理操作:

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

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

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

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

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


_exitexitreturn 对比

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

特性 _exit() (系统调用) exit() (标准库函数) return (在 main 中)
所属标准 POSIX 系统调用 C/C++ 标准库函数 C/C++ 语言关键字
头文件 <unistd.h> <stdlib.h>(C)、<cstdlib>(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操作系统的更多话题,带你从新手小白 成长为一名 Linux 糕手, 感谢您的观看!

如果你觉得这篇文章对你有所帮助,请为我的博客 点赞👍收藏⭐️ 评论💬或 分享🔗 支持一下!你的每一个支持都是我继续创作的动力✨!🙏

如果你有任何问题或想法,也欢迎 留言💬 交流,一起进步📚!❤️ 感谢你的阅读和支持🌟!🎉

祝各位大佬吃得饱🍖,睡得好🛌,日有所得📈,逐梦扬帆⛵!

相关推荐
还是鼠鼠7 分钟前
Node.js 跨域 CORS 简单请求与预检请求的介绍
运维·服务器·vscode·中间件·node.js·express
我命由我123451 小时前
35.Java线程池(线程池概述、线程池的架构、线程池的种类与创建、线程池的底层原理、线程池的工作流程、线程池的拒绝策略、自定义线程池)
java·服务器·开发语言·jvm·后端·架构·java-ee
Chandler241 小时前
Go:方法
开发语言·c++·golang
old_iron4 小时前
vim定位有问题的脚本/插件的一般方法
linux·编辑器·vim
whoarethenext4 小时前
qt的基本使用
开发语言·c++·后端·qt
爱知菜6 小时前
Windows安装Docker Desktop(WSL2模式)和Docker Pull网络问题解决
运维·docker·容器
做测试的小薄6 小时前
Nginx 命令大全:Linux 与 Windows 系统的全面解析
linux·自动化测试·windows·nginx·环境部署
影龙帝皖7 小时前
Linux网络之局域网yum仓库与apt的实现
linux·服务器·网络
月下雨(Moonlit Rain)7 小时前
Docker
运维·docker·容器
虾球xz7 小时前
游戏引擎学习第220天
c++·学习·游戏引擎