2017-2018-1 20155228 《信息安全系统设计基础》第十四周学习总结
教材学习内容总结
虚拟内存
在早期的计算机中,是没有虚拟内存的概念的。我们要运行一个程序,会把程序全部装入内存,然后运行。当运行多个程序时,经常会出现以下问题:
进程地址空间不隔离:没有权限保护,由于程序都是直接访问物理内存,所以一个进程可以修改其他进程的内存数据,甚至修改内核地址空间中的数据。
内存使用效率低:当内存空间不足时,要将其他程序暂时拷贝到硬盘,然后将新的程序装入内存运行。由于大量的数据装入装出,内存使用效率会十分低下。
程序运行的地址不确定:因为内存地址是随机分配的,所以程序运行的地址也是不确定的。
为了更加有效地管理内存并且少出错,现代系统提供了一种对主存的抽象概念,叫做虚拟内存(VM)。虚拟内存是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美交互,它为每个进程提供了一个大的、一致的和私有的地址空间。
虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。
通过一个很清晰的机制,虚拟内存将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,它高效地使用了主存,为每个进程提供了一致的地址空间,从而简化了内存管理,保护了每个进程的地址空间不被其他进程破坏。
物理寻址和虚拟寻址
物理地址
用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。
虽然可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址,但是事实上,这只是一个硬件提供给软件的抽像,内存的寻址方式并不是这样。
所以,说它是“与地址总线相对应”,是更贴切一些,不过抛开对物理内存寻址方式的考虑,直接把物理地址与物理的内存一一对应,也是可以接受的。也许错误的理解更利于形而上的抽像。
虚拟地址
CPU启动保护模式后,程序运行在虚拟地址空间中。注意,并不是所有的“程序”都是运行在虚拟地址中。CPU在启动的时候是运行在实模式的,Bootloader以及内核在初始化页表之前并不使用虚拟地址,而是直接使用物理地址的。使用虚拟寻址,CPU通过生成一个虚拟地址(Virtual Address, VA)来访问主存,这个虚拟地址在被送到内存之前先转换成适当的物理地址。
如果CPU寄存器中的分页标志位被设置,那么执行内存操作的机器指令时,CPU(准确来说,是MMU,即Memory Management Unit,内存管理单元)会自动根据页目录和页表中的信息,把虚拟地址转换成物理地址,完成该指令。将一个虚拟地址转换为物理地址的任务叫做地址翻译(address translation)。就像异常处理一样,地址翻译需要CPU硬件和操作系统之I间的紧密合作。CPU芯片上叫做内存管理单元(Memory Management Unit,MMU)的专用硬件,利用存放在主存中的查询表来动态翻译虚拟地址,该表的内容由操作系统管理。
对于一个要转换成物理地址的虚拟地址,CPU首先根据CR3中的值,找到页目录所在的物理页。然后根据虚拟地址的第22位到第31位这10位(最高的10bit)的值作为索引,找到相应的页目录项(PDE,page directory entry),页目录项中有这个虚拟地址所对应页表的物理地址。有了页表的物理地址,根据虚拟地址的第12位到第21位这10位的值作为索引,找到该页表中相应的页表项(PTE,page table entry),页表项中就有这个虚拟地址所对应物理页的物理地址。最后用虚拟地址的最低12位,也就是页内偏移,加上这个物理页的物理地址,就得到了该虚拟地址所对应的物理地址。
地址空间
多任务操作系统中的每一个进程都运行在一个属于它自己的内存沙盘中,这个沙盘就是虚拟地址空间
(virtual address space),在32位模式下,它总是一个4GB的内存地址块。这些虚拟地址通过页表(page table)
映射到物理内存,页表由操作系统维护并被处理器引用。每个进程都拥有一套属于它自己的页表。
要保证多个应用程序同时处于内存中并且不互相影响,则需要解决两个问题:保护和重定位。一个原始的对前者的解决办法:给内存块标记上一个保护键,并且比较执行进程的键和其访问的每个内存字的保护键。然而,这种方法本身并没有解决后一个问题,虽然这个问题可以通过在程序被装载时重定位程序来解决,但这是一个缓慢且复杂的解决方法。
一个更好的办法是创造一个新的内存抽象:地址空间。地址空间(address space)是一个非负整数地址的有序集合,{0,1,2,…}
如果地址空间中的整数是连续的,那么我们说它是一个线性地址空间(( linear address space)。在一个带虚拟内存的系统中,CPU从一个有N = 2"个地址的地址空间中生成虚拟地址,这个地址空间称为虚拟地址空间(virtual address space){0,1,2,…,N一1}
就像进程的概念创造了一类抽象的CPU以运行程序一样,地址空间为程序创造了一种抽象的内存。地址空间是一个进程可用于寻址内存的一套地址集合。每个进程都有一个自己的地址空间,并且这个地址空间独立于其他进程的地址空间(除了在一些特殊情况下进程需要共享它们的地址空间外)。
地址空间的概念非常通用,并且在很多场合中出现。随着数量的增长,空间变得越来越不够用了,从而导致需要使用更多位数。
地址空间可以不是数字的。互联网域名也是地址空间。这个地址空间是由所有包含2~63个字符并且后面跟着字符串组成的,组成这些字符串的字符可以是字母、数字和连字符。
虚拟内存作为缓存的工具
缓存
许多人认为,“缓存”是内存的一部分,许多技术文章都是这样教授的,事实上这么说是不正确的。 其实,缓存是CPU的一部分,它存在于CPU中
CPU存取数据的速度非常的快,一秒钟能够存取、处理十亿条指令和数据(术语:CPU主频1G),而内存就慢很多,快的内存能够达到几十兆就不错了,可见两者的速度差异是多么的大。
缓存是为了解决CPU速度和内存速度的速度差异问题。
内存中被CPU访问最频繁的数据和指令被复制入CPU中的缓存,这样CPU就可以不经常到象“蜗牛”一样慢的内存中去取数据了,CPU只要到缓存中去取就行了,而缓存的速度要比内存快很多 。
这里要特别指出的是:
因为缓存只是内存中少部分数据的复制品,所以CPU到缓存中寻找数据时,也会出现找不到的情况(因为这些数据没有从内存复制到缓存中去),这时CPU还是会到内存中去找数据,这样系统的速度就慢下来了,不过CPU会把这些数据复制到缓存中去,以便下一次不要再到内存中去取。
因为随着时间的变化,被访问得最频繁的数据不是一成不变的,也就是说,刚才还不频繁的数据,此时已经需要被频繁的访问,刚才还是最频繁的数据,现在又不频繁了,所以说缓存中的数据要经常按照一定的算法来更换,这样才能保证缓存中的数据是被访问最频繁的
DRAM
DRAM(Dynamic Random Access Memory),即动态随机存取存储器,最为常见的系统内存。DRAM 只能将数据保持很短的时间。为了保持数据,DRAM使用电容存储,所以必须隔一段时间刷新(refresh)一次,如果存储单元没有被刷新,存储的信息就会丢失。 (关机就会丢失数据)
为了有助于清晰理解存储层次结构中不同的缓存概念,我们将使用术语SRAM缓存来表示位于CPU和主存之间的L1, L2和L3高速缓存,并且用术语DRAM缓存来表示虚拟内存系统的缓存,它在主存中缓存虚拟页。
在存储层次结构中,DRAM缓存的位置对它的组织结构有很大的影响。回想一下,DRAM比SRAM要慢大约10倍,而磁盘要比DRAM慢大约100 000多倍。因此,DRAM缓存中的不命中比起SRAM缓存中的不命中要昂贵得多,这是因为DRAM缓存不命中要由磁盘来服务,而SRAM缓存不命中通常是由基于DRAM的主存来服务的。而且,从磁盘的一个扇区读取第一个字节的时间开销比起读这个扇区中连续的字节要慢大约100 000倍。归根到底,DRAM缓存的组织结构完全是由巨大的不命中开销驱动的。
页表的命中和缺页
同任何缓存一样,虚拟内存系统必须有某种方法来判定一个虚拟页是否缓存在DRAM中的某个地方。如果是,系统还必须确定这个虚拟页存放在哪个物理页中。如果不命中,系统必须判断这个虚拟页存放在磁盘的哪个位置,在物理内存中选择一个牺牲页,并将虚拟页从磁盘复制到DRAM中,替换这个牺牲页。
这些功能是由软硬件联合提供的,包括操作系统软件、MMU(内存管理单元)中的地址翻译硬件和一个存放在物理内存中叫做页表(page table)的数据结构,页表将虚拟页映射到物理页。每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。操作系统负责维护页表的内容,以及在磁盘与DRAM之间来回传送页。
逻辑地址:CPU所生成的地址。CPU产生的逻辑地址被分为 :p (页号) 它包含每个页在物理内存中的基址,用来作为页表的索引;d (页偏移),同基址相结合,用来确定送入内存设备的物理内存地址。
定义 | 解释 |
---|---|
物理地址 | 内存单元所看到的地址。逻辑地址空间为2^m,且页大小为2^n,那么逻辑地址的高m-n位表示页号,低n位表示页偏移。 |
逻辑地址空间 | 由程序所生成的所有逻辑地址的集合。 |
物理地址空间 | 与逻辑地址相对应的内存中所有物理地址的集合,用户程序看不见真正的物理地址。 |
注:用户只生成逻辑地址,且认为进程的地址空间为0到max。物理地址范围从R+0到R+max,R为基地址,地址映射-将程序地址空间中使用的逻辑地址变换成内存中的物理地址的过程。由内存管理单元(MMU)来完成
分页逻辑地址=P(页号).d(页内位移)
分页物理地址=f(页帧号).d(同上)
P = 线性逻辑地址/页面大小
d= 线性逻辑地址-P*页面大小
考虑一下当CPU想要读包含在VP 2中的虚拟内存的一个字时会发生什么, VP2被缓存在DRAM中。使用我们将在9. 6节中详细描述的一种技术,地址翻译硬件将虚拟地址作为一个索引来定位PTE 2,并从内存中读取它。因为设置了有效位,那么地址翻译硬件就知道VP 2是缓存在内存中的了。所以它使用PTE中的物理内存地址(该地址指向PP 1中缓存页的起始位置),构造出这个字的物理地址。
缺页中断就是要访问的页不在主存,需要操作系统将其调入主存后再进行访问。在这个时候,被内存映射的文件实际上成了一个分页交换文件。
在虚拟内存的习惯说法中,DRAM缓存不命中称为缺页(page fault)。展示了在缺页之前我们的示例页表的状态。CPU引用了VP 3中的一个字,VP 3并未缓存在DRAM中。地址翻译硬件从内存中读取PTE 3,从有效位推断出VP 3未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在此例中就是存放在PP 3中的VP 4。如果VP 4已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP 4的页表条目,反映出VP 4不再缓存在主存中这一事实。
虚拟内存作为内存管理的工具
我们看到虚拟内存是如何提供一种机制,利用DRAM缓存来自通常更大的虚拟地址空间的页面。有趣的是,一些早期的系统,比如DEC PDP-11/70,支持的是一个比物理内存更小的虚拟地址空间。然而,虚拟地址仍然是一个有用的机制,因为它大大地简化了内存管理,并提供了一种自然的保护内存的方法。
到目前为止,我们都假设有一个单独的页表,将一个虚拟地址空间映射到物理地址空间。实际上,操作系统为每个进程提供了一个独立的页表,因而也就是一个独立的虚拟地址空间。图展示了基本思想。在这个示例中,进程的页表将V Pl映射到PP2, VP2映射到PP 7。相似地,进程j的页表将VP 1映射到PP 7, VP 2映射到PP10。注意,多个虚拟页面可以映射到同一个共享物理页面上。
虚拟内存作为内存保护的工具
任何现代计算机系统必须为操作系统提供手段来控制对内存系统的访问。不应该允许一个用户进程修改它的只读代码段。而且也不应该允许它读或修改任何内核中的代码和数据结构。不应该允许它读或者写其他进程的私有内存,并且不允许它修改任何与其他进程共享的虚拟页面,除非所有的共享者都显式地允许它这么做(通过调用明确的进程间通信
系统调用)。
就像我们所看到的,提供独立的地址空间使得区分不同进程的私有内存变得容易。但是,地址翻译机制可以以一种自然的方式扩展到提供更好的访问控制。因为每次CPU生成一个地址时,地址翻译硬件都会读一个PTE,所以通过在PTE上添加一些额外的许可位来控制对一个虚拟页面内容的访问十分简单。图9-10展示了大致的思想。
地址翻译
既然虚拟存储器被分配成一个个虚拟页,每个虚拟页大小的是2^p字节,那肯定有一个方法去识别,某个字节或者某段数据具体属于哪个虚拟页,又在这个页的哪个位置,对否?既然虚拟页大小是2^p字节,说明我们用p位二进制地址就能标识这个页中的每一个字节了。而虚拟地址共n位,也就说还剩n-p位,干嘛呢?当然是标识虚拟页号了,有n-p位就能标识2^(n-p)个虚拟页。(注意哦,2^p的单位是字节,2^(n-p)的单位是“个”千万别搞混!)
页表这个东西,为什么能把虚拟页和物理页对应联系起来呢?事实上,页表条目的PTE数组才是对应关系的关键,PTE数组下标能对应相应的虚拟页号VPN,而数组中存的又是物理页号PPN,这样当VPN与PPN建立强联系后,剩下的偏移位,直接将VPO复制给PPO就完事,于是有了下面的过程图:
这里又会涉及到一个现象,既然物理内存有上一级缓存SRAM,比如说就是L1,CPU中MMU与物理内存之间的任何数据交换都会经过L1的查找和处理。比如,MMU需要和物理内存交换PTE、PTEA(PTE的标号)、PA(物理地址)这些信息,L1都会检查是否先前已有缓存。如果命中,直接由L1提供MMU所需的数据;如果不命中,则由L1向物理内存申请数据,于是就有了下图的过程:
这里又会涉及到一个现象,既然物理内存有上一级缓存SRAM,比如说就是L1,CPU中MMU与物理内存之间的任何数据交换都会经过L1的查找和处理。比如,MMU需要和物理内存交换PTE、PTEA(PTE的标号)、PA(物理地址)这些信息,L1都会检查是否先前已有缓存。如果命中,直接由L1提供MMU所需的数据;如果不命中,则由L1向物理内存申请数据,于是就有了下图的过程:
从形式上来说,地址翻译是一个N元素的虚拟地址空间(VAS)中的元素和一个M元素的物理地址空间(PAS),以下展示了MMU如何利用页表来实现这样的映射,CPU中的一个控制寄存器,页表基址寄存器指向当前页表,n位的虚拟地址包含两部分;一个p位的虚拟页面偏移和一个(n-p)位的虚拟页号.MMU利用虚拟地址(VPN)来选择适当的PTE,将页表条目中物理页号和虚拟地址中VPO串联起来,就得到了相应的物理地址,注意,因为物理页面和虚拟页面都是p字节的, 所以物理页面偏移和(VPO)是一样的.
当页面命中时.CPU硬件的执行步骤:
处理器生成了一个虚拟地址,并把它传给MMU.
MMU生成PET地址,并从高速缓存/主存中得到它.
高速缓存/主存向MMU返回PTE.
MMU构造物理地址,并把它传送高速缓存/主存.
高速缓存/主存返回所请求的数据字给处理器.
页面命中完全是由硬件来处理的,与之不同的是,处理缺页要求硬件和操作系统内核协同来完成,第一到第三如上;
处理器生成了一个虚拟地址,并把它传给MMU.
MMU生成PET地址,并从高速缓存/主存中得到它.
高速缓存/主存向MMU返回PTE.
MMU构造物理地址,并把它传送高速缓存/主存.
高速缓存/主存返回所请求的数据字给处理器.
PTE中的有效位为0,所以MMU触发了一次异常,传递cpu中的控制到操作系统内核中的缺页异常处理程序.
缺页处理程序确定出物理存储器中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘.
缺页处理程序页面调入新的页面,并更新存储器中的PTE.
缺页处理程序返回到原来的进程,再次执行导致缺页的指令,CPU将引起缺页的虚拟地址重新发送给MMU,因为虚拟页面现在缓存在物理存储器中,所以会命中.
上面我们提到如果缺页,缺页异常处理程序将确定一个牺牲页,那么这个牺牲页是怎样确定的呢?
当MMU 翻译每一个虚拟地址时,他还会更新另外两个内核缺页处理程序会用到的位,每次访问一个页时,MMU都会设置A位,称为引用位,内核可以用这个引用位来实现它的页替换算法,每次对一个页进行写了之后,MMU都会设置D位,又称脏位,脏位告诉内核在拷贝替换页之前是否必须写回牺牲页,内核可以通过调用一条特殊的内核模式指令来清除引用位或脏位.
利用TLB加速地址翻译
什么是TLB
TLB的全称是Translation Lookaside Buffer,我们知道,处理器在取指或者执行访问memory指令的时候都需要进行地址翻译,即把虚拟地址翻译成物理地址。而地址翻译是一个漫长的过程,需要遍历几个level的Translation table,从而产生严重的开销。为了提高性能,我们会在MMU中增加一个TLB的单元,把地址翻译关系保存在这个高速缓存中,从而省略了对内存中页表的访问。
为什么有TLB
TLB这个术语有些迷惑,但是其本质上就是一种cache,既然是一种cache,那么就没有什么好说的,当然其存在就是为了更高的performance了。不同于instruction cache和data cache,它是Translation cache。对于instruction cache,它是解决cpu获取main memory中的指令数据(地址保存在PC寄存器中)的速度比较慢的问题而设立的。同样的data cache是为了解决数据访问指令比较慢而设立的。但是实际上这只是事情的一部分,我们来仔细看看程序中的数据访问指令(例如说是把)的执行过程,这个过程可以分成如下几个步骤:
将PC中的虚拟地址翻译成物理地址
从memory中获取数据访问指令(假设该指令需要访问地址x)
将虚拟地址x翻译成物理地址y
从location y的memory中获取具体的数据
instruction cache解决了step (2)的性能问题,data cache解决了step (4)中的性能问题,当然,复杂的系统会设立了各个level的cache用来缓存main memory中的数据,因此,实际上unified cache同时可以加快step (2)和(4)的速度。Anyway,这只是解决了部分的问题,IC设计工程师怎么会忽略step (1)和step (3)呢,这也就是TLB的由来。如果CPU core发起的地址翻译过程能够在TLB(translation cache)中命中(cache hit),那么CPU不需要访问慢速的main memory从而加快了CPU的performance。
TLB工作原理
当需要转换VA到PA的时候,首先在TLB中找是否有匹配的条目,如果有,那么我们称之TLB hit,这时候不需要再去访问页表来完成地址翻译。不过TLB始终是全部页表的一个子集,因此也有可能在TLB中找不到。如果没有在TLB中找到对应的item,那么称之TLB miss,那么就需要去访问memory中的page table来完成地址翻译,同时将翻译结果放入TLB,如果TLB已经满了,那么还要设计替换算法来决定让哪一个TLB entry失效,从而加载新的页表项。简单的描述就是这样了,我们可以对TLB entry中的内容进行详细描述,它主要包括:
物理地址(更准确的说是physical page number)。这是地址翻译的结果。
虚拟地址(更准确的说是virtual page number)。用cache的术语来描述的话应该叫做Tag,进行匹配的时候就是对比Tag。
Memory attribute(例如:memory type,cache policies,access permissions)
status bits(例如:Valid、dirty和reference bits)
其他相关信息。
多级页表
如果使用简单的一级页表,如果进程使用全部4G线性地址空间,那么将需要高达2^20表项(总共地址线是32位,每页大小为4kb,则页偏移量需要低12位,高20位当作页表地址)来保存表示每个进程的页表,若每项4B,则需要4MB的ram来存储页表。即使一个进程并不使用内的所有地址。
使用这种二级模式能够减少每个进程页表所需RAM数量。开始一直没想通,为什么节省了呢?从你最终要存储的表项来看,无论如何你存储的表项是不会少的,而且多级页表还会增加存储开销。
其实是这样的,二级表只是从进程的角度来看,为进程节省了页表项(其实所有的页表存储空间增大了)。二级模式通过只为进程实际使用的那些虚拟内存区请求页表来减少页表,就是进程未使用的页暂时可以不用为其建立页表,因为如果使用一级页表的话,你就必须为所有的4G范围内分配页表,不能细分。每个活动进程必须有一个分配给它的页目录,不过没必要马上为进程的所有页表都分配ram,只有在进程实际需要一个页表时才给该页表分配ram,这样就提高了效率。
注:正在使用的页目录物理地址存放在控制寄存器cr3中。
注:linux进程的线性地址空间分成两部分:
(1)0x00000000~0xbfffffff的线性地址,无论用户态和内核态的进程都可以寻址;
(2)0xc0000000~0xffffffff的线性地址,只有内核态的进程可以寻址。
存储器映射
Linux通过将一个虚拟内存区域与一个磁盘上的对象(object)关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射(memory mapping)。虚拟内存区域可以映射到两种类型的对象中的一种:
- Linux文件系统中的普通文件:一个区域可以映射到一个普通磁盘文件的连续部分,例如一个可执行目标文件。文件区(section)被分成页大小的片,每一片包含一个虚拟页面的初始内容。因为按需进行页面调度,所以这些虚拟页面没有实际交换进人物理内存,直到CPU第一次引用到页面(即发射一个虚拟地址,落在地址空间这个页面的范围之内)。如果区域比文件区要大,那么就用零来填充这个区域的余下部分。
- 匿名文件:一个区域也可以映射到一个匿名文件,匿名文件是由内核创建的,包含的全是二进制零。CPU第一次引用这样一个区域内的虚拟页面时,内核就在物理内存中找到一个合适的牺牲页面,如果该页面被修改过,就将这个页面换出来,用二进制零覆盖牺牲页面并更新页表,将这个页面标记为是驻留在内存中的。注意在磁盘和内存之间并没有实际的数据传送。因为这个原因,映射到匿名文件的区域中的页面有时也叫做请求二进制零的页(demand-zero page)o
无论在哪种情况中,一旦一个虚拟页面被初始化了,它就在一个由内核维护的专门的交换文件(swap file)之间换来换去。交换文件也叫做交换空间(swap space)或者交换区域(swap area)。需要意识到的很重要的一点是,在任何时刻,交换空间都限制着当前运行着的进程能够分配的虚拟页面的总数。
动态存储器分配的方法
虽然可以使用低级的mmap和munmap函数来创建和删除虚拟内存的区域,但是C程序员还是会觉得当运行时需要额外虚拟内存时,用动态内存分配器(dynamic memory allo-cator)更方便,也有更好的可移植性。
动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap)
)。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk(读做“break"),它指向堆的顶部。
分配器将堆视为一组不同大小的块(block)的集合来维护。每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
垃圾收集的概念
在诸如C malloc包这样的显式分配器中,应用通过调用malloc和free来分配和释放堆块。应用要负责释放所有不再需要的已分配块。
未能释放已分配的块是一种常见的编程错误。例如,考虑下面的C函数,作为处理的一部分,它分配一块临时存储:
void garbage()
{
int *p=(int *)Malloc(15213)
return;/*Array p is garbage at this point*/
}
因为程序不再需要P,所以在garbage返回前应该释放P。不幸的是,程序员忘了释放这个块。它在程序的生命周期内都保持为已分配状态,毫无必要地占用着本来可以用来满足后面分配请求的堆空间。
垃圾收集器(garbage collector)是一种动态内存分配器,它自动释放程序不再需要的已分配块。这些块被称为垃圾(garbage)<因此术语就称之为垃圾收集器)。自动回收堆存储的过程叫做垃圾收集(garbage collection)。在一个支持垃圾收集的系统中,应用显式分配堆块,但是从不显示地释放它们。在C程序的上下文中,应用调用malloc,但是从不调用free。反之,垃圾收集器定期识别垃圾块,并相应地调用free,将这些块放回到空闲链表中。
C语言中与存储器有关的错误
间接引用坏指针
在进程的虚拟地址空间中有较大的洞,没有映射到任何有意义的数据。如果我们试图间接引用一个指向这些洞的指针,那么操作系统就会以段异常中止程序。而且,虚拟内存的某些区域是只读的。试图写这些区域将会以保护异常中止这个程序。
间接引用坏指针的一个常见示例是经典的scarf错误。假设我们想要使用scarf从stdin读一个整数到一个变量。正确的方法是传递给scarf一个格式串和变量的地址:
scarf(“%d",&val)
然而,对于C程序员初学者而言(对有经验者也是如此!),很容易传递val的内容,而不是它的地址:
scarf(”%d",val)
在这种情况下,scanf将把val的内容解释为一个地址,并试图将一个字写到这个位置。在最好的情况下,程序立即以异常终止。在最糟糕的情况下,val的内容对应于虚拟内存的某个合法的读/写区域,于是我们就覆盖了这块内存,这通常会在相当长的一段时间以后造成灾难性的、令人困惑的后果。
读未初始化的内存
虽然bss内存位置(诸如未初始化的全局C变量)总是被加载器初始化为零,但是对于堆内存却并不是这样的。一个常见的错误就是假设堆内存被初始化为零:
/* Return y=Ax */
int *matvec(int **A, int *x, int n)
{
int i,j:
int *y=(int *)Malloc(n*sizeof(int));
for (i=0; i<n; i++)
for (j=0; j<n; j++)
y[i]+=A[i][j]*x[j];
return y;
}
程序员不正确地假设向量y被初始化为零。正确的实现方式是显式地将y[i]设置为零,或者使用callow
教材学习中的问题和解决过程
RAM RRAM DRAM三者有什么区别?
RAM,Random-Access Memory,即随机存取存储器,其实就是内存,断电会丢失数据。主要分为SRAM(static)和DRAM(dynamic)。主要的区别在于存储单元,DRAM使用电容电荷进行存储。需要一直刷新充电。SRAM是用锁存器锁住信息,不需要刷新。但也需要充电保持。关于DRAM,其基本的存储单元如下,利用一个晶体管进行控制电容的充放电。
DRAM一般的寻址模式,控制的晶体管集成在单个存储单元中。现在的DRAM一般都是SDRAM,即Synchronous Dynamic Random Access Memory,同步且能自由指定地址进行数据读写。其结构一般由许多个bank组成并利用以达到自由寻址。
而RRAM,指的是Resistive Random Access Memory,这是最近才新研究的技术,并不成熟。利用Memositor(一种记忆电阻,其阻值会根据流过的电流而变化)作为存储单元,优点十分明显,并且和DRAM比起来在array中可以减少控制晶体管的数量,在CMOS chip上已经有所应用。 举crossbar为例,我在一篇文章上看到的芯片
动态存储器分配
分配器有两种基本风格。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。
- 显式分配器(explicit allocator),要求应用显式地释放任何已分配的块。例如,C标准库提供一种叫做malloc程序包的显式分配器。C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。C++中的new和delete操作符与C中的malloc和free相当。
- 隐式分配器(implicit allocator),另一方面,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器(garbage collec-tor),而自动释放未使用的已分配的块的过程叫做垃圾收集(garbage collection) 。例如,诸如Lisp, ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
代码调试中的问题和解决过程
编写一个C程序mmapcopy.c,使用mmap将一个任意大小的磁盘文件复制到stdout。输入文件的名字必须作为一个命令行参数来传递。
#include "csapp.h"
/*
*mmapcopy一uses mmap to copy file fd to stdout
*/
void mmapcopy(int fd, int size)
{
char *bufp;/*ptr to memory-mapped VMarea*/
bufp=Mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0)
Write(1,bufp, size);
return;
}
/*mmapcopy driver*/
int main(int argc, char **argv)
{
struct stat stat;
int fd;
/* Check for required command-line argument*/
if(argc !=2)
{
printf(”usage: %s <filename>\n",argvf[0]);
exit (0);
}
/*Copy the input to stdout*/
fd=Open(argv[1],O_RDONLY, 0);
fstat(fd, &stat);
mmapcopy(fd, stat .st_size);
exit (0);
}
代码托管
本周结对学习情况
学习进度条
代码行数(新增/累积) | 博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
---|---|---|---|---|
目标 | 5000行 | 30篇 | 400小时 | |
第一周 | 200/200 | 2/2 | 20/20 | |
第二周 | 300/500 | 2/4 | 18/38 | |
第三周 | 500/1000 | 3/7 | 22/60 | |
第四周 | 300/1300 | 2/9 | 30/90 | |
第五周 | 400/900 | 2/6 | 6/30 | |
第五周 | 400/900 | 2/6 | 6/30 | |
第六周 | 200/1100 | 1/7 | 6/30 | |
第七周 | 500/1600 | 2/9 | 6/36 | |
第八周 | 300/1900 | 1/10 | 6/42 | |
第九周 | 1000/2900 | 3/13 | 6/48 | |
第十一周 | 200/3100 | 2/15 | 6/56 | |
第十三周 | 300/3400 | 1/16 | 6/64 |
尝试一下记录「计划学习时间」和「实际学习时间」,到期末看看能不能改进自己的计划能力。这个工作学习中很重要,也很有用。
耗时估计的公式
:Y=X+X/N ,Y=X-X/N,训练次数多了,X、Y就接近了。
计划学习时间:6小时
实际学习时间:6小时
改进情况:
(有空多看看现代软件工程 课件
软件工程师能力自我评价表)