标签:
为了更加有效地管理存储器且少出错,现代系统提供了对主存的抽象概念,叫做虚拟存储器(VM)。
虚拟存储器是硬件异常,硬件地址翻译,主存,磁盘文件和内核软件的完美交互。提供了3个重要能力。
为每个进程提供一致的地址空间
保护了每个进程的地址空间不被其他进程破坏。
程序员为什么要理解它?
虚拟存储器是中心的。 虚拟存储器是强大的。
虚拟存储器是危险的
malloc动态分配程序,就会和虚拟存储器交互。本章从两个角度分析。
虚拟存储器如何工作。虚拟存储器。物理地址(Physical Address,PA):计算机系统的主存被组织为M个连续的字节大小的单元组成的数组。每个字节的地址叫物理地址.
CPU访问存储器的最自然的方式使用物理地址,这种方式称为物理寻址。
- 早期的PC,数字信号处理器,嵌入式微控制器以及Cray超级计算机使用物理寻址。
现代处理器使用的是虚拟寻址(virtual addressing)的寻址形式。

CPU通过生成一个虚拟地址(Virtual address,VA)来访问主存。
虚拟地址转换为物理地址叫做地址翻译(address translation)。地址翻译也需要CPU硬件和操作系统之间的紧密结合。
存储器管理单元(Memory Management Unit,MMU)的专用硬件。 地址空间(address space)是一个非负整数地址的有序集合。
如果地址空间中整数是连续的,我们说它是线性地址空间(linear address space)。
在一个带虚拟存储器的系统中,CPU从一个有N=2^n个地址的地址空间中生成虚拟地址,这个地址空间称为虚拟地址空间(virtual address space)。
一个地址空间大小是由表示最大地址所需要的位数来描述的。
N=2^n个地址的虚拟地址空间叫做n位地址空间。32位或64位。一个系统还有物理地址空间,它与系统中物理存储器的M=2^m(假设为2的幂)个字节相对应。
地址空间的概念很重要,因为它区分了数据对象(字节)和 它们的属性(地址)。
字节(数据对象)一般有多个 独立的地址(属性)。每个地址都选自不同的地址空间。 字节 有一个在虚拟地址空间的虚拟地址。物理地址空间的 物理地址。字节。感悟
在讲述这一小章之前,必须交代一下我对
虚拟存储器概念的存疑。
原本我以为虚拟存储器=虚拟内存。
以下是虚拟内存的定义
虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换而在下面的定义我们可以看到
CSAPP中认为虚拟存储器是存放在磁盘上的。
在此,我们姑且当做两者是不同的东西,以后有更深刻的理解,再思考。
虚拟存储器(VM)被组织为一个存放在磁盘上的N个连续字节大小的单元组成的数组。
虚拟地址,这个虚拟地址作为到数组的索引。磁盘上数组的内容被缓存到主存中。
磁盘上的数据被分割称块。 块作为磁盘和主存之间的传输单元。虚拟页(Virtual Page,VP)就是这个块 虚拟页大小为P=2^p字节。物理页,大小也为P字节 页帧(page frame)任何时候,虚拟页的集合都被分为3个不相交的子集。
块没有任何数据与之相关联。 malloc来分配
DRAM表示虚拟存储器系统的缓存,在主存中缓存虚拟页,有两个特点。
DRAM缓存不命中处罚十分严重。 磁盘比DRAM慢100000多倍。DRAM缓存的组织结构由这种巨大的不命中开销驱动。因此有以下特点。
(有些地方不是特别懂,之后看完第六章应该会好点)
虚拟页往往很大。
DRAM缓存是全相联
虚拟页都能放在任何物理页中。更精密的替换算法
DRAM缓存总是写回
直写判断命中和替换又多种软硬件联合提供。
操作系统软件,MMU中的地址翻译硬件和页表(page table)。
页表是存放在物理存储器的数据结构。
页表将虚拟页映射到物理页。页表。操作系统负责维护页表的内容,以及磁盘及DRAM之间来回传送页。

页表就是一个页表条目(Page Table Entry,PTE)的数组. PTE.PTE由一个有效位和n位地址字段。 有效位表明虚拟页是否被缓存。 
在虚拟存储器的习惯说法中,DRAM缓存不命中称为缺页。
处理过程如下:
PT。PTE有效位,发现未被缓存,触发缺页异常。调用缺页异常处理程序
写回)中断结束,重新执行最开始的指令。
DRAM中读取成功。虚拟存储器是20世纪60年代发明的,因此即使与SRAM缓存使用了不同的术语。
块被称为页。页的活动叫做交换(swapping)或者页面调度(paging)。不命中发生时,才换入页面,这种策略叫做按需页面调度(demand paging)。 比如某个页面所指向地址为NULL,将这个地址指向磁盘某处,那么这就叫分配页面。
此时虚拟页从未分配状态 变为 未缓存。
虚拟存储器工作的相当好,主要归功于老朋友局部性(locality)
尽管从头到尾的活动页面数量大于物理存储器大小。
但是在局部内,程序往往在一个较小的活动页面集合工作
这个集合叫做工作集(working set)或者叫常驻集(resident set)
程序有良好的时间局部性,虚拟存储器都工作的相当好。
工作集大于物理存储器大小。这种状态叫颠簸(thrashing). 统计缺页次数
可以利用Unix的getrusage函数检测缺页数量。
实际上,操作系统为每个进程提供一个独立的页表。

因此,VM简化了链接和加载,代码和数据共享,以及应用程序的存储器分配。
简化链接
独立的空间地址意味着每个进程的存储器映像使用相同的格式。
0x08048000(32位)处或0x400000(64位)处开始。一致性极大简化了链接器的设计和实现。
简化加载
加载器可以从不实际拷贝任何数据从磁盘到存储器。将一组连续的
虚拟页映射到任意一个文件中的任意位置的表示法称作存储器映射。Unix提供一个称为mmap的系统调用,允许程序自己做存储器映射。在9.8详细讲解。
简化共享
共享机制.printf.简化存储器分配.
虚拟页连续(虚拟页还是单独的),物理页可以不连续。使得分配更加容易。任何现代操作系统必须为操作系统提供手段来控制对 存储器系统的访问。
方式:在PTE上添加一些格外的许可位来控制访问。

SUP:是否只有在内核模式下才能访问?READ:读权限。WRITE:写权限。如果指令违反了许可条件,触发一般保护性异常,然后交给异常处理程序,Shell一般会报告为段错误(segmentaion fault)。
认识到硬件在支持虚拟存储器中的角色
以下是接下来可能要用到的符号,作参考。

形式上来说,地址翻译是一个N元素的虚拟地址空间(VAS)中的元素和一个M元素的物理地址空间(PAS)元素之间的映射,

以下展示了MMU(Memory Management Unit,存储器管理单元)如何利用页表实现这样的功能

页表基址寄存器(Page Table Base Register,PTBR)指向当前页表。n位的虚拟地址包含两个部分
p位的虚拟页面偏移(Virtual Page Offset,VPO)n-p位的虚拟页号(Virtual Page Number,VPN) MMU利用VPN选取适当的PTE(页面条目,Page Tabe Entry,PTE)页面条目 (PTE)中物理页号(PPN)和虚拟地址中的VPO串联起啦,即是物理地址
PPO和VPO是相同的VPN,PPN都是块,都是首地址而已,所以需要偏移地址PPO,VPO
图(a)展示页面命中,CPU硬件执行过程
MMU。MMU生成PTE地址(PTEA),并从高速缓存/主存请求中得到它。PTE。MMU构造物理地址(PA),并把它传送给高速缓存/主存。页面命中完全由硬件处理,与之不同的是,处理缺页需要 硬件和操作系统内核协作完成。
PTE有效位是零,所以MMU触发异常,传递CPU中的控制到操作系统内核中的 缺页异常处理程序。PTE。在任何使用虚拟存储器又使用SRAM高速缓存的系统中,都存在应该使用虚拟地址 还是 使用 物理地址 来访问SRAM高速缓存的问题。
使用虚拟地址的优点,就是类似于使用虚拟存储器的优点,更好的利用空间。但是设计更复杂。两者的使用需要权衡。
大多数系统是选择物理寻址。
使用物理寻址,多个进程同时在高速缓存中有存储块和共享来自相同虚拟页面的块称为简单的事。
PTE)的一部分。以下是一个例子(将PTE进行高速缓存)。

每次CPU产生一个虚拟地址,MMU就必须查阅一个PTE,以便将虚拟地址翻译为 物理地址。
PTE碰巧缓存在L1中,那么开销就下降到一到两个周期许多系统都试图消除这样的开销,他们在MMU中包含了一个关于PTE的小缓存,称为翻译后备缓冲器(Translation Lookaside Buffer,TLB)。
TLB是一个小的,虚拟寻址的缓存。
PTE组成的块。TLB通常用于高度的相连性如图所示

索引和标记字段是从虚拟地址中的虚拟页号中提取出来的。TLB有T=2^t个组 TLB索引(TLBI)是由VPN的t个最低位组成。(对应于VPO)TLB标记(TLBT)是由VPN中剩余位组成(对应于VPN)下图展示了TLB命中步骤

TLB命中
MMU从TLB取出对应的PTE。MMU将这个虚拟地址翻译成一个物理地址,发送到高速缓存/主存高速缓存/主存所请求的数据字返回给CPU当TLB不命中的时候,MMU必须从L1缓存或内存中取出相应的PTE,并进行类似缺页处理过程。
如果我们有一个32位地址空间,4KB大小的页面(p=2^12)和一个4B的PTE,即使应用所引用的只是虚拟地址空间中很小的一部分,也总是需要一个4MB的页表驻留在存储器中。
所以多级页表的诞生用于解决在很少使用时有一个很大的页表常驻于内存。
计算方式,最多可能要
2^32/4KB=1MB个页面,每个页面需要4B的PTE所以需要4MB大小的页表。
思考虚拟地址是31~p,p-1~0即VPN,VPO。
VPN即可表示页面个数(上文中的1MB),VPO即页面大小(上文中的4KB),显然知道两者相乘为2^32 次方、
用来压缩页表的常用方式是使用层次结构的页表。
页表本身一个优点就是用来解决 内存不够装载程序所用内存的情况,进行动态分配。那么当我们发现内存装载那么大的页表也是负担的时候,显然也可以用类似页表的形式来解决,这就是多级页表。

以下用上图的 两层 作为例子。
总共有9KB个页面,PTE为4个字节。
2KB个页面分配给代码和数据。6KB个页面未分配1023个页面也未分配一级页表中的每个PTE负责映射虚拟地址空间中一个4MB大小的片(chunk).
片都是由1024个连续的页面组成。4MB=1024个页面*PTE大小4字节。如果片i中每个页面都没有分配,那么一级PTE i就为空。
PTE 2~PTE 7片i中有一个被分配了,那么PTE i就不能为空。 三级四级页表的原由也是如此。这种方法从两个方面减少了存储器要求。
PTE为空,那么相应的二级页表就根本不会存在。 
k级页表层次结构的地址翻译。
虚拟地址被分为k个VPN和一个VPO。每个VPN i都是i-1级页表到i级页表的索引。PPN存于k级页表。PPO依旧与VPO相同。此时TLB能发挥作用,因为层次更细,更利于缓存。使得多级页表的地址翻译不比单级页表慢很多。
在这一节里,我们通过一个具体的端到端的地址翻译示例,来综合一下我们学过的内容。
一个在有一个TLB和L1 d-cache的小系统上。作出如下假设:
14位长(n=14)12位长(m=12)64字节(P=2^6)TLB是四路组相连的,总共有16个条目(?)L1 d-cache是物理寻址,高速缓存,直接映射(E=1)的,行大小为4字节,而总共有16个组。(?)存储结构快照



TLB: TLB利用VPN的位进行缓存。页表: 这个页表是一个单级设计。一个有256个,但是这里只列出16个。高速缓存:直接映射的缓存通过物理地址的字段来寻址。 E=1。
处理器包(processor package) TLB Linux 一页4kb数据和指令高速缓存。 L1,L2 八路组相连L3 十六路组相连块大小64字节。Intel QuickPath技术。I/O桥直接通信。L3高速缓存DDR3存储器控制器。
上图完整总结了Core i7地址翻译过程,从虚拟地址到找到数据传入CPU。
Core i7采用四级页表层次结构。 CR3 控制寄存器指向第一级页表(L1)的起始位置 CR3也是每个进程上下文的一部分。CR3也要被重置。一级,二级,三级页表PTE的格式:

P=1时 地址字段包含了一个40位物理页号(PPN),指向适当的页表开始处。
强加了一个要求,要求物理页4kb对齐。
PPO 为12位 = 4kbPPO的大小就跟物理页的大小有关。四级页表的PTE格式:

PTE有三个权限位,控制对页的访问
R/W位确定页的内容是可以 读写还是 只读。U/S位确定用户模式是否能够访问,从而保护操作系统内核代码不被用户程序访问。XD (禁止执行) 位是在64位系统引入,禁止某些存储器页取指令。 当MMU翻译虚拟地址时,还会更新两个内核缺页处理程序会用到的位。
A位
MMU都会设置A位,称为引用位(reference bit).引用位来实现它的页替换算法。D位
写 就会设置D位,又称脏位(dirty bit).脏位告诉内核在拷贝替换页前是否要写回。引用位或脏位。四级页表如何将VPN 翻译成物理地址

VPN被用作页表的偏移量。CR3寄存器包含L1页的物理地址优化地址翻译
在对地址翻译中,我们顺序执行这两个过程
MMU将虚拟地址翻译成物理地址。- 物理地址传送到
L1高速缓存。
然而实际的硬件实现使用了一个灵巧的技巧,允许这两个步骤并行。加速了对高速缓存的访问
例如:页面大小为4KB的Core i7上的虚拟地址有12位的VPO,且PPO=VPO.而且物理地址的缓存,也是
6位索引+6位偏移,刚好是VPO的12位。这不是巧合
- 一方面通过
VPN找PPN。- 另一方面直接通过
PPO对高速缓存进行组选择。- 等找到
VPN后就能立即进行关键字匹配。
目标:对Linux的虚拟存储系统做一个描述,大致了解操作系统如何组织虚拟存储器,如何处理缺页。

内核虚拟存储器
内核虚拟存储器包含内核中的代码和数据。
内核虚拟存储器的某些区域被映射到所有进程共享的物理页面
Linux也将一组连续的虚拟页面(大小等同于系统DRAM总量)映射到相应的一组物理页面。(这句话啥意思???????????????????????????????)
内核虚拟存储器包含每个进程不相同的数据。
Linux将虚拟存储器组织成一些区域(也叫做段)的集合。
一个区域就是已经存在着的(已分配的) 虚拟存储器的连续片,这些片/页已某种形式相关联。
所有存在的虚拟页都保存在某个区域。
区域的概念很重要虚拟地址空间有间隙。一个进程中虚拟存储器的内核数据结构。

内核为系统中每个进程维护了一个单独的任务结构。任务结构中的元素包含或指向内核运行该进程所需要的全部信息。
task_struct mm_struct pgd 第一级页表的基址。pgd存放在CR3控制寄存器mmap vm_area_structs(区域结构) vm_area_structs都描述了当前虚拟地址空间的一个区域(area).vm_start:指向这个区域的起始处。vm_end:指向这个区域的结束处。vm_port:描述这个区域内包含的所有页的读写许可权限。vm_flags:描述这个区域页面是否与其他进程共享,还是私有。 vm_next: 指向链表的下一个区域。MMU在试图翻译虚拟地址A时,触发缺页。这个异常导致控制转移到缺页处理程序,执行一下步骤。

虚拟地址A是合法的吗?
区域结构链表。vm_start和vm_end做比较。 树的数据结构算法查找试图访问的存储器是否合法?
保护异常,终止进程。一切正常的话
存储器映射: Linux通过将一个虚拟存储器区域与一个磁盘上的对象关联起来,以初始化这个虚拟存储器区域的内容,这个过程叫做存储器映射。
虚拟存储器区域可以映射到以下两种类型文件。
Unix文件系统中的普通文件:一个区域可以映射到一个普通磁盘文件的连续部分。
文件区(section)被分成页大小的片,每一片包含一个虚拟页面的初始化内容。初始化,虚拟页面此时还并未进入物理存储器。 CPU第一次引用这个页面。匿名文件 : 一个区域 可以映射到一个匿名文件。
匿名文件由内核创建,包含的全是二进制零。CPU第一次引用这样区域(匿名文件)的虚拟页面时。
牺牲页面全部用二进制零覆盖。虚拟页面标记为驻留在存储器中。又叫请求二进制零的页(demand-zero page)。
交换文件,交换空间。(win下叫做paging file)
一旦一个虚拟页面被初始化了,它就在一个由内核维护的专门的交换文件(swap file)之间换来换去。交换文件也叫交换空间或者交换区域。
需要意识到,在任何时刻,交换空间都限制着当前运行着的进程分配的虚拟页面总数。
这一段不太明白。
共享对象的由来
只读文本区域。 printfUinx shell的tcsh共享对象。一个对象被映射到虚拟存储器的一个区域,一定属于以下两种。
共有对象映射到它的虚拟地址空间的一个区域。 区域的写操作,对于那些也把这个共享对象映射它的虚拟存储器的进程是可见的。磁盘上的原始对象。区域叫做共享区域。私有对象的区域做出的改变,对于其他进程不可见.磁盘上。区域叫做私有区域。
进程1,将共享对象映射到虚拟存储器中,然后虚拟存储器将这一段找一块物理存储器存储。当进程2也要引用同样的共享对象时。
进程1已经映射了这个对象。进程2的虚拟存储器直接指向了那一块进程1指向的物理存储器。即使对象被映射到多个共享区域,物理存储器依旧只有一个共享对象的拷贝。

私有对象使用一种叫做写时拷贝(conpy-on-write)的巧妙技术。
私有对象开始生命周期的方式基本与共享对象一样。
对于每个映射私有对象的进程,相应私有区域的页表条目都被标记为只读。
区域结构(vm_area_structs)被标记为私有的写时拷贝。过程:只要有进程试图写私有区域内的某个页面,那么这个写操作触发保护异常。
故障处理程序会在物理存储器中创建被修改页面的一个新拷贝。页表条目(PTE)指向这个新的拷贝,恢复被修改页面的可写权限。故障处理程序返回,CPU重新执行这个写操作。通过延迟私有对象中的拷贝直到最后可能的时刻,写时拷贝充分使用了稀缺的物理存储器。
了解fork函数如何创建一个带有自己独立虚拟地址空间的新进程。
当fork函数被当前进程调用时。
新进程创建内核数据结构,并分配给它唯一一个PID。新进程创建虚拟存储器。 mm_struct,区域结构和页表的原样拷贝。私有的写时拷贝。私有对象的写时拷贝技术。当fork函数在新进程返回时。
fork时存在的虚拟存储器相同。写时,触发写时拷贝机制。理解execve函数实际上如何加载和运行程序。
Execve("a.out",NULL,NULL);execve函数在当前进程加载并执行目标文件a.out中的程序,用a.out代替当前程序。
加载并运行需要以下几个步骤。
区域结构。映射私有区域。
为新程序的文本,数据,bss和栈区域创建新的区域结构。
区域结构都是私有的,写时拷贝的。a.out文件中的文件和数据区。bss区域是请求二进制零,映射到匿名文件。
a.out中堆,栈区域也是请求二进制零。
映射共享区域
a.out程序与共享对象链接。 设置程序计数器(PC)
execve最后一件事设置PC指向文本区域的入口点。
mmap函数的用户级存储器映射Unix进程可以使用mmap函数来创建新的虚拟存储器区域,并将对象映射到这些区域中。
#include <unistd.h>
#include <sys/mman.h>
void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offset);
返回:若成功时则为指向映射区域的指正,若出错则为MAP_FAILED(-1).
参数解释:

fd,start,length,offset:mmap函数要求内核创建一个新的虚拟存储器区域,最好是从地址start开始的一个区域,并将文件描述符fd指定的对象的一个连续的片chunk映射到这个新的区域。
length字节offset字节的地方开始。statr地址仅仅是个暗示 NULL,让内核自己安排。prot参数prot包含描述新映射的虚拟存储器区域的访问权限位。(对应区域结构中的vm_prot位)
PROT_EXEC:这个区域内的页面由可以被CPU执行的指令组成。PROT_READ:这个区域内的页面可读。PROT_WRITE: 这个区域内的页面可写。PROT_NONE: 这个区域内的页面不能被访问。flag参数flag由描述被映射对象类型的位组成。
MAP_ANON标记位:映射对象是一个匿名对象。MAP_PRIVATE标记位:被映射对象是一个私有的,写时拷贝的对象。MAP_SHARED标记位:被映射对象是一个共享对象。bufp = mmap(NULL,size,PROT_READ,MAP_PRIVATE|MAP_ANON,0,0);
bufp包含新区域地址。munmap函数删除虚拟存储器的区域:
虽然可以使用更低级的mmap和munmap函数来创建和删除虚拟存储器的区域。
但是C程序员还是觉得用动态存储器分配器(dynamic memory allocator)更方便。

动态存储器分配器维护着一个进程的虚拟存储区域,称为堆(heap)。
堆是一个请求二进制零的区域。bss区域,并向上生长(向更高的地址)。brk(break),指向堆顶。分配器将堆视为一组不同大小的块block的集合来维护。
每个块就是一个连续的虚拟存储器片,即页面大小。已分配,要么是空闲。 已分配 已分配的块显式地保留供应用程序使用。已分配的块保持已分配状态,直到它被释放。 释放要么是应用程序显示执行。 隐式执行(JAVA)。 空闲 空闲块可用于分配。空闲快保持空闲,直到显式地被应用分配。分配器有两种基本分格。
显式分配。显式分配器(explict allocator)
释放。C语言中提供一种叫malloc程序显示分配器。
malloc和freeC++
new和delete隐式分配器(implicit allocator)
分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器又叫做垃圾收集器(garbage collector).
垃圾收集(garbage collection).Lisp,ML以及Java等依赖这种分配器。
本节剩余的部分讨论的是显示分配器的设计与实现。
C标准库提供了一个称为malloc程序包的显示分配器。
#include<stdlib.h>
void* malloc(size_t size);
返回:成功则为指针,失败为NULL
malloc 返回一个指针,指向大小为至少size字节的存储器块。 size字节,很有可能是4或8的倍数 块会为可能包含在这个块内的任何数据对象类型做对齐。Unix系统用8字节对齐。malloc不初始化它返回的存储器。 calloc函数。 calloc是malloc一个包装函数。realloch函数如果malloc遇到问题。
NULL, 并设置errno。动态存储分配器,可以通过使用mmap和munmap函数,显示分配和释放堆存储器。
或者可以使用sbrk函数。
#include<unistd.h>
void *sbrk(intptr_t incr);
返回:若成功则为旧的brk指针,若出错则为-1,并设置errno为ENOMEML.
sbrk函数通过将内核的brk指针增加incr(可为负)来收缩和扩展堆。程序通过调用free函数来释放已分配的堆块。
#include<stdlib.h>
void free(void *ptr);
返回:无
ptr参数必须指向一个从malloc,calloc,realloc获得的已分配块的起始位置。 free行为未定义。free没有返回值,不知道是否错了。这里的字=4字节,且malloc是8字节对齐。


程序使用动态存储器分配的最重要原因是:
显式分配器有如下约束条件
缓冲请求。堆。块。 8字节。吞吐率最大化和存储器使用率最大化。这两个性能要求通常是相互冲突的。
目标1:最大化吞吐率
假定n个分配和释放请求的某种序列R1,R2,R3.....Rn
吞吐率 :每个单位时间完成的请求数。通过使分配和释放请求的平均时间最小化 来最大化吞吐率
目标2:最大化存储器利用率
需要增加分配和释放请求的时间。
评估使用堆的效率,最有效的标准是峰值利用率(peak utilization)
假定n个分配和释放请求的某种序列R1,R2,R3.....Rn
有效载荷(payload):如果一个应用程序请求一个p字节的块,那么得到的已分配块的有效载荷是p字节。(很有可能会分配p+1个字节之类的)聚集有效载荷(aggregate payload):请求Rk完成之后,Pk表示当前已分配块的有效载荷之后。又叫做聚集有效载荷。Hk表示堆的当前的大小(单调非递减的)。峰值利用率为Uk

吞吐率和存储器利用率是相互牵制的,分配器设计的一个有趣的挑战就是在两者之间找到一个平衡。
造成堆利用率很低的主要原因是一种称为碎片(fragmentation)的现象。
碎片:虽然有未使用的存储器但不能满足分配要求时的现象。
1.内部碎片:已分配块比有效载荷(实际所需要的)大时发生。
碎片.内部碎片的数量取决于以前请求的模式和分配器的实现方式。 2.外部碎片:当空闲存储器合计起来足够满足一个分配请求,但是没有一个单独的空闲块足够大可以处理这个请求发生的。
外部碎片的量化十分困难。 请求的模式和分配器的实现方式,还要知道将来请求的模式。启发式策略来用少量的大空闲块替换大量的小空闲块。一个实际的分配器要在吞吐率和利用率把我平衡,必须考虑一下几个问题。
9.9.6)9.9.7)9.9.8)
堆块(十分巧妙的利用了本该永远为0的低三位):
块由一个字的头部,有效载荷,以及可能的填充组成。 头部:编码了这个块的大小(包括头部和填充),以及这个块是否分配。 8字节的对齐约束条件 0。0~2^32(只是必须是8的倍数),非0~2^29。是否分配之类的信息。将堆组织为一个连续的已分配块和空闲块的序列。

这种结构就叫做隐式空闲链表
隐式 :
为什么叫隐式链表。
next)来链接起来。头部的长度隐含地链接起来。终止头部(类似与普通链表的NULL)
已分配,大小为零的块优缺点:
开销都与已分配块和空闲块的总数呈线性关系O(N). 字节,也会分配2个字的块。空间浪费。当应用请求k字节的块,分配器搜索空闲链表,查找一个足够大可以放置请求的空闲块。
有一下几种搜索放置策略
首次适配 从头开始搜索空闲链表,选择第一个合适的空闲块。下一次适配 首次适配很类似,但不是从头开始,而是从上一次查询的地方开始。最佳适配 优缺点
首次适配 较大快的搜索时间。下一次适配 存储器利用率低最佳适配
缺点
后面有更加精细复杂的分离式空闲链表。
两种策略
占用所有空闲块
内部碎片(但是如果内部碎片很少,可以接受)优点:能使得 空闲块+已分配块的数量减少
搜索速度。外部碎片(几个字节,很有可能是外部碎片)可能根本放置不了东西,但是却占用了搜索时间,还不如当内部碎片算了放置策略趋向于产生好的匹配中使用。
空闲块,内部碎片也很少。分割空闲块
如果分配器不能为请求块找到合适的空闲块将发生什么?
合并相邻的空闲块(下一节描述)。sbrk函数 大的空闲块假碎片: 因为释放,使得某些时候会出现相邻的空闲块。
碎片),合并却可以(假性),所以叫假碎片。重要的决策决定,何时执行合并?
立即合并
块被释放时,合并所有相邻的块。抖动。推迟合并
在对分配器的讨论中,我们假设使用立即合并。
但要知道,快速的分配器通常会选择某种形式的推迟合并。
Q:释放当前块后,如果要合并下一个块是十分简单,但是合并上一块复杂度却很高。
A:Knuth提出边界标记。
头部的副本。其实就是双向链表啦。
缺点:每个块保持一个头部和脚部,浪费空间。
小块时,产生明显的存储器开销。Q: 如何解决这种开销。
A: 使用边界标记优化方法.
把前面块的已分配/空闲位存放到当前块多出来的低位(000)中。
分配/空闲如果是已分配的,不需要处理。
已分配的不需要脚部。如果是未分配的,需要处理。
未分配的依旧需要脚部。十分优美的优化。
基于隐式空闲链表,使用立即边界标记合并方式,从头到尾讲述一个简单分配器的实现。
序言块
8字节的已分配块。普通块
malloc和free使用结尾块
序言块和结尾块都是用来消除合并边界条件的小技巧。
隐式空间链表就是一个玩具而已,用来介绍基本分配器概念。对于实际应用,还是太简单。
根据定义,程序并不需要一个空闲块的主体。所以可以将空闲块组织成一种显式数据结构。

O(块总数)降低到O(空闲块总数)。不过释放块时可能是线性,也可能是常数(普通的是常数)
取决于空闲链表中块的排序策略。
后进先出(LIFO)策略
新释放的块直接放到双向链表的开始处。(释放常数级别)
(处理的好的话,合并也是常数级别)
地址优先
释放是线性级别。
更好的空间利用率。
缺点:
内部碎片程度。分离存储: 维护多个空闲链表,其中每个链表中的块有大致相等的大小。
大小类(size class)。 大小类。 {1},{2},{3,4},{5~8},...{1025~2048},{2048~+oo}.{1},{2},{3},{4},{5},{6},...{1025~2048},{2048~+oo}. 有关动态存储分配的文献描述了几十种 分离存储方法。
我们介绍两种基本的方法
简单分离存储(simple segregated storage)和分离适配(segregated fit)。大小类
大小类的空闲链表包含大小相等的块,每个块的大小就是这个大小类中最大元素的大小。 {17~32}中,这个类的空闲链表全是32的块。如何分配
检查相应大小最接近的空闲链表
如果非空,简单的分配其中第一块的全部。
如果为空,请求一个固定大小的额外存储器片,将这个片分割,然后加入对应的链表。
常数级如何释放
释放即可,然后分配器将释放后的块直接插入空闲链表。常数级不分割,不合并。
最显著的缺点
内部碎片和外部碎片分配器维护着一个空闲链表的数组。
空闲链表是和一个大小类相关联的,并且被组织称某种类型的显示或隐式链接。链表包含潜在的大小不同的块。 大小是大小类的成员。有许多种不同的分离适配分配器,这里介绍一个简单版本。
如何分配
对适当的空闲链表做首次适配。
成功
分割它。空闲链表。失败
空闲链表释放,合并。
释放一个块,并执行合并,存入相应的空闲链表。分离适配方法是一种常见的选择,C标准库提供的GUN malloc包就是采用的这种方法。
利用率高 分离空闲链表简单的首次适配搜索,其存储器利用率近似对堆的最佳适配搜索。伙伴系统(buddy system)是分离适配的一种特例,其中每个大小类都是2的幂。
大小类
2^m如何分配
请求块大小向上舍入到最接近的2的幂,假设为2^k。2^j,满足(k<=j<=m)2^(j-1) 和 2^(j-1) 两部分,其中半块丢入空闲链表中。 伙伴。j=k。O(log(m)),很低如何释放,合并
合并。 伙伴处于空闲就不断合并,否则就停止。O(log(m)),很低。伙伴系统分配器的主要
优点
缺点
内部碎片。通用目的的工作负载。对于预先知道其中块大小是2的幂的系统,伙伴系统分配器就很有吸引力。
垃圾收集器(garbage collector)是一种动态存储分配器。
垃圾: 它自动释放不再需要的已分配块,这些块称为垃圾(garbage).垃圾收集(garbage collection) :自动回收堆存储的过程叫做垃圾收集。 显式分配堆块,但从不显式释放堆块。垃圾收集器定期识别垃圾快,并调用相应地free,将这些快放回空闲链表。垃圾收集可以追溯到John McCarthy在20实际60年代早期在MIT开发的Lisp系统。
Java,ML,Perl和Mathematic等现代语言系统的一个重要部分。垃圾收集方法,数量令人吃惊。McCarthy自创的Mark&Sweep(标记&清除)算法。 malloc包的基础上,为C和C++提供垃圾收集。
垃圾收集器将存储器视为一张有向可达图。
图的结点被分成一组根结点和一组堆结点
堆结点对应于堆中一个已分配的块。根结点对应于这样一种不在堆中的位置。 指针,寄存器,栈里的变量,或者是虚拟存储区域中读写数据区域中的全局变量p->q意味着块p中的某个位置指向块q中的某个位置 指针。当存在一条任意从根结点出发到达p的有向路径时。
p是可达的。不可达的,不可达结点对应于垃圾。垃圾收集器的角色是维护可达图的某种表示,并释放不可达结点返回给空闲链表。
ML和Java这样的语言的垃圾收集器,对应用如何创建和使用指针都有严格的控制。
C 和 C++ 通常不能维护可达图的一种精确表示。这样的收集器叫做保守的垃圾收集器
保守: 每个可达块都被标记为可达块,但有些不可达块也被标记为可达块。指针由自己管理,系统无法判定数据是否为指针,那么就不好精确的遍历。
如果malloc找不到合适的空闲块,就会调用垃圾收集器。回收一些垃圾到空闲链表。
free。Mark&Sweep 垃圾收集器由标记(mark)阶段和清除(sweep)阶段
标记阶段:标记出根结点的所有可达和已分配的后继。清除阶段:后面的清除阶段释放每个未被标记的已分配块。 头部的低位的一位用来表示是否被标记。标记的算法 就是从根结点开始,对结点的指针数据深搜并标记。
isPtr()来判断是否是指针,p是否指向一个分配块的某个字。 起始位置。
清除的算法 就是遍历图,然后释放未被标记的。
Mark & Sweep(很有意思的一小节,败也指针)C语言的isPtr()的实现有一些有趣的挑战。
C不会用任何类型信息来标记存储器位置。
p是不是一个指针。 java等语言里面,指针全部由系统管理。即使假设是,isPtr()也没没有明显的方式判断p是否指向一个已分配块的有效载荷的某个位置。
解决方法: 将已分配块维护成一颗平衡二叉树。

头部新增Left,和Right Left:地址小于当前块的块。Right:地址大于当前块的块。addr<= p <= (addr + Size)判断是否属于这个块。这样子就能二分查找p 属于那个已分配块。
C语言是保守的原因是,无法判断p逻辑上是指针,还是一个int标量
因为,无论p是个什么玩意,都必须去访问,如果他是指针呢?
int刚好还是某个不可到达块的地址。那么就会有残留。而且这种情况很常见,毕竟指针在数据段里毕竟不是特别多。
但是在java等语言里,指针由系统统一管理,那么很容易就知道p是否是一个指针了。
比如scanf("%d",a); 程序会把a的int值看作指针。而且运行中,无法判断。
scanf("%d",&val);
scanf("%d",val);
读/写区域,造成奇怪的困惑的结果。堆存储器并不会初始化。

calloc.y[i]=0;程序不检查输入串的大小就写入栈中的目标缓冲区
缓冲区溢出错误(buffer overflow bug)。gets()容易引起这样的错误 fgets()限制大小。有的系统里,int 和 int *都是四字节,有的则不同。
没啥好说的。
对指针的优先级用错。
例 :*size-- 本意 (*size)--
忘记了指针的算术操作是以它们指向的对象的大小为单位来进行的,这种大小不一定是字节。


返回一个指针,指向栈里面一个变量的地址。但是这个变量在返回的时候已经从栈里被弹出。
引用了某个已经free掉的块。在C++多态中经常容易犯这个错误。
即是没有回收垃圾。导致内存中垃圾越来越多。
对于守护进程和服务器这样的程序,存储器泄露是十分严重的事。
虚拟存储器是对主存的一个抽象。
虚拟寻址的间接形式来引用主存。 虚拟地址,通过一种地址翻译硬件来转换为物理地址。 虚拟存储器提供三个功能
它在主存中自动缓存最近使用的存放在磁盘上的虚拟地址空间内容。
虚拟存储器缓存中的块叫做页简化了存储器管理,
链接共享数据。存储器分配以及程序加载。存储器保护。地址翻译的过程必须和系统中所有的硬件缓存的操作集合。
L1高速缓存中。 TLB的页表条目的片上高速缓存L1。现代系统通过将虚拟存储器片和磁盘上的文件片关联起来,以初始化虚拟存储器片,这个过程叫做存储器映射。
存储器映射为共享数据,创建新的进程 以及加载数据提供一种高效的机制。可以用mmap 手工维护虚拟地址空间区域。
动态存储器分配,例:malloc 堆的区域显示分配器 C,C++隐式分配器 JAVA等GC是通过不断递归访问指针来标记已分配块,在需要的时刻进行Sweep。
C,C++无法辨认指针导致无法实现完全的GC。 GC。p所指向的块标签:
原文地址:http://blog.csdn.net/zy691357966/article/details/51495197