Linux可以“用完RAM”吗?


20

我在网上看到很多帖子,似乎是在抱怨托管的VPS意外终止进程,因为它们使用了太多的RAM。

这怎么可能?我以为所有现代OS都通过对物理RAM上的内容使用磁盘交换来提供“无限RAM”。它是否正确?

如果进程“由于RAM不足而被杀死”会发生什么情况?


12
没有操作系统具有无限的 RAM。除了机器中的物理RAM芯片外,操作系统通常还可以(可选)使用磁盘上的所谓“交换文件”。当计算机需要的内存超过RAM棒中的内存时,它将一些东西换出到交换文件中。但是,当交换文件达到其容量时-由于您设置了最大大小(典型值)或磁盘已满-您将耗尽虚拟内存。
John Dibling 2013年

@JohnDibling; 因此,除了节省文件系统的磁盘空间外,还有什么理由要限制交换大小吗?换句话说,如果我有20GB的磁盘而只有1GB的文件,是否有任何理由不将交换大小设置为19GB?
themirror

1
为了简化起见,我想说限制交换大小的两个原因是1)减少磁盘消耗和2)提高性能。在Windows下,后者可能比/ * NIX更正确,但是,如果您正在使用磁盘上的交换空间,则性能会降低。磁盘访问是慢于RAM,或慢于RAM,取决于系统。
John Dibling 2013年

9
交换不是RAMzh_CN.wikipedia.org/wiki/Random-access_memory系统中的RAM数量就是系统中的RAM数量(期限)。它不是模棱两可或动态的卷。这是绝对固定的。正如Terdon(+1)所指出的那样,“内存”是一个更加模糊的概念,但是RAM与其他形式的存储之间的区别是非常重要的。磁盘交换不能在很多数量级上替代RAM的性能。过度依赖交换的系统充其量只是临时的,通常是垃圾。
goldilocks 2013年

1
那么磁盘空间现在是无限的了吗?
卡兹(Kaz)2013年

Answers:


41

如果进程“由于RAM不足而被杀死”会发生什么情况?

有时有人说linux默认情况下从不拒绝从应用程序代码获取更多内存的请求-例如malloc()1 事实并非如此;默认使用启发式

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

From [linux_src]/Documentation/vm/overcommit-accounting(所有引号均来自3.11树)。确切地讲,没有明确说明什么是“严重的疯狂分配”,因此我们将不得不通过源头来确定细节。我们还可以使用脚注2(如下)中的实验方法来尝试启发式思考-基于此,我的初步经验观察是,在理想情况下(==系统处于空闲状态),如果您不这样做,没有任何交换,您将可以分配大约一半的RAM,如果有交换,则将获得大约一半的RAM加上所有交换。每个过程或多或少都差不多(但是请注意,此限制动态的,并且由于状态而可能会发生变化,请参见脚注5中的一些观察结果)。

一半的RAM加交换明确是中“ CommitLimit”字段的默认值/proc/meminfo。这就是它的意思-请注意,它实际上与刚才讨论的限制无关(来自[src]/Documentation/filesystems/proc.txt):

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

前面引用的overcommit-accounting doc指出默认vm.overcommit_ratio值为50。因此,如果使用sysctl vm.overcommit_memory=2,则可以调整vm.covercommit_ratio(带有sysctl)并查看结果。3 默认模式为when,CommitLimit并且不强制执行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)
 */

这似乎是一个不错的理由。但是,在没有取证的情况下,#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.
 */

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

如果要查看实际使用中的OOM杀手,请参阅脚注5。


1幻觉吉尔斯幸免于我,见评论。


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

#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;
}            

如果您不了解C,则可以对其进行编译gcc virtlimitcheck.c -o virtlimitcheck,然后运行./virtlimitcheck。它是完全无害的,因为该进程不使用它所要求的任何空间-即,它从未真正使用过任何RAM。

在具有4 GB系统和6 GB交换空间的3.11 x86_64系统上,我失败于〜7400000 kB;数量波动,所以状态可能是一个因素。巧合的CommitLimit/proc/meminfo,它接近in ,但修改该via vm.overcommit_ratio并没有任何区别。但是,在具有64 MB交换空间的3.6.11 32位ARM 448 MB系统上,我无法使用〜230 MB。这很有趣,因为在第一种情况下,其数量几乎是RAM数量的两倍,而在第二种情况下,其数量约为RAM的1/4,这强烈暗示着交换数量是一个因素。当故障阈值降低到〜1.95 GB时(与小型ARM盒非常相似),通过在第一个系统上关闭交换来确认这一点。

但这真的是每个过程吗?好像是。下面的简短程序要求用户定义的内存块,如果成功,则等待您按下return键–这样,您可以尝试同时执行多个实例:

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

#define MB 1 << 20

int main (int argc, const char *argv[]) {
    unsigned long int megabytes = strtoul(argv[1], NULL, 10);
    void *p = malloc(megabytes * MB);
    fprintf(stderr,"Allocating %lu MB...", megabytes);
    if (!p) fprintf(stderr,"fail.");
    else {
        fprintf(stderr,"success.");
        getchar();
        free(p);
    }
    return 0;
}

但是请注意,无论使用什么,这都不严格是关于RAM和交换量的-关于系统状态影响的观察,请参见脚注5。


3表示 当vm.overcommit_memory = 2时系统CommitLimit允许的地址空间量。大概,那么,您可以分配的数量应减去已提交的内容,这显然是该字段。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.

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


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


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

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

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/time.h>
#include <errno.h>
#include <string.h>

/* 90 second "Verbose hungry fork bomb".
Verbose -> It jabbers.
Hungry -> It grabs address space, and it tries to eat memory.

BEWARE: ON A SYSTEM WITH 'vm.overcommit_memory=0', THIS WILL FREEZE EVERYTHING
FOR THE DURATION AND CAUSE THE OOM KILLER TO BE INVOKED.  CLOSE THINGS YOU CARE
ABOUT BEFORE RUNNING THIS. */

#define STEP 1 << 30 // 1 GB
#define DURATION 90

time_t now () {
    struct timeval t;
    if (gettimeofday(&t, NULL) == -1) {
        fprintf(stderr,"gettimeofday() fail: %s\n", strerror(errno));
        return 0;
    }
    return t.tv_sec;
}

int main (void) {
    int forks = 0;
    int i;
    unsigned char *p;
    pid_t pid, self;
    time_t check;
    const time_t start = now();
    if (!start) return 1;

    while (1) {
    // Get our pid and check the elapsed time.
        self = getpid();
        check = now();
        if (!check || check - start > DURATION) return 0;
        fprintf(stderr,"%d says %d forks\n", self, forks++);
    // Fork; the child should get its correct pid.
        pid = fork();
        if (!pid) self = getpid();
    // Allocate a big chunk of space.
        p = malloc(STEP);
        if (!p) {
            fprintf(stderr, "%d Allocation failed!\n", self);
            return 0;
        }
        fprintf(stderr,"%d Allocation succeeded.\n", self);
    // The child will attempt to use the allocated space.  Using only
    // the child allows the fork bomb to proceed properly.
        if (!pid) {
            for (i = 0; i < STEP; i++) p[i] = i % 256;
            fprintf(stderr,"%d WROTE 1 GB\n", self);
        }
    }
}                        

编译这个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.

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

现在尝试使用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!

只有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

八个-这又很有意义,因为当时我有约3 GB的可用RAM和6 GB的交换空间。

完成此操作后,请查看系统日志。您应该看到OOM杀手的报告分数(以及其他);大概与oom_badness


换出内存不是解决内存过剩问题的解决方案(甚至不是解决方案)。内存分配(例如:malloc)是关于请求保留虚拟内存,而不是物理内存。
jlliagre 2013年

1
@jillagre:“交换不是解决内存超过承诺的解决方案(甚至不相关)” ->是的,实际上是这样。很少使用的页面被交换出RAM,从而留出更多RAM来处理由需求分页/分配(这是使超额分配成为可能的机制)引起的页面错误的原因。在某些时候,可能还需要将换出的页面分页回RAM。
goldilocks 2013年

“内存分配(例如:malloc)是关于请求保留虚拟内存,而不是物理内存。” ->对,但是当没有物理映射时,内核可以(并且可以选择)拒绝。当然不是因为进程的虚拟地址空间用完了(或者至少通常不是这样,因为至少在32位系统上也可以这样做)。
goldilocks 2013年

需求分页并不是使内存超出承诺的原因。Linux肯定会在根本没有交换区域的系统上过度提交内存。您可能会使内存与承诺和需求分页混淆。如果Linux对64位进程使用malloc表示“否”,即未将其配置为始终过量使用,那可能是由于内存损坏或所有内存预留的总和(无论是否映射到RAM或磁盘)是否超过阈值阈值,具体取决于配置。这与RAM的使用无关,因为即使在仍有可用RAM的情况下也可能发生。
jlliagre

“需求分页并不是使内存超出承诺范围的原因。” ->也许最好说是虚拟寻址,这使得请求分页和过量承诺成为可能。“ Linux肯定会在根本没有交换区域的系统上过度提交内存。” ->显然,由于需求分页不需要交换;交换中的需求分页只是需求分页的一个特殊实例。再一次,交换解决过度承诺的一种方法,不是从解决问题的意义上说,而是从防止过度承诺可能导致的潜在OOM事件的意义上说。
goldilocks 2013年

16

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

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

因此,系统确实会耗尽内存。此外,仅由于交换引起的I / O操作缓慢,大量交换系统可能在很久之前就变得不可用。人们通常希望尽可能避免交换。即使在具有快速SSD的高端服务器上,性能也会明显下降。在我的笔记本电脑上,该笔记本电脑具有经典的7200RPM驱动器,任何重要的更换操作都会使系统无法使用。交换的次数越多,获取的速度就越慢。如果我不及时终止令人讨厌的过程,那么一切都会挂起,直到OOM终止者介入。


5

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

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

即使在系统未主动进行交换的情况下(例如,如果交换区域中充满了睡眠守护程序内存页面),也可能会发生这种情况。

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


3

当可用RAM耗尽时,内核开始将处理位换出到磁盘。实际上,内核在RAM即将用尽时开始交换a:它在有空闲时刻时开始主动交换,以便在应用程序突然需要更多内存时更加敏感。

请注意,RAM不仅用于存储进程的内存。在典型的运行状况良好的系统上,进程仅使用大约一半的RAM,另一半用于磁盘高速缓存和缓冲区。这在运行的进程和文件输入/输出之间提供了良好的平衡。

交换空间不是无限的。在某个时候,如果进程不断分配越来越多的内存,则来自RAM的溢出数据将填充交换空间。发生这种情况时,尝试请求更多内存的进程将看到其请求被拒绝。

默认情况下,Linux 过量使用内存。这意味着它有时将允许进程使用已保留但未使用的内存运行。过度承诺的主要原因是分叉的工作方式。当一个进程启动一个子进程时,子进程从概念上讲是在父进程的内存副本中运行的-两个进程最初具有具有相同内容的内存,但是随着进程在各自的空间中进行更改,该内容将有所不同。为了完全实现这一点,内核将必须复制父代的所有内存。这会使分叉变慢,因此内核实践写时复制:最初,孩子与父母共享所有的记忆;每当任何一个进程写入共享页面时,内核都会创建该页面的副本以破坏共享。

通常,一个孩子会留下很多未触及的页面。如果内核分配了足够的内存来复制每个派生服务器上父代的内存空间,那么在子进程将不再使用的预留空间中会浪费大量内存。因此,过度使用:内核仅根据子代需要多少页的估计量来保留一部分内存。

如果某个进程尝试分配一些内存而没有足够的内存,则该进程将收到错误响应并按其认为适当的方式进行处理。如果某个进程通过写入必须不共享的共享页面间接请求内存,则情况就不同了。无法将这种情况报告给应用程序:它认为那里有可写的数据,甚至可以读取它-只是编写需要在后台进行一些更为精细的操作。如果内核无法提供新的内存页,则它所能做的就是杀死请求进程,或者杀死其他进程来填充内存。

您可能会认为此时终止请求进程是显而易见的解决方案。但是实际上,这不是很好。该过程可能是一个重要的过程,碰巧现在只需要访问其页面之一,而可能正在运行其他次要的过程。因此,内核包括复杂的启发式方法来选择要杀死的进程-著名的OOM杀手


2

只是为了从其他答案中补充一个观点,许多VPS在任何给定服务器上托管了多个虚拟机。任何单个VM都会有指定数量的RAM供自己使用。许多提供程序都提供“突发RAM”,他们可以在其中使用超出指定数量的RAM。这仅用于短期使用,而那些超出此扩展时间范围的主机可能会因为主机终止进程以减少正在使用的RAM数量而受到惩罚,从而使其他人不会遭受主机过载。


-1

一段时间后,Linux占用了外部虚拟空间。那就是交换分区。当Ram被填满时,Linux将使用此交换区域来运行低优先级进程。


1
没有从交换运行任何进程。虚拟内存分为大小相等的不同单元,称为页面。释放物理内存后,低优先级页面将从RAM中逐出。文件高速缓存中的页面具有文件系统支持时,匿名页面必须存储在swap中。页面的优先级与它所属进程的优先级并不直接相关,而是与页面使用频率有关。如果正在运行的进程尝试访问不在物理内存中的页面,则会生成页面错误,并且在从磁盘中获取所需页面的情况下,将抢占该进程,而优先使用另一个进程。
Thomas Nyman 2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.