进程的创建、终止、等待、程序替换
本节重点
1. 进程的创建
1.1 fork函数初识
1.2 fork的返回值问题
1.3 写时拷贝
1.4 创建多个进程
2. 进程终止
2.1 进程退出码
2.2 进程如何退出
3. 进程等待
3.1 进程等待的原因
3.2 进程等待的方法
3.3 再谈进程退出
3.4 进程的阻塞和非阻塞等待
4. 进程的程序替换
4.1 见见猪跑
4.2 理解原理(是什么、为什么、怎么办)
4.3 一个一个调用对应的方式
4.4 应用场景:模拟shell命令行解释器
本节重点
进程的创建,终止,等待,进程的程序替换(和进程地址空间强相关)
1. 进程的创建
1.1fork函数初识
在之前的进程创建中,已经提到过fork,因此在这里的初识是在原有基础上进一步了解。
在linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h> pid_t fork(void); //返回值:子进程中返回0,父进程返回子进程id,出错返回-1
那么在调用fork函数之前只有一个进程,当进程调用fork时,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程(内核数据结构:PCB地址空间+页表,构建对应的映射关系)
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
对于第三点的添加系统进程列表,我们在之前的进程的章节中介绍是由链表存储,而实际上当时是为了便于理解,操作系统实际上没有那么笨,其实际上是由哈希表存储的,通过struct task_struct
类型的指针数组存储,当运行需要的进程时则将会通过指针找到对应的进程控制块。
1.2fork的返回值问题
对于这个问题,从三个层次去理解。
1. 如何理解fork函数有两个返回值问题?
对于fork函数,当调用时,fork函数内部会有两个执行流,对应父进程和子进程,当fork函数内部代码执行完毕后,子进程也就被创建好了并有可能在OS的运行队列中准备被调度了,父进程和子进程各自执行return,这样在main()函数中调用fork函数时,从fork返回的两个执行流就会分别执行main()调用fork之后的代码,因此我们之前所了看到的两个结果就是父子进程对应的执行流所造成的。
2. 如何理解fork返回之后,给父进程返回子进程pid,给子进程返回0?
父亲:孩子 = 1:n, n>=1,因此孩子找父亲具有唯一性。而由于子进程多,父进程想具体调用某一个子进程时就需要这个子进程得有一个名字才能调用这个子进程,因此给父进程返回对应子进程的pid。
3. 如何理解同一个id值,怎么会保存两个不同的值,让if else if同时执行?
对于pid_t id = fork()
,我们知道返回的本质就是写入,所以谁先返回,谁就先写入对应的id,由于进程具有独立性,因此进程就会进行写时拷贝(上一篇详细描述了),因此同一个id,地址是一样的,但内容却不同
1.3写时拷贝
上一篇的进程地址空间中,我们已经提到过什么是写时拷贝,但不是单独分一个专题去写的,因此,这里总结一下写时拷贝。
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。(虚拟内存就是进程地址空间)
即当我们不修改数据时,父子进程的虚拟内存所对应的物理内存都是同一块物理地址(内存),当子进程的数据被修改,那么就会将子进程修改所对应数据的物理内存出进行写时拷贝,在物理内存中拷贝一份放在物理内存的另一块空间,将子进程虚拟内存与这个新的地址通过页表进行关联。
1.4创建多个进程
创建多个进程,可以使用如下代码:
由于开的进程过多,会导致整个OS崩掉,只需要重启服务器就可以解决了。
2.进程终止
2.1进程退出码
我们在C/C++中,在代码最后都会写上
return 0;
,对于这个返回值我们称它为进程退出码。对于正确的进程一般都以0作为进程退出码,而非0就作为错误的进程的退出码,因此不同的错误对应的退出码也是不同的。
退出码的意义: 0:success, !0:表示失败。!0具体是多少,即表示不同的错误。——数字对人不友好,对计算机友好。
对于如下代码:
这个函数的返回值是4950,因此退出码是1。当进程执行之后可以通过一个命令查看具体的进程退出码:echo $?
但当继续执行这个命令时,发现结果是0,这是因为这个命令只会显示最近一次的进程退出码,而下一个为0的原因就是echo本身也是一个进程,并且正确执行因此显示的是0。
在这里回顾一下之前的函数:strerror(n),n为自然数,即n的不同的值就代表着不同的错误。那我们就可以执行这样的一段代码:
for(int i=0; i<200; i++) { printf("%d: %s\n", i, strerror(i)); }
执行结果发现,只有0代表着success,其他的都对应不同的错误,并且有133个不同的错误,一共有134个进程结果。
而对于我们指定指令的随意选项造成的错误:No such or diectory
就就对应着数值为2的错误。
总结一下:
./mytest
———— 运行一个进程echo $?
————$?
永远记录最近一个进程在命令行中执行完毕时对应的退出码(main->return?😉
进程退出的情况:
- 代码跑完了,结果正确 ———
return 0;
- 代码跑完了,结果不正确———
return !0;
(退出码这个时候起效果。确定对应的错误) - 代码没跑完,程序异常了,退出码无意义。
那么进程如何退出呢?接下来就来解释一下(前两种情况)
2.2进程如何退出
1. main函数return返回
这也是我们经常用的方式
2. 任意地方调用 exit(code)退出
结果显而易见,当我们查看这个进程是如何结束的,直接观察退出码:
此外,在函数内部exit时,进程也会直接结束,函数也不会有返回值,下面就来看看这个例子:
到exit语句就会将进程结束,后面的代码也就不会再去执行了。
3. _exit()退出
我们看一下_exit()是如何退出的。
我们发现其也是和exit()一样的功能。事实上,_exit()
是系统调用的函数,也就是OS,而exit()是库函数,库函数是OS之上的函数,调用exit实际上就是exit内部调用_exit
,但二者之间也会有区别,我们将换行符去掉,来演示一下:exit
结果:
可以看出,进程结束后,会刷新缓冲区,打印的结果暂停2秒也会显示出来。再来看看_exit
:
这样并没有打印出结果,也就是说_exit并没有刷新缓冲区。
因此总结一下二者:
- exit终止进程,主动刷新缓冲区
- _exit终止进程,不会刷新缓冲区
因此用户级的缓冲区一定在系统调用之上,具体位置会在基础IO的时候说明。