为了更有效的管理内存并减少出错,现代操作系统提高了一种对主存的抽象概念,叫做虚拟内存(VM)。
它为每个进程提供了一个大的、一致的、私有的地址空间。通过一个很清晰的机制,虚拟内存提供了三个重要的能力:
1)对主存来说:它将主存看做是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在主存和磁盘之间来回传送数据,通过这种方式,它有效的使用了内存
2)对每个进程来说:它为每个进程提供了一致的地址空间,从而简化了内存管理
3)它保护了每个进程的地址空间不被其他进程破坏
虚拟内存是计算机系统中最重要的概念之一。它的成功的一个主要原因就是它是沉默地、自动地工作,不需要应用程序员的任何干涉。既然虚拟内存在幕后工作得如此之好,为什么程序员还需要理解它呢?有以下几个原因:
虚拟内存的作用:
页表
页表是放在主存(DRAM)中,我们通常使用术语SRAM缓存来表示位于CPU和主存之间的L1, L2, L3 高速缓存,用术语DRAM缓存来表示虚拟内存系统的缓存,它在主存中缓存虚拟页
DRAM比SRAM要慢大约10倍,而磁盘要比DRAM慢大约10 000倍
又是局部性救了我们
当我们中的许多人了解了虚拟内存的概念之后,第一印象是它的效率通常是非常低。因为不命中的处罚很大,我们担心页面调度会破坏程序性能。实际上,虚拟内存工作得很好,这需要归功于我们的老朋友-局部性(locality)
尽管在整个程序运行过程中引用的不同页面的总数可能超出物理内存总的大小,但局部性保证了在任意时刻,程序将趋向于一个较小的活动页面集合上工作,这个集合就做 工作集 或者 常驻集。
当然不是所有的程序都能展现良好的局部性。如果工作集的大小超过了物理内存的大小,那么程序将产生一种不幸的状态,叫做抖动(thrashing),这时页面将不断的换进换出。
有趣的是,一些早期的系统,例如DEC DPP-11/70,支持的是一个比物理内存更小的虚拟地址空间。然而,虚拟地址仍然是一个有用的机制,因为它大大简化了内存管理,并提供了一种自然的保护内存的方法。
按需页面调度和独立的地址空间的结合,对操作系统中内存的使用和管理产生了深远的影响:
地址翻译以一种自然的方式提供了更好的访问控制,因为每次地址转换时硬件都会读取一个PTE,可以在PTE上添加许可位来控制访问。例如SUP表示内核模式下才能访问该页,READ和WRITE表示对页面的读写访问
Linux虚拟内存系统
这张图强调了一个进程虚拟地址空间中内核态的数据结构,例如内核为每个进程维护了一个数据结构task_struct,包含了运行该进程所需要的全部信息(包括PID、指向用户栈的指针、可执行文件名字、PC等)
共享对象
共享对象和私有共享都可以被映射到虚拟内存,进程对共享对象的任何操作,对其他进程都是可见的,而且,这些变化会放映到磁盘上的原始对象中
私有对象使用一种叫做写时复制(copy-on-write)的巧妙技术被映射到虚拟内存中。
只要没有进程试图写,就继续使用共享物理内存中的对象的一个副本;当试图写时会触发保护故障,这时会在物理内存中创建一个新副本,更新页表项指向这个新副本。
内存分配的方式:
使用mmap函数的用户及内存映射
void *mmap(void *start, size_t length, int port, int flags, int fd, off_t offset)
动态内存分配
动态内存分配器维护了一个进程的虚拟内存区域-堆,像malloc,java中的gc
动态内存分配的程序具有更好的移植性,以及避免预先开闭额外的空间(例如动态数组大小)
实现:
隐式空闲链表O(n总块数)
显示空闲链表O(空闲块块数)