Linux进程终止
为什么 main 函数中,总是 return 0,return 其它值可以吗 ❓
对于 main 函数的返回值,我们称之为进程退出码
,它代表进程退出后,结果是否正确,通常进程退出码为 0 代表成功,!0 代表其它含义,如果你愿意你也可以 return 其它值。大部分情况下,main 函数跑完后,默认结果是正确的,所以以前返回的都是 0。
main 函数 return 的值给谁看 ???
其实 main 函数 return 的值是给系统看的,以此来判断进程执行后的结果。
程序员怎么看 main 函数 return 的值吗 ???
echo $?
用来保存最近一次程序运行结束时退出码的值是多少。
💦 进程退出的场景
此文重点学习前两种场景,第三种会学习一部分,后面信号再补充:
- 代码运行完毕,结果正确,退出码为 0
- 代码运行完毕,程序没有崩溃,但因为逻辑问题,结果不正确,退出码为 !0。
- 代码没有运行完毕,程序非正常结束,包括人为终止,此时退出码没有意义。
在之前我们经常会遇到第二种场景,但是它返回的也是 0 ❓
说明之前写的代码并不好,更加规范的写法是如果结果符合预期就返回 0,否则返回 !0。
退出码 ❓
退出码可以人为的定义,比如 0 表示成功,1 表示链表翻转时头节点传野指针了等,也可以使用系统的错误码列表。当程序运行失败时,毫无疑问我们最关心的是为什么会失败。比如你的妈妈很严厉,而你今天考试得了零分,这是一次很失败的考试经历,你妈妈知道后,一定会问你为什么失败,此时你就得告诉妈妈失败的原因。人最擅长的是处理字符串,所以你说是因为迟到了。而计算机擅长处理整型类型的数据,所以才有了 0, 1, 2, 3 等这样的退出码。所以计算机需要将 int 类型的错误码转换为 string 类型的错误码,以供我们认识。
演示 int 到 string 错误码之间的映射 ???
你可以使用系统错误码,但是这种方式是受限的。strerror 可以实现 int 到 string 错误码之间的映射。
所有的父进程都关心子进程退出结果 ❓
大部分情况下通常退出码是父进程关心的,因为父进程费了很大的劲把子进程创建出来干活,活干的怎么样,父进程得知道。但并不是所有的父进程都关心子进程退出结果,比如说公司老板想开除我,然后 hr 找我谈,说你的合同到期了,可以走了,再干下去你也没工资,此时你肯定会走,hr 也不需要关心。换言之,我们后面可能会碰到父进程不需要关心子进程的退出结果的场景。
进程非正常结束 ❓
野指针、/0、越界等都可能导致进程非正常结束,父进程也要关心这种情况,但此时退出码是无意义的。好比,今天考试,因为肚子痛考了 0 分,那么这个理由是可以被妈妈信服的。但因为考试作弊被抓,考了 0 分,这个其实不算理由,因为你都不是正常考完的,后面你再解释的所有理由就毫无意义。
一般这里异常终止时,是由信号终止的,因为涉及信号,不是本文的重点,所以后面再详谈。
💦 进程常见的退出方法
1、正常退出
-
main 函数 return。
可以看到只有 main 函数的 return 才是结束进程,非 main 函数的 return 是结束函数。
-
任何函数 exit。
对于 exit,这里我们使用库函数就足够了。
任何函数 exit,都表示直接终止进程。
2、异常退出
- ctrl + c,信号终止
3、_exit函数
这是系统提供的接口,它的原型同库里的 exit 函数,那么系统的 _exit 和 库函数的 exit 有什么区别 ❓
-
exit 在退出时同默认的 return,会进行后续资源处理,包括刷新缓冲区。
-
_exit 在退出时,不会进行后续资源处理,直接终止进程。
exit 最后也会调用 _exit,但在这之前,exit 还做了其它工作 ???
- 执行用户通过 atexit 或 on_exit 定义的清理函数。
- 关闭所有打开的流,所有的缓存数据均被写入。
- 调用 _exit。
main 函数里都不写 return 和 exit,退出码是啥 ❓
理论上这里的退出码是未定义的、随机的,但实际上,得到的退出码是 0,因为你的 main 函数里总会调用其它函数,成功后,遗留的历史数据是会充当返回值去返回的。
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 个兄弟进程,除了所有进程本身是用双链表链接的,这里与父和子也有链接关系,所以我要离开了,就要把之前的关系统统去掉。
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。
- 点赞
- 收藏
- 关注作者
评论(0)