文章目录
本片我们主要来讲进程控制,讲之前我们先把写时拷贝理清,然后再开始讲进程控制。
1、写时拷贝
我们第一篇进程文章中,讲到了系统接口fork()创建子进程,最后我们提了五个问题,第五个问题:如何理解同一个id变量,怎么会有不同的值? 写时拷贝将为你解答该问题。记不清的伙伴点这里回顾那篇文章
通常,父子代码共享,父子在不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
当父进程创建子进程之后,子进程的页表是拷贝父进程的,但子进程要在数据段进行写入(代码段不支持修改),就需要重新申请空间,将原数据拷贝后再做写入(我们并不是将整块数据进行改写的,可能只是修改部分数据),并修改页表,这部分工作是由操作系统做的。 但是该工作是需要时机的,操作系统并不知道你什么时候是要做写入的。
我们先说一个 结论:父进程创建子进程的时候,首先将自己的读写权限改为只读,然后再创建子进程。
用户是不知道的!用户将来可能会对数据(权限为读写,代码段是只读) 进行写入!此时,页表的转换会因为权限问题出错,这时操作系统就接入了。但是出错也分真假:
- 真出错。代码段是不可以写入的,但是我们修改的区域在code_start~code_end(代码区起始结束区域),这时就是越界/真出错。
- 假出错。对数据区的写入,数据区是可以读写的,只是我们页表中改成了只读。这样的不是出错,是触发进行重新申请内存,拷贝内容的策略机制。
我们终于明白了,子进程拷贝下父进程的页表后,将数据对应的页表条目权限改为只读,通过让操作系统触发异常的方式,让操作系统帮我们进行写时拷贝的,完成后再把对应的页表条目改为读写,没有写入的依旧是只读。
2、进程终止
我们先来提出一个 问题:我们C语言代码main函数最后都有一个return 0,返回0时给谁返回呢?
main函数也是被调用的,所以注定谁调用就给谁返回。我们写一段代码来看看:
#include <stdio.h>
int main()
{
return 10;
}
我们main函数中什么都不写,直接返回值为10。
当编译运行后,它的父进程是bash,会将返回值交给父进程,用指令echo $?获取刚刚的结果。
打印出来这是现象。
?是环境变量,保存的是最近一个子进程执行完毕的退出码。
第二次查看退出码为0,是因为上一个echo执行是成功的,0代表了成功。
由此我们展开下面的话题:
2.1 进程退出场景
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
2.1.1 退出码
在多进程环境中,我们创建子进程就是为了帮我们去做事,这里“我们”是父进程,子进程事做的怎么样,父进程是需要知道的。在main函数中,返回值0代表正确,非0代表错误,父进程就是依靠返回值来判断是否正确的做完了任务。
当返回0,正确大家不会关心这个过程;但是返回非0,意味着错误,我们最想知道的是错误的原因是什么。所以我们可以用不同的数字表示不同的原因!但是不便于人阅读,所以我们需要一些能够将数字转化成错误码的字符串描述方案。C语言给我们有提供一批接口,我们也可以自定义一批我们自己的错误码与错误信息,把不同数字转化成不同出错原因的接口:
我们写一段代码来打印一下所有的退出码与对应的信息:
#include <stdio.h>
#include <string.h>
int main()
{
for(int i = 0; i < 200; i++)
{
printf("%d: %s\n", i, strerror(i));
}
return 0;
}
这就是退出码,不同的退出码代表不同的出错原因。
我们来举例子看一下:
退出码是2,错误信息描述是没有这样的文件或目录。跟我们上面查看的退出码以及对应信息是匹配的。
结论:main函数的退出码是可以被父进程获取的,用来判断子进程的运行结果。
2.1.2 错误码
C语言还有一个错误码,errno,我们下面来学一下看看有什么不同:
我们先写一个代码测试一下:
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{
printf("before: %d\n", errno);
FILE* fp = fopen("./log.txt", "r");
printf("after: %d, error string: %s\n", errno, strerror(errno))