0 回顾
- 操作系统采用分段管理内存,用户也是分段
- 操作系统对物理内存是分页管理
- 所以这两种机制应该结合
- 引出段页结合
1 段页结合
- 段怎么工作? 程序在内存当中割出几个区域
- 然后将程序中的段与区域建立几个映射,比如说把代码段放到什么区域,数据段放到什么区域
- 这样,一个应用程序就放到了段中
- 页怎么工作?
- 把物理内存打散成一页一页固定大小的片
- 假如现在我们的程序也是两个段,怎么把这两个段放到物理内存当中呢?
- 也把它进行打散,比如把一段打散成两个页,然后进行映射
- 怎么结合?
- 可以看出,打散的那一段,就是上方段的内存中的一片
- 这样就将段页进行结合了
- 中间的那个中介内存是什么?肯定不是物理内存,但是很像物理内存,因为物理内存肯定是进行到最后一步了,这还在中间的那一步,所以引出虚拟内存的概念
- 在地址空间中割出一段来给这个程序,给程序中的一段,这个程序中的一段就到了我这个地址上,所以再将地址空间映射到实际的物理地址,就实现了段页的结合
1.1 虚拟内存
- 程序员希望用段,物理内存希望用页,所以尝试段、页结合
- 分段:代码分段+内存分区+段表
- 分页:内存分页+(多级)页表
- 两者组合,让前者内存分区的输出作为后者分页的输入
- 关于逻辑地址、线性地址、虚拟地址、物理地址的理解
- 在用户眼里,虚拟内存当中的就是段
- 在操作系统当中,把这个打散了再一页一页的放在这里
- 所以既支持段,也支持页
- 综上,应用程序有代码段,首先在虚拟内存当中割出一块区域给这个段,然后在操作系统当中虚拟内存的段,再打散成一页一页,然后再和页关联在一起,所以现在用户的代码段已经放到了内存里
- 疑问:为什么不能直接每一个代码段分别分页呢?而是需要虚拟内存作为桥梁呢?
分页过程输入的地址都是一个规整的32位/64位地址;而使用了段的用户代码中,所有地址都是由段地址和段内偏移组成的;把段地址和段内偏移组成的地址先转化为一个规整的32位/64位地址,把这个地址称为虚拟地址,虚拟地址所在的空间称为虚拟内存,就自然而然的引出了虚拟内存的概念
每个用户代码都有多个段,如果直接给段分页,进程的每一个段需要都维护一个页表,经过虚拟内存的中介,只需要给虚拟内存维护一个页表
分页机制已经解决了外部碎片的问题,将段首先映射到虚拟内存,让分页机制的内部碎片更少,让分页机制的空间浪费更少,假设一个程序共有2047KB,分为两个段,1个段是1025KB,一个段是1022KB;如果经过虚拟内存的中介,只需要放到两页上;如果直接给段分页,就要放到3页上
1.2 重定位
- 先从csip取到虚拟地址,再根据页号和偏移进行
- 第一次地址翻译是分段的,第二次是分页的
- 段、页同时存在时的重定位(地址翻译)
- 疑问:段表和页表分别是什么时候初始化的?
- 首先,查询段表,转化为虚拟地址
- 然后,查询页表,转化为物理地址
- 再次提到实验6
1.3 实际的段页内存
- 看代码,看一个实际的段、页式内存管理是怎么实现的
- 程序放入内存,之后开始取址执行
1.4 fork()
- 怎么在虚拟内存中割出一段?
- 首先,使用之前内存分区中讲得分区适配算法,对虚拟内存进行分区,对程序的各个段进行适配,建立段表
- 然后,对虚拟内存占用的空间进行分页,分配物理内存的页,建立页表,同时程序也就载入了内存
虚拟地址从高到低被划分为段号、页号、页内偏移地址,而映射过程是通过拆分出虚拟地址里高位的段号 通过该进程的段表找到该段号对应的页表的存放内存地址 之后通过虚拟地址的页号找到实际的页框号 再将实际的页框号拼接上虚拟地址低位的偏移地址,得到物理地址。
- 整个过程分为几步?
- 在虚拟内存当中割一段出来
- 把用户代码假装放到这里来,怎么假装?建立段表,明确映射关系
- 在物理内存当中,找一段空闲区
- 建立页表,再加上真正的磁盘读写就能把代码真正转移到物理内存当中
- 可以用重定位来使用内存
建立段表映射 -> 建立逻辑段->找到空闲物理内存,建立逻辑段到物理内存页的映射->磁盘读文件
- 分别用段表以及页表来记录区域是怎么对应的
- 从
fork()
开始讲解代码- 之前讲过
fork()
的源码,可以回顾一下 - 在
copy_process
中,调用了copy_mem
copy_mem
,nr
是进程的编号(第几个进程),p
是PCBnew_data_base
就是每个进程在虚拟内存中的起始地址,每个进程占64M虚拟地址空间,互不重叠,给new_data_base
赋值就是在虚拟内存中分区set_base
就是在设置段表基址(段表中存放着每个段对应的基址),在这里的实现中,数据段和代码段是同一个段,这一步就是在建立段表
- 图示
- 老师说这种分割虚拟内存的方法比较弱智,但是学习起来很简单,适合入门
- 由于所有进程的虚拟地址互不重叠,所以所有进程可以共用一套页表;但是现在的操作系统,进程的虚拟地址是会重叠的,所以每个进程应该维护自己的页表
- 之前讲过
1.5 建表
- 分配内存、建页表
- 分配虚存、建段表;分配内存、建页表
copy_page_tables
,子进程和父进程共用物理内存的页,所以子进程实际上不需要新分配内存,直接建页表即可- 首先明确这里是32位地址空间
- 实参
old_data_base
对应形参from
,表示父进程的虚拟内存的起始地址,from
右移20位,和0xffc进行位与,结果赋给from_dir
- 右移22位,得到了页目录号,再乘以4,得到顶级页目录中的索引位置(顶级页目录中的每一项占4字节),所以代码里直接写了右移20位
from_dir
就是指向父进程的第一个二级页目录的指针,*from_dir
就是父进程的第一个二级页目录
- 实参
new_data_base
对应形参to,表示子进程的虚拟内存的起始地址,to右移20位,和0xffc
进行位与,结果赋给to_dir
to_dir
就是指向子进程的第一个二级页目录的指针,*to_dir
就是子进程的第一个二级页目录
siz
e先加上0x3fffff
,然后右移22位,结果赋给size
本身size
表示有多少个二级页目录
from先右移22得到已使用的页目录号,因为每个页目录号对应一个页目录,一个页目录是4字节,所以再乘以4(字节)就得到已使用页目录项所占据的长度,因为地址是从0开始的,这个长度也是下一目录项的起始地址
右移22位拿到了第多少项,相当于拿到了数组下标,具体元素在哪个位置要用下标✖️单个元素大小
- 循环size次,给子进程的二级目录分配页
get_free_page()
使用内嵌汇编从空闲列表中取出一页
- 按照父进程的每一个二级页目录,建立子进程的每一个二级页目录
- mem_map[this_page]++,有点像引用计数的概念,两个虚拟内存页共享一个物理内存页
- 这里用了写时复制的方法
- 图示
- 进程1fork得到进程2,它们的虚拟内存指向了同一块物理内存空间,一页一页对应
- 段表、页表建立之后,就可以使用它们进行地址翻译了
- 查段表、页表进行地址翻译的工作由MMU完成
- 写时复制,进程2的*p=8,会写到新的页里面
- 我的理解:子进程如果没有写操作,就可以以只读的方式共享父进程的内存页;如果有写操作,就会使用写时复制,产生子进程独立的内存页,并且需要修改页表
2 总结
下次再听一遍