内存管理
纯分段
最初分段的目的是实现各段可以随意增长
啥意思呢?在既没有分段也没有分页的年代,程序装载进入内存是紧挨着放的,一条指令或数据紧挨着一条指令或数据.这就导致一个啥后果呢?
我想使用malloc获取一些堆空间,但是堆已经被左右两个块夹住了,大小固定了,找不到想要的空闲空间
分段之后,各段在内存中任意位置存放,一个程序在内存中可能被分割成几块,不必连续存放,两个程序可能交叉着在内存中存放
如果堆就放在一个堆段,堆顶指针指向该段的一头,如果该方向上紧挨着没有其他段,那么堆就可以变大了
分段还实现了,代码和数据的分离,代码放在一个段,数据放在一个段
可以简单意淫一下分段是啥样的:
1
2
3
4
5
6
7 segment Code:
int main(){
printf("%s",Data:buffer);
return 0;
}
segment Data:
char buffer[]="helloworld"只是意淫,因为x86-64上已经废除分段了,我也不知道真的分段程序怎么写
但是计组书上讲的老古董8086上的汇编语言是有明确的分段的
分段使得段权限管理很方便
段的功能是由程序员指定的,程序员可以把只读代码都放一个Code段,可读写数据都放在Data段,只读数据都放在Rodata段等等,每个段都指定一下访问权限rwx就可以限定怎么访问它了,违反了指定好的权限的访问,操作系统会报告段错误
纯分段中是没有甚么"虚拟内存"概念的,因为虚拟内存的实现要分页,那么内存条子多大,地址空间就有多大,即物理内存
画个图意思意思纯分段系统上程序在内存中的存储状态:
段表
作战时一个师下辖三个团,那么师部就得维护这三个团的信息,包括:
1.团部人员信息,这方便师部联系团指挥员,这可能在师部里有一个电话本本
2.该团当前所在位置,这方便师部部署战术任务(注意战术动作),这可能在师部里有一个沙盘
3.该团当前人员数量,这方便师部进行伤亡统计和兵员补给,这可能师部里有一个专门记录的本本
同样,一个进程被分成若干段,每个段都是大小可变的,每个段可以被安排在内存的任意地址,每个段是甚么访问属性,这也需要一个数据结构维护
每个段占用一个表项,现在我们可以想到,该表项至少应该有的内容
1.段地址,进程访问该段必须
2.段大小,检查访问越界错误必须
3.段访问权限,段保护必须
想不出还需要维护段的啥信息了,是时候看看权威怎么想这个事情的了
他的段表包括了三个项目:段号,段长,基址
段号是啥呢?程序员编程的时候会指定段名,比如Code,Data等,但是计算机并不喜欢这么长的信息,一个英文字母用ASCII编码都需要一个字节,那么一个"Code"就得编4字节,32位
如果程序员的程序就分了两个段,Data和Code,那么只用一个符号位就可以表示两种状态,分三段则需要2位
编译器会无视所有段名,将所有段从上到下顺次编号,这应该可以说是离散化的思想P1496 火烧赤壁 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
把名字都映射成顺次增加的整数还有一个好处就是,段号还需要存储在段表中吗?
段表第一项存第一段,段表第二项存第二段...
段表也只需要把编译器翻译成的0,1,2,...n号段按顺序存起来
那么实际上段表项只有段长和基址两个信息,而这些我们都想到了,我们甚至想到了保护措施
在纯分段的系统上,段表被放在哪里呢?
操作系统是常驻内存的,进程控制块PCB由操作系统维护,每个进程又只有一个段表,
那么很自然的就会想到,段表放在进程PCB中,由操作系统维护
果真如此吗?
非也,诚如是则PCB过于臃肿,操作系统占用的内存空间会因为段表变的非常大
实际上PCB只需要维护一个指向段表内存地址的指针和段表的长度
在进程被调度运行时,段表地址和段表长度会被放到硬件段表寄存器中
地址翻译
加入纯分段系统上,给出一个32位的内存地址,
高16位为段号,低16位为段内偏移
高位地址为0x0002h
,即段号,首先要和段表长度进行比较,如果段号大于等于段表长度则产生越界中断
如果段号小于段表长度,那么用段表地址+段号相当于一个基址变址寻址,去查2号段即段表里面从上往下数第三条记录,发现基址为40K
,段长6K
低16位为0x0100h=0.25K<6K
因此该段内偏移量是合法的,不会发生越界中断
最终基址+段内偏移=1010 0000 0000 0000+1 0000 0000=1010 0001 0000 0000=0xA100
即物理地址
地址空间维度
纯分页系统中用户进程地址空间是一维的,直接给出一个物理地址就可以寻址
纯分段系统中用户进程地址空间是二维的,需要通过段:段内偏移
指定一个物理地址
每个段内都是从0开始开始编址
分段的好处
1.方便共享
代码和只读数据可以在物理内存中只有一个段,但被多个进程的段表项目指向
实际上后来的段页式中,共享库就是这样用的
碎片
外部碎片:
画个图立刻清楚
内部碎片:
在内存分段系统上没有内部碎片问题
在分页系统上,假设一个页是4KB,一个进程的地址空间要33KB,那么前32K正好8页,第九页上只用了1KB,剩下这3KB就是内部碎片
段页式
段页式结构,对用户来说,可以感受到的是分段,实际使用的时候和纯分段几乎相同
操作系统负责分页工作,对用户不可见
一个段可能由多个页组成,比如一个8K的段就有可能由两个4k的页组成,这个段就管理两个页
一个进程对应一个段表,每个段维护一个表,因此一个有多个段的进程对应多个页表
纯分段结构中,段表存放的是段基址和段长度,而段页式结构中,段表中存放的是页表长度,页表存放块号,页表存放的是内存块号
从物理寻址到虚拟寻址
虚拟内存是一伙子异想天开的人造出来的巧夺天工,在虚拟内存之前,是符合普通人认知的物理内存
计算机主存(目前可以直接认为成内存条)可以看成是一个巨大的数组,他有M个连续的单元,每个单元大小是一个字节,各个单元线性分布
物理寻址就是直接在内存条上寻址,CPU想要读写哪个单元的内容,只需要指定该单元的编号,或者说下标
物理寻址的一个相对完整的过程:
1.CPU指定物理地址,将该地址信息送到地址总线
2.CPU指定对该地址是读还是写操作,将该控制信息发往控制总线
3.CPU从数据总线上对相应内存单元进行读写操作
既然物理寻址方法如此自然易懂,为什么还要引入一个相对晦涩的虚拟寻址呢?
其中的一个原因是,内存条太小了,想要把一些磁盘空间也乔装打扮一下当成内存使用
还有更高级的原因,比如更方便地管理内存
虚拟空间是对上层而言的概念,而物理空间是对下层而言的,
用户感到的是虚拟空间,有限的内存上似乎可以运行无限多的进程,开无限多的进程独立地址空间,
而对操作系统来说,实际上的"资源"就只有内存条那固定死的地址空间.
操作系统通过及时地将用户暂时不用的进程的物理空间换给其他进程的虚拟空间使用,让用户产生错觉认为内存很大.
当用户又要继续使用刚才暂时不用的进程时,此时该进程占用的"资源"刚才被操作系统从内存中搬到磁盘中,然后交给了别的进程.
因此操作系统又会启动缺页处理,把磁盘中该进程的信息重新搬回来放到内存中使用.
这就好比用工荒,又好比小学时做过的一道数学题
400个士兵守一个方形的城池,每时每刻都有士兵阵亡.
如何保证敌人每时每刻看到每面墙上都有至少100个士兵在防守的假象?
400个士兵均分4组站在4个角楼上就可以造成每面城墙有200名士兵的假象
东北角的士兵就同时起到忽悠东面和北面两个方向敌人的作用
在虚拟内存概念中,主存就起到了这个东北角士兵的作用
虚拟寻址相对于物理寻址多了一个硬件MMU(内存管理单元)和一个步骤即地址翻译.
并且CPU指定的虚拟地址有可能并不放在主存中,而是放在磁盘中,这就发生了缺页,操作系统会一系列操作给他整的不缺喽然后继续执行,这都是后话了
最简化的虚拟寻址模型:
一个相对完整的虚拟寻址过程:
1.CPU指定一个虚拟地址,发往MMU内存管理单元(MMU也是CPU中集成的一部分)
2.MMU将虚拟地址翻译成物理地址,送往地址总线(由于MMU是CPU的一部分,因此还是CPU将该物理地址送往地址总线)
3.CPU指定对该物理地址的读或者写操作,将控制信息送往控制总线
4.CPU通过数据总线对该内存单元进行读或者写操作
地址空间
物理地址空间和虚拟地址空间
一个512MB的内存条上的地址空间即物理地址空间是多大?
一个单元一个字节,\(512MB=512*2^{10}KB=512*2^{20}B=2^{29}B\)即物理地址空间编号范围为:\([0,2^{29})\)
物理地址空间就是内存条上的地址空间数
虚拟地址空间是指想要给用户造成的假象中,让用户感觉出来的内存大小,实际上是磁盘上的一个连续巨大数组
还是以士兵守城举例,一共400个活人,每面墙上分100个士兵,不可能再多了,这就是物理地址空间
但是士兵都站在角楼可以造成每面墙都有200个士兵的假象,这就是虚拟空间
通常虚拟地址空间会比物理空间大,否则虚拟空间没有存在的意义
绷不住了
为什么不直接把物理空间做大?物理空间即内存条,相对磁盘贵得多.如果有钱自然可以整一个不用磁盘,只用内存(还涉及到断点是否能保存的问题)的计算机,现在对于私人电脑而言,比如联想拯救者y9000p2021h,显然不现实
数据对象和地址空间的关系
数据对象就是存放在地址空间上的数据,其在地址空间中的位置或者说下标就是其属性
比如一个char一个字节,存放在一个内存单元中,
一个int四个i直接,存放在四个连续的内存单元中,
每个数据对象都会有一个虚拟地址空间地址,当其所在进程被实际运行时,它有可能在物理空间中有一个物理空间地址
虚拟内存作为缓存工具
分页
虚拟空间比物理空间大,自然不能一股脑塞进物理空间里.
应该是用到虚拟空间的某一块就从虚拟空间中把这一块搬到物理空间中
这就好比一个有5个坑的厕所但是有20个人要扔炸弹,自然要挑最急或者最先排队的5个人去扔炸弹,20个人一起扔炸弹有很大可能把炸弹扔别人身上或者扔外边
这里选5个人一组去扔炸弹就好比从虚拟空间中选出一部分块放到物理内存中接收CPU的访问
为什么CPU不能直接去磁盘访问?
这从量上举例
就好比中国有13亿人口就要挖13个上厕所的坑,
其一正常人不是每时每刻都在扔炸弹,就好比磁盘中的数据不是每时每刻都要被CPU访问
其二建13亿个厕所走到路上得随处可见的坑(我密恐犯了),类比计算机中就需要从CPU到磁盘之间部署总线,磁盘一般比较大,比如1个T,那么地址总线宽度就得\(log_2 1T\)
从质上举例子,内存速度远快于磁盘,
CPU去访问内存,然后内存去访问磁盘,就好比师长向团长下达命令,团长去团里下命令,要找士兵许三多,
但是CPU去访问磁盘就好比师长直接向师广大士兵下达命令,找许三多这个人.
规范的术语:
虚拟内存(Virtual memory,VM)
物理内存分割成的块叫做物理页(Physical Page,PP)
虚拟内存分割成的块叫做虚拟页(Virtual Page,VP)
虚拟内存分割成块是因为物理内存放不下,那为什么物理内存也要分块?
这就好比20个人去一个10个坑厕所扔炸弹,20个人里有10男10女,扔炸弹这种事做不到男女搭配干活不累,需要5个坑放在男厕所,5个坑放在女厕所,然后10男分两组去男厕扔炸弹,女同理
都是人但是因为性别就得分开扔炸弹
有些连续虚拟内存块就得分开了放到物理内存里
虚拟内存中的一个虚拟页在被使用的时候要搬到物理内存中,复制到一个物理页上,
用不到的虚拟页其对应的物理页有可能就被让给其他虚拟页使用
可以说某一时刻一个正在被使用的物理页是一个虚拟页的快照,
当然物理页可以修改,这会导致物理页和其对应的虚拟页内容有差异,这种情况下应该怎么办呢?当该物理页将要被让给其他虚拟页时需要将改动写回其对应的虚拟页
页属性
页属性:物理页和虚拟页有相同的大小\(P=2^p\)字节
由于物理地址空间比虚拟地址空间小,因此显然物理页数量比虚拟页数量少
根据虚拟页是否被使用以及是否正在被使用,虚拟页可以分成三种
分配与否就是指该虚拟页是否存储了信息
缓存与否就是该虚拟页是否被复制到物理页供CPU访问
什么是缓存?
看电影的时候也会遇到"缓存"这个概念,缓存有点一劳永逸的概念,第一次加载需要花费一些时间,但是以后对相同内容的重复访问就快得多了
什么叫"缓存在DRAM中",就是指虚拟页已经拷贝到内存上建立了物理页,方便CPU直接访问内存而不用与磁盘打交道
因此内存条在存储系统中可以看成是CPU和磁盘之间的缓存器,就好比cache是CPU和磁盘之间的缓存器,只不过内存比cache大得多慢得多
显然VP数量多与PP,VP的二进制地址编号更长
幼年的页表
CPU或者说虚拟内存系统怎么知道它想要访问的虚拟页是否已经被拷贝到内存条上成为物理页了呢?
这就好比班主任要约谈某个倒霉蛋,但是班主任怎么知道这个倒霉蛋有没有来学校呢?班主任会先看一下签到表判断一下倒霉蛋来没来,来了则直接约谈,没来则先从家里叫到学校然后再约谈
让虚拟内存系统掌握目前有哪些虚拟页拷贝成了物理页,要在==主存上==放一个页表(Page Table,PT)
为什么要放到主存上?
还能放到哪里呢?CPU的寄存器里?磁盘里?
寄存器稀松了了的几个,每一个最多存放一个64位数4字节,而一个页表表项数成千上万,每个表项都是以字节为单位.显然CPU寄存器放不开?
放磁盘里那和CPU直接访问磁盘上的数据有啥区别?
也只能放在内存里了
页表项(PTE)数是根据虚拟内存确定的,虚拟页有几个,就有多少个页表项
页表项按照顺序表方式排列,下标从0到虚拟页数-1
,与虚拟页一一对应
有效位表明该虚拟页是否已经在物理内存中创建了物理页,
页表项剩下的部分是物理页号,光知道一个虚拟页创建了一个物理页还不够,还得知道这个物理页在哪里
这就好比20个爷们去只有5个坑的男厕扔炸弹,厕所所长为了方便惩罚扔不准炸弹的爷们,给五个坑标上0,1,2,3,4,然后让每个人扔炸弹的时候报告自己对哪个坑输出,
然后厕所所长记录一张如厕表,
坑[0]=老八,坑[1]=张三...
就能根据坑号责任到人,如果1号坑的爷们占着坑不扔炸弹,所长就则给外面的人说1号坑闲置,来个爷们把上一个爷们给挤掉
如果1号坑的爷们快速地扔完炸弹走了,所长就把1号坑标记为闲置状态
之所以说"幼年的页表",是因为实际上的页表项目还要记录很多信息,这里只是最简化的页表
页命中
CPU想要访问某个虚拟地址,但是这个虚拟地址对应的页是否已经被缓存到内存中了呢?
如果是则"命中"
这就好比20个爷们去一个5坑男厕,一个找茬想让老八表演绝活,但是绝活只能靠在厕所中食用炸弹完成,于是找茬的去问所长,老八是否正在坑上,
所长也挺好奇的,查了一下如厕表,发现
坑[0]=老八
,老八确实正在扔炸弹,这就命中了否则所长就要让外面的老八挤掉一个站着茅坑不扔炸弹的张三然后表演绝活
在计算机上怎么判定是不是呢?
根据CPU给出的虚拟地址首先查虚拟页表,先看有效位,如果为1则命中,根据页表项后面的物理页号去物理内存中去访问物理页
如果有效位为0则表明该虚拟地址对应的页还没有拷贝到物理内存里,触发缺页异常,这都是后话了
虚拟地址和虚拟页的关系?
虚拟页是一个虚拟单元集合,一个虚拟页集合了一些连续的虚拟单元,每个虚拟单元都有一个虚拟地址
虚拟页的地址就是第一个虚拟单元的虚拟地址
因此给定一个虚拟地址查页表的时候应该是查询该虚拟地址是否属于某个虚拟页的辖区
缺页
没有命中的情况就是缺页
什么情况下判定为缺页?
cpu指定的虚拟地址查页表后发现有效位为0,立刻引发缺页中断
缺页了怎么整才能让它不缺?
我原来认为缺页了CPU就直接越俎代庖地去访问磁盘了,这经过前文的学习显然是想当然
缺页之后选择一个"不是很重要"的物理页(比如PP3)给他扬了,修改指向该物理页PP3的页表项PTE4,有效位置0,
此举的目的是当先前的虚拟页VP4再次使用时,需要重新拷贝到物理页.如果此时不即使修改PTE4的有效位则再次使用VP4时一查表,发现已经缓存好了,直接访问PP3了,可是PP3实际上是VP3的拷贝
这里"选择一个不是很重要的物理页"涉及到页面调度算法,这是另一本黑叔叔,<<现代操作系统>>中的内容
然后用需要用到的虚拟页VP3拷贝到该物理页PP3位置,挤掉VP4,
修改对应的页表项PP4,有效位 置1然后物理页表地址写上刚才拷贝到的物理页的地址PP3
都改完之后,重新执行刚才的指令,此时虚拟地址就已经被缓存了,不会再发生缺页了
一些规范术语:
交换或者页面调度:再内存和磁盘之间传送页
页面调入或者磁盘换入,页从磁盘传送到内存,方向都是相对于内存而言的,
方向总是相对于更靠近CPU的器件而言的
按需页面调度:只有不得不进行页面调度即发生了缺页时,才进行页面调度的调度方式
现代操作系统一般使用按需页面调度
虚拟内存作为内存管理工具
每个进程都有自己独立的==虚拟==地址空间,注意不是物理地址空间
这就要求每个进程都有独立的页表
物理空间只有一个,就是内存条,或者说主存
进程的虚拟地址空间看似是连续地开了一大片,实际上有可能在物理内存上东一块西一块.
不同的进程可以有共享的物理页,这时动态库的物质基础
这里有一个问题,如果两个进程的代码段都是从虚拟地址的
0x400000
开始,岂不是对应了磁盘上的同一虚拟页?==可以提出一种猜想==
进程的虚拟地址空间是要小于磁盘上全部的虚拟地址空间的
进程的虚拟地址并不是磁盘上的虚拟地址空间的下标,
而是相对于磁盘上几个连续的页面组成的一个子虚拟地址空间而言的
该子虚拟空间从0开始重新编号
虚拟内存作为内存保护工具
啥是内存保护?
比如用户不允许修改内核的数据
只读数据无法被修改,代码段也不许被修改
不允许修改其他内存的虚拟地址空间
进程的虚拟地址空间独立,很自然的就保证了进程不允许修改其他内存的虚拟地址空间,
这就好比让两个在水上步行球中的人打架,两个球距离最近也就是相切,不会相互嵌入
这个球就好比进程独立的虚拟地址空间
...
幼年的页表成熟了一些,成了带许可位的页表
在页表项中加上许可位,就可以限制进程对该页读写访问,
sup许可位限制普通用户和管理员的区别
违反这些许可条件的指令将会导致段错误
地址翻译
符号约定
地址翻译就是从虚拟地址计算出物理地址的过程
咱就是说这个MAP函数这么抽象有必要这样写一下吗
这个映射是在MMU中完成的
最简单的地址翻译
每个进程可以有自己的独立页表,需要一个页表基址寄存器指向页表基地址,作用类似于段寄存器
一个精确到存储单元的虚拟地址分成虚拟页号和虚拟页偏移量两部分
确定到页用到页号,确定到页上的一个单元需要页偏移量
由于物理页大小和虚拟页大小相同,因此物理页偏移量和虚拟页偏移量是相同的
虚拟页数量一般会大于物理页数量,因此虚拟页号长度一般长于物理页号,即\(n>m\)
翻译过程:
1.CPU指定一个虚拟地址给MMU
2.MMU将该虚拟地址的\([p,n-1]\)位作为虚拟页号去查页表,剩下\([0,p-1]\)位作为页偏移量
3.虚拟页号查页表对应项,如果有效位是0则缺页中断,否则命中,如果命中则:
4.从页表中读取物理页号\([p,m-1]\),和页偏移量\([0,p-1]\)拼成物理地址
5.CPU将该物理地址放到地址总线上,准备访问内存
考虑高速缓存的地址翻译
原来的地址翻译,CPU直接访问内存,现在在CPU和内存之间再加一级缓存,L1高速缓存
处理器只能发出虚拟地址,MMU翻译成物理地址,处理器不能直接发出物理地址
页表还是存放在内存中,高速缓存存放的是很少一部分的页表项目还有数据
1.处理器发出虚拟地址VA之后进入MMU进行翻译,得到一个虚拟页号
2.如果没有L1则下一步要根据虚拟页号,到内存中访问页表了,而现在有L1,要先检查L1中有没有该虚拟页号对应的页表条目.
3.如果L1命中,并且对应页表条目有效位为1,则不再访问内存,直接从L1中取出该页表条目的物理页号给MMU.
4.如果L1不命中,则还需访问内存,从真正的页表中找到该虚拟页号对应的页表条目,然后根据有效位判断是否缺页,
如果内存也命中即有效位为1则不缺页,将该页表条目返回L1,从L1中挤出一条相对不重要的记录.
然后MMU再访问L1获得页表条目的物理页号,(这次L1必然命中),然后拼成物理地址
如果发生缺页,也是内存和磁盘之间页的传递,与L1没有关系,无需讨论
然后CPU访问物理地址也因L1有所变化
在没有L1时,MMU拿到物理地址之后会加到地址总线上,但是现在MMU拿到物理地址后还是会先查L1,如果L1命中,则直接取出数据通过内部总线传递给CPU,免去了访问内存和外总线的过程
当L1不命中时才会查内存上的物理地址,然后取出其上的地址,然后在L1中挤掉一个相对不重要的记录,存入该物理地址及其数据的键值对
TLB加速翻译的地址翻译
TLB:翻译后备缓冲器(Translation Lookaside Buffer)
这个玩意儿是加速翻译的,啥意思呢.
如果五秒前我问了别人,"你好"用英语怎么说并获得了回答,
五秒后我要对一个米国人打招呼,这时需要翻译
但凡比鱼聪明点的人都还记得"你好"用英语怎么说,这就是TLB命中
但是如果五秒前我尝试寄了十个甚至更多的英语单词,其中即使包括"你好",五秒后我也不一定能想起来,这就是后来的翻译挤掉了前面翻译的缓存,然后TLB不命中
此时想不起来就应该再问懂哥儿,"你好"用英语怎么说,然后记住,方便奉承下一个米国人用,这就是TLB不命中之后干的事
处理器指定一个虚拟地址交给MMU,MMU首先不会尝试查L1或者内存中的页表进行翻译,而是首先访问TLB,看看刚才是不是已经翻译过并且还记得,如果命中则直接用刚才记住的翻译得到物理地址
如果TLB没有命中则老老实实去查高速缓存页表项,要是再不命中则老老实实去查内存页表,要是还不命中则缺页
可气的是,总有一伙子人能设计地这些缓冲几乎百发百中
手工模拟地址翻译
题目环境
考虑上TLB,L1缓存,手工模拟一个地址翻译过程
系统参数:
这里几路相联,几个组实际上就是将线性的缓存器改成了阵列,按照行列存储
每个页面大小\(64Bytes=2^6Bytes\),一个内存单元\(1Bytes\),因此一个页管理\(2^6\)个地址,页内偏移量就得是一个6位二进制数,剩下的高位才是页号
TLB中缓存的是刚才查过的翻译,可以认为是
[VPN,PPN]
键值对,MMU给出一个VPN,如果TLB中有键为该VPN的键值对,则TLB给出PPN值,此时TLB命中,否则TLB不命中则需要查L1或者内存页表进行地址翻译
这里给出了一个假定的TLB缓存情况
页表是单级结构,这里给出了假定的页表的情况
高速缓存L1通过物理地址字段进行寻址,这里给出了假定的缓存情况
将虚拟地址0x03d7
翻译成物理地址
0xA虚拟地址格式
0xB地址翻译
TLB命中,物理页号0x0D
拼接页偏移得到物理地址
只要是TLB中能找到的记录,都不会缺页,这是因为,最近的记忆会挤掉老的记忆,TLB中只要能找到,说明最近被翻译过
参数 | 值 |
---|---|
VPN | 0xf |
TLB索引(TLBi) | 0x3 |
TLB标记(TLBt) | 0x3 |
TLB命中? | 是 |
缺页 | 否 |
PPN | 0xd |
0xC物理地址格式
0 | 0 | 1 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 1 |
---|
0xD物理内存引用
缓存命中
参数 | 值 |
---|---|
字节偏移CO | 0x3 |
缓存索引CI | 0x5 |
缓存标记CT | 0xD |
缓存命中 | 是 |
返回的缓存字节 | 0x1D |
多级页表的地址翻译
以两级页表为例
单级页表 的时候,一条页表项对应一个虚拟页,一张页表就可以管理整个虚拟内存的页
如果虚拟地址空间更大,页更多,则页表项目更多,而页表也是存放在内存中的,如果页表也大到内存装不下该当如何?
这就好比刚上大学的时候所有的学习资料都可以放在一个文件夹里
但是日积月累,学习资料文件夹变得臃肿,想要找到一个文件就像大海捞针
因此应该建立子文件夹,比如
/reverse
,/pwn
,/web
等等时间长了每个子文件夹又会臃肿,又可以根据时间或者难易程度建立子子文件夹
高级页表的表项是低级页表的索引
这里二级页表和前面的单级页表作用类似,直接索引虚拟页,但是也有些许区别
单级页表结构中只有一个页表,第i个页表项就指向第i个虚拟页
这里二级页表有多个,每个二级页表只对应虚拟内存中的连续的1024个虚拟页,二级页表的表项都是从0开始编号的但是虚拟内存中的页是统一编号的
一级页表的作用是索引二级页表
一级页表的表项,其有效位标志着对应二级页表对应的1024个虚拟页都没有被缓存过
但凡这1024个虚拟页中有一个被缓存,其在二级页表中的表项的有效位为1,则该二级页表对应一级页表中的表项有效位就得是1
只有一级页表是常驻内存的,只有有效位为1表项对应的二级页表才会被搬进内存,不用的时候还得被挤出去
k级页表的地址翻译
一级页表表项中存放的是二级页表的索引,二级页表表项中存放的是三级页表的索引,以此类推,直到最低级页表,其表项才是物理地址,
从高级页表一直索引到低级页表的过程中,但凡有一个页表项的有效位为0则引发缺页
缺页也是逐级修复的,
只有最低级页表项才存放实际的物理页号,然后物理页号和页偏移拼起来组成物理地址
可气的是,虽然有这么多级,但是总有人能设计得它的速度能和单级页表媲美
英特尔酷睿i7/Linux内存系统地址翻译
多级页表结构,每个进程允许有自己私有的页表层次结构
页大小采用4KB,四级页表结构
这里第一次见到"CR3控制寄存器",其作用是指向一级页表的起始位置,CR3是每个进程上下文的一部分,每个进程都有自己独立的页表结构,执行A进程时CR3就应该指向A进程的一级页表起始位置
一级页表可以有多个吗?
还是说只有一个一级页表,管理所有的二级页表,然后进程从二级页表开始有自己的独立地址空间
高级页表(除了直接管理虚拟内存的最低级页表),其结构如下:
最低级页表结构如下
Linux虚拟内存系统
进程的虚拟内存
进程虚拟内存部分(用户栈向下)是进程的独立的虚拟地址空间,
内核虚拟空间被所有进程共享,共享的实现是通过页表的一些表项指向相同的物理地址,然后页表项上标明内核虚拟内存只读
共享库等技术的实现也是基于虚拟内存的,但都是后话了
Linux虚拟内存区域组成
虚拟内存分段,被分段的虚拟内存区域就一定已经被分配了
只要是存在的虚拟页就一定属于某个段,
未使用的虚拟页不会被记录,没有页表指向该位置
task_struct
指向内核运行该进程的所有信息
PID,指向用户栈的指针rsp,可执行目标文件名字,程序计数器rip
mm_struct
描述虚拟内存的当前状态,其中的两个字段pgd,mmap
pgd
指向一级页表的基址
mmap
指向vm_area_structs
链表,该链表的每一个链表项都描述了当前虚拟内存地址空间的一个段.
每个
vm_area_struct
链表项都有五部分组成
1
2
3
4
5 vm_start:段起始地址
vm_end:段结束地址
vm_prot:段的读写权限
vm_flags:段共享或者进程私有标志等信息
vm_next:指向下一个段
当该进程被执行的时候,pgd会被放到CR3
寄存器中
Linux缺页异常处理
CPU指定一个虚拟地址,然后去vm_area_struct
各个表项去查该虚拟地址是否属于\([vm_{start},vm_{end}]\)之间,如果各个链表项都不包含该地址则说明该地址没有被分配,发生段错误
如果没有发生段错误则检查该内存访问的性质,是读还是写,如果对只读区域进行写入则报告保护异常
上述两种情况都通过了则表明这是真的缺页了,使用某种页调度算法牺牲一个物理页,用这个缺页挤掉
内存映射
啥是内存映射呢?
以共享虚拟内存举例
两个进程的页表相互独立,但是可以指向物理内存中的同一区域,比如共享库
通过将一个虚拟内存区与磁盘上一个对象关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射
共享对象
如果同时打开多个终端bash,它们会共享同一块只读代码区.多个程序调用库函数printf,但是实际最后调用到的printf只在物理内存中唯一存在
通过一个例子说明共享对象的过程
一开始时共享对象也只是虚拟内存上的一些页或者说段,尚未被任何进程映射.
现在进程1将共享对象映射到自己虚拟内存中的某个位置,并在物理内存中建立了物理页
现在进程2也想映射共享对象,内核判断进程1已经映射过该共享对象,即共享对象已经存在于物理内存中,那么只需让进程2的相关页表项指向该共享对象在内存中快照的物理页
私有写时复制对象
什么时候一个对象不得不每个进程分别映射到不同的物理内存了,什么时候才会真的在物理内存上开两个对象的空间
在两个进程都没有尝试向私有对象写东西时,私有对象表现得和共享对象没有区别,物理内存中也只存在一份拷贝,因为这足以满足读的要求
当其中一个进程试图写私有对象时,由于要保证进程的虚拟地址空间独立,因此不得不将该私有对象做一个拷贝,在内存中存放两个私有对象,两个进程的页表项目各自挑一个指向
fork函数
1 | pid_t fork(void);//子进程返回0,父进程返回子进程的pid,出错返回-1 |
1.子进程获得一个pid
2.子进程得到与父进程一模一样的用户级虚拟地址空间拷贝
用户级虚拟地址空间包括
代码段
数据段
堆
共享库
用户栈
3.子进程获得与父进程任何打开的文件描述符的副本
1 |
|
运行结果:
1 | actived #fork函数前只有父进程,fork之后才会两个进程都执行 |
子进程和父进程都打印到控制台表明两者共享文件描述符1(标准输出)
内存动态分配
动态内存分配器(dynamic memory allocator),维护进程的堆区
堆顶指针为brk,堆的生长方向与栈相反,堆从低地址向高地址生长,栈从高地址向低地址生长
都是小端模式
分配器将堆看成一组大小不同的块集合,每个块是一个连续的虚拟内存片
已分配的块就是正在被使用的块,空闲块就是尚未被使用的块,可以被分配
根据谁来释放分配块,分配器可以分成两种
显式分配器:比如C语言的malloc和free,C++的new和delete,要求程序员手动释放分配块
隐式分配器:分配器自动回收不再使用的分配块,因此隐式分配器又叫垃圾收集器,比如Java中的分配器
关于分配器的实现,这个实验是一定要做到的,但不是现在
习题订正
1.
这个题的问法太屑了,相当于给你说已知a是b他老子,问a他爹是谁
需要注意的是逻辑地址和物理地址是啥
逻辑地址就是磁盘交换分区,虚拟地址空间中的一个地址,
物理地址就是内存条子,物理地址空间中的一个地址
每个.c源程序编译成.o可重定位目标文件之后,其虚拟地址空间都是从0开始编址
然后多个.o(也有可能有.a静态库文件)链接成.out可执行目标文件,其虚拟地址空间还是从0开始编址的,比如\([0,0x100)\)
在装载时(shell调用execve函数将进程加载进入物理地址空间),有可能将该.out虚拟地址空间的0号字节装载进入内存条子这个物理地址空间的0x100处,那么该进程对应的物理地址空间就可能是\([0x100,0x200)\)(不考虑分页),如果考虑分页,那么虚拟地址空间可能被划分成多个虚拟页,在用到时被拷贝到一个物理页.
综上,形成逻辑地址的阶段是链接阶段
翻译成物理地址的阶段是装载
4.
本题我选的B,纯粹是瞎选,选的时候就知道必定不对
出错是因为没有重视覆盖与交换技术,在此做一个复习
覆盖:
铁打的固定区,流水的覆盖区,用到谁就先把覆盖区存一下,然后把需要的从外存中拎出来,直接盖在先前的覆盖区上
算法竞赛中使用滚动数组,感觉也是覆盖的思想
交换:
在CSAPP上我们学过虚拟内存,缺页时发生替换的思想就是交换思想
这样看覆盖和交换都是新的替换旧的,好像说的是一个事情,但是区别:
只有虚拟内存技术可以物理上拓展主存容量,而覆盖和交换技术在虚存之前就存在了
覆盖和交换还是在有限大的内存条子上做文章,只能通过扔掉当前用不到的,实现节省主存空间的作用
9.
这个题A,B选项都不太熟悉,借机了解一下存储管理方式吧
存储管理方式从进程地址空间的连续性上分为连续方式和非连续方式
连续方式,不行
连续方式包括单一连续分配(单道连续分配),固定分区分配(多道固定连续分配),连续动态分配(多道可变连续分配)
单道连续分配是最早最low逼的方式,整个内存条子上最多允许一个进程独占
多道固定连续分配意思是内存条子上划出几个块,每个块让一个进程独占,各个进程老死不相往来
非连续方式,行
非连续方式包括分段,分页,段页,都是比较近或者最近的操作系统正在使用的技术,比较熟悉
比如x86上使用段页式结构,x86-64上使用分页结构
14.
这个题要清晰重定位的各种类型,谁来负责重定位,什么时候重定位
什么是重定位?装入时对目标程序中指令和数据的修改过程
一定要重定位吗?不一定,早期low逼程序和low逼内存系统上,程序编译完成之后一个各个变量指令的物理地址就知道了,不存在逻辑地址一说,比如在单道连续分配的内存系统上,一共就只有一个进程执行,整什么逻辑地址真是多次一举
凡是需要重定位的,一定有逻辑地址和物理地址的区分,重定位的过程就是将逻辑地址翻译成物理地址这个地址变换的过程
本题中A和B,都是连续存储方式,只要确定好程序在物理内存中的基地址,那么程序所有指令数据的地址就都确定了,根本不需要重定位,编译时决定物理地址即可
重定位的类型?静态重定位和动态重定位
静态重定位指地址变换在装载时一次完成
本题中D,段式结构就是静态重定位
动态重定位指地址变换在运行时才会进行
用于分页系统,因为虚拟页实际加载进入哪一个物理页,这由操作系统页面置换算法决定,不到运行时,是不知道虚拟页到底被加载到哪里的,该虚拟页上的数据和指令自然无法被重定位
18.
我一开始选的是地址映射,我是这样考虑的
x86-64上任何进程都是从0x400000
这个地址开始的,通过地址映射,该虚拟地址被翻译成不同的物理地址,因此不会出现两个进程虚拟地址空间指向同一块物理地址空间的情况,即避免了进程的相互干扰
我选的D,但是答案是B
意思是一个进程有一个基址寄存器和边界寄存器,该进程内任意内存访问不得超过两个寄存器规定的范围,因此由内存保护实现
21.
感觉这种题没有什么意义,让学生考虑段表和页表的大小,
用户物理地址空间=总空间-页表或段表表占的空间
你直接问页表大还是段表大不就行了?
你直接问页和段谁大小可变不就行了?
非得绕着弯说,根考察三年级学生两年后你和你老子谁年龄大一样.
你不就是想考察段大小可变,页大小固定这个事儿吗
24.
单道系统上某一时刻只有一个程序在运行,只需要维护一个重定位寄存器,谁在执行就把谁的基地址放到重定位寄存器上
26.
还是考察对四个选项概念是不是认识
"可变分区"实际上还是连续内存的low逼方法
前面14题已经分析过了,分页存储管理是动态链接的
"有利于动态链接"不如说"必须动态链接"
28.
考察对"可重入"的理解
CSAPP上我们学过"可重入"函数,就是不访问临界区的线程安全函数
既然不访问临界区,那么多个进程共享这一块也是没问题的,
只需要将共享区放在内存上,让有需要的进程引用本共享区,不需要每个进程分别拷贝一份
怎么就减少对换数量了呢?
啥叫"兑换"?不是缺页置换
不需要每个进程都加载共享区,一共加载一次就可以
29.
这里"代价"是啥呢?
分段时需要操作系统维护段表,分页时需要维护页表
段表和页表也要占用内存,这就是代价
知道的越少,人越觉得自己厉害,叫什么穷开心不是吗?
分区是最早的内存管理方式,只需要维护有几个区,每个区放了啥.并且区相对于页和段大得多,操作系统维护的区表相对于页表段表会小很多,因此代价少
34.
前两条说的是真对,需要注意的是内碎片和外碎片是啥
第三条,影响磁盘访问时间的主要因素是啥呢?页面多大则需要从磁盘中拷贝出相应大小的页面,拷贝的越多用时越长,怎么就"主要因素通常不是页面大小了",III项纯粹故意说很长的假话吓唬人
那么影响磁盘访问时间的主要因素是啥呢?
寻道时间、旋转延迟、数据传输时间
页面大小会影响数据传输时间,当然是主要因素
35.
这个题的ACD三要都是白给
这让我想到高三做过的一道化学题
"取10.00ml稀释液的过程中,酸式滴定管的初始页面为0.20ml,左手控制活塞向锥形瓶中加稀释液,此时眼睛应该____
"
你说写"睁着"吧,理论上也对,闭着眼儿万一倒手上把手烧个窝儿,还必须得睁着眼儿
那为啥不能写"睁着"呢?这睁着不是废话吗.这个题就想考察会不会说滴定流程的套话
在本题中我一开始选的A,这也是废话,页面大小一定得依据内存大小确定啊,要是内存4G,你整一个8G大小的页面有个锤子用呢?
本题就想考察"页面大小固定"这个事儿
但是D一定是不对的,外存就是一群乌合之众没有排面,内存少而精,外存一定是为内存服务的
C对不对呢?也有道理,比如如果数据总线宽度为64位,那么页面大小就1Byte占8位,都不够cpu拿来塞牙缝的.但是出题的认为这都不用说,是废话
36.
什么叫不懂装懂啊?
"方便操作",怎么操作,什么操作?你把"操作"去掉也是一个意思
就说"A.方便"
不妨把话说的更明白一点
"A.好"
好个球子啊
因为BCD这些优点显而易见,出题的实在想不到说个啥缺点,就整了一个"A.方便操作"
实际上这个答案啥也没说,就好比让你评价一下学校对卢雷事件的处理,你说了个"好!",怎么好了?给校风学风带来啥影响?给其他学生有啥影响?给外界啥影响?你是一个字不说,因为现在的处理方式吐不出象牙来
39.
14题复习了重定位之后显然这个题选B,但是C和D我怎么没在教材上见到过
因为压根就没这两个概念.
这不就是某些政治家某些上级以及某些令人恶心的文科学科的口头禅吗
正确的,直接的,中肯的,雅致的,客观的,完整的,立体的,全面的,辩证的,形而上学的,雅俗共赏的,一针见血的,直击要害的,错误的,间接的,虚假的,庸俗的,主观的,残缺的,平面的,片面的,孤立的,辩证法的..
落实,夯实,搞好,坚持,推进,改善,提高....
美国化,本土化,最大化,冲国化...
政治跨考计算机的是不是就要选C了