Fault injection
http://lwn.net/Articles/209257/
The framework can cause memory allocation failures at two levels: in the slab allocator (where it affects kmalloc() and most other small-object allocations) and at the page allocator level (where it affects everything, eventually).
Page owner
http://lwn.net/Articles/121656/
The kernel lock validator
http://lwn.net/Articles/185666/
The "too small to fail" memory-allocation rule
http://lwn.net/Articles/627419/
内核开发者长久以来被告知,除了少数情况外,在系统没有足够的资源的情况下尝试申请内存可能会失败。结果,在设计好的代码里面,每次调用内存分配函数,比如kmalloc(),vmalloc()或__get_free_pages()都伴随着一些小心设计的错误处理代码。但是,内存管理系统的实际实现可能跟上面说的不一样。这种不同可能导致一些不幸的运行时行为,但是修改它可能使情况变得更糟。
关于这个问题的讨论出现在Tetsuo Handa发表了一个关于如何解决某个出现的特定问题的疑问。事情的大体过程是这样的:
1. 一个当前占用相对来说比较少内存的进程引用了一个XFS文件系统操作,这个操作需要申请内存。 2. 内存管理子系统尝试满足内存申请的需求,但是发现没有足够的内存。然后内存子系统首先尝试直接重声明(direct reclaim)内存(强制一部分页换出内存),如果还没有释放足够需要的内存,则继续调用OOM killer。 3. OOM killer挑选进程并杀死它。 4. 被杀死的进程在退出的时候必须执行一些在同一个XFS文件系统上的操作。这些操作需要获取一些锁,而这些锁正被引起这个有问题的内存分配的进程(1中的进程)所占用。然后,系统死锁。
换句话说,申请内存的进程不能进行下去是因为它正等待分配调用返回。这个调用需要有足够的空闲内存才会返回,而空闲内存需要通过其他进程退出才能释放。同时,OOM killer也在等待这个进程退出才能继续(可能)选择其他进程退出。但是这个进程不能退出,因为它需要一些被申请内存的进程占用的锁。系统锁住了,然后系统的所有者开始认真的考虑切换到某个版本的BSD系统了。
当被问到这个问题时,XFS maintainer Dave Chinner很快回复说为什么内存管理代码会调用OOM killer而不是返回内存分配失败。他说XFS的代码能很好的处理内存分配失败。对他来说,返回内存分配失败可能比随机杀死一个进程并可能锁住系统更好。然后,内存管理系统的maintainer说:
“(这是因为)有一个不成文的规定:low-order(<=PAGE_ALLOC_COSTLY_ORDER)的GFP_KERNEL内存分配不会失败。这是一个很久以前的决定,现在修改它会很困难。”
Dave怀疑的回复说:
“我们总是被告知内存分配不保证成功,曾经,除非__GFP_NOFAIL标志被设置,但是这已经过时了,没人再被允许使用它了。 很多代码依赖内存分配来继续运行或在低内存的情况下的系统上失败。页缓存就是其中之一,这也就意味这所有的文件系统也存在这种依赖。我们不是显式的要求内存分配失败,我们只是希望内存分配失败出现在低内存的情况下。我们在过去的15年里一直这样设计和coding的。”
“too small to fail”分配对大多数内核来说是指少于或等于八个连续页的分配,这(八个页)相对比较大。没人确切的知道这条规则是何时进入内核的,它在Git之前就被引入了。Johannes Weiner解释说,当时的想法是这样的,如果这么小的内存分配都不能被满足,那么系统将变得不可用;也不存在除了调用OOM killer之外其他可行的替代方法。这可能是正确的,但是锁住一个内核正在处理内存分配错误的系统也会导致系统不可用。
讨论中提到的一个方法是给特定的申请增加__GFP_NORETRY标志。这个标志会导致在资源不足的情况下即使小的内存分配申请也会失败。但是,就像Dave提到的,用__GFP_NORETRY去修复潜在的死锁请求就是一个“打狗熊”的游戏,总会有更多的熊熊,并且他们会取得最终的胜利。
最终的方法可能就是移除“too small to fail”规则并让内存分配函数按照大多数内核开发者想的那样去运行。Johannes的信息里包含了一个采用这种方法的patch:如果直接重声明没有释放任何内存,它就会让无休止的重声明循环退出(并给内存分配申请返回失败)。但是,就像他提到的,经过如此长时间的尝试后order-0分配都不能满足,这太可怕了。
说它可怕是有两个原因,一个是不是所有的内核开发者都认真检查每次的内存分配并考虑合适的恢复路径。但更糟的是,因为小内存分配不会失败,内核中几乎所有的上千的的错误恢复路径从来没有执行过。开发者可以使用内核中的“fault injection”框架来测试这些代码,但是事实上,几乎没有开发者会这样做。所以这些错误恢复路径代码不仅仅是未被使用到,而且相当大一部分在第一时间都没被测试过。
如果这个不成文的“too small to fail”规则被移除了,所有的这些错误恢复代码就会第一次被执行。某种程度上,内核将会增加上千行未经测试的并且只会在罕见的系统已出错的情况下运行的代码。毫无疑问,这将会导致很多bug和潜在的安全问题。
这让内存管理开发者陷入两难的境地。让内存分配代码按照建议的那样运行会给内核引入很多难以调试的问题。但是,“too small to fail”也有它自己的缺点;同时,越来越复杂的内核锁住可能让情况变得更糟;它也会浪费相当长的开发时间来开发从不会执行的错误恢复代码。即使如此,在如此晚的时间点上引入low-order内存分配失败可能太可怕了,即使长期来看可能让内核变得更好。