分类: 服务器与存储
2012-03-26 09:04:16
当代计算机使用分层存储结构--寄存器->CPU缓存->内存->外存.其中内存是当前程序性能最重要的资源,是程序最大的瓶颈。
计算机里运行的每个程序都拥有独立完整连续的虚拟内存,而数据实际存储在物理内存中。物理内存内的数据是不连续的,以页(Page)为最小单元,大小一般 为4K。大多数情况下,物理内存要比虚拟内存小.所以并不能装下虚拟内存中的的所有数据,这时就需要把一些数据暂时存放在外存中。当数据需要被使用而不在 物理内存中时(页面失效),则先把一些暂时不用的数据交换到外存中,再把需要的数据从外存中交换到物理内存中。
32位虚拟内存映射到32位物理内存和64位虚拟内存映射到64位虚拟内存的情况比较常见,但还有一种情况是32位虚拟内存映射到64位物理内存的情况。 这种情况出现在当CPU寄存器为32位,而物理内存超过4G时,为了取到物理内存超过4G的部分,需要对物理内存64位取址。
当CPU执行一个从内存读取数据的指令时,它得到一个虚拟地址。虚拟地址需要通过MMU(Memory Management Unit)转换为物理内存的地址,才能从实际的内存中读取数据。32虚拟内存地址以10 10 12的形式分为三部分。前面10位用来索引PDE(Page Directory Entry),中间10位用来索引PTE(Page Table Entry),最后的12位用来索引页面(Page)从而得到真正的内存地址。
PDE
PDE(Page Directory Entry)是存放在内存中的一张表,通过它可以索引到PTE(Page Table Entry)。10位的PDE最多可以存放210项PTE。PDE的地址存放在寄存器CR3中。当进程切换时,CR3的值也跟着改变,这也意味着每个进程都拥有自己的PDE,这样便保证了每个进程都有自己独立的内存空间而互不干扰。因为每项都存放一个指针(4个字节),所以表的大小是4K。
PTE
PTE(Page Table Entry)也是存放在内存中的一张表,通过它可以索引到页面(Page)。10位的PTE最多可以存放210项页面。PTE的大小也是4K。
Page
页面是物理内存中的最小单元,大小在32位是4K,64位下可以是4K,2M甚至是1G。每次分配的内存大小必须是页面大小的整数倍。比如4K的页面,当 程序需要新的内存时,不超过4K也会以4K的大小分配,如果需要6K则会分配8K。页面是可被交换出物理内存,暂存到外存中的。(注:PDE和PTE是始 终存放在物理内存中,不会被交换的。)当程序需要的数据所在的页面不在物理内存中时,就会产生页面失效(Page Fault),继而需要把它从外存中交换进来。页面失效是非常耗时的,为了提高程序的性能,应该尽可能地避免页面失效。
利用这种分层结构,最终可以将虚拟内存映射到物理内存。这种分层结构有两个优点:一个是确保了每个进程都有自己独立的虚拟内存空间而不会互相干扰;二是节 省内存空间,因为一个程序一般是不能用完所有的页面,只有需要时才会在内存创建新的页面,如果没有分层结构,则页面的索引表将会非常大,其中包含了大量的 NULL项,而分层结构解决了这个问题。不仅如此,因为有些进程是可以共享一些内存的(比如同一程序的不同实例可以共享可执行代码),所以它们只要令 PDE或PTE中的项中的指针值相同就行。
PAE
考虑到32位虚拟内存映射到64位物理内存的情况,需要将PTE的项扩展为64位(8字节),这时PTE的大小变为8K,在计算机里,4K是一个非常便利的大小,所以为了保持PTE4K的大小,可以将PTE的项数减半(为29=512), 同样的,PDE里的每项也是64位,项数减半。因为PTE和PDE只需要9位来索引,这时虚拟地址的布局变为2 9 9 12。前面多出的2位用来索引一张新表PDPT(Page Directory Pointer Table),一共有四项,每项指向一张PDE(1G地址空间),这样虚拟内存的大小仍然是4G,但是可以映射到超过4G的物理内存中。这时寄存器CR3 再存放PDE,而是PDPT的地址,这种物理内存的扩展就称为PAE(Physical Address Extensions)。
操作系统往往会共享大量的内存空间,比如Windows有2G的内存共享,而Linux有1G的内存共享,有了PAE后,只需要共享PDPT中的项就可以,从而节省了大量的内存。
PTE的后12位
注意到PTE中存放的每项的值,与虚拟内存地址的后12位合并,就得到了真实的内存地址,因此PTE每项的后12位其实是无用的。但是这12位是不会被浪费的,页面的状态,比如是否在物理内存中,是属于操作系统内核还是任何进程都能访问等信息,都可以存放在这12位里。
Flag | Meaning |
Global | Page belongs to Kernel, and is thus global across all processes |
Dirty | Page has been modified and cannot be reused until ommitted |
Accessed | Page has been recently accessed (for LRU "clock" algorithm) |
Acahe Disable | Page may not be cached |
Write Through | Write this page to disk (disables write caching) |
Owner | User-mode (Ring 3) page of Kernel-Mode (Ring 0) page |
Writable | Is page writable or read only |
Valid | Page is a valid page, mapping to a physical. Always set to "1" |
64位虚拟内存
64位虚拟内存地的分布为9 9 9 9 12,即4张索引表和4K的页面;也可以是9 9 9 21,即3张索引表和2M的页面;也可以是9 9 30,即2张索引表和1G的页面。一般1G的页面不常见,因为页面太大页面失效的代价也会很大。
注意到64位地址前面的16位并没有被用到,这样做的原因是后面的48位已经足够索引现在的物理内存。就目前的内存大小来看,把虚拟内存的取址空间支持到 几T是没有意义的,而且过多的分层会影响效率。正因为层数比32位多,地址映射的时间更长,所以64位机在某些情况下要比32位机慢。不要迷信64位机一 定比32位机快。
Lazy Allocation
对于Lunix和Windows 7,操作系统采取了Lazy Allocation的方式分配内存。比如用户用malloc申请一块1M的内存,这时操作系统仅仅是创建了索引表,而不真正地创建页面,这样物理内存实 际只增加了4K,当真正访问这块内存时,操作系统才真正地创建页面,并填充数据。但是Windows XP并不是这样,当用户申请一块内存时,它便立即分配这块内存。微软也发现这种方式的缺陷,所以在Windows 7加以改进。
动态内存和静态内存:
进程中有堆和栈,堆中动态分配内存,栈用来给局部变量分配内存,当局部变量离开它的作用域时,就会被自动释放。C语言标准里有个函数alloca,可以在 栈上分配不定大小(运行时确定)的内存,同值得注意的是,用这个函数申请的内存会在函数返回时自动释放,和局部变量一样。
页面失效(Page Fault):
页面失效的原因可以是:
Access denied: page is in memory, but is ring0 and you are in ring3;
Access delayed: page is not in memory but should be:
-> Page is declared, allocated but swapped;
-> Page is declared, allocated but unitiaiized;
-> Page is not declared (and not allocated);
对CPU而言所有类型的页面失效都是一样的,而且CPU可以从寄存器CR2中获取页面失效的地址。但是操作系统内核决定怎么处理页面失效:
if access denied -> Process is killed;
if page is not declared -> Process is killed;
if page is declared/allocated/swapped ->process is suspended, kernel page process gets control, gets page;
if page is declared/allocated/uninitialized -> kernal actully inialize.
TLB(Translation Lookaside Buffer)
大多数CPU除了MMU外,还有TLB(页表缓冲,与L1,L2缓存没有本质区别,只不过TLB存储页表数据,L1、L2缓存存储实际数据)。如果页表已 经缓存在TLB中,那么CPU只要1个时钟周期就能取到实际的地址,否则通过MMU要耗用30个时钟周期,64位下MMU要耗用50 个时钟周期。