在Linux下,虚拟内存果真是无限的吗?
Disclaimer:时间仓促,许多论点无法给出证据印证。本文仅是一篇基于直觉的记录性随笔,不保证内容的正确性与准确性。
几乎每一个对系统编程与Linux操作系统有所了解的人都会建立一个直观的印象:虚拟内存的存在是为了让进程无需关心物理内存的管理——这个任务交由操作系统处理了。
因此,每个进程在运行时能够看到无限的虚拟内存。
让我们回顾一下在Linux下进程是怎么拿到内存的:一般地,进程对malloc
的调用一直追溯到系统调用mmap
,mmap
在内核态中修改当前进程的页表,建立内存映射后返回。
在这个新建立的页第一次真正被访问时,引发缺页中断,一个真实的页面被装入物理内存(这里可能存在一点问题,一般mmap
过后,新建的页面倾向于马上被访问。这时等到第一次访问再引发缺页,就多做了一次syscall,感觉有些笨拙)。
如果mmap给定的参数不合适(如当前进程的虚拟内存空间不足以建立所要求大小的连续页面),或系统用尽了所有物理内存以及swap空间时,mmap
就会失败。
这给了笔者一个错觉:在相当长的一段时间内,笔者都认为malloc
的失败基本上不可能发生,因为:1. 64位虚拟内存空间提供了约131T可用内存,这个大小很难被触及;2. 现代计算机的内存正在变得越来越大,把内存占满也越来越不容易出现。
然而,虚拟内存与物理内存之间并不像上文所述的那样割裂。一个直观的认识是,如果系统有32G内存,那么它一定不能容纳33个真实使用了1G内存的进程。
指出真实这个词非常重要,因为进程完全可以浮夸地只mmap
而不写入,这样页表看起来占用很大,但却没有实际的内容。
为此Linux允许对内存的overcommit,即允许所有进程建立的虚拟内存映射大小超过机器可用的物理内存总大小。
与之相对地,关闭overcommit意味着对机器上运行的所有进程施加这样的限制,即使它们看到的仍然是131T的虚拟内存。
因此,malloc
失败的原因又多了一个:触犯了系统虚拟内存总大小的限制。
这背后存在一定的trade-off:打开overcommit的好处是允许这种浮夸的mmap
,使得整个系统在物理内存被真正装满in-use的页面之前都允许建立新的页面;而关掉它的好处是基本上告别的OOM,系统的内存永远够用,所有会使内存超过限制的操作都会直接被拒绝,一个典型的例子是fork
。
overcommit的行为控制可以由内核内的一个参数控制,其被置为0,1,2时的语义分别为允许合理的overcommit,不允许oc,总是允许oc。
这就给程序员提供了两个选项:1. 使用允许oc,提高对RAM的利用率;2. 不允许oc,但是使用更多的swap来保证那些几乎永远不会再使用到的mapping有地方放。
这样做会出现一个最明显的问题:在swap的时候速度很慢(因为是硬盘)。可以说,在需要进行oc的场合使用swap来缓解时,其性能不如打开oc。
Useful Links:
一个比较生动的解释,包含了一点fork之后的故事:https://unix.stackexchange.com/a/441508
这个回答的原问题,为什么要进行overcommit:https://unix.stackexchange.com/questions/441364/what-is-the-purpose-of-memory-overcommitment-on-linux
一点我看不懂但是很令人震撼的讨论:https://news.ycombinator.com/item?id=20143277
一篇看起来挺详细但是我懒得看的文章:https://web.archive.org/web/20171206121412/http://engineering.pivotal.io/post/virtual_memory_settings_in_linux_-_the_problem_with_overcommit/
问题:
-
众所周知,进程之间share很多共享库,这些共享库是COW的(这也是为什么它们被称为共享库)。在计算虚拟内存总大小的时候,这些COW的页面也会被计算在内吗?它们仍全部都是虚拟的页面(即
MAP_PRIVATE
,所以使用shared或private并不能区分)。 -
内核建立的映射也要被计算在内吗?每个进程的一半地址都留给了内核,这看起来也是相当大的一块空间。