一、什么是退出码
为什么 main 函数中,总是 return 0,如果我们 return 其它值可以吗 ❓
#include <stdio.h>
int main()
{
return 0;
} 答案是肯定可以!对于 main 函数的返回值,我们称之为进程退出码,它代表进程退出后,结果是否正确!
通常进程退出码为 0 代表成功,非0 则代表其它含义,如果你愿意你也可以 return 其它值。大部分情况下,main 函数跑完后,默认结果是正确的,所以我们以前返回的都是 0。
所以这里给的建议就是如果未来我们的程序中比较关心进程的退出码的时候,那么要返回特定的数据表明特定的错误信息;而如果不必关心进程退出码的话,我们直接 return 0 即可!
🔴 值得注意的是,这个退出码只记录最近一个进程在命令行中执行完毕时对应的退出码!
二、如何查看退出码
main 函数 return 的值是给谁看的呢?那肯定不用说,因为是系统调用的 main 函数,那肯定是返回给系统(系统如何调用的我们后面会讲),以此来判断进程执行后的结果!
这里又要引入一个新的环境变量: ?`**
假设我们写一个程序最后 return 250,看看它的结果怎么样:
结果就是 250 啦,但是有没有发现很奇怪,就是第二次和第三次调用 echo $? 的时候这个退出码已经变成 0 了,这是为啥 ❓❓❓
其实很简单,因为 echo 也是一个指令,既然是指令那么肯定就是会创建一个子进程,这个子进程也会有退出码返回的,所以我们每次打印的其实是最近一次的退出码!(当然 echo 这种指令属于内建指令,比较特殊,后面会讲)
三、如何查看错误信息
还记得我们在 C 语言中学到的一个函数 strerror() 吗,它就是用来打印错误信息的,它大概有 134 种标识!
它的函数声明如下:
#include <string.h>
char* strerror(int errnum); 下面我们写个程序调用 strerror() 来查看一下全部的错误信息:
#include <stdio.h>
#include <string.h>
int main()
{
for(int i = 0; i < 134; ++i)
{
printf("num[%d]:%s\n", i, strerror(i));
}
return 0;
}
运行结果:
num[0]:Success
num[1]:Operation not permitted
num[2]:No such file or directory
num[3]:No such process
num[4]:Interrupted system call
......
num[129]:Key was rejected by service
num[130]:Owner died
num[131]:State not recoverable
num[132]:Operation not possible due to RF-kill
num[133]:Memory page has hardware error 可以看到不同的错误码对应的出错误信息!
接下来我们在命令行故意写错指令,看看报的是什么错误:
Ⅱ. 进程退出的情况
我们的进程只可能出现以下三种情况,不可能再出现其它的可能:
- 代码运行完毕,结果正确,退出码为
0 - 代码运行完毕,程序没有崩溃,但因为逻辑问题,结果不正确,退出码为
非0 - 代码没有运行完毕,程序非正常结束(比如野指针、越界等操作,包括人为
ctrl+c或除0错误终止)此时退出码没有意义。
一般来说退出码在第二种情况下才起作用!
Ⅲ. 进程常见的退出方法
一、正常退出
- 在主函数中
return(注意非主函数中return只是返回值而不是退出程序!) - 调用用户级别函数
exit() - 调用系统级别函数
_exit()
对于后两个方法,我们在下面会详细的讲!
二、异常退出
比如 ctrl + c,进程通过接收到信号而终止!
三、exit() 与 _exit()
① exit()
我们在 C/C++ 中也经常看到这个函数,一般用于我们进程的不正确结果的退出,在非主函数中也是直接退出进程的!
下面来看看它的函数声明:
#include <stdlib.h>
void exit(int status);
// 作用:任何函数 exit,都表示直接终止进程。
// 参数:status定义了进程的终止状态,父进程通过wait来获取该值
// 说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255。 至于上面的参数 status,我们目前只需要知道它是退出码即可,它主要是和我们后面学的进程等待部分有关系,后面会讲!
当 exit(-1) 时,结果如下:
[liren@VM-8-2-centos process]$ make
gcc -o mypro proc.c -std=c99
[liren@VM-8-2-centos process]$ ./mypro
[liren@VM-8-2-centos process]$ echo $?
255② exit() 与 _exit() 的区别
_exit() 是系统提供的接口,它的原型同库里的 exit 函数,函数的声明如下:
#include <unistd.h>
void _exit(int status); 其实本质上因为 _exit() 是系统级别的接口,所以 exit() 其实就是去调用的 _exit(),只不过要多做了一些工作,所以会有一些细节上面的差别,比如说下面这些工作:
- 执行用户通过
atexit()或on_exit()定义的清理函数 - 关闭所有打开的流,所有的缓存数据均被写入
- 调用
_exit()
总结下来它们两者的差别如下:
- exit() 在退出时与默认的
return一样,会进行后续资源处理,包括刷新缓冲区 - _exit() 在退出时,不会进行后续资源处理,直接终止进程
写个代码展示一下它们的区别:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("running ...");
sleep(3);
// 比较两者的区别
exit(1);
// _exit(1);
printf("done ...");
return 0;
}
四、如果 main 函数里都不写 return 和 exit,退出码是啥 ❓
理论上这里的退出码是未定义的、随机的,但实际上得到的退出码是 0,因为你的 main 函数里总会调用其它函数,成功后,遗留的历史数据是会充当返回值去返回的。
#include <stdio.h>
int main()
{
printf("hello world");
}
运行结果:
[liren@VM-8-2-centos process]$ ./mypro
hello world[liren@VM-8-2-centos process]$ echo $?
0 main 函数里啥也不做,可以看到退出码依旧是 0,不必太纠结,这个本就是标准未定义的。
站在操作系统角度,如何理解进程终止 ❓
之前我们说过,进程创建,操作系统要做的事:把程序加载到内存、创建对应的 pcb、地址空间、页表、构建地址空间到物理内存的映射关系、把进程放在运行队列调度。那么进程终止肯定是曾经进程创建的相反工作,核心是归还资源。
- “ 释放 ” 曾经为了管理进程所维护的所有的数据结构对象
这里的释放,在操作系统里,并不是真的把数据结构对象销毁,而是设置为不用状态,然后保存起来,如果这样不用的对象多了,就有了一个 “数据结构池”。
我们在 C/C++ 中都使用过库函数 malloc、操作符 new 来申请过内存,内存是硬件,malloc、new 一定是向操作系统申请,而这个过程相对比较耗时。比如你是某某市的首富的儿子,要去银行贷款 100 万,银行会让你填张表、排队、审核你的条件,最后说 5 天之后放款。你的钱用完了,还需要去银行再贷款 200 万,又重复贷款流程。你的钱又用完了,又还需要去银行再贷款 300 万,又重复贷款流程。那么对银行来讲,只要你不嫌麻烦,银行当然没问题,可是实际对你来讲,每次贷款都需要等很长的时间,效率太低,所以干脆你可以直接贷款 1000 万,这 1000 万我们称之为资金池,后面你想用 100 万、200 万,就不用去找银行了,你直接从你的资金池里拿,后面你盈利了,就把钱还给银行。 这就意味着你一次申请一大块内存,可以节省你频繁的从用户空间向内核空间要资源的过程,我们把申请的一大块空间叫做 内存池,所以池本质是为了提高用户的效率。
我们创建进程,就需要生成 task_struct、mm_struct 等各种数据结构,那么就需要往已经申请好的内存池空间来存储,此时需要对该空间进行强制类型转换为 task_struct*,每个进程创建生成的数据结构,都要进行强制类型转换,太麻烦了,内存池也没规定必须得是这样的结构。所以这里使用了一个链表结构,里面存储的是没有人使用的 task_struct 等数据结构,如果要释放进程所维护的数据结构,那么就把数据结构对象链入链表中,如果进程创建,需要对应的数据结构,就直接从数据结构池里拿,这样就不用申请空间和强制类型转换了。我们可以维护各种各样的数据结构,整体我们称之为 数据结构池。
所以我们就可以把不要的数据结构,包括里面的数据一起保留在废弃队列里,创建进程时,先从废弃队列里找,如果有合适的节点,就直接拿去用,如果没有,就再重新开辟。好比有 100 个实习生要在这几天陆续入职公司,公司会为每个实习生配电脑,如果每来一个实习生,公司就去京东上买台电脑,这样做一方面下单、邮寄,效率太低了;另一方面,每一个人都用新电脑,对于公司来说成本太高了。所以当一个人离职时,并不是把这个人所有信息都销毁、其所使用的电脑卖掉,而是把所有不用的电脑放在一起,当有新的员工入职时,直接从电脑池里拿。
所以在 Linux 中,这种释放规则叫做 Slab 分配器,它的核心工作是完成在 Linux 内核中数据结构级别的内存分配。
- “ 释放 ” 程序代码和数据占用的内存空间
所以有了上面的理解,我们就知道这里的 “ 释放 ” 不是把代码和数据清空,而是把内存设置为无效。比如你从 U 盘里拷贝一个 3G 的电影到你的电脑,你会发现速度特别慢,拷贝 30 秒钟,后来你看完了,你花了 1 秒钟删掉它,这里就有个问题。
如果删除的过程和写入的过程是一个相似的、相反的逻辑,写的过程是在磁盘上把数据以二进制写好,删的过程是相反,那么它们所花的时间应该是相同的 ❓
实际我们在进行删除时,就是对所对应的空间标识为无效,这就意味着它是可以被覆盖的,写入新数据的同时就是在覆盖老数据。所以这里想说的是计算机里的释放并不是真的释放,要么就是利用 Slab 分配器以数据结构的方式缓存起来,要么就是把空间设置为无效,你都可以进行二次覆盖。也就是说以前我们经常看到的把文件删除后,文件就跑到回收站里了,此时并不是真正的删除,而是设置为无用状态,本质是临时删除放进回收站的文件只是在注册表中状态被改为无用状态,而再对回收站中的文件进行删除时,就意味着文件在注册表中被除名,但是文件的数据仍在,所以,即使我们把回收站的文件清空了,照样可以通过注册表来恢复文件。
内存空间怎么做到无效 ❓
内存也要进行管理,其也有对应的数据结构,如果没有人指向这个内存,此时这个内存就是无效的,后面我们学习文件以及多进程时会证明内存无效。
- 取消曾经该进程的链接关系
比如我是子进程,我有 1 个父进程,3 个兄弟进程,除了所有进程本身是用双链表链接的,这里与父和子也有链接关系,所以我要离开了,就要把之前的关系统统去掉。