2

I have following code to allocate a huge amount of data and if it is more than there is memory available (here it is 32GB) it should throw an exception. Using:

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

The exception is never caught. The application just terminates, or crashes the operating system.

What did I do wrong?

NathanOliver
  • 171,901
  • 28
  • 288
  • 402
goaran
  • 353
  • 2
  • 11
  • Physical memory is not the same thing as available memory –  Aug 03 '18 at 14:16
  • [See the notes for std::vector::push_back()](https://en.cppreference.com/w/cpp/container/vector/push_back#Notes). Try catching `std::exception` instead. – Yksisarvinen Aug 03 '18 at 14:17
  • 2
    So you leak 32768 bytes on every success? – François Andrieux Aug 03 '18 at 14:20
  • At the very least, when you're looking for exceptions, have a default catch clause to ensure that you catch **any** exception that gets thrown, and not just the one you're expecting. – Pete Becker Aug 03 '18 at 14:23
  • @Frank: i know, but the exception should be thrown anyway. to Yksisarivinen: did not work eighter to Hans: the system is a Debian Stretch with the gcc lib provided by the distribution – goaran Aug 03 '18 at 14:24
  • 3
    This might the common Linux bug where Linux fails to inform the application that it's out of memory. – MSalters Aug 03 '18 at 14:25
  • 2
    If probably failed to allocate the std::bad_alloc object as well. Do mention the C++ library implementation you use. Do keep in mind if this crashes the OS then you are building a house on a foundation made from mashed potatoes. Assume nothing. – Hans Passant Aug 03 '18 at 14:27
  • @MSalters: Thanks for the hint, I'll try on windows later. – goaran Aug 03 '18 at 14:28
  • Without knowing what your program is doing (e.g. what other functions are doing), or what host system is, no answer to your question is possible. The code, as shown, is not guaranteed to throw exceptions, particularly on systems that allow memory allocations to always (lazily) succeed, and for failure to only occur if your program actually attempts to USE the allocated memory. – Peter Aug 03 '18 at 14:28
  • @MSalters - depends on how you look at it. Some consider it a feature, not a bug. And the behaviour you describe is a configuration option on such systems. – Peter Aug 03 '18 at 14:30
  • @Francois: in the real implementation the emergency_memory is a member of the class, i just copied it there for the example to make it more compact to read, but your right ;) – goaran Aug 03 '18 at 14:31
  • 2
    In linux there is a setting that allows overcommitting. It the application requires more memory than there is available, allocation succeeds but no memory is truly allocated. When the code tries to read/write a memory page, only then is it allocated. Finally, if there is no available pages in the system, the kernel simply kills the process. To disable it, try echo 2 > /proc/sys/vm/overcommit_memory – Michael Veksler Aug 03 '18 at 14:33
  • 1
    @HansPassant AFAIK the C++ exception system must reserve space for that beforehand so throwing `std::bad_alloc` should never fail due to lack of memory. – Konrad Rudolph Aug 03 '18 at 14:34
  • Sure, sure, we can't see the mashed potatoes. – Hans Passant Aug 03 '18 at 14:35
  • @HansPassant I seriously doubt this crashes the OS (unless embedded … unlikely with 32 GiB main memory?). Much more likely, OP misspoke. – Konrad Rudolph Aug 03 '18 at 14:38
  • The os may seem to crash when it starts paging like crazy. For such an amount of memory, it can take many long minutes, or more, to write so many pages to disk. – Michael Veksler Aug 03 '18 at 14:44
  • @KonradRudolph: I am not totally sure if the OS crashed completely, the screen got black and for some minutes nothing happened so rebooted. In another case it logged out the user and showed the login screen. – goaran Aug 03 '18 at 14:50

3 Answers3

5

Your new operator has to get the memory from somewhere. As new is user space code that has no connection to real memory whatsoever, all that it can do is to ask the kernel via the syscall sbrk() or the syscall mmap() for some memory. The kernel will respond by mapping some additional memory pages into your process's virtual address space.

As it happens, any memory pages that the kernel returns to user processes must be zeroed out. If this step were skipped, the kernel might leak sensible data from another application or itself to the userspace process.

Also it happens, that the kernel always has one memory page that contains only zeros. So it can simply fulfill any mmap() request by simply mapping this one zero page into the new address range. It will mark these mapping as Copy-On-Write, so that whenever your userspace process starts writing to such a page, the kernel will immediately create a copy of the zero page. It is then that the kernel will grope around for another page of memory to back its promises.

You see the problem? The kernel does not need any physical memory up unto the point where your process actually writes to the memory. This is called memory over-committing. Another version of this happens when you fork a process. You think the kernel would immediately copy your memory when you call fork()? Of course not. It will just do some COW-mappings of the existing memory pages!

(This is an important optimization mechanism: Many mappings that are initiated never need to be backed by additional memory. This is especially important with fork(): This call is usually immediately followed by an exec() call, which will immediately tear down the COW-mappings again.)

The downside is, that the kernel never knows how much physical memory it actually needs until it fails to back its own promises. That is why you cannot rely on sbrk() or mmap() to return an error when you run out of memory: You don't run out of memory until you write to the mapped memory. No error code return from the syscall means that your new operator does not know when to throw. So it won't throw.

What happens instead is that the kernel will panic when it realizes that it has run out of memory, and start shooting down processes. That is the job of the aptly named Out-Of-Memory killer. That's just to try to avoid rebooting immediately, and, if the heuristics of the OOM-killer work well, it will actually shoot the right processes. The killed processes won't get so much as a warning, they are simply terminated by a signal. No user-space exception involved, again.


TL;DR: Catching bad_alloc exceptions on an over-committing kernel is next to useless.

cmaster - reinstate monica
  • 38,891
  • 9
  • 62
  • 106
2

Linux overcommits memory. This means that malloc() does not really allocate, it only makes the process to believe it does. The actual allocation happens when a memory page is really needed, after a page-fault. For that reason, malloc() does not fail even if it needs to allocate more memory than is actually available.

I have tested your code, and it also crashes on my Linux. To actually have a bad_alloc exception, one can simply disable overcommitting.

This is described in detail in Virtual memory settings in Linux - The Problem with Overcommit .

Disabling overcommitting works for me:

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

This setting has two drawbacks. There will be more cases when malloc() returns NULL, even though it used to work before this setting. The second drawback is that this is not persistent, and gets back to the original value after reboot. For that, you need to update the boot scripts.

EDIT: To make it work after reboot you can sudo vi /etc/crontab (see man 5 crontab) and then add the line

@reboot     root echo 2 > /proc/sys/vm/overcommit_memory
Michael Veksler
  • 8,217
  • 1
  • 20
  • 33
0

bad_alloc will throw if the malloc fails, and the malloc will fail if the kernel call for more memory fails. In the case of Linux, the kernel will commonly let the process succeed in taking more memory, and then if Linux feels like a process is hogging to much memory, it'll just kill it. This is different than if Linux simply denied more memory to the process, which would cause a well-formed bad_alloc.

The solution would be to override new/delete (Which are simple malloc/free calls and a check for malloc == 0), to functions that also keep track of memory usage. Then, new could throw bad_alloc when memory usage is too high, before Linux unceremoniously kills it.

Nicholas Pipitone
  • 4,002
  • 4
  • 24
  • 39
  • " it'll just kill it" ... It'll just kill something. The OOM killer isn't choosy. It might just kill the login session instead. – Stephen M. Webb Aug 03 '18 at 15:07
  • 1
    I highly doubt the login session will be using nearly enough memory to warrant that. The OOM killer will still kill in rough order of memory usage. (Especially with modern cgroups handling `/proc/1234/oom_score` very precisely). Technically, Linux isn't obligated to choose intelligently though, so fair enough. – Nicholas Pipitone Aug 03 '18 at 15:11
  • Overriding `new`/`delete` is an exercise in futility: The "problem" is that the *syscall* to get the memory won't fail. There is zero indication to the process whether the kernel still has enough memory to back its promises. Also, the standard implementation already *does* check for a failing syscall in order to throw a `bad_alloc` when appropriate, but since the syscall won't fail, it's powerless to throw. So, all you'd be doing is to reimplement an already implemented but virtually useless feature. – cmaster - reinstate monica Dec 12 '22 at 16:03