一、进程创建
pid_t fork(); //创建一个子进程
关于fork函数更详细的介绍,请看硬件与软件的桥梁:冯诺依曼体系、操作系统和初始进程的深度解析及从env到mm_struct:环境变量与虚拟内存的底层实现。
我们都知道数据段在页表当中的权限是可读的 ,但是一旦创建了子进程,操作系统会把权限改为只读
的。当子进程尝试写入时,操作系统就会"报错"。
OS要对当前情况进行判断分类:
1、真的是野指针吗 ?如果是,就终止进程
。
2、不是 ,a. 发生写时拷贝 b. 更改回权限(rw)
。

我们都知道进程是具有独立性的,但是父子进程的代码和数据是共享的。那么,有以下几个问题:
1. 为什么创建子进程之后,不直接把数据分开呢,直接拷贝不就完了,为什么要写时拷贝?
首先,直接拷贝将父子进程的数据分开是可以的 。但是,如果子进程对数据只是只读的呢,那就没有必要进行拷贝。父进程的代码和数据有100MB,每次创建子进程都要拷贝,那岂不是要浪费时间及内存资源。写时拷贝的本质就是一种按需获取
。
子进程读取父进程的数据,直接用父进程的就可以,子进程要进行写入,再给子进程申请,写时拷贝的本质也是一种惰性申请,提高内存使用率
。
2. 为什么要拷贝,直接开辟对应的空间不就好了?
一说拷贝,大多数人想到的都是对数据进行覆盖式的写入,但是如果是a = 20, a++呢?这是不是也是一种写入,在原数据上进行写入。所以我们需要对原始数据进行拷贝
。
扩展问题:从今天开始,你在C/C++上申请空间malloc or new的时候,需要在物理内存上开辟空间吗?
不需要了。开辟虚拟地址空间即可,真正用的时候,OS才会做内存级申请,构建完整的映射关系。本质就是惰性申请
。
二、进程终止
提两个问题:
1. 进程终止,操作系统要做什么?
这个问题,大家应该可以回答出来,进程终止时,操作系统要释放进程的资源,停止CPU对进程的调度,更新进程状态
。
2. main() return 0; return 0什么意思?给谁了?
return 0我们把它叫做进程退出时的退出码。退出码会被"系统"获得,用来让系统辨别,该进程的执行情况。
那么,我们该如何来证明呢?写一段程序。
echo $? //最近一个进程执行完的退出码

可以看到,这个退出码是被获得了的。那如果我们再echo一下呢?

咦?它为什么是0呢?别忘了echo本身也是一个进程。这一次获得的退出码不再是mycode.c的了。
3. 为什么要有退出码呢?
我们创建进程就是为了让进程完成任务的,进程把任务完成的怎么样了,得有一个结果吧,那么,你怎么知道结果呢?
类似的道理,父进程交给子进程一个任务,父进程怎么知道子进程完成的怎么样呢?
0表示程序运行成功。
!0表示失败。1, 2,3, 4,表明不同失败的原因。
举个例子:前几天学校在考试,今天成绩出来了。你爸一看你的成绩99分,他是不会去问你为什么考这么多的,但是你要是考个鸡蛋回来,你爸不仅会噼里啪啦揍你一顿,还得问你为什么。
语言已经提供了一些错误原因。这些退出码(错误码)适合计算机去看,但却并不适合我们人来分析原因。
char* strerror(int errnum) //将错误码转化为可读错误信息字符串
那么,语言为我们提供了多少个错误原因呢?我们可以测一下。
可以看到,一共有134个错误原因,剩下的都是未知的。
我们可以执行ls命令验证一下。

可以看到,在当前目录下是没有aabbcc.txt这个文件的,所以我们直接使用ls命令就报错了。退出码为2,是与我们前面的错误原因一致的。
将来这个退出码会被写到子进程PCB里的
。现在,你知道为什么父进程要拿到子进程的pid了吗 。父进程通过子进程的pid就可以找到子进程,获取子进程的信息,这也是为什么子进程在退出时,状态设置为僵尸状态,不能立即释放所有资源,要保留PCB的原因
。
进程退出,无非就是三种情况:
1、代码跑完,结果正确。
2、代码跑完,结果不对。
3、代码没跑完,进程异常了。
代码跑完了,结果对不对,我们可以根据退出码
来判断。
代码异常了,退出码还有意义吗 ?答案是没有了
。
例子:今天在考试,只有三种情况,考试正常结束,成绩好不好,考试作弊了,直接终止考试,那么成绩还有用吗?
所以,进程异常了,退出码是没有意义的。我们的问题应该是进程为什么会异常?
如果是除0错误,那么CPU会发生错误(硬件错误),如果是野指针呢,那么就是软件错误 。而操作系统作为软硬件资源的管理者
,进程异常了,操作系统首先要知道。
操作系统一般都是杀掉这个进程 ,通过退出信号杀掉进程的
。
例子:学校里有人打架斗殴,校长要知道这件事情。情节严重的,是要开除的。
4. 进程退出的方法
方法一 :main 函数进行 return n,n表示该进程的退出码
。

方法二 :直接调用exit(n),n表示进程的退出码
。
void exit(int status);

方法三 :直接调用系统调用_exit(n)
。
void _exit(int status);

那么,看到这里大家应该会有疑问吧。
4.1. return vs exit
作为一个深谙C语言的你,应该清楚 return 一般表示函数调用结束
,main函数 return ,表示进程退出。
exit 表示进程结束,在你的代码中,任何地方调用,都会导致进程退出
。

可以看到,进程的退出码是1,而不是10。那如果我们改为exit呢?结果还会一样吗。

可以看到,这一次进程的退出码不再是1而是10,说明调用exit函数直接就让进程结束了。
4.2. exit vs _exit

可以看到,在前三秒内是没有打印在显示器上的,所以,数据是被写入到缓冲区里的
。

main函数中return表明进程结束 ,输出缓冲区,在进程结束的时候,会自动被刷新
。

exit终止进程,也会主动刷新缓冲区
。
那么 _exit 呢?

_exit直接终止进程,不会刷新缓冲区
。
最佳实践:exit(n)用来进程退出。
我们为什么要学习_exit呢?我们已经知道库和系统调用是上下层的关系 ,系统调用的存在就是为了保证用户安全的访问操作系统
,也就是说,只有系统调用才可以和操作系统交互。
进程退出时,需要释放资源,包括代码和数据,页表,虚拟地址空间和PCB。这些工作都是由操作系统完成的,我们调用了C语言的库函数exit,为什么可以让进程退出呢?不是说好只有系统调用才可以访问操作系统吗。
进程终止,绝对要调用系统调用,必须让操作系统完成真正的进程删除退出!所以,库函数的底层一定封装了系统调用
。
我们在使用printf("我是一个进程!");
向显示器打印的时候,不就是在用软件访问硬件吗,访问硬件就必须经过操作系统。我们常说的缓冲区到底是什么呢?它又在哪里呢?
首先,我们所说的缓冲区肯定不是操作系统内部的缓冲区 。因为,如果是的话,exit和_exit都应该会刷新缓冲区里的数据。那么,它应该是库缓冲区,在库里面,所谓缓冲区其实就是一段内存空间
。
例如:在C语言中,我们使用过
FILE* fopen(const char* path, const char* mode);
打开文件,那么fopen函数内部一定会打开,关闭文件,其次它的返回值FILE*是一个文件指针,那么FILE是什么呢?
不卖关子了,FILE就是C语言提供的结构体,它在库里面
,那么fopen函数的返回值既然是FILE*,也就意味着C函数内部自己申请了FILE结构体,自己维护的结构体内部,可以存在一个大的内存空间(毕竟linux下一切皆文件)
,char inbuffer[1024]; char outbuffer[1024];,这不就是输入输出缓冲区吗。
今天的文章分享到此结束,感觉还不错的给个一键三连吧。
