使用malloc分配比实际存在更多的内存

10

每次从标准输入读取字母'u'时,此代码片段将分配2Gb的内存,并在读取'a'后初始化所有分配的字符。

#include <iostream>
#include <stdlib.h>
#include <stdio.h>
#include <vector>
#define bytes 2147483648
using namespace std;
int main()
{
    char input [1];
    vector<char *> activate;
    while(input[0] != 'q')
    {
        gets (input);
        if(input[0] == 'u')
        {
            char *m = (char*)malloc(bytes);
            if(m == NULL) cout << "cant allocate mem" << endl;
            else cout << "ok" << endl;
            activate.push_back(m);
        }
        else if(input[0] == 'a')
        {
            for(int x = 0; x < activate.size(); x++)
            {
                char *m;
                m = activate[x];
                for(unsigned x = 0; x < bytes; x++)
                {
                    m[x] = 'a';
                }
            }
        }
    }
    return 0;
}

我在一台有3GB内存的Linux虚拟机上运行此代码。使用htop工具监视系统资源使用情况时,发现malloc操作没有反映在资源上。

例如,当我仅输入一次'u'(即分配2GB堆内存)时,在htop中看不到内存使用量增加2GB。只有在我输入'a'(即初始化)时,我才会看到内存使用量增加。

因此,我能够“malloc”比实际内存更多的堆内存。例如,我可以malloc 6GB(这比我的RAM和交换空间内存都多),而malloc将允许它(即malloc不会返回NULL)。但是当我尝试初始化分配的内存时,我可以看到内存和交换空间填满直至该进程被杀死。

-我的问题:

1.这是内核bug吗?

2.有人能解释一下为什么允许这种行为吗?


2
顺便提一下,你对 gets() 的调用会导致缓冲区溢出。解决方案是,把它扔掉。 - Yu Hao
你可能会遇到未定义行为。你无法确定在main函数开始时未初始化的input[0]是否为q,这只是你的运气。使用g++-Wall进行编译。 - Basile Starynkevitch
6个回答

17

这被称为内存超额配置。您可以通过以root身份运行以下命令来禁用它:

 echo 2 > /proc/sys/vm/overcommit_memory

我不喜欢它(所以我总是禁用它),但这不是内核功能。请参阅malloc(3)mmap(2)proc(5)

NB: 通常使用echo 0而不是echo 2也可以起作用,但并非总是如此。请阅读文档(特别是我刚刚链接的proc手册页)。


感谢您的更正。已将0替换为2。实际上,0通常足够好用。 - Basile Starynkevitch

10
根据 man malloc (在线文档地址): 默认情况下,Linux 采用乐观的内存分配策略。这意味着当 malloc() 返回非 NULL 时,并不能保证该内存确实可用。
所以,当您只想分配过多的内存时,它会“欺骗”您;当您想使用已分配的内存时,它会尝试为您找到足够的内存空间,如果找不到足够的内存空间,程序可能会崩溃。请注意保留原有的 HTML 标签。

5
不,这不是内核错误。你发现了一种称为“延迟分页”(或超额提交)的东西。
在使用malloc (...)分配的地址之前,内核只会“保留”地址范围。当然,这真的取决于你的内存分配器和操作系统的实现,但大多数良好的内存分配器在内存第一次被使用之前不会产生大部分内核开销。
Hoard分配器是一个很大的罪犯,它几乎从来没有利用支持延迟分页的内核,通过广泛的测试,您可以始终减轻任何分配器中延迟分页的影响,如果在分配后立即填充整个内存范围。
像VxWorks这样的实时操作系统永远不会允许这种行为,因为延迟分页会引入严重的延迟。从技术上讲,它所做的就是将延迟推迟到以后的不确定时间。

如果您想了解更详细的讨论,您可能会对IBM的AIX操作系统如何处理页面分配超额承诺感兴趣。


3

初始化/处理内存应该可以正常工作:

memset(m, 0, bytes);

此外,您可以使用 calloc 函数,它不仅会分配内存,而且还会为您填充零:

char* m = (char*) calloc(1, bytes);

3
这是Basile提到的过度提交内存的结果。然而,解释有点有趣。
基本上,在Linux(POSIX?)中尝试映射额外内存时,内核只会保留它,只有在应用程序访问其中一个保留页面时才会实际使用它。这允许多个应用程序保留超过实际总RAM /交换的数量。
除非您拥有实时操作系统或其他需要明确知道谁将在何时和为什么需要什么资源的环境,否则这是大多数Linux环境中理想的行为。
否则,某人可能会出现,malloc所有RAM(实际上并未执行任何操作),并OOM您的应用程序。
这种懒惰分配的另一个示例是mmap(),其中您拥有一个虚拟映射,文件可以适合其中 - 但您只有少量的实际内存用于此项工作。这使您能够mmap()大型文件(大于可用RAM),并像正常文件句柄一样使用它们,这很方便。
- n

2
它是“可取”的行为这一事实是一个观点问题,而我强烈不同意;在我看来,内存过度承诺总是不好的(因为它会隐藏开发人员的错误)。 - Basile Starynkevitch
1
如果有人能够澄清这种行为是仅限于Linux还是在POSIX中,那就太好了。 - mASOUD
2
它是Linux特定的,而不是POSIX。 - Basile Starynkevitch
谢谢澄清!我忘记了这是一个Linux特有的东西。我不同意过度承诺总是一件“坏事”。很容易看出你实际使用了多少内存,以及你分配了多少内存。不要因噎废食。 - synthesizerpatel
1
我们仍然都认为这是一个观点问题!而且,使用MAP_SHARED映射真实文件的mmap不会使用过度承诺(因为该文件用作“交换空间”)。 - Basile Starynkevitch

2

1.这是内核漏洞吗?

不是。

2.有人能解释一下为什么允许这种行为吗?

有几个原因:

  • 缓解需要知道最终内存需求的压力 - 通常情况下,应用程序希望能够分配一个它认为是其实际所需上限的内存量。例如,如果正在准备某种报告,无论是进行初始计算以计算报告的最终大小,还是使用realloc()来成功地扩展更大的区域(存在复制的风险),都可能会使代码变得非常复杂并影响性能,而将每个条目的最大长度乘以条目数可能非常快捷和简单。如果您知道虚拟内存相对于应用程序的需求来说是相当丰富的,那么分配更大的虚拟地址空间非常便宜。

  • 稀疏数据 - 如果您有多余的虚拟地址空间,则可以具有稀疏数组并使用直接索引,或者分配具有慷慨的capacity()到size()比率的哈希表,可以导致非常高的性能系统。两者在数据元素大小是内存分页大小的倍数或失败时(更大或小的整数分数)时效果最佳(在开销/浪费和有效使用内存缓存方面)。

  • 资源共享 - 考虑提供“每秒1千兆比特”的连接给建筑物中的1000个消费者的ISP - 他们知道如果所有消费者同时使用它,他们将获得大约1兆比特,但依靠他们的现实世界经验,尽管人们要求1千兆比特并在特定时间希望获得很大一部分,但不可避免地存在较低的最大值和更低的并发使用平均值。与互联网连接相同的见解应用于内存,允许操作系统支持比它们本来会更多的应用程序,并以合理的平均成功率满足期望。就像共享的互联网连接随着更多用户同时提出需求而速度下降一样,从磁盘上的交换内存进行页面换出可能会启动并降低性能。但与互联网连接不同的是,交换内存有一个限制,如果所有应用程序确实尝试并发使用内存,使得该限制被超过,则一些应用程序将开始收到报告内存耗尽的信号/中断/陷阱。总之,启用此内存超额承诺行为后,仅检查malloc()/new返回的非NULL指针是不足以保证物理内存实际上可用的,程序稍后可能仍会接收到信号,因为它尝试使用该内存。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接