对Linux的进程控制的研究
进程控制
进程控制 即让不同的程序完成不同的任务 ,然后终止进程 。也可以认为进程控制就是通过各种方式对进程的生命周期的各个阶段进行操作。
复习fork函数
在 Linux 中 fork 函数会从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
c
#include <unistd.h>
//调用的头文件
pid_t fork(void);
//返回值:自进程中返回0,父进程返回子进程id,出错返回 - 1
进程调用 fork,当控制转移到内核中的 fork 代码后,内核做的工作:
- 分配新的内存块和内核数据结构(即 PCB )给子进程。
- 将父进程部分数据结构内容拷贝至子进程。
- 添加子进程到系统进程列表当中。
fork返回,开始调度器调度。

当一个进程调用 fork 之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序。
c
#include <unistd.h>
//fork、getpid在unistd.h
#include <sys/types.h>
//pid_t在types.h
#include <stdlib.h>
//exit在stdlib.h
#include <stdio.h>
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
[Bjarne@VM-8-8-centos cTest]$ make
gcc a.c -o a.exe -std=c99
[Bjarne@VM-8-8-centos cTest]$ ./a.exe
Before: pid is 43676
After:pid is 43676, fork return 43677
After:pid is 43677, fork return 0
[Bjarne@VM-8-8-centos cTest]$
这里看到了三行输出,一行 before ,两行 after 。进程 43676 先打印 before 消息,然后它有打印 after 。另一个 after 消息有 43677 打印的。注意到进程 43677 没有打印 before ,为什么呢?如下图所示。

所以,fork 之前父进程独立执行,fork 之后,父子两个执行流分别执行。fork 之后,谁先执行完全由调度器决定。
fork 函数返回值:子进程返回 0 ,父进程返回的是子进程的 pid。
写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入 ,便以写时拷贝的方式各自分配一份副本。具体见下图:

当 fork 运行之后,操作系统在创建子进程的时候,它会将自己的页表项中几乎所有的页表项,尤其是数据的页表项,直接由读写改成只读。
之后有进程传达修改物理内存的需求 后,页表就会因为权限问题而出错 。此时就给了操作系统介入的时间和机会,这时会出现两种情况:
- 真的出错了。比如代码的设计就是 "图谋不轨" 想修改真正的只读数据段,比如字符常量区。
- 正常修改栈区和堆区的数据。这时会通过让操作系统触发异常的方式,让操作系统进行写时拷贝并修改相关的数据。拷贝和修改工作完成之后,再把页表映射条目改成读写,然后就可以正常访问了。
多进程代码示例
多进程即父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
例如这个代码表示最简单的多进程:
c
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
typedef void(*callback_t)();
void worker(){
int cnt=3;
while(cnt--){
printf("child process,pid:%d ppid:%d cnt:%d\n",getpid(),getppid(),cnt);
sleep(1);
}
}
void createSubProcess(int n,callback_t cb){
for(int i=0;i<n;i++){
sleep(1);
pid_t id=fork();
if(id==0){//只有子进程能进来
//领取唯一编号和指定任务
printf("create child process success: %d\n",i);
cb();
exit(0);//完成任务及时终止
}
}
}
int main(){
createSubProcess(10,worker);
//只有父进程走到这里
sleep(100);
return 0;
}
多个子进程的代码,整体思路就是通过循环和 fork 生成子进程,每个子进程领取到唯一一个进程编号,通过函数指针获取自己的任务,完成任务后通过 exit 终结进程。父进程再进行最后的收尾工作即可。
这里打开另一个渠道,通过死循环脚本进行监视:
bash
$ while :; do ps ajx | head -1 && ps ajx | grep a.exe | grep -v 'grep';echo "-----";sleep 1;done
交互界面:
bash
# 界面1
[Bjarne@VM-8-8-centos cTest]$ gcc a.c -o a.exe -std=c99
[Bjarne@VM-8-8-centos cTest]$ ./a.exe
create child process success: 0 # 进程编号
child process,pid:3606 ppid:3593 cnt:2
child process,pid:3606 ppid:3593 cnt:1
create child process success: 1
child process,pid:3615 ppid:3593 cnt:2 # 当前编号生成的进程
child process,pid:3606 ppid:3593 cnt:0 # 之前的子进程
child process,pid:3615 ppid:3593 cnt:1
create child process success: 2
child process,pid:3622 ppid:3593 cnt:2
child process,pid:3615 ppid:3593 cnt:0 # 每个子进程输出3次
child process,pid:3622 ppid:3593 cnt:1
# 省略
^C
[Bjarne@VM-8-8-centos cTest]$
# 界面2
[Bjarne@VM-8-8-centos cTest]$ while :; do ps ajx | head -1 && ps ajx | grep a.exe | grep -v 'grep';echo "-----";sleep 1;done
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
-----
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
25816 3593 3593 25816 pts/0 3593 S+ 1001 0:00 ./a.exe
-----
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
25816 3593 3593 25816 pts/0 3593 S+ 1001 0:00 ./a.exe
3593 3606 3593 25816 pts/0 3593 S+ 1001 0:00 ./a.exe
-----
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
25816 3593 3593 25816 pts/0 3593 S+ 1001 0:00 ./a.exe
3593 3606 3593 25816 pts/0 3593 S+ 1001 0:00 ./a.exe
3593 3615 3593 25816 pts/0 3593 S+ 1001 0:00 ./a.exe
-----
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
25816 3593 3593 25816 pts/0 3593 S+ 1001 0:00 ./a.exe
3593 3606 3593 25816 pts/0 3593 S+ 1001 0:00 ./a.exe
3593 3615 3593 25816 pts/0 3593 S+ 1001 0:00 ./a.exe
3593 3622 3593 25816 pts/0 3593 S+ 1001 0:00 ./a.exe
-----
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
25816 3593 3593 25816 pts/0 3593 S+ 1001 0:00 ./a.exe
3593 3606 3593 25816 pts/0 3593 Z+ 1001 0:00 [a.exe] <defunct>
3593 3615 3593 25816 pts/0 3593 S+ 1001 0:00 ./a.exe
3593 3622 3593 25816 pts/0 3593 S+ 1001 0:00 ./a.exe
# 省略进程3634的信息,因为父进程没有结束,子进程进入僵尸状态
-----
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
25816 3593 3593 25816 pts/0 3593 S+ 1001 0:00 ./a.exe
3593 3606 3593 25816 pts/0 3593 Z+ 1001 0:00 [a.exe] <defunct>
3593 3615 3593 25816 pts/0 3593 Z+ 1001 0:00 [a.exe] <defunct>
3593 3622 3593 25816 pts/0 3593 S+ 1001 0:00 ./a.exe
# 省略
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
-----
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
-----
^C
[Bjarne@VM-8-8-centos cTest]$
所有子进程,包括谁先被调度,谁后被调度,无法确定,这个取决于操作系统的调度器。
进程调用失败的原因
系统中有太多的进程,或实际用户的进程数超过了限制,fork 会调用失败,此时 fork 会返回小于 0 的数。
fork 函数返回小于 0 的数,要么是系统里进程太多,操作系统没资源创建进程;要么是给用户设置了可创建进程的数量上的限制,超过了这个限制,操作系统就拒绝创建进程。
进程终止
进程终止即将各种地址、空间、页表都释放,最多保留一个 PCB。
进程退出场景有3种:
- 进程运行完毕 ,结果正确;
- 进程运行完毕 ,结果不正确。
- 进程异常终止 ,或进程调用失败。
- 把这个事情做完了,但是做的结果不太好。
- 第二种把这个事情做完了,那么结果非常好。
- 这个事没做完,中间出问题导致事情无法进行下去。
生活中很多事物可以代入,例如考试。进程也不例外。
最常见的进程一般是 C/C++ 程序生成,所以这里研究和 C/C++ 相关联的进程信息,环境是 Linux。
进程退出测试
进程常见退出方法:
正常终止:
- ( C/C++)从
main返回。 - 调用
exit终止当前进程。 _exit。
异常退出:
- 例如 ctrl + c,以及各种信号终止。
可以通过 echo $? 查看进程退出码。$? 这个本地变量会记录最近一次在命令行上执行的进程,或者指令的退出码。
Linux 只要是通过命令行启动的进程,该进程的父进程就是 bash,因此 C/C++ 程序会将
main函数的返回值返回给bash,因此就会被本地变量$?得到。
例如这个程序:
c
#include<stdio.h>
int main(){
FILE* fp=fopen("t.txt","r");
if(fp==NULL){
return 2;//这里的2被$?接收
}
return 0;
}
Linux 交互界面:
bash
[Bjarne@VM-8-8-centos cTest]$ make
gcc a.c -o a.exe -std=c99
[Bjarne@VM-8-8-centos cTest]$ ./a.exe
[Bjarne@VM-8-8-centos cTest]$ echo $?
2 # 第一次没有文件,于是给不正常的返回值
[Bjarne@VM-8-8-centos cTest]$ touch t.txt
[Bjarne@VM-8-8-centos cTest]$ ./a.exe
[Bjarne@VM-8-8-centos cTest]$ echo $?
0 # 第二次创造了文件
[Bjarne@VM-8-8-centos cTest]$
也可以通过 $? 查看非内建命令的错误码。例如这个交互界面展示的部分指令:
bash
[Bjarne@VM-8-8-centos ~]$ ls noexitfile
ls: cannot access noexitfile: No such file or directory
[Bjarne@VM-8-8-centos ~]$ echo $?
2 # ls展示不存在的文件
[Bjarne@VM-8-8-centos ~]$ cd work
[Bjarne@VM-8-8-centos work]$ echo $?
0 # 正常进入目录work
[Bjarne@VM-8-8-centos work]$ cd noexitdir
-bash: cd: noexitdir: No such file or directory
[Bjarne@VM-8-8-centos work]$ echo $?
1 # 进入不存在的目录
[Bjarne@VM-8-8-centos work]$
main函数返回值和退出码
在 main 函数中使用 return 是 C/C++ 进程最常用的退出进程的方法,return 同样可作为其他函数结束的标志。
main 函数也是函数,它也要被调用,所以注定了谁调用 main 函数,main 函数的这个返回值就返回给谁。
例如 Windows 下的 Visual Studio 2019,调试时打开监视窗口中的调用堆栈,可看到
main函数被哪些函数调用:这是一个Windows C++ 程序启动调用堆栈,展示了从操作系统加载到
main函数执行的完整过程。每个部分的信息和作用:
调用顺序 模块/函数 语言 作用描述 在进程生命周期中的角色 9 vs2019test.exe|main() 行10 C++ 用户程序入口点 执行程序员编写的业务逻辑代码 8 vs2019test.exe|invoke_main() 行78 C++ 调用用户main函数 准备argc, argv参数并调用用户main函数 7 vs2019test.exe|__scrt_common_main_seh() 行288 C++ 带结构化异常处理的CRT主函数 设置SEH异常处理机制 6 vs2019test.exe|__scrt_common_main() 行331 C++ CRT通用主函数 安全检查初始化和主流程控制 5 vs2019test.exe|mainCRTStartup(void*) 行17 C++ C运行时库主入口函数 初始化全局变量、静态变量和C运行时环境 4 kernel32.dll|764b5d490 未知 进程命令行和环境变量处理 准备进程执行参数 3 kernel32.dll|框架可能不正确和/或缺失 未知 Windows API子系统初始化 设置Win32环境和线程本地存储 2 ntdll.dll|7766d6db0 未知 NT层执行上下文设置 处理PE文件加载和基础执行环境 1 ntdll.dll|7766d6610 未知 Windows内核级别进程初始化 最底层的进程创建和环境设置
而当 main 函数调用 return 语句后,进程也就终止,return 后的返回值就是进程的退出码。这个退出码是一个数字,数字本身的含义需要人为去定义其含义。
这个返回值会被父进程接收。所以可以通过 main 函数的返回值来得知进程是否正常运行。
假设这个返回值不是 0 ,而是别的数字,则父进程会认为进程运行失败。然后作为程序员需要关心的是进程运行失败的原因。
例如
/usr/include/asm-generic/errno-base.h中描述了 1 到 34 号退出码。
c//省略条件编译 #define EPERM 1 /* Operation not permitted */ #define ENOENT 2 /* No such file or directory */ #define ESRCH 3 /* No such process */ //碍于篇幅,省略 #define ERANGE 34 /* Math result not representable */
/usr/include/asm-generic/errno-base.h也描述了部分退出码的信息。
/usr/include/sysexits.h也有部分退出码。具体可在 Linux 下打开这些文件进行查看。
main 函数最后的返回值通常是 0 ,表示进程正常运行完毕 ,非零就表示失败。当返回值为0,此时很少关心进程出了什么问题,一般都是关心代码的算法逻辑出错。
经典考试考了满分,家长不会过问;考不及格,说明有去考试,没有作弊且考完了,然后考的不理想,相当于没有完成任务,家长最喜欢问为什么没有考好。
进程(软件或软件的代码)也是一样,能跑就别动它,否则就是它有问题,需要修改。
自定义退出码
最简单的方法是,main 函数 return 不同的值来自定义进程的退出码,然后自己再单独陈列退出码表示的字符串。一般情况下, 0 表示程序运行成功,非 0 表示程序运行失败,这个规则一般需要遵守,除非程序员私底下有约定。
例如自定义退出码的表达(一般由父进程调用):
c
const char *err_string[]={
"Success",
"error1",
"error2"
//,...
};
这个退出码被操作系统限制在 [0,225],即实际上返回的是 return x%256;。即无论用户给的退出码 x 有多不合理,只要按照整型的方式解读,都会被识别成无符号整型,然后对 256 取模。
例如这个程序:
c
int main(){
return 114514;//这个程序什么都没有,但可以运行
}
Linux的交互界面:
bash
[Bjarne@VM-8-8-centos cTest]$ make
gcc a.c -o a.exe -std=c99
[Bjarne@VM-8-8-centos cTest]$ ./a.exe
[Bjarne@VM-8-8-centos cTest]$ echo $?
82
[Bjarne@VM-8-8-centos cTest]$ cat a.c
#include<stdio.h>
int main() {
return 114514;
}
[Bjarne@VM-8-8-centos cTest]$
strerror和错误码
strerror 函数用于将系统的错误码转换为 可读的错误描述字符串 。具体参考 man strerror 。错误码 也是一个数字,用于表示一个库函数 或一个系统调用的调用情况。
这个错误码通常是 errno 的值,当 C 语言的库函数运行失败时,可通过 errno 的值间接表示程序出错的原因。
例如这里随手捏造一个文件不存在却要以读的方式打开的错误,然后通过 strerror 和 errno 查看错误:
c
#include<stdio.h>
#include<string.h>
#include<errno.h>
int main() {
FILE* fp = fopen("noexit", "r");
printf("%d, %s", errno, strerror(errno));
return 0;
}
输出(无论在Linux,还是Windows,都是一样的输出结果):
2, No such file or directory
这个程序将展示所有的出错码:
c
#include<stdio.h>
#include<string.h>
int main(){
for(int i=0;i<136;i++){
//第134个之后的退出码都不认识
printf("%d:%s\n",i,strerror(i));
}
return 0;
}
Linux的交互界面:
bash
[Bjarne@VM-8-8-centos cTest]$ make
gcc a.c -o a.exe -std=c99
[Bjarne@VM-8-8-centos cTest]$ ./a.exe
0:Success
1:Operation not permitted
2:No such file or directory # 要打开的文件不存在
3:No such process # 进程不存在
4:Interrupted system call
5:Input/output error # I/O错误
# 省略
13:Permission denied # 权限不足
# 省略
58:Unknown error 58
# 省略
134:Unknown error 134
135:Unknown error 135
[Bjarne@VM-8-8-centos cTest]$
出错码和退出码的联系
一般 C 语言提供的接口 (指库函数 ),在调用时如果出错了,会通过修改 errno 全局变量的值来表达出错原因。
不只是 C 语言的库函数,大部分系统调用 例如 fork 也会设置 errno 变量,详细见man fork。
因为操作系统内核的实现绝大部分都是使用 C 语言,这部分系统调用也遵守部分 C 语言的规则。具体是哪些也不方便记录,因为数量太多,只能在使用时留意。
所以 errno 会记录整个进程在运行过程中,调用库函数或者系统调用出错的情况。但出错了,父进程也不一定进行拦截。
例如这个代码,在上文已经证实 errno 会被改成 2 。
c
#include<errno.h>
#include<stdio.h>
int main(){
//开始很可能是0,只要别提前运行过指令或其他非正常进程
printf("before: %d\n",errno);
FILE* fp=fopen("dsdfa","r");
//根据错误码获取错误信息,一般是2
printf("after: %d, errno string:%s\n",errno,strerror(errno));
return 0;
}
但 errno 只会记录最近的一次修改记录,因此只会记录最近发生的错误信息。若出现多个错误,则需要调试或设置监视。
总结就是:
- 错误码通常是提供一个库函数或一个系统调用的调用情况。
- 退出码通常是一个进程退出的时候,它的退出结果。
错误码和退出码虽然是两个不同的概念,但它们都可以用来表示进程出错的详细原因。
复习C语言的概念:函数通常叫做子程序。
所以 C 语言的
main函数主要用于表明进程的运行是否成功,失败的话原因是什么。而普通函数则带有特殊含义,例如普通函数本身就是某个算法或数学公式的模板,返回值就是运算的结果。
进程异常
进程异常可以理解为程序没运行完 ,然后被终止了。对应进程终止的第3种情况。
一旦进程出异常 ,退出码的结果就没有意义。
例如张三高考作弊,被追究刑事责任,事后有人问张三的高考成绩数字,这个时候就没有任何意义。
但无论进程以哪种方式结束,进程运行结束后 ,首先要判断 的是是否真的完成了任务(代码跑没跑完),而不是因为异常导致非正常终止。
例如这里人为制造一个错误:
c
int main(){
int* p=0;
*p=100;//被页表拦截的段错误
return 0;
}
交互界面:
bash
[Bjarne@VM-8-8-centos cTest]$ make
gcc a.c -o a.exe -std=c99
[Bjarne@VM-8-8-centos cTest]$ ./a.exe
Segmentation fault # 段错误
[Bjarne@VM-8-8-centos cTest]$
段错误 即野指针访问非法内存造成的错误。
在语言学习阶段,C/C++ 代码被异常终止叫程序崩溃 。但在操作系统的视角 ,C/C++ 代码最后都变成了进程,所以更准确的说法是进程异常。
绝大部分出现异常的进程,都是被操作系统通过信号的方式终止的。部分情况例如用户通过创建子进程来完成某个任务,若触发了父进程的某种条件,父进程通过系统调用和 PID 终止进程。
查看信号信息,可在 Linux 交互界面输入 kill -l。之前的段错误,还有除0错误(3/0;)等,都可以在信号中找到,例如除 0 错误对应的信号是 8 号(SIGFPE,即 Floating point exception 首字母),段错误对应的信号是 11 号信号。详细见进程信号。
这里做一个测试:通过给一个正常进程发送信号造成异常终止。
就是给进程扣帽子,让进程异常终止且操作系统反馈的错误和信号相符,但和进程的运行状态不符。
C
#include<stdio.h>
#include<unistd.h>
int main() {
while(1){
printf("%d\n",getpid());
sleep(1);
}
return 0;
}
交互界面:
bash
# 界面1,负责运行前台进程
[Bjarne@VM-8-8-centos cTest]$ make
gcc a.c -o a.exe -std=c99
[Bjarne@VM-8-8-centos cTest]$ ./a.exe
25787
# 省略
25787
Segmentation fault # 死循环被判断成段错误
[Bjarne@VM-8-8-centos cTest]$ echo $?
139 # 进程被信号终止,退出码为128加信号编号
[Bjarne@VM-8-8-centos cTest]$
# 界面2,发送11号信号
[Bjarne@VM-8-8-centos cTest]$ kill -11 25787
[Bjarne@VM-8-8-centos cTest]$
_exit和exit
这两个函数都可以用于终止进程。
exit是 C 语言的库函数,使用时需要展开头文件stdlib.h。_exit是系统调用,使用时需要展开头文件unistd.h。
查询 exit 时要打开3号手册(man 3 exit),而查询 _exit 需要打开2号手册(man 2 _exit)。
exit函数测试
exit 等价于在 main 函数中使用 return 语句。任意地点调用 exit ,后续代码将不会再被执行。
首先是在 main 函数中使用 exit:
c
#include<stdlib.h>
int main(){
exit(66);
return 0;//exit先被执行,因此return语句不会执行
}
交互界面:
bash
[Bjarne@VM-8-8-centos cTest]$ make
gcc a.c -o a.exe -std=c99
[Bjarne@VM-8-8-centos cTest]$ ./a.exe
[Bjarne@VM-8-8-centos cTest]$ echo $?
66
[Bjarne@VM-8-8-centos cTest]$
所以在 main 函数中,使用 exit 和使用 return 没有区别。
但若是普通函数中:
c
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int f1(){
return 11;//11不会终止进程
}
int f2(){
exit(22);//进程将在这里终止,所以检测到的返回值是22
printf("test");
return 22;
}
int f3(){
return 33;
}
int main(){
printf("pid:%d\n",getpid());
f1();
f2();
f3();
return 0;
}
交互界面:
bash
[Bjarne@VM-8-8-centos cTest]$ make
gcc a.c -o a.exe -std=c99
[Bjarne@VM-8-8-centos cTest]$ ./a.exe
pid:7951
[Bjarne@VM-8-8-centos cTest]$ echo $?
22
[Bjarne@VM-8-8-centos cTest]$
_exit函数测试
_exit 原型:
c
#include <unistd.h>
void _exit(int status);
//参数:status 定义了进程的终止状态,父进程通过wait来获取该值
虽然 status 是 int ,但是仅有低 8 位可以被父进程所用。其他不合理的数字,例如 -1,二进制补码是 0xFFFFFFFF,但只有最后的 0xFF 会被读取,也就是 255 。这和退出码的设定保持一致。
但_exit 和 exit 的真正区别:
_exit不会处理缓冲区,缓冲区内可能存在的数据会被当成垃圾处理。exit会处理缓冲区。
例如这个测试样例:
c
#include<stdio.h>
#include<unistd.h>
int main(){
printf("This is first printf, you can look it.\n");
printf("This is second printf, You can look it.");
sleep(3);
_exit(1);
return 21;
}
交互界面:
bash
[Bjarne@VM-8-8-centos cTest]$ make
gcc a.c -o a.exe -std=c99
[Bjarne@VM-8-8-centos cTest]$ ./a.exe
This is first printf, you can look it.
[Bjarne@VM-8-8-centos cTest]$ # 这里sleep生效,但结束后依旧没有输出第2句
对比 exit :
c
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(){
printf("This is first printf, you can look it.\n");
printf("This is second printf, You can look it.");
sleep(3);
exit(1);
return 21;
}
交互界面:
bash
[Bjarne@VM-8-8-centos cTest]$ make
gcc a.c -o a.exe -std=c99
[Bjarne@VM-8-8-centos cTest]$ ./a.exe
This is first printf, you can look it.
This is second printf, You can look it.[Bjarne@VM-8-8-centos cTest]$
# 这里sleep运行完后,输出第2句。
exit 和 _exit 在刷新缓冲区的测试中,还得出了了一个结论:缓冲区不在操作系统内核中,否则系统调用不可能不对缓冲区进行刷新处理。这个结论是否正确,有机会再进行更详细的研究。
但实际上 exit 最后也会调用 _exit , 但在调用 _exit 之前,还做了其他工作:
- 执行 用户通过
atexit或on_exit定义的清理函数。 - 关闭所有打开的流 ,所有的缓存数据均被写入(刷新缓冲区)。
- 调用
_exit。
实例:

