谢谢阅读,如有错误请大佬留言!!
目录
谢谢阅读,如有错误请大佬留言!!
抛出总结
开始介绍
发现问题
进程地址空间(虚拟地址)
页表
物理内存与进程地址空间映射
缺页中断基本概念
写时拷贝的原理(基于缺页中断)
抛出总结
进程:何为进程进程 = 内核数据结构(PCB+mm_struct+页表(MMU))+ 代码和数据
开始介绍
发现问题
看一份代码
我们知道,当子进程出现写实拷贝的时候,将共享的数据拷贝一份,为子进程独立存储。
让我们运行该代码。
分析结果:1、写实拷贝前:在写实拷贝前子进程与父进程的flag数据相同,数据地址相同,无可厚非为写时拷贝前,父子进程共享一块数据空间
2、写实拷贝:子进程准备修改flag数据,先发生写实拷贝-父进程的flag数据拷贝一份,然后拷贝的空间给子进程形成独立,然后子进程的数据flag改变为222。
3、写实拷贝后,我们观察结果:子进程与父进程的flag值确实发生了不一样了,但是!!我们惊奇的发现他们的地址居然一模一样!同一个地址怎么能保存不一样的值呢??
得出结论,我们看见的地址,其实并不是直接物理内存上面的地址,这里我们看见的地址其实是进程进程地址空间(虚拟地址)。
进程地址空间(虚拟地址)
我们常见这个表其实是不是内存上面的数据区分布,其实是进程空间分布图。它其实一种结构体类型。
让我们讲个小故事更加了解虚拟内存:
有一个有钱的富翁他对他的朋友非常的好,但是他的朋友们都不知道富翁有其他朋友,以为富翁只有他一个朋友,富翁有一千万元,他对所有朋友说哎呀我的钱都可以借给你,但是你不能一下子借太多,要经过我的同意才行。富翁所有的朋友都认为自己可以向富翁借款一千万,所有他们就先规划了这一千万怎么用。富翁也可以同时放贷给许多朋友。故事先暂停一下。
这里的富翁换成物理内存,而朋友们换成进程,所有的进程都是独立的其实,但是进程都认为一个人独占了整个物理内存资源,所以就事先规划了内存使用的分布。划分了并不代表拥有,只是划分了而已
这里我们介绍一下inux的进程地址空间:struct mm_struct{} 这个结构体就是进程地址空间结构体
这里的每对数据其实就是对应着每个段的开始和结束。
但是单单只有进程地址空间也没办法,毕竟所有的进程都认为自己独占了物理内存,所以必须加上一些东西-->页表+查叶表。
页表
查页表:也就是在页表上查询数据一个硬件设施(这里我们不做过度说明)
页表:Linux在启动过程中,要首先进行内存的初始化,那么就一定要首先创建页表。我们知道每个进程都拥有各自的进程空间,而每个进程空间又分为内核空间和用户空间。
以32位计算机为例,每个进程有4G的虚拟空间,其中0-3G属于用户地址空间,3G-4G属于内核地址空间,内核地址空间是所有进程共享的,因此内核地址空间的页表也是所有进程共享的。
Linux内核中用户进程内存页表的管理是通过一个结构体mm_struct来描述的
让我们抽象的描述页表
左边是进程地址空间,而右边是物理地址空间,页表是承接进程地址空间与物理地址空间的桥梁。
接下来我们将物理内存、页表、进程地址空间建立一个初步的了解:
物理内存与进程地址空间映射
举个例子:现在我们进程中main函数地址我们需要存在物理内存中先将main虚拟地址传入页表进程空间列
然后在操作系统将该进程main函数真实地址与对应虚拟地址对应。
这样当我们需要访问main时的时候我们的操作系统就会让cpu根据该进程的页表映射关系找到实际的函数代码入口。
小知识点,为了让cpu快速在虚拟地址访问到进程入口处,我们无论是哪个进程的main函数地址都是一样的。
观察进程pid:两个不同进程加载到了内存,但是他们的main函数地址居然时相同的,发现虽然进程不同当时main函数入口是相同的,然后再从与对应的物理内存实际映射找到物理内存上该进程main实际的地址:我们的
继续观察
让我们同时运行程序
两份代码同时加载在进程,同时为R状态,但是他们的main地址居然相同,有一次的告诉我们我们取得main函数地址为虚拟地址。这些进程都有自己的PCB,mm_struct、页表,所有他们访问内存实际其实是,通过映射关系访问,而不是直接去内存访问数据
各访问各的。
缺页中断基本概念
个人理解:缺页中断就是操作系统先暂停对进程通过页表访问物理内存,然后操作系统对物理内存进行操作(拷贝父进程数据(写实拷贝),申请动态内存空间),然后再让进程操作该空间数据
我们先写份代码:
#include<iostream>
#include<unistd,h>
int main()
{
int*p=new int[10];
*p=10086;
*(p+1)=10087;
*(p+2)=10088;
return 0;
}
我们向内存申请40各字节的空间。确实现在空间的使用全给我了。但是我并没有立刻使用空间,而是过了10秒才使用。如果这个空间一直等待我我写入数据的话,大大的浪费了内存的使用效率。所有操作系统会先让急需内存的进程先使用空间,当我需要写入的时候,再去给我开辟空间。
画图理解:
第一步先去申请空间:我们在进程地址空间查看是可以开辟40各字节的空间,允许开辟,然后返回空间地址值,这里我们这里申请的是虚拟地址空间,然后反馈告诉进程,申请空间成功。(其实并没有在物理内存中申请)。
第二,我们进程进入休眠状态,如果我们申请的是物理内存,那么这10秒我们申请的物理内存就要一直等待被当前进程使用,现在我们申请的只是虚拟内存,这并不占用物理内存40个字节,这样这40个字节空间可以被其他的进程先使用
当10秒过去后,我们cpu运行当前进程,需要写入数据,这时不会立刻写入数据而是,先发生中断,也叫做缺页中断。操作系统先去物理内存申请40个字节空间然后与该进程建立映射关系,然后才将数据写入空间(这里写一份可能就开辟4个字节空间,还有36字节空间不会开辟,未学习地方,以后回来补充)。
看步骤
休眠结束当我们需要在这块空间写入空间,先暂停写入
I:操作系统先在物理内存开辟空间。
II:将开辟的空间与进程地址空间建立映射关系(操作会进行到访问进程)
III、最后通过映射关系,在物理地址上写入数据
全图:
并不是所有的地址都映射在页表上:
如果是这样,4G物理内存只能跑一个进程甚至一个都跑不了。一页项有物理内存与虚拟内存,不止要4g空间。
所有页表也是按需申请页表项的。
写时拷贝的原理(基于缺页中断)
运行这段代码得到结果,我们知道发生了写实拷贝
让我们看看怎么回事
首先:这是父进程的进程信息,val存放在虚拟地址数据区,映射在物理内存上为0x00afcd。
发生创建子进程,其实就是将PCB、mm_struct、页表拷贝一份给子进程,那么还没写实拷贝前,其实所有的数据都是和父进程一模一样的。就是直接拷贝一份父进程数据给子进程。创建子进程时,将父进程的 虚拟内存
与 物理内存
映射关系复制到子进程中,并将内存设置为只读(设置为只读是为了当对内存进行写操作时触发 缺页异常
)。
他们所有数据都是一样的,映射关系也是相同的。
当我们想要改变子进程的val值会发生写实拷贝。发现该数据为在页表项为只读发生缺页中断,拷贝该物理空间数据,将映射关系改为映射拷贝的物理内存数据。
这里改变子进程页表的val权限,不会影响父进程的val权限,防止父进程创建多个子进程。文章《linux内核写时复制机制源代码解读》,里面说的挺清楚,在缺页异常时,只复制内存,然后把自己进程的页项设为可写,不修改其他进程的页表。当其他子进程写相应内存时,再进入一次异常。
谢谢阅读,如有错误请大佬留言!!