虚拟内存是什么? 它是对主存和 I/O 设备的抽象, 这一点在漫谈进程和线程中已经提及过, 也就是说, 虚拟内存是将内存看做硬盘的高速缓存, 内存中只保存程序的活动区域, 根据需要在硬盘和内存之间传输数据; 同时, 虚拟内存为每个进程提供一个一致的地址空间, 比如说 32 位计算机, 每个进程的地址范围是 0,4G. 此外, 虚拟内存保护每个进程的地址空间不被其他进程破坏, 那么, 通过何种方法使得每个进程的地址空间是独立的? 看完本文, 这个问题你自然就知道答案了.
内存管理的要求
针对批处理系统, 程序顺序执行, 程序依次装入内存运行, 一个新装入的程序会完全覆盖老的程序.
针对分时系统, 多个程序并发执行, 要把尽可能多的程序装入内存, 但是物理内存的空间是有限的, 经常需要将程序换入换出, 这样一来, 系统性能就变差了; 程序在内存中要区分开.
如上图, 程序 1, 程序 2, 程序 3 装入到内存, 而程序 2 运行完成被换出, 内存空闲出 20k, 然后进来程序 4, 大小为 25K, 此时, 只有两处空闲块, 10K 和 20K, 没有一处是符合条件的, 应该怎么办? 一个明显的办法就是将两块空闲区域进行合并, 形成一个大小为 30K 的空闲块满足程序 4.
注意: 此时描述的内存的物理地址, 物理内存使用的是连续地址, 下面详细介绍物理地址.
物理地址
使用物理地址的缺点: 当多个程序同时对某个地址进行操作时就会引起冲突, 如下图所示, 程序 1 和程序 2 中都有指令 movl eax,(100). 针对这个问题怎么解决呢?
在装载程序的时候, 修改指令的地址. 例如程序 2 中的(100)+1000, 其中 1000 代表这个程序的开始地址, 而程序 1 中的(100)+0. 这样做是很困难的, 因为需要我们理解所有的指令. 既然这个方法不好, 那么有没有其他方法呢? 答案就是逻辑地址.
逻辑地址
CPU 有一个内存管理单元(MMU), 它有一个基址寄存器, 它保存着每个程序的开始地址, 比如说程序 1 的基址寄存器是 1000, 偏移量是 200, 转换成物理地址是 1200.
分页
假设一个程序很大, 需要占据所有内存, 而内存管理的一个要求就是把尽可能多的程序装入内存, 两者相互矛盾. 应对方法就是分页, 就是说每个程序开始运行时只会加载部分数据到内存中, 操作系统会为每个进程维护一个页表, 页表是维护虚拟页和物理页的映射关系, 当页表中的虚拟页对应的物理页是空白时, 操作系统会发生缺页中断. 分页的理论依据是局部性原理(空间局部性 + 时间局部性), 也正是这个原因, 程序在大部分时间内不需要进行页面置换. 如果发生缺页中断, 缺页中断处理程序读取磁盘, 选择一个空闲物理页面, 修改页表, 重新执行程序.
注意事项
每个进程都要有一个页表, 进程 PCB 有指向页表的指针
页表访问要非常快(硬件缓存来拯救: 转换缓冲区 --TLB)
页表可能非常大(2^32 的内存空间, 每个页大小 2^12 , 页表中需要 2^20 个页表条目, 假设每个条目 4Byte, 需要 4M 空间来存放表, 而且每个进程都需要 4M, 这是非常占用空间的. 可以采用多级页表, 反向页表等技术来解决).
分页具体流程 以 CPU 执行 MOV (0x560) EAX 为例, CPU 内部会将逻辑地址进行拆分成页号和偏移量, 然后将逻辑地址转换成物理地址.
页面置换算法
内存是有限的, 不可能把所有的页面都装进来, 缺页时需要进行页面置换.
页面置换背后是个通用的问题 (web 服务器的缓存, Redis,Memcached 的缓存等等).FIFO(先进先出) 先进先出算法思想很简单, 当内存满了, 优先置换出最先进入内存的页面. 但是它存在一个问题: 经常被访问的数据有可能被换入换出, 下面我就举个简单的栗子. 假设只有 3 个物理页面, 逻辑页面的访问次序是: 7 0 1 2 0 3 0 4.
LRU(最近最少使用)
LRU 算法就是所有页用栈组成, 当栈满了, 且新增加元素没有命中, 则将栈底元素淘汰, 新增元素放到栈顶; 当栈满了, 且新增元素命中, 则只需要将新增元素移动到栈顶位置即可. 假设只有 3 个物理页面, 逻辑页面的访问次序是: 7 0 1 2 0 3 0 4.
Clock 算法
Clock 算法是 LRU 算法的近似实现, 它为每个页加一个引用位, 默认值为 0, 无论读还是写, 都置为 1, 它把所有的页组成一个循环队列, 选择淘汰页的时候, 扫描引用位, 如果是 1 则改成 0(相当于再给该页面一次存活的机会), 并扫描下一个; 如果该引用位是 0, 则淘汰该页, 换入新的页面. 假设只有 3 个物理页面, 逻辑页面的访问次序是: 3 4 2 6.
分段
上述介绍的页表机制是面向机器的, 而为了程序员更好的理解程序, 我们的先辈们又提出了分段概念, 就是将程序划分为若干段部分, 每一段都有独立的功能, 例如: 代码段, 数据段, 栈, 堆等. 通过分段技术, 我们把内存空间分成一个个可以自治的段, 而且把内存从一维空间变成了一个二维空间.
段页结合
段页结合流程: 首先根据段表信息, 将逻辑地址转换成另一个逻辑地址, 在转换的过程中会判断偏移量是否超过指定长度, 如果没有超过, 则, 则根据页表将逻辑地址转换成物理地址.
虚拟内存具体实现
这里介绍 Linux 中的虚拟内存的具体实现, 如下图, task_struct 结构体是进程描述符, 属于进程管理 (PCB), 其中, mm(memory manage) 表示内存管理, 它指向 mm_struct 结构体, 它描述 linux 下进程的虚拟地址空间, 它又包含两个重要字段: pgd,mmap, 其中, pgd 指向第一级页表的基址, 而 mmap 指向一个 vm_area_struct(区域结构)的链表, vm_start,vm_end 分别表示数据的起始地址, vm_prot 描述的是这个区域包含的所有页的读写许可权限; vm_flags 描述这个区域是和别的进行共享的, 还是该进程私有的
Linux 缺页中断: MMU(内存管理单元)试图翻译一个虚拟地址 A, 当这个虚拟地址对应的物理地址不在内存中是, 触发一个缺页中断. 虚拟地址 A 是合法的吗? 地址 A 在某个区域地址内吗(vm_start,vm_end), 如果不存在, segement fault! 段错误. 如果存在, 则接着判断进程是否可以读, 写, 执行这个区域内页面的权限? 如果没有权限, 触发保护异常. 经过上诉两项判断, 如果都是正常的, 最后才开始真正的缺页处理, 从硬盘装载数据, 修改页表.
来源: https://www.cnblogs.com/neal-ke/p/8711561.html