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