一、内存使用
内存使用:将程序放到内存中,PC指向开始地址
重定位:
- 修改程序中的地址
- 逻辑地址 ----> 物理地址
什么时候完成重定位?
- 编译时(不灵活,程序只能放在内存固定位置、速度快):嵌入式系统
- 载入时(灵活,速度相对来说慢):普通系统
程序载入内存后,还需移动:交换(swap)
重定位最合适的时机 —— 运行时重定位
地址翻译:每执行一条指令,都要从逻辑地址算出物理地址
内存使用过程:
- 程序编译好(地址不改)
- 程序执行(创建进程、创建PCB)
- 找空闲内存
- 空闲地址(起始地址)赋给PCB
- PC设在起始地址
- 执行(执行过程中每个地址都要地址翻译)
二、内存分段
程序员眼中的程序:由若干段组成,每个段(堆栈段:向下增长;程序段只读;数据段可写;函数库……)有各自的特点、用途
- 用户可以根据各段自身特点分治每个段
- 程序分段放入内存符合用户观点,提升内存使用效率
定位具体指令:< 段号 (ds) : 段内偏移 >
每个进程的PCB放一堆基址,分别对应有哪些段,每个段都有基址
进程段表:PCB中用来存储每个段的基址
- GDT:OS对应的段表
- LDT:每个进程对应的段表
程序分段使用内存:
- 程序分段,每段放入一段内存
- 每段基址放入LDT表,即初始化LDT表完成
- LDT表(或者LDT表表头指针:???不确定,求解答)赋给PCB
- 根据PC指针 + LDT表取指、执行
- 执行每条指令时,查找LDT表,根据表中基址 + 程序中逻辑地址找到物理地址
- 时间片用完后或其他情况引发进程切换(switch)
- 每段段基址写回LDT表和PCB
- LDTR寄存器切换
- 段选择子被装入LDTR,即LDT表描述符自动被装入LDTR
- 根据LDTR+PC执行
LDT表、GDT表、LDTR、段选择子详解:
- LDT表可以有若干张,每个进程可以有一张,我们可以这样理解GDT和LDT:GDT为一级描述符表,LDT为二级描述符表
- LDT嵌套在GDT之中
- LDTR记录局部描述符表的起始位置,LDTR的内容是一个段选择子。由于LDT本身同样是一段内存,也是一个段,所以它也有个描述符描述它,这个描述符就存储在GDT中,对应这个描述符表也会有一个选择子,LDTR装载的就是这样一个选择子
- LDTR可以在程序中随时改变,通过使用lldt指令
例:
如果我们想在表LDT2中选择第三个描述符所描述的段的地址12345678h
- 首先需要装载LDTR使它指向LDT2 使用指令lldt将Selector2装载到LDTR
- 通过逻辑地址(SEL:OFFSET)访问时SEL的index=3代表选择第三个描述符;TI=1代表选择子是在LDT选择,此时LDTR指向的是LDT2,所以是在LDT2中选择
- 此时的SEL值为1C (二进制为11 1 00):OFFSET=12345678h。逻辑地址为1C:12345678h
- 由SEL选择出描述符,由描述符中的基址(Base)加上OFFSET可得到线性地址,例如基址是11111111h,则线性地址=11111111h+12345678h=23456789h
- 此时若再想访问LDT1中的第三个描述符,只要使用lldt指令将选择子Selector1装入再执行2、3两步就可以了(因为此时LDTR又指向了LDT1)
三、内存分区与分页
1.可变分区管理:在使用内存时,维护下面两个表:
- 空闲分区表:始址、长度
- 已分配分区表:始址、长度、标志(标记哪个进程使用该内存)
(1)当申请的内存大小,在空闲分区表中有多个内存区间满足时:
- 最佳适配(O(n)):最接近于申请的内存大小的分区
空闲分区会被分割成细小碎片
- 最差适配(O(n)):在满足内存大小前提下,选择与申请内存相差最大的
空闲分区会被均匀分割
- 首先适配(O(1)):在空闲分区表中找到的第一个满足申请内存大小的分区
表查的快、运行速度快
如果OS中的段内存申请很不规则(有时需要很大的一个内存块,有时又很小),最佳适配算法最好
分段 ------> 虚拟内存
分页 ------> 物理地址
- 可变分区会造成内存碎片,内存使用效率不高
- 内存紧缩:将空闲分区合并,需要移动一个段(复制内容)
- 内存碎片依靠内存紧缩解决,内存紧缩耗时长
2.内存分页
- 将物理内存分成页(4K),不需要内存紧缩,一个程序段最大的内存浪费是4K(一页,无限接近于4K)
- 页中的地址需要重定位,页表记录
0x2240: 2240 / 4K = 商……余数
- 商为页号(12位, 0x2240右移12位 --->2)
- 余数为页内偏移(后3位 --->240)
0x3240: 页框号 *4K = 物理地址 = 页框号左移12位+页内偏移
内存使用分页和内存分段使用原理相同,只是分页分的是物理内存,分段分的是程序段;分页用的不是LDT表存放相关信息,用的是页表;寄存器也为CR3寄存器
四、多级页表与块表
为提高内存空间利用率,也应该小,但是页小,页表就大了
1.页表中只存放进程用到的页:
查找慢、运行速率低,故不可以只放进程用到的页;页表中的页号应连续,这样查找就为页表起始表头+偏移量,查找方便,但是这样占用内存多,浪费
2.多级页表:页目录表+页表
- 2^10 * 4KB = 4M = 2^10 * 2^2 * 2^10 = 2^20 * 2^2 = 4M(2^10个页指向的内存物理空间
- 4字节地址为32位
多级页表提高了空间利用率,但增加了访存的次数,每增加一级页表,访存次数增加1次
TLB(快表)是寄存器,一组相联快速寄存器
五、段页结合的实际内存管理
- 段面向用户
- 页面向硬件
虚拟内存映射到内存这一过程对用户是透明的
1.段页同时存在的重定位
内存管理核心:内存分配
- 进程申请内存
- 虚拟内存采用分区法分割部分虚拟内存给进程
- 代码段放入虚拟内存,并建立段表(即完成段表初始化)
- 代码段分割,放入页
- 建立页表(即页表初始化)
- 重定位使用内存
0x300:逻辑地址 通过段表找到虚拟内存地址 ;虚拟内存地址通过页表找到物理地址
六、代码分析
- 每个进程占64M虚拟地址空间,互不重叠(理论上):进程不重叠,页号不重叠,可共用一套页表;实际:基本重叠
- from_page_table 和 to_page_table 分开,即将父子进程页表分开,两个页表内容相同
- 父子进程共用虚拟内存
- 父子进程各有一套页表,内容完全一样
- 子进程在写入内存前要改写页表,实现父子进程分离
- 父子进程LDT表不同;刚初始化时,附近进程页表内容相同;运行时,修改子进程页表,实现父子进程内存分离
- 进程放入内存页(逻辑地址 ----> 物理地址)
七、内存换入——请求调页
实现虚拟内存需要内存换入换出
- 用户可随意使用虚拟内存,就像单独拥有所有内存
- 虚拟内存 通过OS映射,用户不知道这个过程:透明 转成物理内存
1.请求调页
2.一个实际系统的请求调页
(1)请求调页,缺页中断(14号中断)
- IDT表:中断向量表
(2)处理中断:中断现场保护(压栈)
(3)do_no_page:
- 虚拟地址页内偏移置0(address&=0xfffff000),得到虚拟页号
- 申请物理空闲页,得到空闲页:get_free_page
- 从磁盘读入页到空闲页
- 物理页、虚拟页建立映射(写页表):put_page
八、内存换出
算法评价准则:缺页次数
1.FIFO页面置换
2.MIN页面置换:理想算法,无法预测将来
- 选择最远将使用的页淘汰,是最优方案
3.LRU页面置换:利用程序局部性,公认的很好页面置换算法
- 最近最常未使用的页淘汰
(1)LRU的准确实现:时间戳(time step)
- 每页维护一个时间戳:时间戳实行时间长,时间片很可能溢出
(2)LRU的准确实现:页码栈:时间长,代价大
LRU准确实现代价大,所以要用近似实现
(3)LRU近似实现——将时间计数变为是和否(cloak算法)
- 每个页增加1个引用位(R位):每访问1页,引用位置为1(最近访问过)
- 每次访问一页时,硬件自动设置该位
- 选择淘汰页:扫描该位,为1时清0,并继续扫描,为0时淘汰该页
4.cloak算法的分析与改造(最近没有使用)
(1)如果缺页很少,会所有R=1:记录了太长的历史信息
- hand scan(指针)一圈后,淘汰当前页,将调入页插入hand位置,hand前移1位,退化为FIFO
- 解决方法:定时清除R位
- 再来一个扫描指针,且该指针移动速度快
- 清除R位指针移动速度 > 用来选择淘汰页移动速度
5.给进程分配多少页框
- 分配多:请求调页导致的内存高效利用就没用了
- 分配少:系统出现颠簸/抖动(CPU利用率急剧下降的现象)
- Belady现象:对有的页面置换算法,页错误率可能会随着分配帧数增加而增加
- FIFO会产生Belady异常。
- 栈式算法无Belady异常,LRU,LFU(最不经常使用),OPT都属于栈式算法
实际中,OS根据实际情况来调整页框数
操作系统梳理:
swap in / swap out -----> 虚拟内存 -----> 段页内存管理 -----> 程序载入 -----> 进程(多进程图像)