信息安全系统设计基础第十四周学习总结
虚拟存储器
提供的三个重要能力
1、将2主存看成是一个储存器在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,高效实用主存
2、为每个进程提供了一点的地址空间,从而简化了储存器管理
3、保护了每个进程的地址空间不被其他进程破坏
9.1 物理和虚拟寻址
计算机系统的主存被组织成由M个连续的字节大小的单元组成的数组,每个字节都有唯一的物理地址
物理寻址和虚拟寻址
9.2 地址空间
是一个非负整数地址的有序集合。如若地址空间中的整数是连续的,那么这就是一个线性地址空间
地址空间区分了数据对象(字节)和他们的属性(地址)
基本思想:允许每个数据对象有多个独立的地址,其中每个地址都选自一个不同的地址空间
9.3 虚拟存储器作为缓存的工具
任何时刻,虚拟页面的结合都被分为三个不相交的子集:
1.未分配的
2.缓存的
3.未缓存的
9.3.1 DRAM缓存的组织结构
DRAM缓存表示虚拟存储器系统的缓存,他在主存直观缓存虚拟页。DRAM是全相连的,所以任何虚拟页都可以放在任何的物理页中
9.3.2 页表
页表将虚拟页映射到物理页,每次地址翻译硬件讲一个虚拟地址转换为物理地址时都会读取页表
页表的基本组织结构:就是一个页表条目PTE
虚拟地址空间中的每个页在页表中一个固定偏移量处都有一个PTE
9.3.3 页命中
9.3.4 缺页
DRAM缓存不命中
9.4 虚拟存储器作为存储器管理的工具
虚拟地址大大的简化了存储器管理,并提供了一种自然保护存储器的方法好。多个虚拟页可以映射到同一个物理页面上
按需页面调度和独立的地址空间的结合,对系统中成存储器的使用和管理造成了深远的影响
·简化链接
·简化加载
·简化共享
·简化存储器分配
9.5虚拟存储器作为存储器保护的工具
每次CPU生成一个地址时,地址翻译硬件都会读一个PTE,所以字啊PTE上添加额外的许可位来控制对一个虚拟页面的访问
9.6 地址翻译
地址翻译符号小结
页面命中时,CPU执行步骤:
第一步:处理器生成一个虚拟地址,并把它传送给MMU
第二步:MMU生成PTE地址,并从高速缓存/主存请求得到他
第三步:高速缓存/主存向MMU返回PTE
第四步:MMU构造物理地址,并把它传送给高速缓存/主存
第五步:高速缓存/主存返回所请求的数据字给处理器
处理缺页,要求硬件和操作系统协作完成
第一步到第三步同上
第四步:PTE中的有效位是0,MMU触发异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序
第五步:程序确定物理存储器中的牺牲页,如果页面被修改,则换出到磁盘
第六步:程序页面调入新的页面,并更新存储器中的PTE
第七步:程序返回到原来的进程,再次执行导致缺页的指令
9.6.1 结合高速缓存和虚拟存储器
主要的思路:地址翻译发生在高速缓存查找之前
9.6.2 利用TLB加速地址翻译
翻译后备缓冲器TLB:是一个小的,虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块,具有高度的相连性
9.6.3 多级页表
用来压缩页表的常用方法是使用层次结构的页表,这种方式从两个方面减少了存储器要求:
第一点:节约,如果一级页表中的PTE是空的,那么相应的二级页表就不会存在
第二点:减压,只有一级页表存在主存中,只有经常使用的二级页表才需要缓存在主存中
9.8 存储器映射
存储器映射:Linux通过将一个虚拟存储器区域与一个磁盘上的对象关联起来,一初始化这个虚拟存储器区域的内容,可以映射到两种类型的对象中的一种:
1、unix文件系统中的普通文件
2、匿名文件(请求二进制零的页)
无论哪种情况,一旦一个虚拟页面被初始化可他就在一个由内核维护的专门的交换文件之间转换,交换文件也叫交换空间,在任何时刻,交换空间都限制着当前运行着的进程能够分配的虚拟页的总数
9.8.1 再看共享对象
一个对象可以被映射到虚拟存储器的一个区域,要么作为共享对象要么作为私有对象。
私有对象是使用一种叫做写时拷贝被映射到虚拟存储器的。
9.8.3 再看fork函数
当fork函数被当前进程调用时,内核为新进程创建各种数据结构并分配给她唯一的PID
当fork在新进程中返回时新进程现在的虚拟存储器刚好和调用fork时存在的虚拟存储器相同
再看execve函数
加载并运行a.out的步骤:
·删除已存在的用户区域
·映射私有区域
·映射共享区域
·设置程序计数器
9.8.4使用mmap函数的用户级存储器映射
mmap函数要求内核创建一个新的虚拟存储器区域,最好是从地址start开始
#include<unist.h>
#include<sys/mman.h>
void *mmap(void *start,size_tlength,int prot,int flags,
int fd ,off_t,offset)
munmap函数删除虚拟存储器的区域
9.9 动态存储器分配
动态存储器分配器维护着一个进程的虚拟存储器区域,称为堆。
分配器的两种风格:
显示分配器
隐式分配器(垃圾收集器)
9.9.1 malloc和free函数
malloc函数从堆中分配块
#include<stidlb.h>
void *malloc(size_t size)
返回一个指针,指向大小为至少size字节的存储器块,这个块可能会包含字啊这个块内的任何数据对象类型做对齐
动态存储器分配器还可以通过使用mmap和munmap函数,显式的分配和释放堆存储器,还可以使用sbrk函数
#include<unist.h>
void *sbrk(intptr_t incr)
free函数释放已分配的堆块
9.9.2 为什么要使用动态存储器分配
原因是经常执行到程序实际运行时才能知道某数据结构的大小,硬编码不便于维护。一种更好的方法是在运行时,在已知了n的值之后,动态的分配这个数组
9.9.3 分配器的要求和目标
约束条件:
处理任意请求序列
立即响应要求
只使用堆
对齐块
不修改已分配的块
相互冲突的两个目标
目标1、最大化吞吐率
目标2、最大化存储器利用率
9.9.4 碎片
利用率低的主要原因是碎片,碎片分为内碎片和外碎片
这个在操作系统中有学过不再浪费时间了
9.9.6 隐式空闲链表
一个块是由一个字的头部,有效载荷以及可能的一些额外的填充组成
优点:简单
缺点:任何操作的开销
很重要的一点就是意识到系统对齐要求和分配器对块格式的选择会对分配器上的最小块大小有强制性要求
9.9.7 放置已分配的块
分配器执行这种搜索的方式是由放置策略确定的,常见的策略
首次适配
优点:将最大的空闲块保留在链表的后面
缺点:在靠近链表起始处留下小空闲的碎片,增加较大块的搜索时间
下一次适配
优点:运行速度快
缺点:利用率低
最佳适配
优点:利用率好
缺点:要求对堆进行彻底的搜查
9.9.8 分割空闲块
9.9.9 获取额外的堆存储器
分配器额外的存储器转换成了一个大的空闲块,将这个块插入到空闲链表中,然后被请求的块放置在这个新的空闲块中
9.9.10 合并空闲块
假碎片:邻接的空闲块可能发生的现象。
为了解决假碎片,任何实际的分配器都必须合并相邻的空闲块,这个过程就叫合并,重要的合并策略:立即合并和推迟合并
9.9.11 带边界标记的合并
9.9.12 实现一个简单的分配器
- 一般分配器设计
- 操作空闲列表的基本常数和宏
- 创建初空闲链表
- 释放和合并块
- 分配块
9.9.13 显式空闲链表
将空闲块组织成某种形式的显式数据结构
释放一个块的时间是线性的,取决于空闲链表中块的排序策略
后进先出:在一个常数时间内完成
地址顺序:比首次适配有更高的利用率,接近最佳适配的利用率
9.9.14 分离的空闲链表
分离存储:维护多个空闲链表,其中每个链表中的块由大致相等的大小
基本的方法:
简单分离存储:每个大小类的空闲链表包含大小相等的块,每个块的大小就是大小类总最大元素的大小
优点:分配和释放都是很快的的常数时间操作
每个块只有很少的存储器开销
已分配块不需要头部脚部
最小块大小就是一个字
缺点:容易造成内部和外部碎片
不会合并空闲块
分离适配:分配器维护着一个空闲链表的数组
优点:快速高效
伙伴系统:分离适配的特例
9.10 垃圾收集
垃圾收集器是动态存储分配器,自动释放程序不在需要的已分配块
9.10.1 垃圾收集器的基本知识
根节点和堆节点
9.10.2 makr&sweep垃圾收集器
makr&sweep垃圾收集器由标记阶段和清除阶段
9.11 C程序中常见的与存储器有关的错误
9.11.1 间接引用坏指针
进程的虚拟地址空间有较大的洞,没有映射到任何有意义的数据 经典的scanf错误
scanf("%d",&val)---->scanf ("%d",val)
9.11.2 读未初始化的存储器
代码
9.11.3 允许栈缓冲区溢出
使用fgets函数限制输入串的大小
void bufoverflow()
{
char buf[64]
gets(buf)
return;
}
9.11.4 假设指针和他们指向的对象是相同大小
int **makeArray1(int n ,int m)
{
int i;
int **A = (int **)Malloc(n sizeof(int))
for(i = 0;i < n; i++)
A[i] = (int *)Malloc(m *sideof(int))
return A;
}
9.11.5 造成错位错误
int **makeArray2(int n ,int m)
{
int i;
int **A = (int **)Malloc(n sizeof(int))
for(i = 0;i <= n; i++)
A[i] = (int *)Malloc(m *sideof(int))
return A;
}
9.11.6 引用指针而不是塔所指的对象
9.11.7 误解指针运算
int *search(int *p,int val)
{
while(*p && *p,int val)
p + = sizeof(int);
return p
}
9.11.8 引用不存在的变量
int *stackref()
{
int val;
return &val
}