目录

内存

内存简介

内存架构:缓存 -> 主存 -> 磁盘 -> 磁带

内存管理:该机制负责对内存架构进行管理,使程序在内存架构的任何一个层次上的存放对于用户来说都是一样的。用户无须担心自己的程序是存储在缓存、主存、磁盘还是磁带上,反正运行、计算、输出的结果都一样。而内存管理实现这种媒介透明的手段就是虚拟内存。

虚拟内存

虚拟内存系统负责为程序提供一个巨大的、稀疏的、私有的地址空间的假象,其中保存了程序的所有指令和数据。操作系统在专门硬件的帮助下,通过每一个虚拟内存的索引,将其转换为物理地址,物理内存根据获得的物理地址去获取所需的信息。

作为用户级程序的程序员,可以看到的任何地址都是虚拟地址。只有操作系统,通过精妙的虚拟化内存技术,知道这些指令和数据所在的物理内存的位置。

目标:透明、效率、保护(一个程序不能访问另一个程序地址空间。)

地址空间

地址空间(address space)是一个非负整数 地址 的有序合集:{0,1,2,… }

在一个带虚拟内存的系统中,CPU 从一个有 N= 2 的 n 次方 个地址的地址空间中生成虚拟地址,这个地址空间就称为虚拟地址空间:{0,1,2,3,…., N-1}。

一个系统还有一个物理地址空间,对应于系统中物理内存的 M 个字节: {0,1,2,3,…, M-1}。

一个地址空间的大小通常是由表示最大地址所需要的位数来描述的,比如,一个包含 N = 2 的 n 次方 个地址的虚拟地址空间就叫做一个 n 位地址空间,现代操作系统通常支持 32 位或者 64 位虚拟地址空间。

虚拟内存空间分布

用户空间内存从低到高是五种不同的内存段:

  1. 只读段:代码和常量等
  2. 数据段:全局变量等
  3. 堆:动态分配的内存,从低地址开始向上增长
  4. 文件映射:动态库、共享内存等,从高地址开始向下增长
  5. 栈:包括局部变量和函数调用的上下文等,栈的大小是固定的。一般 8MB

基址极限地址翻译

翻译过程非常简单:物理地址=虚拟地址+程序所在区域的起始地址

程序所在区域的起始地址称为(程序)基址。

地址保护也变得非常简单,只要访问的地址满足下列条件即为合法访问:程序所在区域的起始地址≤有效地址≤程序所在区域的起始地址+程序长度

由此可见,我们只需要设置两个端值:基址和极限,即可达到地址翻译和地址保护的目的。这两个端值可以由两个寄存器来存放,分别称为基址寄存器和极限寄存器。

基址极限(包括分段之后包含多个基址极限也存在)管理模式的问题:

  1. 空间浪费(外部碎片太多)
  2. 地址空间增长困难。这有两层意思:一是指空间增长效率低下;二是空间增长存在天花板限制。

分页

分页系统的核心就是将虚拟内存空间和物理内存空间皆划分为大小相同的页面,如 4KB、8KB 或 16KB 等,并以页面作为内存空间的最小分配单位,一个程序的一个页面可以存放在任意一个物理页面里。

在分页系统下,一个程序发出的虚拟地址由两部分组成:页面号和页内偏移值。例如,对于 32 位寻址的系统,如果页面大小为 4KB,则页面号占 20 位,页内偏移值占 12 位。

分页的优势:

  1. 物理空间是页面的整数倍,并且空间分配以页面为单位,将不会再产生外部碎片。
  2. 空间增长也容易解决:只需要分配额外的虚拟页面,并找到一个闲置的物理页面存放即可。

分页的缺点:

  1. 页表很大。(使用多级页表解决)
  2. 页表和多级页表都会使内存访问次数增加,导致翻译速度下降。(使用 TLB 解决)

页表

为了记录地址空间的每个虚拟页放在物理内存中的位置,操作系统通常为每个进程保存一个数据结构,称为页表(page table)。 例如,对于 32 位寻址的虚拟地址,如果页面大小为 4KB,则虚拟页面数最多可以达到 220,即 1048576 个虚拟页面。那么页表的记录条数就为 1048576 条。

页面记录内容:缓存禁止位、访问位、修改位、保护标识区、在内存否、物理页面号。

内存管理单元(MMU)对虚拟地址的翻译只是对页面号的翻译,即将虚拟页面号翻译成物理页面号。而对于偏移值,则不进行任何操作。这是因为虚拟页表和物理页表大小完全一样,虚拟页面里的偏移值和物理页面里的偏移值完全一样,因此无须翻译。

内存交换(swap space)

交换空间就是在硬盘上开辟一部分空间用于物理页的移入和移出。交换空间让操作系统为多个并发运行的进程都提供巨大地址空间的假象。

内存交换通常在许多进程运行且内存吃紧时进行,而系统负荷降低就暂停。例如:在发现许多进程运行时经常发生缺页,就说明内存紧张,此时可以换出一些进程;如果缺页率明显下降,就可以暂停换出。

内存交换技术主要是在不同进程(或作业)之间进行,而内存覆盖则用于同一程序或进程中。

缺页中断

在分页系统里,一个虚拟页面既有可能在物理内存,也有可能保存在磁盘上。如果 CPU 发出的虚拟地址对应的页面不在物理内存,就将产生一个缺页中断。

缺页中断服务程序首先根据虚拟地址计算出该地址在相应程序文件里面的位移量或偏移量(off-set),然后要求文件系统在这个偏移量的地方进行文件读操作(读一个页面)。

在最坏情况下,每次新的访问都是对一个不在内存的页面进行访问,即每次内存访问都产生一次缺页中断,这样每次内存访问皆变成一次磁盘访问,而由于磁盘访问速度比内存可以慢一百万倍,因此整个系统的效率急剧下降。这种现象就称为内存抖动

外中断是指由 CPU 执行指令以外的事件引起,如 I/O 完成中断,表示设备输入/输出处理已经完成,处理器能够发送下一个输入/输出请求。此外还有时钟中断、控制台中断等。

而异常时由 CPU 执行指令的内部事件引起,如非法操作码、地址越界、算术溢出等。

页面更换算法

缺页中断服务程序将负责把位于磁盘上的数据加载到物理内存来。如果物理内存还有空闲页面,那就直接使用空闲的页面。但如果物理内存已满,则需要挑选某个已经使用过的页面进行替换。

公平算法主要包括下面 4 种:

  1. 随机算法
  2. 先来先出(FIFO)算法
  3. 第二次机会算法
  4. 时钟算法

非公平算法则包括如下 5 种:

  1. 最优算法
  2. NRU 算法
  3. LRU 算法
  4. 工作集算法
  5. 还有一种混合算法,它既想保持公平,又含有区别对待的考虑:工作集时钟算法。

页表地址翻译过程

页表命中 CPU 硬件的执行步骤:

  1. CPU 生成一个虚拟地址,并把它传送给 MMU。
  2. MMU 生成 PTE(页表条目) 地址。
    1. 命中 TLB 的
      1. MMU 从 TLB 中取出对应的 PTE
      2. MMU 将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存。
      3. 高速缓存/主存将所请求的数据字返回 CPU。
    2. 未命中 TLB
      1. 从高速缓存/主存中请求这个 PTE
      2. 高速缓存/主存向 MMU 返回 PTE。
        1. 未缺页
          1. PTE 有效位为 1,MMU 构造物理地址,并把它传送给高速缓存/主存。
          2. 高速缓存/主存将所请求的数据字返回 CPU。
        2. 缺页
          1. PTE 有效位为 0,触发一次中断,传递 CPU 中的控制到操作系统内核中的缺页中断处理程序。
          2. 缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘。
          3. 缺页处理程序调入新的页面,并更新内存中的 PTE。
          4. 缺页处理程序返回原来的进程,再次执行导致缺页的指令, CPU 将引起缺页的虚拟地址重新发送给 MMU ,
          5. 因为虚拟页面现在存在主存中,所以会命中,主存将所请求的数据字返回 CPU。

内存操作 API

栈内存

栈内存的申请和释放操作是编译器来隐式管理的,所以有时也称为自动(automatic)内存。

C 中申请栈内存很容易。比如,假设需要在 func()函数中为一个整形变量 x 申请空间。

1
2
3
4
void func() {
    int x;
    ...
}

分配堆内存

malloc 只需要一个 size_t 类型参数,该参数表示你需要多少个字节。

1
2
3
#include <stdlib.h>
...
void *malloc(size_t size);

堆内存释放

要释放不再使用的堆内存,程序员只需调用 free():

1
2
3
int *x = malloc(10 * sizeof(int));
...
free(x);

free 和 malloc 都属于库调用,但是本身是建立在一些系统调用之上的。

访问堆的一个具体单元,需要两次访问内存,第一次得取得指针,第二次才是真正的数据,而栈只需访问一次。另外,堆的内容被操作系统交换到外存的概率比栈大,栈一般是不会被交换出去的。

参考

《操作系统之哲学原理第 2 版》邹恒明

《操作系统导论》