【Linux 进程控制】—— 进程亦生生不息:起于鸿蒙,守若空谷,归于太虚
欢迎来到一整颗红豆的博客✨,一个关于探索技术的角落,记录学习的点滴📖,分享实用的技巧🛠️,偶尔还有一些奇思妙想💡
本文由一整颗红豆原创✍️,感谢支持❤️!请尊重原创📩!欢迎评论区留言交流🌟
个人主页 👉 一整颗红豆
本文专栏➡️Linux驾驭之道 掌控操作系统的艺术与哲学
生生不息:起于鸿蒙,守若空谷,归于太虚
- 进程创建
- 再识fork函数
- fork函数返回值
- 写时拷贝 `Copy-On-Write`
- 进程等待
- 进程等待的方法
- `wait`
- `waitpid`
- 获取子进程 status
- 阻塞等待与非阻塞等待
- 阻塞等待(`Blocking Wait`)
- 非阻塞等待(`Non-blocking Wait`)
- 进程终止
- 进程退出场景
- 进程退出码
- 错误码
- _exit函数和exit函数
- return 退出
- `_exit`、`exit` 和 `return` 对比
- 写在最后
进程创建
再识fork函数
之前在 Linux进程状态 这篇文章中,我们已经为大家介绍过Linux系统中一个非常重要的系统调用 —
fork
,今天我们在来重谈fork
函数,让大家对这个系统调用有更深刻的理解。
在 Linux中 fork
函数是非常重要的函数,它从已存在进程中创建⼀个新进程。创建出来的新进程叫做子进程,而原进程则称为父进程。
在Linux参考手册中,fork函数的原型如下:(man 2 fork
指令查看)
NAMEfork - create a child processSYNOPSIS#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之后,就有两个⼆进制代码相同的进程。并且它们都运行到相同的地方。但每个进程都将可以开始属于它们自己的旅程,看如下程序:
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;
}
输出:
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 确定具体原因
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
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
具体功能:
- 阻塞父进程,直到等待到任意一个子进程终止。
参数:
status
:输出型参数,用来存储子进程退出状态的指针(可为NULL
,表示不关心状态)。
返回值:
- 成功:返回终止的子进程PID。失败:返回-1(如无子进程)。
waitpid
#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
wait
和 waitpid
,都有⼀个 status
参数,该参数是⼀个输出型参数,由操作系统填充。
- 如果传递
NULL
,表示不关心子进程的退出状态信息。 - 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status
不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图
(只研究 status 低16比特位):
如何理解呢?
子进程的退出分为两种情况:
- 正常终止
高 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)。
- 做个小总结:
低 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非零,提取子进程退出码。(查看进程的 退出码)
示例一:子进程正常退出
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;
}
输出:
子进程运行中... PID=56153
子进程正常退出,退出码: 42
示例二:子进程被信号终止
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;
}
输出:
子进程运行中... PID=56203
子进程被信号终止,信号编号: 11
阻塞等待与非阻塞等待
在 Unix/Linux 中,父进程通过 wait 或 waitpid 函数等待子进程结束。它们的核心区别在于是否允许父进程在等待子进程时继续执行其他任务。
阻塞等待(Blocking Wait
)
父进程调用 waitpid 后,会一直挂起(阻塞),直到目标子进程终止。在阻塞期间,父进程无法执行其他操作,直到子进程退出。
pid_t waitpid(pid_t pid, int *status, 0); // options 参数为 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("子进程退出码: %d\n", WEXITSTATUS(status));}}
}
非阻塞等待(Non-blocking Wait
)
父进程调用 waitpid 时,若子进程未结束,则父进程立即返回,而不是挂起。父进程可以继续执行其他任务,同时定期检查子进程状态。需结合循环实现非阻塞式轮询(
polling
)。
关键选项:宏 WNOHANG
(定义在 <sys/wait.h>
中)。
pid_t waitpid(pid_t pid, int *status, WNOHANG);
示例:非阻塞轮询方式
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 |
典型应用 | 简单脚本、单任务场景 | 多进程管理、事件驱动程序 |
进程终止
进程= 内核数据结构 + 进程自己的代码和数据
进程终止是进程生命周期的最后一个阶段,涉及资源释放、状态通知及父进程回收等关键步骤。进程终止的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的代码和数据。
进程退出场景
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
如何理解这三种进程退出的场景呢?举个例子
代码运行完毕,结果正确
- 程序完整执行了所有逻辑,未触发任何错误或异常。
- 输出结果与预期完全一致,符合功能需求或算法目标。
int sum(int a, int b)
{return a + b;
}int main()
{int result = sum(3, 5);printf("Result: %d\n", result); // 输出 8,结果正确return 0;
}
输出:
Result: 8
代码运行完毕,结果不正确
- 程序正常结束(无崩溃或异常),但输出结果与预期不符。
- 通常由逻辑错误、算法错误或数据处理错误导致。
例如:
// 错误实现:本应计算阶乘,但初始值错误
int factorial(int n)
{int result = 0; // 错误!应为 result = 1for (int i = 1; i <= n; i++){result *= i;}return result;
}int main()
{printf("5! = %d\n", factorial(5)); // 输出 0,结果错误return 0;
}
代码未执行完毕,异常终止
- 程序未执行完毕就中途崩溃或被强制终止。
- 通常由运行时错误、资源限制或外部信号触发。
- 比如除零错误,对空指针解引用等异常
例如:
int main()
{int *ptr = NULL;*ptr = 42; // 对空指针解引用,触发段错误printf("Value: %d\n", *ptr);return 0;
}
段错误:
Segmentation fault
再比如:
int main()
{int a = 10;int b = a / 0; // 程序除零异常printf("Value: %d\n", b);return 0;
}
浮点数异常:
Floating point exception
进程常见退出方法
正常终止(可以通过 echo $?
查看进程退出码)
- 从main返回
- 调用exit
- _exit
异常退出:
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 副本(多线程安全)。
头文件:
#include <errno.h>
与之对应的是 strerror
函数,该函数可以将对应的错误码转化成字符串描述的错误信息打印出来,方便程序员调试代码。
实际上,我们可以通过 for
循环来打印查看Linux系统下所有的错误码以及其错误信息:
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
则分别对应一个错误信息。
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
错误码的应用:
int main()
{FILE *fp = fopen("invalid.txt", "r");//以只读方式打开不存在的文件会出错if (fp == NULL){// 使用 strerror 获取错误描述printf("%d->%s\n", errno,strerror(errno)); return 1; //退出码设为1}return 0;
}
输出:
2->No such file or directory
使用错误码和对应的错误信息可以帮助程序员快速定位错误模块,调试程序,掌握错误码的使用与调试技巧,是提升 Linux 编程效率和系统可靠性的关键。
_exit函数和exit函数
_exit函数
在 Linux 系统中,
_exit()
是一个直接终止进程的系统调用,它会立即终止当前进程,并通知操作系统回收资源,但不执行任何用户空间的清理操作。
#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
是一个用于正常终止程序执行的标准库函数。它会执行一系列清理操作后终止进程,并将控制权交还给操作系统。
#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还做了其他的清理工作:
我们举个例子,帮大家直观的感受一下这两者的区别:
示例一:使用 exit 函数
int main()
{printf("hello");exit(0);
}
输出:
[root@localhost linux]# ./a.out
hello[root@localhost linux]#
示例二:使用 _exit 函数
int main()
{printf("hello");_exit(0);
}
输出:
[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() 函数,并将返回值作为参数传递给它。
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++ 语言关键字 |
头文件 | <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 糕手, 感谢您的观看!
如果你觉得这篇文章对你有所帮助,请为我的博客 点赞👍收藏⭐️ 评论💬或 分享🔗 支持一下!你的每一个支持都是我继续创作的动力✨!🙏
如果你有任何问题或想法,也欢迎 留言💬 交流,一起进步📚!❤️ 感谢你的阅读和支持🌟!🎉
祝各位大佬吃得饱🍖,睡得好🛌,日有所得📈,逐梦扬帆⛵!
相关文章:
【Linux 进程控制】—— 进程亦生生不息:起于鸿蒙,守若空谷,归于太虚
欢迎来到一整颗红豆的博客✨,一个关于探索技术的角落,记录学习的点滴📖,分享实用的技巧🛠️,偶尔还有一些奇思妙想💡 本文由一整颗红豆原创✍️,感谢支持❤️!请尊重原创…...
K8s常用基础管理命令(一)
基础管理命令 基础命令kubectl get命令kubectl create命令kubectl apply命令kubectl delete命令kubectl describe命令kubectl explain命令kubectl run命令kubectl cp命令kubectl edit命令kubectl logs命令kubectl exec命令kubectl port-forward命令kubectl patch命令 集群管理命…...
WebChat 一款非常好用的浏览器侧边栏 AI 问答插件
文章目录 使用方法及效果展示划线引用自定义工具自定义模型设置 主要功能1. 划线引用功能2. 自定义划线工具3. 聊天功能4. 历史记录管理5. 界面特性 安装方法方法一:直接安装发布版本(推荐)方法二:从源码构建安装(开发…...
kubernetes入门篇之创建一个nginx容器
上几篇讲了部署master和worker node 及网络插件calico, 现在开始实际运行一个容器。 1. 新建nginx.yaml文件 方式1:直接创建一个pod 和一个 service,一般不直接这样创建,该方式仅适用于测试或学习 apiVersion: v1 kind: Pod …...
回顾 | 2025香港Web3嘉年华:CertiK以创新技术定义安全未来
4月6日至9日,Web3安全巨头CertiK亮相2025香港Web3嘉年华。活动期间,CertiK不仅设立独立展位与广大Web3生态参与者深入互动,更通过高层次的技术交流与前沿研究成果展示,成为本届盛会备受瞩目的焦点。 耶鲁大学计算机科学系教授、C…...
HTML5的笔记
文章目录 1.HTML的概念1.1HTML的基本骨架 2.标签语法2.1标签的关系 3.标签3.1双标签3.1.1标题标签<h1~h6>3.1.2段落标签<p>3.1.3文本格式化标签3.1.4超链接标签<a>3.1.5音频和视频标签audio和<vedio>3.1.6列表标签3.1.7表格标签 3.2单标签3.2.1换行标签…...
LeetCode.2843. 统计对称整数的数目
统计对称整数的数目 题目解题思路思路1.v1Code 思路优化1.v2Code 思路优化1.v3Code复杂度分析 题目 2843. 统计对称整数的数目 给你两个正整数 low 和 high 。 对于一个由 2 * n 位数字组成的整数 x ,如果其前 n 位数字之和与后 n 位数字之和相等,则认…...
Java常用工具算法-6--秘钥托管云服务3--微软zure Key Vault
Azure Key Vault是微软Azure提供的一项服务,旨在帮助用户安全地存储和管理敏感信息,如加密密钥、证书和密码等。它提供了一个集中的位置来保护这些重要资产,并且通过细粒度的访问控制和审计日志来确保安全性。 1、主要功能 (1&a…...
表格开启聚光灯,查看数据不错行-Excel易用宝
面对如此庞大的一个表格,每次找数据就像走迷宫一样,有时看到了数据,眼神不好的小丽小手一抖还会选择到其他数据上,我问她个数据,她经常给我报个错的数据,我说怎么数据总是对不上号。 对于大表格防看错行这…...
解决java使用easyexcel填充模版后,高度不一致问题
自定义工具,可以通过获取上一行行高设置后面所以行的高度 package org.springblade.modules.api.utils;import com.alibaba.excel.write.handler.RowWriteHandler; import com.alibaba.excel.write.metadata.holder.WriteSheetHolder; import com.alibaba.excel.wr…...
【25软考网工笔记】第二章 数据通信基础(1)信道特性 奈奎斯特 香农定理
一、信道特性 1. 数据通信概念 1)通信系统的基本元素 通信目的: 传递信息。 信源: 产生和发送信息的一端,即信息发送的源头。 信宿: 接收信息的一端,即信息的目的地。 信道: 信源和信宿之间的通信线路,用于传输信息。 信号变换:…...
2024年React最新高频面试题及核心考点解析,涵盖基础、进阶和新特性,助你高效备战
以下是2024年React最新高频面试题及核心考点解析,涵盖基础、进阶和新特性,助你高效备战: 一、基础篇 React虚拟DOM原理及Diff算法优化策略 • 必考点:虚拟DOM树对比(同级比较、Key的作用、组件类型判断) …...
【Code】《代码整洁之道》笔记-Chapter11-系统
第11章 系统 “复杂要人命。它消磨开发者的生命,让产品难以规划、构建和测试。” 11.1 如何建造一个城市 你能自己掌管一切细节吗?大概不行。即便是管理一个既存的城市,也是靠单人能力无法做到的。不过,城市还是在运转&#…...
MySQL数据库编程总结
MySQL数据库编程总结 一、数据库概述 数据库定义 • 数据库是管理数据的软件系统,用于高效存储、管理和检索数据,减少冗余。 • 核心功能:通过SQL语言定义、操作数据,维护完整性和安全性。 常见数据库 • MySQL、Oracle、SQL Ser…...
MySQL学习笔记7【InnoDB】
Innodb 1. 架构 1.1 内存部分 buffer pool 缓冲池是主存中的第一个区域,里面可以缓存磁盘上经常操作的真实数据,在执行增删查改操作时,先操作缓冲池中的数据,然后以一定频率刷新到磁盘,这样操作明显提升了速度。 …...
HTML应用指南:利用GET请求获取全国汉堡王门店位置信息
在当今快节奏的都市生活中,餐饮品牌的门店布局不仅反映了其市场策略,更折射出消费者对便捷、品质和品牌认同的追求。汉堡王(Burger King)作为全球知名的西式快餐品牌之一,在中国市场同样占据重要地位。自进入中国市场以…...
STM32+EC600E 4G模块 与华为云平台通信
前言 由于在STM32巡回研讨会上淘了一块EC600E4G模块以及刚办完电信卡多了两张副卡,副卡有流量刚好可以用一下,试想着以后画一块ESP32板子搭配这个4G模块做个随身WIFI,目前先用这个模块搭配STM32玩一下云平顺便记录一下。 实验目的 实现STM…...
【Spring】IoC详解:五大类注解、类Bean的存储(上)
1.IoC本质 IoC(Inversion of Control,控制反转) 是Spring框架的灵魂,它颠覆了传统编程中“谁用谁造”的逻辑。简单来说,IoC就是把对象创建和管理的控制权从程序员手中“反转”给一个外部容器,让代码更灵活…...
图片压缩后失真?3款工具还原高清细节
在当今,图片的使用无处不在。为了便于存储和传输,我们常常会对图片进行压缩。然而,不少人发现,压缩后的图片往往变得模糊,失去了原本的清晰度和细节。那么,当遇到这种情况时,我们该如何将模糊的…...
2025中国移动云智算大会|彩讯企业级AI应用产品引关注
2025中国移动以“由云向智,共绘算网新生态”为主题,精心打造了一场智能科技展。中国移动携手生态伙伴带来涵盖算力、工具、模型、应用等覆盖多样化场景的AI应用服务,赋能生产方式、生活方式、社会治理方式的数智化解决方案,充分释…...
在新一代人工智能技术引领下的,相互联系、层层递进的明厨亮灶开源了
明厨亮灶视频监控平台是一款功能强大且简单易用的实时算法视频监控系统。它的愿景是最底层打通各大芯片厂商相互间的壁垒,省去繁琐重复的适配流程,实现芯片、算法、应用的全流程组合,从而大大减少企业级应用约95%的开发成本。AI技术可以24小时…...
修图自由!自建IOPaint服务器,手机平板随时随地远程调用在线P图
前言:在这个人人都想当摄影师的时代,一张完美的照片简直比中彩票还难。但别担心,今天我来给大家揭秘一个超级神器——IOPaint!这款免费开源的AI工具不仅能一键移除照片中的杂物和路人,还能智能扩展图片内容,…...
PyTorch实现二维卷积与边缘检测:从原理到实战
本文通过PyTorch实现二维互相关运算、自定义卷积层,并演示如何通过卷积核检测图像边缘。同时,我们将训练一个卷积核参数,使其能够从数据中学习边缘特征。 1. 二维互相关运算的实现 互相关运算(Cross-Correlation)是卷…...
解决Server doesn‘t support Accept-Ranges问题
Cannot download differentially, fallback to full download: Error: Server doesnt support Accept-Ranges (response code 200) 解决方案 修改nginx配置文件支持Accept-Ranges(范围请求) server {...location ^~/ {default_type multipart/byterang…...
处理Excel表不等长时间序列用tsfresh提取时序特征
我原本的时间序列格式是excel表记录的,每一行是一条时间序列,时间序列不等长。 要把excel表数据读取出来之后转换成extract_features需要的格式。 1.读取excel表数据 import pandas as pd import numpy as np from tsfresh import extract_features mda…...
Linux __命令和权限
目录 一、几个指令 bc uname -r 指令 重要的几个热键 二、Shell命令以及运行原理 为什么有外壳 外壳是如何工作的 什么是操作系统,为什么要有操作系统 三、文件类型 1、Linux的文件类型 2、文件类型 四、用户 用户问题和切换问题 增加普通用户 root -&…...
IO流——字符输入输出流:FileReader FileWriter
一、文件字符输入流:FileReader 作用:以内存为基准,可以把文件中的数据以字符的形式读入到内存中去 public class Test5 {public static void main(String[] args) {try (Reader fr new FileReader("E:\\IDEA\\JavaCodeAll\\file-io-t…...
【大模型理论篇】DeepResearcher论文分析-通过在真实环境中的强化学习实现深度研究
1. 背景与问题 大模型(LLMs)配合网络搜索功能已经展现出在深度研究任务中的巨大潜力。然而,目前的方法主要依赖两种途径: 人工设计的提示工程(Prompt Engineering):这种方法依靠手动设计的工作流…...
大数据(7.5)Kafka Edge在5G边缘计算中的革新实践:解锁毫秒级实时处理的无限可能
目录 一、5G时代边缘计算的算力革命1.1 传统架构的延迟困境1.2 5G网络特性与Kafka适配 二、Kafka Edge核心架构设计2.1 分层处理架构2.2 关键技术创新点2.2.1 协议优化2.2.2 轻量化存储引擎 三、5G场景落地实践3.1 智能工厂预测性维护3.2 全息远程医疗会诊 四、性能优化深度实践…...
【基于开源insightface的人脸检测,人脸识别初步测试】
简介 InsightFace是一个基于深度学习的开源人脸识别项目,由蚂蚁金服的深度学习团队开发。该项目提供了人脸检测、人脸特征提取、人脸识别等功能,支持多种操作系统和深度学习框架。本文将详细介绍如何在Ubuntu系统上安装和实战InsightFace项目。 目前github有非常多的人脸识…...
kafka怎么保证消息不被重复消费
在 Kafka 中,要保证消息不被重复消费,可从消费者端和生产者端分别采取不同策略,下面为你详细介绍: 消费者端实现幂等消费 幂等消费是指对同一条消息,无论消费多少次,产生的业务结果都是一样的。 业务层面…...
一个批量文件Dos2Unix程序(Microsoft Store,开源)
这个程序可以把整个目录的文本文件改成UNIX格式,源码是用C#写的。 目录 一、从Microsoft Store安装 二、从github获取源码 三、功能介绍 3.1 运行 3.2 浏览 3.3 转换 3.4 转换(无列表) 3.5 取消 3.6 帮助 四、源码解读 五、讨论和…...
Python及Javascript的map 、 filter 、reduce类似函数的对比汇总
A. 在Python中,map 和 filter 是两个非常有用的内置函数,它们分别用于对可迭代对象中的每个元素执行某种操作,并返回结果。在JavaScript中,虽然没有内置的 map 和 filter 函数,但是可以使用数组的 map() 和 filter() …...
Linux中OS的管理和进程的概念
一、OS的管理 1.1操作系统宏观的理解 OS的本质是一款进行资源管理的软件 图示: 1.2OS存在的意义 1.2.1计算机的分层式管理结构 最底层的硬件部分遵循“冯诺依曼体系” ,每一种硬件都在驱动层中有着自己对应的“驱动程序” 在OS中,驱动管…...
Spring定时任务修仙指南:从@Scheduled到分布式调度的终极奥义
各位被Thread.sleep()和while(true)折磨的Spring道友们!今天要解锁的是Spring生态自带的定时任务三件套——Scheduled、TaskScheduler、Async定时组合技!无需引入外部依赖,轻松实现从简单定时到分布式调度的全场景覆盖!准备好抛弃…...
Node.js多版本共存管理工具NVM(最新版本)详细使用教程(附安装包教程)
目录 前言 一、Nvm下载 二、Nvm安装 三、配置nodeJS 前言 NVM(Node Version Manager)是一个用于管理多个Node.js版本的工具,主要帮助开发者在同一设备上轻松安装、切换和卸载不同版本的Node.js,解决项目间版本冲突问题。 一、…...
管道魔法木马利用Windows零日漏洞部署勒索软件
微软披露,一个现已修复的影响Windows通用日志文件系统(CLFS)的安全漏洞曾被作为零日漏洞用于针对少数目标的勒索软件攻击中。 01 攻击目标与漏洞详情 这家科技巨头表示:"受害者包括美国信息技术(IT)…...
Devops之Argo:Argo 是什么,和现在常用的Jenkins之间的区别
Argo CD(Argo Continuous Delivery 的缩写)是一款基于 GitOps 的声明式 Kubernetes 持续交付工具。它提供了一种以 Git 为中心的方法来管理和部署应用程序到 Kubernetes 集群。Argo CD 遵循 GitOps 的原则,即将应用程序的预期状态存储在 Git …...
从 60 FPS 掉帧到 7.6 倍提速Rust + WebAssembly 优化《生命游戏》的实战指南
一、构建 FPS 统计器:用 performance.now() 实时观察性能变化 要优化,就要先 测量。我们在 JavaScript 端添加一个 fps 对象,结合 performance.now() 来监控每一帧的耗时,并统计最近 100 帧的平均 FPS、最小 FPS、最大 FPS&#…...
jmeter 集成ZAP进行接口测试中的安全扫描 实现方案
以下是将 JMeter 集成 ZAP(OWASP Zed Attack Proxy)进行接口测试中安全扫描的实现方案: 1. 环境准备 JMeter 安装:从 JMeter 官方网站(https://jmeter.apache.org/download_jmeter.cgi)下载并安装 JMeter,确保其版本稳定。ZAP 安装:从 ZAP 官方网站(https://www.zapr…...
Hyperlane 文件分块上传服务端
Hyperlane 文件分块上传服务端:高效、可靠、易用的文件上传解决方案 引言 在现代 Web 开发中,文件上传是许多应用的核心功能之一。然而,随着文件大小的增加和网络环境的复杂性,传统的单次文件上传方式已经难以满足需求。Hyperla…...
BT面板docker搭建excalidraw遇到的问题
1.傻瓜式拉取镜像 2.点击创建容器 3.暴露端口 4.放行端口和服务器安全组,如果用的是轻量型服务器,那就关闭防火墙 下面放图...
Qt之OpenGL使用Qt封装好的着色器和编译器
代码 #include "sunopengl.h"sunOpengl::sunOpengl(QWidget *parent) {}unsigned int VBO,VAO; float vertices[]{0.5f,0.5f,0.0f,0.5f,-0.5f,0.0f,-0.5f,-0.5f,0.0f,-0.5f,0.5f,0.0f };unsigned int indices[]{0,1,3,1,2,3, }; unsigned int EBO; sunOpengl::~sunO…...
【仿Mudou库one thread per loop式并发服务器实现】项目介绍+前置技术知识点
HTTP协议模块实现 1. 项目实现的目标2. 项目储备知识2.1 HTTP服务器2.2 Reactor模型 3. 功能模块划分3.1 SERVER模块3.1.1 Buffer模块3.1.2 Socket模块3.1.3 Channel模块3.1.4 Poller模块3.1.5 EventLoop模块3.1.6 Connection模块3.1.7 7. Acceptor模块3.1.8 TimerQueue模块3.1…...
Open Interpreter:重新定义人机交互的开源革命
引言 在人工智能技术蓬勃发展的今天,人机交互的方式正经历着前所未有的变革。Open Interpreter,作为一个开源项目,正在重新定义我们与计算机的互动方式。它允许大型语言模型(LLMs)在本地运行代码,通过自然…...
Shell编程之条件语句
目录 一.条件测试操作 1.文件测试 2.整数值比较 3.字符串比较 4.逻辑测试 二:if条件语句 1.if语句的结构 (1)单分支if语句 (2)双分支if语句 (3)多分支if语句 2.if语句应用示例 &…...
Python编程快速上手 让繁琐工作自动化笔记
编程基础 字符串使用单引号...
高性能文件上传服务
高性能文件上传服务 —— 您业务升级的不二选择 在当今互联网数据量激增、文件体积日益庞大的背景下,高效、稳定的文件上传方案显得尤为重要。我们的文件分块上传服务端采用业界领先的 Rust HTTP 框架 Hyperlane 开发,凭借其轻量级、低延时和高并发的特…...
【从零开始学习JVM | 第二篇】HotSpot虚拟机对象探秘
对象的创建 1.类加载检查 虚拟机遇到一条new的指令,首先去检查这个指令的参数能否在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行类的加载过程。 2.分配内存 在类…...
浅谈前端开发中的 npm、cnpm、pnpm、yarn各自特点
在前端开发中的 npm、cnpm、pnpm、yarn 等工具都是包管理器(Package Manager),用于安装/更新/卸载 JavaScript 项目的依赖。 下面我详细地给你梳理下这些包管理器的作用、特点和适用场景👇 一. npm(Node Package Mana…...