内存那些事儿(上)

文章翻译自 http://landley.net/writing/memory-faq.txt 。阅读这篇文章需要注意的一点是原文作者写这篇文章的内核版本比较老,应该是 2.x,因此某些细节和现在的 5.x 版本内核不一致。不过理论大体还是一致的。

现有的 Linux 内存管理资料有哪些?

什么是虚拟内存?

虚拟内存提供了一组可软件控制的内存地址,并使得每个进程拥有属于自己独特的内存视图。虚拟地址仅在给定的上下文中有意义,譬如某个进程上下文。相同的虚拟地址在不同的上下文中可以表示不同的含义。

虚拟地址范围和 CPU 寄存器大小有关。在 32 位系统中,每个进程拥有 4GB 的虚拟地址空间,这通常比系统实际拥有的内存多。

虚拟地址通过处理器的内存管理单元(Memory Management Unit, MMU)解释,MMU 使用一个叫做页表的数据结构来查找虚拟地址实际映射的内存空间。

虚拟地址用来实现延迟分配(lazy allocation),交换(swapping),文件映射(file mapping),写时复制(copy on write),碎片整理(defragmentation)等等。如果想学习更多这方面的内容,可以阅读 Ulrich Drepper 的《每个程序员应该了解的内存知识有哪些?》第三部分:http://lwn.net/Articles/253361/。

什么是物理内存?

物理内存是一种以低延迟、小粒度记录数据的存储硬件。物理内存地址是存储硬件上的一个特定的内存单元,它被发送到内存总线以找到对应的内存单元来进行读或写操作。

提供物理内存的存储硬件有 DIMM(DRAM),SD 卡(flash),声卡(帧缓冲区和纹理内存),网卡(I/O 缓冲区)等等。

只有内核会直接使用物理内存,用户空间使用虚拟地址。

如果想要学习更多的物理内存知识,可以查阅以下文章:

什么是内存管理单元(Memory Management Unit, MMU)?

内存管理单元是 CPU 的一部分,其作用是用来解释虚拟地址。通过虚拟地址来读/写/执行都需要通过内存管理单元找到对应的物理地址来读/写/执行,或者触发页面异常(page fault)来让内核负责处理(译注:譬如访问的虚拟地址还没分配物理内存就会触发 page fault)。

MMU 的存在使得每个进程都有自己独一无二的虚拟地址空间(在 32 位系统最大是 4GB),即使物理内存是受存储硬件限制的有限资源。

每个物理地址在系统中都是独一无二的,而虚拟地址在每个进程中是独一无二的。

什么是页表?

页表是一种包含进程内存映射和相关资源的数据结构,每个进程都有自己的页表,内核在页表中也有一些页表项,譬如磁盘缓存。

32 位 Linux 系统使用 3 级页表结构,分别是 Page Upper Directory (PUD), Page Middle Directory (PMD),
and Page Table Entry (PTE)。(64 位系统可以使用 4 级页表)。

如果想学习更多页表的知识,可以访问 http://en.wikipedia.org/wiki/Page_table

什么是内存映射?

内存映射是一组描述连续虚拟地址范围属性的页表条目。 每个内存映射包含起始地址、长度、权限(譬如该进程是否有读/写/执行权限访问该内存范围)和相关资源(譬如物理页面,交换页面,文件内容等等)。

创建一个内存映射仅是分配了虚拟内存,而没有分配物理内存(存储页表需要分配少量物理内存)。物理页面在发生 page fault 时分配,它是在 page fault 处理函数中按需分配的。

页表可以视为内存映射的集合,内存映射有匿名页映射、文件页映射、设备映射、共享映射和写时复制映射这几种类型。

mmap 系统调用可以为当前进程创建一个新的内存映射,munmap 系统调用则可以丢弃一个已存在的内存映射。

内存映射不能相互重叠,如果想通过 mmap 创建一个系统调用,mmap 会返回错误(译注:MAP_FIXED 标识会使已有的内存映射解除,然后创建一个新的内存映射)。

没有内存映射的虚拟地址范围称作 unmapped 区域,如果访问 unmapped 区域,会触发一个内核不能处理的 page fault,page fault 处理函数将会给访问 unmapped 区域的进程发生 SIGSEGV 信号,这种现象通常是操作野指针出现的。

默认情况下,Linux 保留每个进程虚拟地址空间的前几 KB 甚至前几 MB unmapped,这样当进程访问了空指针(译注:空指针包括值很小,明显不正确的地址),就会触发内核无法处理的 page fault,使内核向进程发送 SIGSEGV 信号以图杀死进程。

如果想要学习更多内存映射的知识,可以参考

什么是共享页面?

多个页表项可以映射到一个相同的物理页。通过这些虚拟地址(属于不同的进程)访问可以获取到相同的内容,修改它的内容对所有共享这个页面的进程是立即可见的。(共享的可写映射是一种常见的高性能进程间通信机制。)

内核中会跟踪每个物理页面映射的 PTE 数,当计数为 0 时,页面就会移到可释放内存链表中。

什么是匿名映射?

匿名内存是一种没有文件或者设备背景的内存映射。程序从操作系统中分配的堆和栈内存就属于匿名内存。

在匿名内存分配的最初阶段,它只分配了虚拟内存,新创建的内存映射指向了内核中一个已分配好的零页(零页是填充零的单页物理内存,由操作系统维护),之后通过写时复制分配物理页面。由于匿名映射的虚拟内存一开始都是映射到一个已经填充了 0 的页面,所以一开始即使没有分配物理内存,尝试去读这片内存区域也是可以的,只不过读的结果是 0。但是如果尝试去写这个页面,就会在 page fault 流程中触发写时复制机制分配新的物理页以允许写操作继续执行。(注意,prezeroing 优化改变了写时复制的实现细节,不过写时复制的理论还是一样的。prezeroing 优化可以查阅 https://lwn.net/Articles/117881/。)因此,调用分配内存的系统调用仅会分配虚拟内存,“脏”的匿名页才会分配物理内存。

脏的匿名页可以被写到交换空间,但是如果没有交换空间,那么匿名页会一直驻留在内存中直到释放。

匿名映射可以通过将 MAP_ANONYMOUS 标志传给 mmap() 系统调用来创建。