c++ bad_alloc 未捕获异常

c++ bad_alloc exception not catched

我有以下代码来分配大量数据,如果它超过可用内存(这里是 32GB),它应该抛出异常。使用:

bool MyObject::init()
{    
    char* emergency_memory = new char[32768];
    try
    {
        std::vector<std::vector<MyData> > data_list;
        std::vector<MyData> data;
        data.resize(1000);

        for(size_t i=0; i<1000; i++)
        {
           data_list.push_back(data);
           data_list.push_back(data);
        }       
    }
    catch (const std::bad_alloc& e)
    {
        delete[] emergency_memory;        
        std::cout << "Data Allocation failed:" << e.what() << std::endl;
        return false;
    }
    return true;
}

从未捕获到异常。应用程序刚刚终止,或使操作系统崩溃。

我做错了什么?

如果 malloc 失败,

bad_alloc 将抛出,如果内核调用更多内存失败,malloc 将失败。在 Linux 的情况下,内核通常会让进程成功占用更多内存,然后如果 Linux 感觉某个进程正在占用大量内存,它就会将其杀死。这与如果 Linux 只是拒绝向进程提供更多内存不同,后者会导致格式良好的 bad_alloc.

解决方案是覆盖 new/delete(这是简单的 malloc/free 调用和检查 malloc == 0),以还跟踪内存使用情况的函数。然后,new 可以在内存使用率过高时抛出 bad_alloc,然后 Linux 毫不客气地杀死它。

Linux 过度使用内存。这意味着 malloc() 并没有真正分配,它只是让进程相信它在分配。实际分配发生在页面错误之后真正需要内存页面时。因此,即使需要分配比实际可用内存更多的内存,malloc() 也不会失败。

我测试了你的代码,它也在我的 Linux 上崩溃了。要真正有一个 bad_alloc 异常,可以简单地禁用过度使用。

这在 Virtual memory settings in Linux - The Problem with Overcommit 中有详细描述。

禁用过度使用对我有效:

sudo sh -c 'echo 2 > /proc/sys/vm/overcommit_memory'

此设置有两个缺点。 malloc() returns NULL 时会出现更多的情况,即使在这个设置之前它曾经工作过。第二个缺点是这不是持久的,重启后会恢复到原来的值。为此,您需要更新引导脚本。

编辑: 要使其在重启后工作,您可以 须藤六 /etc/crontab (参见 man 5 crontab) 然后添加行

@reboot     root echo 2 > /proc/sys/vm/overcommit_memory

您的 new 操作员必须从某处获取内存。由于 new 是用户 space 代码,与实际内存没有任何联系,它所能做的就是通过系统调用 sbrk() 或系统调用 mmap() 向内核询问一些记忆。内核将通过将一些额外的内存页面映射到您进程的虚拟地址 space.

来响应

碰巧,内核return发送给用户进程的任何内存页面都必须清零。如果跳过此步骤,内核可能会将敏感数据从另一个应用程序或自身泄漏给用户space 进程。

还有一种情况是,内核总是有一个只包含零的内存页。因此它可以通过简单地将这个零页映射到新地址范围来简单地满足任何 mmap() 请求。它会将这些映射标记为写入时复制,这样每当您的 userspace 进程开始写入此类页面时,内核将立即创建零页面的副本。然后内核将四处寻找另一页内存来支持它的承诺。

你看到问题了吗? 在您的进程实际写入内存之前,内核不需要任何物理内存。这称为内存过度使用。当您 fork 一个进程时,会发生这种情况的另一个版本。你认为内核会在你调用 fork() 时立即复制你的内存?当然不是。它只会对现有内存页面进行一些 COW 映射!

(这是一个重要的优化机制:许多启动的映射永远不需要额外的内存支持。这对于 fork() 尤其重要:此调用通常紧随 exec() 调用,这将立即再次拆除 COW 映射。)

缺点是,内核永远不知道它实际需要多少物理内存,直到它无法支持自己的承诺。这就是为什么当你 运行 内存不足时你不能依赖 sbrk()mmap() 到 return 一个错误:你不会 运行 内存不足直到您写入映射内存。系统调用中没有错误代码 return 意味着您的 new 运算符不知道何时抛出。所以不会抛出。

相反,当内核意识到 运行 内存不足时,它会崩溃,并开始关闭进程。这就是名副其实的 Out-Of-Memory 杀手的工作。这只是为了避免立即重新启动,而且,如果 OOM-killer 的试探法运作良好,它实际上会触发正确的进程。被杀死的进程不会得到警告,它们只是被一个信号终止。再次没有涉及用户space异常。


TL;DR:在过度提交的内核上捕获 bad_alloc 异常几乎没有用。