目录
- 快速了解:
- 引入最基本的理解:
- 细节:
- 如何理解地址空间:
- a.什么是划分区域:
- b.地址空间的理解:
- 为什么要有进程空间?
- 进一步理解页表与写时拷贝:
快速了解:
先来看这样一段代码。
运行结果:
因为子进程会继承父进程的代码和数据,所以这种结果我们是可以接受并理解的
但是如果我们对这段代码进行一个小修改:
运行结果:
我们说过父子进程是有独立性的,
而就现在来说进程 = 操作系统内核数据结构 + 代码与数据
,那么一个新的进程创建势必会创造新的操作系统内核数据结构 ,而代码是只读的,那么数据是怎样进行处理的呢?
对于上边的例子:
地址是一样的,但是数据却不一样!这就说明&val的地址不是真实地物理地址,是虚拟地址
引入最基本的理解:
我们在学习C语言时肯定见过这样图
我们申请的堆上的空间,以及上篇文章讲过的命令行参数与环境变量等等都在这里申请的。
实际上这个东西并不是真的物理内存,而是虚拟内存,
根本上是个结构体(在linux中叫做struct mm_struct
),有那么多的结构体我们肯定要管理,那就用到了“先描述,在组织
”的
物理内存与虚拟内存的关系如下图:
解释:
我们写好的可执行程序先创建一系列内核数据结构,再将代码和数据加载到内存。
我们的g_val的虚拟地址在mm_struct内,通过页表映射到物理内存。
因为我们的子程序会继承父进程的代码和数据(故虚拟地址与页表的映射关系等会被子进程完全继承),因此我们的父子进程是指向同一个物理内存。
重点来了!
因为我们说过进程之间具有独立性,那么必然父子进程之间是不能相互影响的,那我们现在指向的都是同一块物理内存空间,怎样做才能不破坏独立性的规则呢?
答案是写时拷贝
!
意思是在写入数据时,先开辟一块新空间,将旧空间数据拷贝过去,在进行写入。
这就解决了这个问题。
可能这里有人会说:
既然都要求独立性了,那我们创建子进程时能不能直接就开辟好新的空间呢?
答案是没有必要,因为有时我们并不写入,要写入时在写时拷贝,这就是按需申请
,通过调整拷贝的时间达到节约空间的目的,况且我们有非常多的数据实际上是并不会发生写入操作的。
细节:
目前我们大概理解了虚拟地址,
但是我们刚刚说的只是很笼统的框架,很多细节还没有说到。
如何理解地址空间:
研究这个问题前我们需要先研究两个子问题。
a.什么是划分区域:
我们的虚拟地址空间实际上是个结构体,这个结构体里存放着很多的start和end属性进行空间划分
这就很像我们上学时划的38线,今天你多一点,明天我又夺回来。
b.地址空间的理解:
回到问题本身。
我们先来画个示意图帮助大家建立一个框架。
结论:32位下进程地址空间理论最大值为4GB,但实际上只占用了其中的一小部分。这是因为虽然一个进程的地址空间理论上可以达到4GB,但其中内核空间占据了1GB,剩余的3GB才是用户空间,可供进程自身使用。
为什么要有进程空间?
这个东西是随着时代发展逐渐形成的,在最开始时进程是直接指向真是物理内存的。但因为指向的物理内存是很杂乱无序的。
故我们现在引出来第一个好处:
- 将无序变成有序,让内存以统一的视角看待物理内存与自己运行的各个区域·。
- 由进程与物理地址直接相连->由虚拟地址+页表作为中转站,
解耦
- 也是最重要的一点,对物理内存进行保护。
我们解释一下最后一点:
这就像你的压岁钱被你妈妈收走,说等你有需要了再像我要,等你缺学习用品时,你妈妈一听就给了你一点钱,
但是等你想买4399一卡通时,你妈妈就会拒绝你的请求,保护了压岁钱。
这也就避免你错误的修改数据等等操作而改变了真正的物理内存而造成错误,在页表阶段就给你拦截下来。
进一步理解页表与写时拷贝:
页表其实更加复杂一些,
这里图示只是简单说明了一下,他的功能其实更加全面。
进程专题到此全部讲解完毕了