Linux 会“耗尽 RAM”吗?

the*_*ror 21 linux kernel swap virtual-memory ram

我在网上看到一些帖子,显然有人抱怨托管 VPS 意外杀死进程,因为他们使用了过多的 RAM。

这怎么可能?我认为所有现代操作系统都通过对物理 RAM 上的任何内容使用磁盘交换来提供“无限 RAM”。这样对吗?

如果一个进程“由于内存不足而被杀死”,可能会发生什么?

gol*_*cks 42

如果一个进程“由于内存不足而被杀死”,可能会发生什么?

有时有人说 linux 在默认情况下从不拒绝来自应用程序代码的更多内存请求——例如malloc(). 1 这实际上不是真的;默认使用启发式

明显的地址空间过度使用被拒绝。用于典型系统。它确保严重的疯狂分配失败,同时允许过度使用以减少交换使用。

From [linux_src]/Documentation/vm/overcommit-accounting(所有引用均来自 3.11 树)。究竟什么算作“严重的疯狂分配”并没有明确说明,因此我们必须通过源头来确定细节。我们也可以使用脚注 2(下面)中的实验方法来尝试对启发式进行一些反思——基于此,我最初的经验观察是,在理想情况下(== 系统空闲),如果你不这样做没有任何交换区,您将被允许分配大约一半的 RAM,如果您有交换区,您将获得大约一半的 RAM 加上所有交换区。这或多或少是每个进程(但请注意,此限制动态的,可能会因状态而变化,请参阅脚注 5 中的一些观察结果)。

一半的 RAM 加上交换区是/proc/meminfo. 这就是它的意思——请注意,它实际上与刚才讨论的限制无关(来自[src]/Documentation/filesystems/proc.txt):

CommitLimit:基于过量使用比率('vm.overcommit_ratio'),这是当前可在系统上分配的内存总量仅当启用了严格的过量使用记帐('vm.overcommit_memory' 中的模式 2)时,才会遵守此限制。CommitLimit 使用以下公式计算: CommitLimit = ('vm.overcommit_ratio' * Physical RAM) + Swap 例如,在具有 1G 物理 RAM 和 7G 交换的系统上,'vm.overcommit_ratio' 为 30,它将产生7.3G 的 CommitLimit。

之前引用的 overcommit-accounting 文档指出默认vm.overcommit_ratio值为 50。因此,如果您sysctl vm.overcommit_memory=2,则可以调整 vm.covercommit_ratio(与sysctl)并查看结果。3 默认模式,whenCommitLimit不强制执行,只有“明显的地址空间过度使用被拒绝”,是 when vm.overcommit_memory=0

虽然默认策略确实有一个启发式的每个进程限制来防止“严重的疯狂分配”,但它确实让整个系统自由地变得严重疯狂,明智的分配。4 这意味着在某些时候它可能会耗尽内存,并且必须通过OOM 杀手宣布某些进程破产。

OOM杀手杀死了什么?不一定是在没有内存时要求内存的进程,因为这不一定是真正有罪的进程,更重要的是,不一定是最能迅速使系统摆脱问题的进程。

这是从这里引用的,它可能引用了 2.6.x 源:

/*
 * oom_badness - calculate a numeric value for how bad this task has been
 *
 * The formula used is relatively simple and documented inline in the
 * function. The main rationale is that we want to select a good task
 * to kill when we run out of memory.
 *
 * Good in this context means that:
 * 1) we lose the minimum amount of work done
 * 2) we recover a large amount of memory
 * 3) we don't kill anything innocent of eating tons of memory
 * 4) we want to kill the minimum amount of processes (one)
 * 5) we try to kill the process the user expects us to kill, this
 *    algorithm has been meticulously tuned to meet the principle
 *    of least surprise ... (be careful when you change it)
 */
Run Code Online (Sandbox Code Playgroud)

这似乎是一个不错的理由。然而,在没有取证的情况下,#5(它是#1 的冗余)似乎是一个艰难的销售实现,而#3 是#2 的冗余。因此,考虑将其缩减为 #2/3 和 #4 可能是有意义的。

我查看了最近的一个来源 (3.11) 并注意到此评论在此期间发生了变化:

/**
 * oom_badness - heuristic function to determine which candidate task to kill
 *
 * The heuristic for determining which task to kill is made to be as simple and
 * predictable as possible.  The goal is to return the highest value for the
 * task consuming the most memory to avoid subsequent oom failures.
 */
Run Code Online (Sandbox Code Playgroud)

这是关于#2 的更明确一点:“目标是 [杀死] 消耗最多内存的任务以避免随后的 oom 失败,”以及暗示 #4(“我们想要杀死最少数量的进程(一个) )

如果你想看到 OOM 杀手在行动,请参阅脚注 5。


1谢天谢地,吉尔斯摆脱了我的错觉,请参阅评论。


2这是一个简单的 C 语言,它要求越来越大的内存块来确定何时请求更多内存会失败:

/*
 * oom_badness - calculate a numeric value for how bad this task has been
 *
 * The formula used is relatively simple and documented inline in the
 * function. The main rationale is that we want to select a good task
 * to kill when we run out of memory.
 *
 * Good in this context means that:
 * 1) we lose the minimum amount of work done
 * 2) we recover a large amount of memory
 * 3) we don't kill anything innocent of eating tons of memory
 * 4) we want to kill the minimum amount of processes (one)
 * 5) we try to kill the process the user expects us to kill, this
 *    algorithm has been meticulously tuned to meet the principle
 *    of least surprise ... (be careful when you change it)
 */
Run Code Online (Sandbox Code Playgroud)

如果你不懂 C,你可以编译这个gcc virtlimitcheck.c -o virtlimitcheck,然后运行./virtlimitcheck. 它是完全无害的,因为该进程不使用它要求的任何空间——即,它从不真正使用任何 RAM。

在具有 4 GB 系统和 6 GB 交换空间的 3.11 x86_64 系统上,我失败了大约 7400000 kB;数字会波动,所以也许状态是一个因素。这恰好接近CommitLimitin /proc/meminfo,但修改这个 viavm.overcommit_ratio没有任何区别。但是,在具有 64 MB 交换空间的 3.6.11 32 位 ARM 448 MB 系统上,我失败了大约 230 MB。这很有趣,因为在第一种情况下,数量几乎是 RAM 数量的两倍,而在第二种情况下,它大约是 1/4——强烈暗示交换量是一个因素。这通过在第一个系统上关闭交换得到证实,当故障阈值下降到约 1.95 GB 时,与小 ARM 机器的比率非常相似。

但这真的是每个进程吗?看来是。下面的短程序要求用户定义的内存块,如果成功,则等待您按回车——这样您就可以同时尝试多个实例:

/**
 * oom_badness - heuristic function to determine which candidate task to kill
 *
 * The heuristic for determining which task to kill is made to be as simple and
 * predictable as possible.  The goal is to return the highest value for the
 * task consuming the most memory to avoid subsequent oom failures.
 */
Run Code Online (Sandbox Code Playgroud)

但是,请注意,无论使用情况如何,这都不是严格意义上的 RAM 和交换量——有关系统状态影响的观察,请参见脚注 5。


3 CommitLimit指的是当 vm.overcommit_memory = 2 时系统允许的地址空间量。据推测,您可以分配的数量应该是减去已经提交的数量,这显然是该Committed_AS字段。

一个可能有趣的实验证明了这一点,即添加#include <unistd.h>到 virtlimitcheck.c 的顶部(参见脚注 2),并fork()while()循环之前添加。如果没有一些乏味的同步,不能保证像这里描述的那样工作,但是很有可能会,YMMV:

> sysctl vm.overcommit_memory=2
vm.overcommit_memory = 2
> cat /proc/meminfo | grep Commit
CommitLimit:     9231660 kB
Committed_AS:    3141440 kB
> ./virtlimitcheck 2&> tmp.txt
> cat tmp.txt | grep Failed
Failed at 3051520 kB.
Failed at 6099968 kB.
Run Code Online (Sandbox Code Playgroud)

这是有道理的——详细查看 tmp.txt,您可以看到进程交替分配越来越大的分配(如果您将 pid 放入输出中,这会更容易),直到一个显然已经声明足够另一个失败为止。获胜者然后可以自由地抓住一切到CommitLimit减号Committed_AS


4值得一提的是,在这一点上,如果您还不了解虚拟寻址和需求分页,那么首先使过度承诺成为可能的是内核分配给用户级进程的根本不是物理内存——它是虚拟地址空间。例如,如果一个进程为某事保留了 10 MB,这将被布置为一系列(虚拟)地址,但这些地址尚未对应于物理内存。当访问这样的地址时,这会导致页面错误然后内核尝试将其映射到实际内存中,以便它可以存储实际值。进程通常保留比它们实际访问更多的虚拟空间,这允许内核最有效地使用 RAM。然而,物理内存仍然是一种有限资源,当它全部映射到虚拟地址空间时,必须消除一些虚拟地址空间以释放一些 RAM。


5首先警告:如果您尝试使用vm.overcommit_memory=0,请确保先保存您的工作并关闭所有关键应用程序,因为系统将冻结约 90 秒,某些进程将死亡!

这个想法是运行一个在 90 秒后超时的分叉炸弹,分叉分配空间,其中一些将大量数据写入 RAM,同时向 stderr 报告。

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>

#define MB 1 << 20

int main (void) {
    uint64_t bytes = MB;
    void *p = malloc(bytes);
    while (p) {
        fprintf (stderr,
            "%lu kB allocated.\n",
            bytes / 1024
        );
        free(p);
        bytes += MB;
        p = malloc(bytes);
    }
    fprintf (stderr,
        "Failed at %lu kB.\n",
        bytes / 1024
    );
    return 0;
}            
Run Code Online (Sandbox Code Playgroud)

编译这个gcc forkbomb.c -o forkbomb。首先,尝试一下sysctl vm.overcommit_memory=2——你可能会得到类似的东西:

6520 says 0 forks
6520 Allocation succeeded.
6520 says 1 forks
6520 Allocation succeeded.
6520 says 2 forks
6521 Allocation succeeded.
6520 Allocation succeeded.
6520 says 3 forks
6520 Allocation failed!
6522 Allocation succeeded.
Run Code Online (Sandbox Code Playgroud)

在这种环境下,这种叉子炸弹走不远。请注意,“says N forks”中的数字不是进程总数,而是导致该进程的链/分支中的进程数。

现在尝试使用vm.overcommit_memory=0. 如果将 stderr 重定向到文件,则可以在之后进行一些粗略的分析,例如:

> cat tmp.txt | grep failed
4641 Allocation failed!
4646 Allocation failed!
4642 Allocation failed!
4647 Allocation failed!
4649 Allocation failed!
4644 Allocation failed!
4643 Allocation failed!
4648 Allocation failed!
4669 Allocation failed!
4696 Allocation failed!
4695 Allocation failed!
4716 Allocation failed!
4721 Allocation failed!
Run Code Online (Sandbox Code Playgroud)

只有15处理未能分配1 GB -证明为overcommit_memory = 0启发式通过状态的影响。有多少进程?查看 tmp.txt 的末尾,可能 > 100,000。现在如何才能真正使用 1 GB?

> cat tmp.txt | grep WROTE
4646 WROTE 1 GB
4648 WROTE 1 GB
4671 WROTE 1 GB
4687 WROTE 1 GB
4694 WROTE 1 GB
4696 WROTE 1 GB
4716 WROTE 1 GB
4721 WROTE 1 GB
Run Code Online (Sandbox Code Playgroud)

八 - 这又是有道理的,因为当时我有大约 3 GB 的可用 RAM 和 6 GB 的交换空间。

执行此操作后查看系统日志。您应该看到 OOM 杀手报告分数(除其他外);大概这与oom_badness.


ter*_*don 17

如果您只将 1G 数据加载到内存中,则不会发生这种情况。如果你加载更多怎么办?例如,我经常处理包含数百万个概率的大型文件,这些文件需要加载到 R 中。这需要大约 16GB 的 RAM。

在我的笔记本电脑上运行上述过程将导致它在我的 8GB RAM 被填满后立即开始疯狂交换。反过来,这会减慢一切,因为从磁盘读取比从 RAM 读取慢得多。如果我的笔记本电脑有 2GB 的 RAM 和只有 10GB 的可用空间怎么办?一旦进程占用了所有 RAM,它也会填满磁盘,因为它正在写入交换,而我没有更多的 RAM 和更多的空间可以交换到(人们倾向于将交换限制到专用分区而不是一个正是因为这个原因而交换文件)。这就是 OOM 杀手进来并开始杀死进程的地方。

所以,系统确实会耗尽内存。此外,仅仅因为交换导致的 I/O 操作缓慢,大量交换系统可能会在这种情况发生之前很久就变得无法使用。人们通常希望尽可能避免交换。即使在具有快速 SSD 的高端服务器上,性能也会明显下降。在我的笔记本电脑上,它有一个经典的 7200RPM 驱动器,任何重大的交换本质上都会使系统无法使用。它交换的越多,它变得越慢。如果我不迅速终止有问题的进程,一切都会挂起,直到 OOM 杀手介入。


jll*_*gre 5

当没有更多 RAM 时,进程不会被杀死,当它们以这种方式被欺骗时它们会被杀死:

  • Linux 内核通常允许进程分配(即保留)一定数量的虚拟内存,该数量大于实际可用的内存(RAM 的一部分 + 所有交换区)
  • 只要进程只访问他们保留的页面的一个子集,一切都运行良好。
  • 如果一段时间后,进程尝试访问它拥有的页面但没有更多页面可用,则会发生内存不足的情况
  • OOM 杀手选择其中一个进程,不一定是请求新页面的进程,然后杀死它以恢复虚拟内存。

即使系统没有主动交换,也可能发生这种情况,例如,如果交换区域充满了休眠的守护进程内存页面。

这在不会过度使用内存的操作系统上永远不会发生。有了它们,没有随机进程被杀死,但是第一个在耗尽时请求虚拟内存的进程有 malloc(或类似的)返回错误。因此,它有机会妥善处理这种情况。但是,在这些操作系统上,也可能会发生系统耗尽虚拟内存而仍有可用 RAM 的情况,这很令人困惑并且通常会被误解。