3.2 虚拟内存管理
直接映射
众所周知, 操作系统也是软件. 之前也提过所有的软件都是使用的虚拟地址, 虚拟地址翻译规则由操作系统提供. 既然操作系统使用的也是虚拟地址, 那么物理地址是哪里来的?
对于操作系统本身, 它可以访问所有的物理内存, 但是它也是需要页表的. 不过它的页表非常简单, 直接将一块连续的虚拟空间映射到连续的物理内存空间, 虚拟内存地址与物理内存地址保持简单的线性映射. 当操作系统要访问内存时, MMU 只需要给虚拟地址加上一个偏移量就可以了. 当然, 这个偏移量也不是固定的, 每次启动都不一样, 不然操作系统的内存就很容易被读出来了.
当操作系统想要直接访问内存时, 从页表里读出来的物理地址还需要转成虚拟地址才能交给 CPU, 因为 CPU 只处理虚拟地址. 不过只需要一加一减就可以了, 不会影响性能.
虚拟内存段分布
管理页表映射
之前好像只讲了页表项是怎么使用的, 不过好像没说页表项的创建与删除. 页表项的创建有两种方式, 立即映射和延迟映射.
立即映射
立即映射就是指执行到int[10] a;
就分配一块空间给 a. 这里只是一个比喻, 实际上并不会立刻分配空间, 只是说立即映射会在声明连续内存区域后, 马上分配对应的物理内存. mmap
就是立即映射的.
延迟映射
可以尝试声明一个很大的数组, 但是不使用它. 没意外的话代码是能跑的, 但是如果使用了就跑不了. 这就是延迟映射, 先声明内存区域, 等到真正使用才会分配物理内存, 创建对应的页表项. 延迟映射可以节约物理内存.
但是延迟映射有个问题, 第一次访问时由于没有对应的页表项, 会触发缺页异常, 导致操作系统介入, 同时还有一些上下文的切换, 降低了一些性能.
缺页异常
翻译错误
页错误
缺页异常与 Segmentation Fault
虚拟地址需要分配后才能使用.
如果分配了但是没使用, 第一次访问时触发缺页异常
如果没分配也没使用, 第一次访问时触发 Segmentation Fault
扩展功能
共享内存
众所周知, 我们可以使用 fork 来创建一个进程. 并且这个新的进程跟原进程一模一样. 这个时候如果我们再分配一块物理空间给新进程感觉有点浪费了, 毕竟它们一模一样, 至少前面是一样的. 这个时候就可以使用共享内存了, 让它们的虚拟页都映射到同一块物理内存中.
这种共享内存的方式节约了物理内存, 实现性能加速.
如果有一天某个进程想要修改共享内存了, 那就再分配一块物理内存给这个进程, 并把之前那一块内存拷贝到新的内存里, 然后进行 重映射 .
这种往内存里写了再进行拷贝的方式叫 写时拷贝 , 避免了不必要的内存拷贝.
内存去重
基于写时拷贝的机制, 我们可以让很多进程使用共享内存, 修改的时候再分配物理内存给它们. 因此操作系统可以每隔一段时间或者在某个时间点进行内存扫描, 找到具有相同内容的物理页, 并把它们合并. 这是由操作系统发起的, 对用户态透明, 应用程序可以说完全不知道发生了什么.
不过这也存在一些隐患, 有侧信道攻击的风险. 因为被合并的页会导致访问延迟明显, 攻击者很容易知道发生了什么, 并确认目标进程中含有构造的数据.
内存压缩
当内存资源不太够的时候, 操作系统会选择一部分"最近不太会使用"的内存页进行数据压缩, 从而释放空闲内存.
- windows10
压缩后的数据还是存放在内存里. 需要访问这些数据时操作系统解压就可以了.
- linux
linux 有个叫 zswap 的东西, 它是换页过程中磁盘的缓存.
linux 在内存压缩时, 会将准备换出的数据压缩并写入 zswap 区域(这里是内存), 在合适的时间点会写入磁盘.
这么做可以减少甚至避免磁盘 I/O, 延长设备寿命.
大页
众所周知, 四大天王有五个是很正常的. 4 级页表只有二级或者三级页表也很正常.
在 4 级页表中, 某些页表项可以只保留两级或者三级页表.
比如 L2 页表项, 它可以不指向 L3 页表的基址, 而是指向一个 2M 的物理页(64 位系统).
在页表项中, 它的第一位标识该页表项存储的物理地址指向的是下一级页表还是物理页. 如果是 1, 那么指向下一级页表; 否则指向物理页.
既然指向物理页的话, 那原本为下级或者下下级准备的偏移地址就可以用来做页内偏移了. 比如在 ARM64 中, 48 位的虚拟地址, 如果发现对应的 L2 页表项指向的是物理页, 那么就有 12+9=21 位可以作为页内偏移了.
ARM64 中, 12 位作为页内偏移, 剩下的 36 位均分给四个页表, 作为页表的索引. 这里只用到了 L0 级, L1 级和 L2 级页表索引, L3 级空出来了.
21 位的地址可以指向 2MB 的物理页.
内存的读取是以字节为单位的, 一个地址对应一个字节的数据. 这里 21 位地址, 地址空间总共 2^21, 也就是 2M. 然后一个对应 1B, 所以是 2MB 的物理页.
大页的好处
减少了 TLB 缓存项的使用, 提高 TLB 的命中率
减少页表的级数, 提升遍历页表的效率
坏处
未使用整个大页而造成的物理内存浪费
增加管理内存的复杂度