
之前在 Linux进程状态 这篇文章中,我们已经为大家介绍过Linux系统中一个非常重要的系统调用 —
fork,今天我们在来重谈fork函数,让大家对这个系统调用有更深刻的理解。
在 Linux中 fork 函数是非常重要的函数,它从已存在进程中创建⼀个新进程。创建出来的新进程叫做子进程,而原进程则称为父进程。
在Linux参考手册中,fork函数的原型如下:(man 2 fork 指令查看)
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之后,就有两个⼆进制代码相同的进程。并且它们都运行到相同的地方。但每个进程都将可以开始属于它们自己的旅程,看如下程序:
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() 返回
pid_t类型(通常为 int 通过 typedef 定义),用于表示进程ID(PID)。
fork创建成功:
为什么给父进程返回子进程的pid,这个问题我们之前已经讨论过:
一个父进程可以创建一个或者多个子进程,父进程需要通过返回值获得新创建的子进程的唯一标识符(正整数),从而可以管理创建的多个子进程(如发送信号、等待终止等)
为什么子进程返回0
子进程返回0,标识自己为子进程,子进程通过返回值 0 确认自己的身份。子进程无需知晓父进程的PID(实际上可以通过
getppid()获取)
fork创建失败:
返回 -1并设置错误码:
错误码:
if (pid == -1) {
perror("fork failed"); // 输出类似 "fork failed: Resource temporarily unavailable"
}常见错误码:
EAGAIN:进程数超过限制(RLIMIT_NPROC)或内存不足。ENOMEM:内核无法分配必要数据结构所需内存。Copy-On-Write写时拷贝(COW)是 Linux 中 fork() 系统调用的核心优化机制,它使得进程创建变得高效且资源友好,通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自⼀份副本。
为什么需要写时拷贝?
在传统的进程创建方式中,fork() 会直接复制父进程的所有内存空间给子进程。这种方式存在明显问题:
COW 的解决思路:
具体见下图:

因为有写时拷贝技术的存在,所以父子进程得以彻底分离!完成了进程独立性的技术保证! 写时拷贝,是⼀种延时申请技术,可以提高整机内存的使用率。
写时拷贝的工作流程
1、 fork() 调用时
2、进程尝试写入内存
Page Fault)。内核介入处理:操作系统会由用户态陷入内核态处理异常
3、后续操作
之前我们在讲进程概念的时候讲过,如果父进程创建出子进程后,如果子进程已经退出,父进程却没有对子进程回收,那么就子进程就会变成 “僵尸进程” ,造成内存泄露等问题。
在Linux系统中,进程等待是父进程通过系统调用等待子进程终止并获取其退出状态的过程,主要目的是避免僵尸进程并回收子进程资源。
进程等待的必要性
僵尸进程问题:
Zombie),占用系统资源。wait#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);具体功能:
参数:
status:输出型参数,用来存储子进程退出状态的指针(可为 NULL,表示不关心状态)。返回值:
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:报告已停止的子进程(如被信号暂停)。返回值:
WNOHANG 且无子进程终止:返回0。做个总结:

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 = 1
for (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 $? 查看进程退出码)
异常退出:
ctrl + c,信号终止进程退出码(退出状态)可以告诉我们最后⼀次执行的命令的状态。在命令结束以后,我们可以知道命令是成功完成的还是以错误结束的。通常是你程序中mian函数的返回值,其基本思想是,程序返回退出代码 0 时表示执行成功,没有问题。 0 以外的任何代码都被视为不成功。
退出码是一个 8 位无符号整数(8-bit unsigned integer),因此取值范围为 2^8=256 个值。
Linux Shell 中的常见退出码:

sudo 权限的情况下使用 yum;SIGINT 或 ^C )和 143 ( SIGTERM )等终止信号是非常典型的,它们属于 128+n 信号,其中 n 代表信号编号。这里需要补充一点:
进程退出码和错误码是两个完全不同的概念,不要混为一谈!
在 Linux 系统中,错误码(
Error Codes)是操作系统用于标识程序运行中遇到的各类问题的核心机制。这些错误码通过全局变量errno(定义在<errno.h>头文件中)传递,帮助开发者快速定位和调试问题。
要理解错误码,首先要认识全局变量 error
例如:fork函数调用失败后,会立刻返回-1,并设置全局变量 error

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函数
在 Linux 系统中,
_exit()是一个直接终止进程的系统调用,它会立即终止当前进程,并通知操作系统回收资源,但不执行任何用户空间的清理操作。
#include <unistd.h>
void _exit(int status);status:进程的退出状态码,范围是 0~255。父进程可以通过 wait() 或 waitpid() 获取该状态码。当前进程调用 _exit() 后,操作系统会立即介入,会从用户态陷入内核态,执行以下操作:
stdio)的缓冲区。SIGCHLD 信号: 通知父进程子进程已终止,并传递退出状态码 status。ZOMBIE(僵尸进程),直到父进程通过 wait() 回收其资源。本质上,_exit() 最终会调用 Linux 内核的 exit_group 系统调用(sys_exit_group),终止整个进程及其所有线程。其内核处理流程如下:
释放进程资源:
更新进程状态:
TASK_DEADSIGCHLD 信号。调度器介入:
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)注册的函数。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 n等同于执行exit(n),因为调用main的运行时函数会将main函数的返回值当做 exit 的参数。
状态码传递:
main函数中的 return 语句返回一个整数值(通常称为退出状态码),表示程序的执行结果:
return与exit()的关系
隐式调用exit():
int main()
{
return 42; // 等价于 exit(42);
}return的执行流程
当在main函数中执行return时,程序会做以下几件事:
清理操作:
调用exit():运行时调用exit(),执行以下操作:
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 函数中使用,确保资源自动释放。