一、进程创建
1.1 初识fork函数
在调用fork函数后,内核会执行以下操作:
1.2 写时拷贝
通常情况下,父子进程共享代码和数据,当没有写入操作时,数据也共享。一旦有写入操作,写时拷贝机制会为写入的进程创建数据副本。
1.3 fork函数的意义
1.4 fork调用失败的原因
二、进程终止
问题引入:为什么main函数要返回0?返回不同数值的意义是什么?
——> 成功只有一种情况,但失败可能有无数种原因和理由!因此,main函数的返回值本质上是告知父进程进程运行的结果。如果失败,可以用不同的数字表示不同的错误原因。
2.1 运行结果不正确
2.2.1 main函数返回
在进程中,谁会关心我的运行情况?——> 父进程!
实际上,main函数本质上也是一个被调用的函数,其返回值是告知父进程自己运行的情况。
2.2.2 退出码概念
父进程可以通过获取子进程的退出码,不同的退出码代表不同的原因。
问题1:为什么需要退出码?遇到问题直接printf错误原因或查看结果不就可以了吗?
——> 没有规定程序必须打印错误信息。例如,错误可能不是通过显示器输出,而是通过网络写入。
问题2:错误码适合计算机查看,但不适合人查看。我们是否可以将其转换为字符串形式的错误信息?
——> strerror函数可以将错误码转换为字符串形式的错误信息。这些退出码本质上就是错误码,由系统提供。
我们可以打印出一些错误码对应的信息。
问题3:我们的退出码使用的是系统提供的错误码体系,我们能否创建自己的退出码体系?
——> 该体系由C标准库提供,但我们编写的代码通常不是纯C代码,因此通常会创建自己的退出码体系。
问题4:为什么父进程要关心子进程的运行状况?
——> 父进程创建子进程的目的是让子进程执行不同的代码流,完成特定任务。父进程本身只是一个执行者,真正关心的是用户,用户需要知道子进程完成任务的情况。
问题5:全局变量errno
——> 保存最后一次执行的错误码。

这种写法可以在进程返回前直接获取错误码,然后将其转换为错误信息打印出来,并且在进程结束后让父进程知道运行情况。
2.2.3 库函数exit
exit和return的区别:在main函数中,exit和return是等价的,因为exit表示退出进程,而main函数执行完return也会退出进程。但在其他函数中,return表示函数返回。
2.2.4 系统调用接口_exit
_exit和exit的区别:_exit是系统调用接口(更底层),exit是库函数。exit最终也会调用_exit,但在调用_exit之前会执行其他操作:

因此,exit比_exit多做了一层重要工作,即刷新缓存。我们还可以得出另一个结论:缓存区绝对不在内核区!——> 因为如果在内核区,系统调用的_exit在终止时也会刷新缓存区。由于现代操作系统不会浪费时间和空间,所以缓存区肯定是由用户区维护的!(_exit看不到缓存区,所以这项工作只能由exit完成)。
2.2 异常中止
使用退出码可以告知父进程执行情况,但如果进程异常中止呢?那连运行完毕都无法完成,更别提结果是否正确。因此,异常是最先需要被检测到的!一旦发生异常,代码通常未能完全执行,即使执行完毕,错误码也无法信任,此时退出码就失去了意义。
举个例子:就像平时考试一样,如果你考得不好,大家会关心你为什么考不好,但如果你作弊了,性质就变了,即使考得再好也让人觉得不可信。
因此,进程结束后应首先判断该进程是否异常,然后才能确定退出码是否可用。
除0错误:
野指针(段错误)
类似除0和野指针这样的错误,会触发一些硬件级别的错误。例如,除0会导致CPU的状态寄存器出现溢出错误,而野指针(访问的虚拟地址在页表中找不到映射或只有只读权限)最终会转换为一些硬件级别的信号,告知操作系统。
因此,父进程需要关心子进程为什么异常,以及发生了何种异常,系统会通过信号告知我们的进程发生了异常。
因此,我们最关键的是要查看父进程是否收到了信号,如果没有收到信号则没有异常(具体如何接收信号涉及到进程等待的知识)。
三、进程等待
3.1 如何理解
3.1.1 是什么
通过系统调用接口wait/waitpid,来对子进程进行状态检查和回收。
3.1.2 为什么
3.1.3 怎么做
父进程通过调用wait/waitpid方法来解决僵尸进程回收问题,并获取子进程退出情况。
3.2 wait和waitpid
3.2.1 wait解读
wait:(等待任意一个子进程)
问题1:父进程等待时,我希望获取子进程的哪些信息?
——> (1)子进程的代码是否异常?(2)没有异常,结果是否正确,不正确的原因是什么?
问题2:为什么父进程不定义全局变量的status,而必须使用wait等系统调用来获取状态?
——> 使用全局变量的话,由于进程具有独立性,子进程如何修改自己的status,父进程都看不到!(虽然表面上是一份代码),所以这个过程必须通过系统调用接口让操作系统帮助我们获取子进程的一些数据!(因为OS不信任任何人)
问题3:为什么int被分为好几个部分?
——> 我们不仅需要知道是否发生异常,还需要知道退出状态,所以这个int需要拆分成bit位。
(1)低7位判断是否异常 status&0x7F
(2)第8位core dump标志
(3)次8位判断退出原因 (status>>8)&0xFF

3.2.2 阻塞和非阻塞轮询
如果子进程一直不退出,父进程在调用wait时默认不会返回,处于阻塞状态——> 通过这个我们可以知道阻塞不仅仅发生在向硬件发送请求时等待其状态准备好,还可以发生在父进程等待子进程结束以获取其状态。
如何理解非阻塞轮询?我们讲个小故事:
在这个过程中,你就是用户,打电话的过程就是调用系统调用的过程,而小张就是操作系统。当你打电话询问小张的过程其实就是想操作操作系统询问:“你当前的状态准备好了没有?(检查状态)”小张说等会就下来,于是你挂电话,这其实就是你检查不成功,先结束系统调用(系统调用立即返回),这就是非阻塞!而你一直给小张打电话其实就是轮询(不断询问,有while循环),所以加在一起就是非阻塞轮询!
这个过程其实就是阻塞!也就是系统调用会卡住,会被链接到子进程的一个阻塞队列中等待。
这个过程描述的就是,阻塞的方式虽然简单且应用较多,但也比较呆,因为父进程在等待的时候什么也干不了。非阻塞轮询相比较于阻塞来说,可以多做一些自己的事情,比如说可以做一些检查的工作!
——> 一般来说这种事都是一些比较轻的工作,因为我们核心的任务是等待子进程,所以一般来说都是做一些检查之类的简单任务。

3.2.3 waitpid解读参数:
-1:等待任意一个子进程,和wait等效
0: 等待那个id和该pid相等的子进程
2. int *status
(1) WIFEXITED(status) : 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)其实等价于status&0x7F
(2) WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)其实等价于(status>>8)&0xFF
(3)NULL:不关心子进程的状态


进程本质上是一个多叉树,父进程只关心自己直系的子进程!
3. int options
0:代表阻塞等待的方式,就是子进程没结束系统调用就一直等
WNOHANG: 代表非阻塞轮询的等待方式,若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
轮询的话必然要维护一个while循环,然后根据情况去break,不然就可能出现子进程还没结束父进程就挂了的情况。
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
调用出错,比方说等待的不是自己的子进程。
3.2.4 多进程的代码逻辑
如果是多进程的话,waitpid的第一个参数可以用-1,让父进程等待任意一个子进程,然后子进程有多少最好用一个宏,这样父进程可以知道子进程的数目,轮询的时候我们就不能一下子break掉,而是需要维护一个计数器,没等待完一个子进程就去统计一下。
创建很多的子进程,但具体哪个先执行是由调度器决定的,但我们必须知道的就是最后一个结束的必然是父进程,因为子进程都是它创建的,所以它理所应当去回收所有的子进程。
进程最重要的三个核心:进程创建、进程等待、进程终止。所以我们在需要多进程的时候,我们的代码核心首先要考虑以下要素:(1)需要有循环fork创建子进程;(2)需要在合适的时候让子进程退出(常用exit);(3)父进程必须等待子进程(阻塞就是一直卡住等,非阻塞轮询就是得需要一个while循环,反复调用),它有义务回收所有子进程!

以上就是Linux:进程的创建、终止和等待的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号