20145312《信息安全系统设计基础》第14周学习总结
教材内容总结
第九章 虚拟存储器
- 虚拟存储器是计算机系统最重要的概念之一,它是对主存的一个抽象。
- 三个重要能力:
它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,高效的使用了主存
它为每个进程提供了一致的地址空间,从而简化了存储器管理
它保护了每个进程的地址空间不被其他进程破坏
9.1 物理和虚拟寻址
1.物理地址
- 计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组,每字节都有一个唯一的物理地址PA。
- 根据物理地址寻址的是物理寻址。
2.虚拟地址 - 虚拟存储器被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。
- 使用虚拟寻址时,CPU通过生成一个虚拟地址VA来访问主存,这个虚拟地址在被送到存储器之前先转换成适当的物理地址(这个过程叫做地址翻译,相关硬件为存储器管理单元MMU)地址空间
9.2 地址空间
1.地址空间
- 地址空间是一个非负整数地址的有序集合:{0,1,2,……}
2.线性地址空间 - 地址空间中的整数是连续的。
3.虚拟地址空间 - CPU从一个有 N=2^n 个地址的地址空间中生成虚拟地址,这个地址空间成为称为虚拟地址空间。
4.地址空间的大小 - 由表示最大地址所需要的位数来描述。
- N=2^n:n位地址空间
9.3 虚拟存储器作为缓存的工具
- 虚拟存储器——虚拟页VP,每个虚拟页大小为P=2^平字节
- 物理存储器——物理页PP,也叫页帧,大小也为P字节。
- 任意时刻,虚拟页面的集合都被分为三个不相交的子集:
未分配的:VM系统还没分配/创建的页,不占用任何磁盘空间。
缓存的:当前缓存在物理存储器中的已分配页
未缓存的:没有缓存在物理存储器中的已分配页
9.3.1 DRAM缓存的组织结构
DEAM缓存的组织结构完全是由巨大的不命中开销驱动。
DRAM缓存是全相联的,任何虚拟页都可以放置在任何的物理页中。
DRAM缓存总是使用写回。
9.3.2 页表
页表:将虚拟页映射到物理页,就是一个页表条目的数组。
PTE:由一个有效位和一个n位地址字段组成的,表明了该虚拟页是否被缓存在DRAM中。
9.3.3 页命中
当CPU读取一个字的时候,地址翻译硬件将虚拟地址作为一个索引来定位PTE,并从存储器中读取它。
9.3.4 缺页
DRA缓存不命中称为缺页。
在不命中发生时,换入页面的策略称为按需页面调度。
9.4 虚拟存储器作为存储器管理的工具
- 操作系统为每个进程提供了一个独立的页表,也就是一个独立的虚拟地址空间。
- 抖个虚拟页面可以映射到同一个共享物理页面上。
- 存储器映射:将一组连续的虚拟页映射到任意一个文件中的任意位置的表示法。
- VM简化了链接和加载、代码和数据共享,以及应用程序的存储器分配。
9.5 虚拟存储器作为存储器保护的工具
- PTE的三个许可位:
SUP:表示进程是否必须运行在内核模式下才能访问该页
READ:读权限
WRITE:写权限
9.6 地址翻译
- 地址翻译就是一个N元素的虚拟地址空间VAS中的元素和一个M元素的物理地址空间PAS中元素之间的映射。
- 页面基址寄存器PTBR指向当前页表。
- MMU利用VPN选择适当的PTE。
- PPO=VPO。
9.6.1 结合高速缓存和虚拟存储器
- 首先,在既使用SRAM高速缓存又使用虚拟存储器的系统中,大多数系统选择物理寻址
- 主要思路是地址翻译发生在高速缓存之前
- 页表目录可以缓存,就像其他的数据字一样
9.6.2 利用TLB加速地址翻译
步骤:
CPU产生一个虚拟地址
MMU从TLB中取出相应的PTE
MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存
高速缓存/主存将所请求的数据字返回给CPU
9.6.3 多级页表
- 以两层页表层次结构为例,好处是:
如果一级页表中的一个PTE是空的,那么相应的二级页表就根本不会存在
只有一级页表才需要总是在主存中,虚拟存储器系统可以在需要时创建、页面调入或调出二级页表,只有最经常使用的二级页表才缓存在主存中。
- 多级页表的地址翻译:
9.7 案例研究
9.7.1 Core i7地址翻译
- 在这里,PTE有三个权限位:
R/W位:确定内容是读写还是只读
U/S位:确定是否能在用户模式访问该页
XD位:禁止执行位,64位系统中引入,可以用来禁止从某些存储器页取指令
- 还有连个缺页处理程序涉及到的位:
A位,引用位,实现页替换算法
D位,脏位,告诉是否必须写回牺牲页
9.7.2 Linux虚拟存储器系统
- Linux为每个进程维持了一个单独的虚拟地址空间.
- 内核虚拟存储器包括:内核中的代码和数据结构。
- 一部分区域映射到所有进程共享的物理页面,另一部分包含每个进程都不相同的数据。
9.8 存储器映射
- 即指Linux通过将一个虚拟存储器区域与一个磁盘上的对象关联起来,以初始化这个虚拟存储器区域的内容的过程。
- 映射对象:
Unix文件系统中的普通文件
匿名文件(全都是二进制0)
9.8.1 共享对象和私有对象
(1)共享对象
- 共享对象对于所有把它映射到自己的虚拟存储器进程来说都是可见的
- 即使映射到多个共享区域,物理存储器中也只需要存放共享对象的一个拷贝。
(2)私有对象 - 私有对象运用的技术:写时拷贝
- 在物理存储器中只保存有私有对象的一份拷贝
9.8.2 execve函数
- 使用execve函数将a.out程序加载到存储器的过程Execve("a.out",NULL,NULL);
- 具体经过的步骤如下:
删除已存在的用户区域
映射私有区域
映射共享区域
设置程序计数器
9.8.3 使用mmap函数的用户级存储器映射
- 创建新的虚拟存储器区域
#include <unistd.h>
#include <sys/mman.h>
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
成功返回指向映射区域的指针,若出错则为-1
- 参数含义:
start:这个区域从start开始
fd:文件描述符
length:连续的对象片大小
offset:距文件开始处的偏移量
prot:访问权限位,具体如下:
PROT_EXEC:由可以被CPU执行的指令组成
PROT_READ:可读
PROT_WRITE:可写
PROT_NONE:不能被访问
flag:由描述被映射对象类型的位组成,具体如下:
MAP_ANON:匿名对象,虚拟页面是二进制0
MAP_PRIVATE:私有的、写时拷贝的对象
MAP_SHARED:共享对象
- 删除虚拟存储器:
include <sys/mman.h>
int munmap(void *start, size_t length);
成功返回0,失败返回-1
从start开始删除,由接下来length字节组成的区域。
9.9 动态存储器分配
- 堆:是一个请求二进制0的区域,紧接在未初始化的bss区域后开始,并向上(更高的地址)生长。有一个变量brk指向堆的顶部
- 分配器的两种基本风格:
显示分配器-malloc和free
隐式分配器/垃圾收集器
9.9.1 malloc和free函数:
- 系统调用malloc函数,从堆中分配块:
#include <stdlib.h>
void *malloc(size_t size);
成功返回指针,指向大小至少为size字节的存储器块,失败返回NULL
- 系统调用free函数来释放已分配的堆块:
#include <stdlib.h>
void free(void *ptr);
无返回值
ptr参数必须指向一个从malloc、calloc或者reallov获得的已分配块的起始位置。
- 使用动态存储器分配的原因:因为经常知道程序实际运行时,它们才知道某些数据结构的大小。
9.9.2 分配器的要求和目标:
- 要求
处理任意请求序列
立即响应请求
只使用堆
对齐块
不修改已分配的块
- 目标:
最大化吞吐率(吞吐率:每个单位时间里完成的请求数)
最大化存储器利用率——峰值利用率最大化
9.9.3 碎片
- 内部碎片
- 发生在一个已分配块比有效载荷大的时候
- 易于量化
- 外部碎片
- 发生在当空闲存储器合计起来足够满足一个分配请求,但是没有一个单独的空间块足以处理这个请求时发生
- 难以量化,不可预测。
9.9.4隐式空闲链表
- 堆块的格式:由一个字的头部,有效荷载,和可能的额外填充组成。
- 空闲块通过头部中的大小字段隐含地连接着,分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。
- 系统对齐要求和分配器对块格式的选择会对分配器上的最小块大小有强制的要求。
9.9.5 放置已分配的块——放置策略
- 首次适配
从头开始搜索空闲链表,选择第一个合适的空闲块 - 下一次适配
从上一次搜索的结束位置开始搜索 - 最佳适配
检索每个空闲块,选择适合所需请求大小的最小空闲块
9.9.6 申请额外的堆存储器
- 用到sbrk函数:
#include <unistd.h>
vid *sbrk(intptr_t incr);
成功则返回旧的brk指针,出错为-1
- 通过将内核的brk指针增加incr来扩展和收缩堆。
9.9.7 合并空闲块
- 合并是针对于假碎片问题的,任何实际的分配器都必须合并相邻的空闲块。
- 有两种策略:立即合并/推迟合并
9.9.8 显式空闲链表
- 隐式空闲链表与显式空闲链表区别
- 分配时间
隐式的,分配时间是块总数的线性时间
显式的,是空闲块数量的线性时间
- 链表形式
隐式——隐式空闲链表
显式——双向链表,有前驱和后继,比头部脚部好使。
- 显式空闲链表排序策略:
- 后进先出
- 按照地址顺序维护
9.9.9分离的空闲链表
- 分离存储有两种基本方法:
- 简单分离存储
每个大小类的空闲链表包含大小相等的块,每个块的大小就是这个大小类中最大元素的大小。
如果链表非空:分配其中第一块的全部
如果链表为空:分配器向操作系统请求一个固定大小的额外存储器片,将这个片分成大小相等的块,并且连接起来成为新的空闲链表。
优点:时间快,开销小
缺点:容易造成内部、外部碎片 - 分离适配
每个空闲链表是和一个大小类相关联的,并且被组织成某种类型的显示或隐式链表,每个链表包含潜在的大小不同的块,这些块的大小是大小类的成员。
这种方法快速并且对存储器使用很有效率。
- 伙伴系统——分离适配的特例
其中每个大小类都是2的幂。这样,给定地址和块的大小,很容易计算出它的伙伴的地址,也就是说:一个块的地址和它的伙伴的地址只有一位不同。
优点:快速检索,快速合并。
9.10 垃圾收集
- 垃圾收集器是一种动态存储分配器,它自动释放程序不再需要的已分配块,这些块被称为垃圾,自动回收堆存储的过程叫做垃圾收集。
9.10.1 基本知识
垃圾收集器将存储器视作一张有向可达图,只有当存在一条从任意根节点出发并到达p的有向路径时,才说节点p是可达的,而不可达点就是垃圾。
9.10.2 Mark&Sweep垃圾收集器
- 有两个阶段:
标记:标记出根节点的所有可达的和已分配的后继
清楚:释放每个未被标记的已分配块。
9.10.3 C保守的Mark&Sweep——平衡二叉树
根本原因是C语言不会用类型标记来标记存储器位置。
9.11 C程序中常见的与存储器有关的错误
- 间接引用坏指针:在进程的虚拟地址空间中有较大的洞,没有映射到任何有意义的数据,如果试图引用一个指向这些洞的指针,操作系统就会以段异常来终止程序。典型的错误是:scanf("%d",val);
- 读未初始化的存储器:虽然bass存储器位置总是被加载器初始化为0,但对于堆存储器却并不是这样的。
- 允许栈缓冲区溢出:如果一个程序不检查输入串的大小就。写入栈中的目标缓冲区,程序就会出现缓冲区溢出错误。
- 假设指针和指向他们的对象大小是相同的。
- 造成错位错误。
- 引用指针,而不是他所指向的对象。
- 误解指针运算:忘记了指针的算术操作是以它们指向的对象的大小为单位来进行,而这种大小单位不一定是字节。
- 引用不存在的变量。
- 引用空闲堆块中的数据。
- 引起存储器泄露:当不小心忘记释放已分配块,而在堆里创建了垃圾时,就会引起存储器泄露
课后作业中的问题和解决过程
练习题9.3
给定一个32位的虚拟地址空间和一个24位的物理地址,对于下面的页面大小P,确定VPN,VPO,PPN,PPO的位数。VPN——虚拟页号;VPO——虚拟页面偏移量;PPN——物理页号;PPO——物理页面偏移量。
答:
P = 1KB——>VPN = 22,VPO = 10,PPN = 14,PPO = 10
P = 4KB——>VPN = 20,VPO = 12,PPN = 12,PPO = 12
补:实践
实现一个简单的分配器
主要函数
mm_init函数:
int mm_init(void) //初始化,成功返回0,失败返回
{
mem_init();
if ( (heap_listp = mem_sbrk(4 * WSIZE)) == (void *)-1)
return -1;
PUT(heap_listp, 0);
PUT(heap_listp + WSIZE, PACK(8, 1)); //序言块头部
PUT(heap_listp + 2*WSIZE, PACK(8, 1)); //序言块尾部
PUT(heap_listp + 3*WSIZE, PACK(0, 1)); //结尾块
heap_listp += 2*WSIZE;
if (extend_heap(CHUNKSIZE/WSIZE) == NULL)
return -1;
return 0;
}
mm_free函数:
void mm_free(void *bp) //释放bp指向块的内存
{
size_t size = GET_SIZE(HDRP(bp));
PUT(HDRP(bp), PACK(size, 0));
PUT(FTRP(bp), PACK(size, 0));
coalesce(bp); //并合前后块
}
mm_malloc函数:
void *mm_malloc(size_t size) //分配size字节大小的块,返回指向块的指针
{
size_t asize; //调整过的size
size_t extendsize;
void *bp;
if (size == 0)
return NULL;
if (size < DSIZE)
asize = 2 * DSIZE;
else
asize = DSIZE * ((size + (DSIZE) + (DSIZE - 1)) / DSIZE);
if ( (bp = find_fit(asize)) != NULL)
{
place(bp, asize);
return bp;
}
extendsize = MAX(asize, CHUNKSIZE);
if ( (bp = extend_heap(extendsize/WSIZE)) == NULL )
return NULL;
place(bp, asize);
return bp;
}
extend_heap函数:
//工具函数定义
static void *extend_heap(size_t words) //拓展堆的可用空间,返回原堆顶地址(mem_brk),失败返回NULL
{
char *bp;
size_t size;
size = (words % 2) ? (words + 1) * WSIZE : words * WSIZE; //保持双字对齐
if ((long)(bp = mem_sbrk(size)) == -1)
return NULL;
PUT(HDRP(bp), PACK(size, 0));
PUT(FTRP(bp), PACK(size, 0));
PUT(HDRP(NEXT_BLKP(bp)), PACK(0, 1));
return (void *)bp;
}
coalesce函数:
static void *coalesce(void *bp) //并合bp指向块的前后块,返回并合后的块指针
{
size_t prev_alloc = GET_ALLOC(HDRP(PREV_BLKP(bp))); //上一块是否分配
size_t next_alloc = GET_ALLOC(HDRP(NEXT_BLKP(bp))); //下一块是否分配
size_t size;
if (prev_alloc && next_alloc)
{
return bp;
}
else if (prev_alloc && !next_alloc)
{
size += GET_SIZE(HDRP(NEXT_BLKP(bp)));
PUT(HDRP(bp), PACK(size, 0));
PUT(FTRP(bp), PACK(size,0));
}
else if (!prev_alloc && next_alloc)
{
size += GET_SIZE(HDRP(PREV_BLKP(bp)));
PUT(FTRP(bp), PACK(size, 0));
PUT(HDRP(PREV_BLKP(bp)), PACK(size, 0));
bp = PREV_BLKP(bp);
}
else
{
size += GET_SIZE(HDRP(PREV_BLKP(bp))) +
GET_SIZE(FTRP(NEXT_BLKP(bp)));
PUT(HDRP(PREV_BLKP(bp)), PACK(size, 0));
PUT(FTRP(NEXT_BLKP(bp)), PACK(size, 0));
bp = PREV_BLKP(bp);
}
return bp;
}
find_fit函数:
static void *find_fit(size_t size) //寻找第一个空间大于size的空闲块,返回其地址,未找到时,返回NULL
{
void *bp;
for (bp = heap_listp; GET_SIZE(bp) > 0; bp = NEXT_BLKP(bp))
{
if (GET_SIZE(HDRP(bp)) >= size && GET_ALLOC(HDRP(bp)) != 1) //返回第一块未分配且空间大于size的空闲块
return bp;
}
return NULL;
}
place函数:
static void place(void *bp, size_t asize) //分割find_fit返回的块,创建块结构
{
size_t bsize = GET_SIZE(HDRP(bp));
if ( (bsize - asize) > 2*DSIZE ) //最小块为16字节,分割块
{
PUT(HDRP(bp), PACK(asize, 1));
PUT(FTRP(bp), PACK(asize, 1));
bp = NEXT_BLKP(bp);
PUT(HDRP(bp), PACK(bsize - asize, 0));
PUT(FTRP(bp), PACK(bsize - asize, 0));
}
else //不用分割
{
PUT(HDRP(bp), PACK(asize, 1));
PUT(FTRP(bp), PACK(asize, 1));
}
}
主函数:
int main()
{
mem_init(); //初始化模型
mm_init(); //初始化分配器
int *a = mm_malloc(sizeof(int)); //测试int
*a = 1;
char *b = mm_malloc(sizeof(char)); //测试char
*b = 'z';
double *c = mm_malloc(sizeof(double)); //测试double
*c = 2.0;
printf("a = %d\nb = %c\nc = %f\n", *a, *b, *c);
}
运行代码
出现错误
根据错误提示找到crt1.o
打开后是乱码,不知道如何解决。
本周代码截图
学习进度条
代码行数(新增/累积) | 博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
---|---|---|---|---|
目标 | 5000行 | 30篇 | 400小时 | |
第一周 | 200/200 | 2/2 | 20/20 | |
第二周 | 200/400 | 2/4 | 18/38 | |
第三周 | 100/500 | 1/5 | 10/48 | |
第四周 | 250/750 | 1/6 | 10/58 | |
第五周 | 100/850 | 1/7 | 10/68 | |
第六周 | 100/950 | 1/8 | 12/80 | |
第七周 | 200/1150 | 1/9 | 12/92 | |
第八周 | 124/1274 | 2/11 | 10/102 | |
第九周 | 205/1479 | 2/13 | 5/107 | |
第十周 | 646/2125 | 2/15 | 9/116 | |
第十一周 | 421/2546 | 2/17 | 12/128 | |
第十二周 | 752/3298 | 3/10 | 11/139 | |
第十三周 | 1001/4299 | 1/21 | 12/151 | |
第十四周 | 672/4971 | 1/22 | 9/160 |