20145215《信息安全系统设计基础》第十四周学习总结

20145215《信息安全系统设计基础》第十四周学习总结

教材学习内容总结

  • 虚拟存储器的三个重要能力:
    • 它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,高效的使用了主存。
    • 它为每个进程提供了一致的地址空间,从而简化了存储器管理。
    • 它保护了每个进程的地址空间不被其他进程破坏。
  • 程序员需要理解虚拟存储器的三个原因:
    • 虚拟存储器是中心的:它是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的交互中心;
    • 虚拟存储器是强大的:它可以创建和销毁存储器片、可以映射存储器片映射到磁盘某个部分等等;
    • 虚拟存储器若操作不当则十分危险。

物理和虚拟寻址

  • 计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组,每字节都有一个唯一的物理地址(PA)。CPU根据物理地址访问存储器的方式是物理寻址。

  • 使用虚拟寻址时,CPU通过生成一个虚拟地址VA来访问主存,这个虚拟地址在被送到存储器之前先转换成适当的物理地址,地址翻译通过CPU芯片上的存储器管理单元完成。

地址空间

  • 地址空间是一个非负整数地址的有序集合:
    • 线性地址空间:地址空间中的整数是连续的。
    • 虚拟地址空间:CPU从一个有 N=2^n 个地址的地址空间中生成虚拟地址,这个地址空间成为称为虚拟地址空间。
    • 地址空间的大小:由表示最大地址所需要的位数来描述。
    • 物理地址空间:与系统中的物理存储器的M个字节相对应。
  • 虚拟存储器的基本思想:主存中的每个字节都有一个选自虚拟地址空间的虚拟地址和一个选自物理地址空间的物理地址。

虚拟存储器作为缓存的工具

  • 虚拟存储器——虚拟页(VP),每个虚拟页大小为P=2^p字节。
  • 物理存储器——物理页(PP),也叫页帧,大小也为P字节。
  • 任意时刻,虚拟页面的集合都被分为三个不相交的子集:
    • 未分配的:VM系统还没分配(创建)的页,不占用任何磁盘空间。
    • 缓存的:当前缓存在物理存储器中的已分配页。
    • 未缓存的:没有缓存在物理存储器中的已分配页。

DRAM缓存的组织结构

  • 不命中处罚很大
  • 是全相联的——任何虚拟页都可以放在任何的物理页中
  • 替换算法精密
  • 总是使用写回而不是直写

页表

  • 页表:是一个数据结构,存放在物理存储器中,将虚拟页映射到物理页,就是一个页表条目的数组。
  • 页表就是一个页表条目PTE的数组。
    • PTE:由一个有效位和一个n位地址字段组成的,表明了该虚拟页是否被缓存在DRAM中。
  • 页表的组成:有效位+n位地址字段
  • 如果设置了有效位:地址字段表示DRAM中相应的物理页的起始位置,这个物理页中缓存了该虚拟页。
  • 如果没有设置有效位:
    • 空地址:表示该虚拟页未被分配
    • 不是空地址:这个地址指向该虚拟页在磁盘上的起始位置。

页命中

  • 当CPU读取一个字的时候,地址翻译硬件将虚拟地址作为一个索引来定位PTE,并从存储器中读取它。

缺页

  • 缺页:就是指DRAM缓存不命中。
  • 缺页异常:会调用内核中的缺页异常处理程序,选择一个牺牲页。
  • 页:虚拟存储器的习惯说法,就是块
  • 交换=页面调度:磁盘和存储器之间传送页的活动
  • 按需页面调度:直到发生不命中时才换入页面的策略,所有现代系统都使用这个。

又是局部性救了我们

  • 局部性原则保证了在任意时刻,程序将往往在一个较小的活动页面集合上工作,这个集合叫做工作集/常驻集。
  • 所以只要程序有良好的时间局部性,虚拟存储器系统就能工作的相当好。
  • 颠簸:工作集大小超出了物理存储器的大小。

虚拟存储器作为存储器管理的工具

  • 操作系统为每个进程提供了一个独立的页表,也就是一个独立的虚拟地址空间。
  • 多个虚拟页面可以映射到同一个共享物理页面上。
  • 存储器映射:将一组连续的虚拟页映射到任意一个文件中的任意位置的表示法。
  • 按需页面调度和独立的虚拟地址空间的结合简化了链接和加载、代码和数据共享,以及应用程序的存储器分配。
    • 简化链接:独立的地址空间允许每个进程的存储器映像使用相同的基本格式,而不管代码和数据实际存放在物理存储器的何处。
    • 简化加载:虚拟存储器使得容易想存储器中加载可执行文件和共享文件对象。
    • 简化共享:独立地址空间为操作系统提供了一个管理用户进程和操作系统自身之间共享的一致机制。
    • 简化存储器分配:虚拟存储器为向用户进程提供一个简单的分配额外存储器的机制。

虚拟存储器作为存储器保护的工具

  • 通过在PTE上添加一些额外的许可来控制对一个虚拟页面的内容访问。
  • PTE的三个许可位:
    • SUP:表示进程是否必须运行在内核模式下才能访问该页
    • READ:读权限
    • WRITE:写权限

地址翻译

  • 地址翻译:一个N元素的虚拟地址空间(VAS)中的元素和一个M元素的物理地址空间(PAS)之间的映射。

  • MAP: VAS → PAS ∪ ∅

    • MAP = A' ,如果虚拟地址A处的数据在PAS的物理地址A'处
    • MAP = ∅ ,如果虚拟地址A处的数据不在物理存储器中
  • CPU中的一个控制寄存器页表基址寄存器指向当前页表,n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO) 和一个(n-p)位的虚拟页号,页表条目中的物理页页号和虚拟地址中的VPO串联起来,就得到了相应的物理地址。

  • 当页面命中时,CPU硬件执行步骤

    • 处理器生成虚拟地址,传给MMU
    • MMU生成PTE地址,并从高速缓存/主存请求得到他
    • 高速缓存/主存向MMU返回PTE
    • MMU构造物理地址,并把它传给高速缓存/主存
    • 高速缓存/主存返回所请求的数据给处理器。
  • 处理缺页时,CPU硬件执行步骤

    • 处理器生成虚拟地址,传给MMU
    • MMU生成PTE地址,并从高速缓存/主存请求得到他
    • 高速缓存/主存向MMU返回PTE
    • PTE中有效位为0,触发缺页异常
    • 确定牺牲页
    • 调入新页面,更新PTE
    • 返回原来的进程,再次执行导致缺页的指令,会命中

结合高速缓存和虚拟存储器

  • 在既使用SRAM高速缓存又使用虚拟存储器的系统中,大多数系统选择物理寻址。
  • 两者结合的主要思路是地址翻译发生在高速缓存之前。
  • 页表目录可以缓存,就像其他的数据字一样。

利用TLB加速地址翻译

  • TLB:翻译后备缓冲器,是一个小的、虚拟存储的缓存,其中每一行都保存着一个由单个PTE组成的块
  • 步骤:
    • CPU产生一个虚拟地址
    • MMU从TLB中取出相应的PTE
    • MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存
    • 高速缓存/主存将所请求的数据字返回给CPU

多级页表

  • 多级页表——采用层次结构,用来压缩页表。
  • 以两层页表层次结构为例,好处是:
    • 如果一级页表中的一个PTE是空的,那么相应的二级页表就根本不会存在;
    • 只有一级页表才需要总是在主存中,虚拟存储器系统可以在需要时创建、页面调入或调出二级页表,只有最经常使用的二级页表才缓存在主存中。

案例研究:Intel Core i7/Linux存储器系统

core i7地址翻译

  • PTE的三个权限位:

    • R/W位:确定内容是读写还是只读
    • U/S位:确定是否能在用户模式访问该页
    • XD位:禁止执行位,64位系统中引入,可以用来禁止从某些存储器页取指令
  • 缺页处理程序涉及到的位:

    • A位:引用位,实现页替换算法
    • D位:脏位,告诉是否必须写回牺牲页

Linux虚拟存储器系统

  • linux为每个进程维持了一个单独的虚拟地址空间,其中,内核虚拟存储器位于用户栈之上;

  • 内核虚拟存储器包含内核中的代码和数据结构,还有一些被映射到一组连续的物理页面(主要是便捷地访问特定位置,比如执行I/O操作的时候需要的位置)

  • linux将虚拟存储器组织成一些区域(也叫做段)的集合。一个区域就是已经存在的(已分配的)虚拟存储器的连续片;

  • 允许虚拟地址空间有间隙;内核不用记录那些不存在的页,这样的页也不用占用存储器;

  • 一个具体区域的区域结构:

    • vm _start:指向这个区域的起始处;
    • vm _end:指向这个区域的结束处;
    • vm _prot:描述这个区域内所包含的所有页的读写许可权限;
    • vm _fags:描述这个区域内的页面是与其他进程共享的,还是这个进程私有的,等等;
    • vm _next:指向链表的下一个结构。
  • Linux缺页异常处理:

存储器映射

  • 存储器映射:Linux通过将一个虚拟存储器区域与一个磁盘上的对象关联起来,以初始化这个虚拟存储器区域的内容的过程。
  • 映射对象:
    • Unix文件系统中的普通文件
    • 匿名文件(全都是二进制0)
  • 一旦一个虚拟页面被初始化了,它就在一个由内核维护的专门的交换文件之间换来换去。交换文件也叫交换空间,或交换区域。

再看共享对象

  • 共享对象
    • 共享对象对于所有把它映射到自己的虚拟存储器进程来说都是可见的。
    • 即使映射到多个共享区域,物理存储器中也只需要存放共享对象的一个拷贝。
  • 私有对象
    • 私有对象运用的技术:写时拷贝
    • 在物理存储器中只保存有私有对象的一份拷贝

再看fork函数

  • 当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟存储器,它创建了当前进程的mm_struct、区域结构和页表的原样拷贝。它将两个进程中的每个页面都为标记只读,并将两个进程中的每个区域结构都标记为私有的写时拷贝。
  • 当fork在新进程中返回时,新进程现在的虚拟存储器刚好和调用fork时存在的虚拟存储器相同。当这两个进程中的任一个后来进行写操作时,写时拷贝机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

再看execve函数

  • 使用execve函数将a.out程序加载到存储器的过程
Execve("a.out",NULL,NULL);
  • 加载并运行所需要的步骤如下:
    • 删除已存在的用户区域。
    • 映射私有区域。
    • 映射共享区域。
    • 设置程序计数器。

使用mmap函数的用户级存储器映射

  • Unix进程可以使用mmap函数来创建新的虚拟存储器区域,并将对象映射到这些区域当中
    void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offest);
		若成功则为指向映射区域的指针,若出错则为MAP_FAILDE(-1)
  • munmap函数删除虚拟存储器的区域。
    int munmap(void *start,size_t length);
    	若成功则返回0,若失败则返回-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获得的已分配块的起始位置。
  • 使用动态存储器分配原因:经常直到程序实际运行时,才知道某些数据结构的大小。

分配器的要求和目标

  • 显示分配器的要求:

    • 处理任意请求序列
    • 立即响应请求
    • 只使用堆
    • 对齐块
    • 不修改已分配的块
  • 目标:

    • 最大化吞吐率:最大化存储器利用率——峰值利用率最大化
    • 吞吐率:每个单位时间里完成的请求数

碎片

  • 碎片:虽然有未使用的存储器,但是不能用来满足分配请求。
    • 内部碎片:发生在一个已分配块比有效载荷大的时候,易于量化。
    • 外部碎片:发生在当空闲存储器合计起来足够满足一个分配请求,但是没有一个单独的空间块足以处理这个请求时发生。难以量化,不可预测。

隐式空闲链表

  • 堆块的格式:由一个字的头部,有效荷载,和可能的额外填充组成。
  • 将堆组织成一个连续的已分配块和空闲块的序列:
    • 空闲块通过头部中的大小字段隐含地连接着,分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。
  • 需要:特殊标记的结束块。
  • 系统对齐要求和分配器对块格式的选择会对分配器上的最小块大小有强制的要求。

放置已分配的块

  • 分配方式有:
    • 首次适配:从头开始搜索空闲链表,选择第一个合适的空闲块
    • 下一次适配:从上一次搜索的结束位置开始搜索
    • 最佳适配:检索每个空闲块,选择适合所需请求大小的最小空闲块

申请额外的堆存储器

  • sbrk函数
#include <unistd.h>
vid *sbrk(intptr_t incr);

成功则返回旧的brk指针,出错为-1
  • 通过将内核的brk指针增加incr来扩展和收缩堆。

合并空闲块

  • 合并是针对于假碎片问题的,任何实际的分配器都必须合并相邻的空闲块。
  • 两种策略:
    • 立即合并
    • 推迟合并

垃圾收集

  • 垃圾收集器是一种动态存储分配器。,自动释放程序已经不再需要的已分配块(垃圾)。

基本知识

  • 垃圾收集器将存储器视为一张有向可达图,图的节点被分配为一组根节点和一组堆节点。当存在一条从任意根节点出发到并到达P的有向路径时,就称节点P是可达的。

Mark&Sweep垃圾收集器

  • Mark&Sweep垃圾收集器由标记阶段和清除阶段组成,标记阶段标记出根节点所有可达的和已分配的后继,清除阶段释放每个未被标记的已分配块。

  • 在对Mark&Sweep的描述中使用下列函数:

    • ptr isPtr(ptr p):如果p指向一个已分配块中的某个字,那么就返回一个指向这个块起始位置的指针b,否则返回NULL。
    • int blockMarked(ptr b):如果已经标记了块b,就返回true。
    • int blockAllocated(ptr b):如果块b是已分配的,就返回true。
    • void markBlock(ptr b):标记块b。
    • int length(ptr b):返回块b的以字为单位的长度(不包括头部)。
    • void unmarkBlock(ptr b):将块b的状态由已标记的改为未标记的。
    • ptr nextBlock(ptr b):返回堆中块b的后继。

C程序中常见的与存储器有关的错误

间接引用坏指针

  • 在进程的虚拟地址空间中有较大的洞,没有映射到任何有意义的数据,如果试图引用一个指向这些洞的指针,操作系统就会以段异常来终止程序。

  • 典型的错误是:scanf("%d",val);,没有加&符号

读未初始化的存储器

  • 虽然bass存储器位置总是被加载器初始化为0,但对于堆存储器却并不是这样的。
  • 常见的错误就是假设堆存储器被初始化为0.

允许栈缓冲区溢出

  • 如果一个程序不检查输入串的大小就写入栈中的目标缓冲区,程序就会出现缓冲区溢出错误。

假设指针和指向他们的对象大小是相同的。

  • 一种常见的错误是假设指向对象的指针和他们所指向的对象是大小相同的。

造成错位错误。

  • 一种很常见的覆盖错误来源

引用指针,而不是他所指向的对象。

  • 注意C的优先级和结合性

误解指针运算

  • 忘记了指针的算术操作是以它们指向的对象的大小为单位来进行,而这种大小单位不一定是字节。

引用不存在的变量

  • 理解栈的规则,有时会引用不再合法的本地变量。

引用空闲堆块中的数据

  • 一个相似的错误是引用已经被释放了的堆块中的数据。

引起存储器泄露

  • 当不小心忘记释放已分配块,而在堆里创建了垃圾时,就会引起存储器泄露。

实践

实现一个简单的分配器

主要代码

  • 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);
}
  • 运行结果:

本周代码托管截图

  • 代码托管链接:click here
  • 代码行数统计:

心得体会

  • 本章关于分配器部分的理解并不是很透彻,只是大致的将书上的代码敲上去,然后编写了一个main函数进行了测试,关于其中的很多细节还有待进一步的揣摩。
  • 关于最后一节C程序中常见的与存储器有关的错误,个人看完之后感觉受益匪浅。之前编程过程中就遇到很多类似的问题,每次解决的过程一般都是把错误放到百度里搜,然后根据百度里的解决过程一个一个试,解决了就解决了,没解决便无从下手,错误也不知道具体是由于什么原因而导致的,为什么会产生这样的错误。看完这一节之后,有一种豁然开朗的感觉,下次编代码遇到类似的错误时就能够更好的解决这些问题。

学习进度条

代码行数(新增/累积) 博客量(新增/累积) 学习时间(新增/累积) 重要成长
目标 5000行 30篇 400小时
第一周 0/0 1/2 25/45 学习了几个Linux核心命令
第二周 55/55 2/4 27/72 学会了vim,gcc以及gdb的基本操作
第三周 148/203 1/5 23/95 对信息的表示和处理有更深入的理解
第五周 72/275 1/6 25/120 对汇编语言有了更深的理解
第六周 56/331 2/8 30/150 安装了Y86模拟器
第七周 61/392 1/9 22/172 理解了局部性原理和缓存思想在存储层次结构中的应用
第八周 0/392 1/10 20/192 复习前几章内容
第九周 132/524 2/12 24/216 了解了Linux操作系统提供的基本I/O服务
第十周 420/524 2/14 20/236 对常用指令的代码进行了分析调试,加深了理解
第十一周 1017/1541 2/16 26/262 对系统调用有了更深的认识
第十二周 0/1541 1/17 18/280 复习前几章的知识及代码
第十三周 1001/2542 1/18 23/303 加深了对多线程的理解
第十四周 224/2766 1/19 22/325 了解了虚拟存储器的概念

参考资料

posted @ 2016-12-17 23:27  L.X.M  阅读(395)  评论(4编辑  收藏  举报