views:

282

answers:

5

Hi,

I'm currently working on an exception-based error reporting system for Windows MSVC++ (9.0) apps (i.e. exception structures & types / inheritance, call stack, error reporting & logging and so on).

My question now is: how to correctly report & log an out-of-memory error?

When this error occurs, e.g. as an bad_alloc thrown by the new op, there may be many "features" unavailable, mostly concerning further memory allocation. Normally, I'd pass the exception to the application if it has been thrown in a lib, and then using message boxes and error log files to report and log it. Another way (mostly for services) is to use the Windows Event Log.
The main problem I have is to assemble an error message. To provide some error information, I'd like to define a static error message (may be a string literal, better an entry in a message file, then using FormatMessage) and include some run-time info such as a call stack.
The functions / methods necessary for this use either

  • STL (std::string, std::stringstream, std::ofstream)
  • CRT (swprintf_s, fwrite)
  • or Win32 API (StackWalk64, MessageBox, FormatMessage, ReportEvent, WriteFile)

Besides being documented on the MSDN, all of them more (Win32) or less (STL) closed source in Windows, so I don't really know how they behave under low memory problems.

Just to prove there might be problems, I wrote a trivial small app provoking a bad_alloc:

int main()
{
    InitErrorReporter();  

    try
    {
        for(int i = 0; i < 0xFFFFFFFF; i++)
        {
            for(int j = 0; j < 0xFFFFFFFF; j++)
            {
                char* p = new char;
            }
        }
    }catch(bad_alloc& e_b)
    {
        ReportError(e_b);
    }

    DeinitErrorReporter();

    return 0;
}

Ran two instances w/o debugger attached (in Release config, VS 2008), but "nothing happened", i.e. no error codes from the ReportEvent or WriteFile I used internally in the error reporting. Then, launched one instance with and one w/o debugger and let them try to report their errors one after the other by using a breakpoint on the ReportError line. That worked fine for the instance with the debugger attached (correctly reported & logged the error, even using LocalAlloc w/o problems)! But taskman showed a strange behaviour, where there's a lot of memory freed before the app exits, I suppose when the exception is thrown.


Please consider there may be more than one process [edit] and more than one thread [/edit] consuming much memory, so freeing pre-allocated heap space is not a safe solution to avoid a low memory environment for the process which wants to report the error.

Thank you in advance!

+3  A: 

"Freeing pre-allocated heap space...". This was exactly that I thought reading your question. But I think you can try it. Every process has its own virtual memory space. With another processes consuming a lot of memory, this still may work if the whole computer is working.

Alex Farber
k, but supposed there where two threads in one application where both ran different libraries' functions. One process -> one virtual memory space. So when one throws a bad_alloc then frees pre-allocated space, the other one could allocate it?The problem is when I use OS / SDK functions, I don't know if they internally rely on head space, so using pre-allocated space to operate on was no solution I thought.
DyP
have I to suspend all other threads before I can use this technique?
DyP
Another threads may immediately use pre-allocated memory which is just released... Every activity should be stopped to be sure that error report succeeds.
Alex Farber
+1  A: 

Please consider there may be more than one process consuming much memory, so freeing pre-allocated heap space is not a safe solution to avoid a low memory environment for the process which wants to report the error.

Under Windows (and other modern operating systems), each process has its own address space (aka memory) separate from every other running process. And all of that is separate from the literal RAM in the machine. The operating system has virtualized the process address space away from the physical RAM.

This is how Windows is able to push memory used by processes into the page file on the hard disk without those processes having any knowledge of what happened.

This is also how a single process can allocate more memory than the machine has physical RAM and yet still run. For instance, a program running on a machine with 512MB of RAM could still allocate 1GB of memory. Windows would just couldn't keep all of it in the RAM at the same time and some of it would be in the page file. But the program wouldn't know.

So consequently, if one process allocates memory, it does not cause another process to have less memory to work with. Each process is separate.

Each process only needs to worry about itself. And so the idea of freeing a pre-allocated chunk of memory is actually very viable.

TheUndeadFish
I agree. What's about two threads? See Alex Farber's answer.
DyP
Huh? This is not correct. While every process has its own VA space, there are a limited number of physical frames and swap space to satisfy a page-in request. If the total commit charge goes above this number, every process's allocation requests fail. If you get into this situation, freeing a chunk of memory means any process in the system is up to taking it (i.e. any process will be able to again raise the commit charge).
Paul Betts
@Paul: You have a point. No matter what the OS does there will always be a limit eventually. Though, outside of situations where the memory mechanisms of the OS have been maxed out, I wonder if it isn't reasonable for programs act as if they will always have their full virtual address space to work with. But I guess that can be situation-specific, depending on how likely the end-user machine will max out it resources and how useful it would actually be to log in response to such a situation.
TheUndeadFish
Yeah, there is a limit. It's the size of your hard drive. That's a lot of memory. On the vast majority of machines, the RAM is less than 50% allocated, and there's so much HDD space available, the reality is that your process only has to worry about itself.
DeadMG
I think the main limit is the virtual address space in 32 bit applications. See http://msdn.microsoft.com/en-us/library/aa366778(VS.85).aspx but that's not the main problem. When your only problem is that your address space is full (how to determine that?) you could just release a chunk of memory and continue work.
DyP
@DeadMG It isn't the size of your HD, it's the preconfigured size of the swap file, which is usually around 2-4GB
Paul Betts
@Paul Betts: Windows can increase that size whenever it likes.
DeadMG
@DeadMG It can dynamically increase the size up to a point, but there is a limit
Paul Betts
@Paul Betts: Yes, the limit being the size of the harddrive, assuming that you're on NTFS. Really, most processes use only a tiny, tiny fraction of their 4GB, and the reality is that there's ample free physical memory to go around, even when you start capping your 2GB.
DeadMG
@DeadMG http://blogs.technet.com/b/markrussinovich/archive/2008/11/17/3155406.aspx - "The maximum is either three times the size of RAM or 4GB, whichever is larger"
Paul Betts
Yes, so earlier in the article, he allocated 29GB. Where did that all go?
DeadMG
I think DeadMG, you're talking about a "physical" limit (a limit of OS capabilities) whereas Paul Betts cites the limit of the OS' auto size mode. In that article Paul Betts referrs to, the second last paragraph deals with this auto mode (= max size choosed by the OS -> 3*RAM or 4 GB) whereas the very last paragraph deals with the limit of OS capabilities (-> 32-bit Windows [...] 16TB [, ...] 64-bit [...] 16TB [...]. For all versions, [...] up to 16 paging files [...].)
DyP
A: 

You can't use CRT or MessageBox functions to handle OOM since they might need memory, as you describe. The only truly safe thing you can do is alloc a chunk of memory at startup you can write information into and open a handle to a file or a pipe, then WriteFile to it when you OOM out.

Paul Betts
So you mean, rather than pre-allocate and then free a chunk, you would instead pre-allocate an area to be used specifically to be used for handling the data recorded in the OOM scenario (a sort of scratch buffer for assembling the strings or whatever is desired).Assuming WriteFile doesn't ever have a reason to fail in an OOM scenario, then I'd have to agree that this sounds like the best guaranteed way to record the information.
TheUndeadFish
Yes, this is what the OS does if it has to guarantee that something works in out-of-memory conditions (like being able to raise an SEH exception, ntdll always keeps one preallocated in its back pocket)
Paul Betts
Do you know how the creation of the different kinds of Win32 objects behaves in OOM conditions? I.e. Kernel objects such as files (-> file handles) and GUI objects such as MessageBoxes? As far as I know, at least Kernel objects are not part of your virtual memory / adress space and therefore the OS actually allocates memory for them.
DyP
@DyP: You're wrong. The OS does indeed allocate inside your address space. However, the way the two are separated, it'll probably be fine. That's why on a 32bit machine you get 2GB of limit, not 4GB- because the OS keeps the other 2GB for whatever it needs. However, if you fail an allocation, it's because your half is full - not necessarily the whole space. There's no guarantee that the OS 2GB is or isn't in any state, including full or not.
DeadMG
@DyP Kernel objects also require memory, and they are also subject to system-wide OOM - if an allocation fails, it will end up propagating back using GetLastError
Paul Betts
@Paul: Sure. I didn't doubt that ^^ but since I didn't found anything about it on MSDN, i thought it was better to ask. Maybe there was already some reserved memory part from the OS where it could store Kernel objects (physically). (But it does not seem so)
DyP
-1. Actually, MSDN documents that a message boix wit MB_SYSTEMMODAL can be used to report out-of-memory conditions. Some Microsoftie blog documented that it was expressly implemented to do that in a default setting (MB_OK | MB_ICONHAND | MB_SYSTEMMODAL as far as I recall). Will try to dig it up.
peterchen
A: 

Summing up some comments & research, I first take down some info about memory to be able to explain some approach I found.

There are three kinds of things people use to call "memory":

  • Virtual memory (when wrongly used for "virtual address space")
  • heap space (that is either physical RAM space or page file space)
  • (real) physical memory space

When you use new (or HeapAlloc or VirtualAlloc with MEM_COMMIT), the OS reserves a virtual address range, allocates heap space and maps both.
You can use AWE (e.g. AllocateUserPhysicalPages) to allocate physical memory w/o mapping it to a virtual address range (use VirtualAlloc with MEM_RESERVE | MEM_PHYSICAL to make up for the mapping).

Now, to take some advantages from this, I'm sorry i need to go into detail and unfortunately write a lot of text on this memory topic. I tried to point out the important things using bold print.


Possible approaches in out-of-memory conditions to log / report an error:

  • You cannot rely on CRT or STL to behave "correctly" under out-of-memory conditions.
  • You can pre-allocate a chunk of memory (that is, virtual memory / adress space & physical / page file memory) and use it for buffers
  • You cannot (simply) pre-allocate a chunk of memory and free it to provide free memory to CRT and other functions, since freed memory (= physical memory) can be allocated by every thread and process
  • You can use HeapLock (Win32) to prevent other threads of your current process from allocating / deallocating heap space
  • There are at least two heaps in your app: the process's heap (GetProcessHeap()) and the CRT heap (_get_heap_handle())
  • the standard new operator internally uses HeapAlloc on the CRT heap (!)
  • memory is split into pages, you can determine their size with GetSystemInfo(), dwPageSize
  • You can allocate whole pages of physical memory (w/o virtual memory mapping) using AWE (AllocateUserPhysicalPages, VirtualAlloc to reserve address space, MapUserPhysicalPages)
  • You can use HeapReAlloc with HEAP_REALLOC_IN_PLACE_ONLY to re-size an allocated chunk of memory

Now, my approach: [note: I edited it to work with HeapReAlloc and only one buffer]

  • If you just need some buffers to work with, use either stack space global/static variables (.BSS / .DATA / ...) in your c++ program or pre-allocated heap space (even better: pre-allocated physical memory -> AWE). Stack space Global/static variables are probably a better idea since they're part of your .EXE and therefore do not need to be allocated (-> no exceptions)
  • If you have to use CRT / STL / Win32 functions, you have to make sure they a) do not require heap space or b) there is enough heap space available

Of course, the trick is to provide heap space to CRT / ... functions when an out-of-memory condition appears (indicated by an bad_alloc, for example). They internally use malloc / HeapAlloc, that means they try to allocate heap space and reserve a virtual address range. So, if you have only some pre-allocated buffers, you had to prevent further memory allocation and remap their allocation requests to your pre-defined buffer. You could make use of _set_new_handler and / or overwrite the global new op, but for me, this didn't work (std::string yet called the normal new).
So, if you cannot redirect allocation queries, you have to provide free physical memory (or at least heap space) that can only be allocated by your process (= that is locked for other processes)! To do this, you could make use of the (physical!) memory layout AFAIK, i.e. that the physical memory is split into pages. Consider the following approach:

  1. When initialising you error reporter, (if you have more than one thread already running), lock the heap and pre-allocate a a chunk of memory at the size of one memory page; make sure it's aligned to a page and does not cover two pages.
  2. Unlock the heap and begin to work (don't forget to use a try-catch!).
  3. When catching the bad_alloc, do not try to use CRT or STL or whatever functions you don't know the code of. Of course, do not try to allocate memory.
  4. In your error handler, lock the heap and resize your pre-allocated buffer in place.
  5. You now have exactly that size you freed to work with; you can use STL or CRT functions if they do not exceed the free heap space.
  6. Don't forget to unlock the heap!

All of it relys on HeapAlloc and HeapReAlloc: When you allocate a whole memory page and later re-allocate it in place (resize it), you still should have that page reserved, but with free space on it.

In code, it's something like:

SYSTEM_INFO sSysInfo;
GetSystemInfo(&sSysInfo);

HANDLE hCRTHeap = (HANDLE)_get_heap_handle();
BYTE* pcBackupBuffer = (BYTE*)HeapAlloc(hCRTHeap, 0, sSysInfo.dwPageSize - 1);

try
{
    for(size_t i = 0; i < 0xFFFFFFFF; i++)
    {
        BYTE* pc = new BYTE[32];
    }
    printf("NO bad_alloc thrown at the for loop\r\n");
}catch(bad_alloc&)
{
    printf("bad_alloc thrown at the for loop\r\n");
}

try
{
    BYTE* test = new BYTE[32];
    printf("NO bad_alloc thrown at the first test\r\n");
}catch(bad_alloc&)
{
    printf("bad_alloc thrown at the first test\r\n");
}

/*pcBackupBuffer = */HeapReAlloc(hCRTHeap, HEAP_REALLOC_IN_PLACE_ONLY, pcBackupBuffer, 1);

try
{
    BYTE* test = new BYTE[200];
    printf("NO bad_alloc thrown at the second test\r\n");
}catch(bad_alloc&)
{
    printf("bad_alloc thrown at the second test\r\n");
}

Output:

bad_alloc thrown at the for loop
bad_alloc thrown at the first test
NO bad_alloc thrown at the second test

On Vista x64, VS 2008, x86 Debug, after allocating approx. 2.069.692 K RAM (says taskman). Should be out-of-virtual-address-space exceptions.
Further testing with 2 instances, enhanced code (enhanced output and tests), build against x64 / Release w/o debugger attached, 4 GB RAM + max. 2 GB page file showed similar behaviour concerning the HeapReAlloc feature. Just some strange behaviour: Even after throwing a bad_alloc, sometimes an equal allocation (some lines below) still works w/o exception o.O

Some words about the sizeing:

  • I used the BYTE[32] in the for loop because the new op actually allocates a bit more (in debug mode) than the size passed as parameter, and I wanted to allocate each possible mem page so that the first test will really fail (and allocating only one byte each time is really sloooow)
  • the second test fails allocating more memory than freed, I don't know exactly why. Maybe the page was filled up with the BYTE[32] from the for loop, so I've to figure out the exact amount to allocate from the page of pcBackupBuffer


It's all kind of a dirty hack, so I have to admit: use this only in special (emergency ^^) cases and do not to rely on it. But you might get 3 or 4 K of memory, and even get further semi-pages repeating this trick. I think that might be enough for simple CRT / Win32 / STL functions to operate on.

DyP
This is pretty wrong too, heap space is not different than VA space.
Paul Betts
Please explain that to be. MSDN says, you can reserve a virtual address range w/o allocating neither physical memory nor page file space. That's why i made a difference here between the term "virtual memory" (used [wrongly] instead of "VA space") and "heap space".
DyP
I think the difference you're getting confused on is reserved vs. committed - when you mmap a file, you've got VA space reserved but no memory is committed (since it really is backed by a file). When you allocate memory, you both reserve it (i.e. create a PTE for it), and commit it (tell the OS that it must have physical frames or pagefile on hand to back that memory)
Paul Betts
I thought I considered that in my answer. The best way to support CRT / ... functions would be to provide committed but not yet mapped memory, but that seems impossible to me, since they internally use HeapAlloc (new op at least) which ONLY can allocate. Therefore, I choosed the way using pages; I do NOT reserve a page and advise the CRT to use it, I try to block a page and release space from it (that is a VA range as well as committed physical / page file space) but w/o releasing the whole page so no other process can get that physical / page space.
DyP
+1  A: 
  • pre-allocate the buffer(s) you need
  • link statically and use _beginthreadex instead of CreateThread (otherwise, CRT functions may fail) -- OR -- implement the string concat / i2a yourself
  • Use MessageBox (MB_SYSTEMMODAL | MB_OK) MSDN mentions this for reporting OOM conditions (and some MS blogger described this behavior as intended: the message box will not allocate memory.)

Logging is harder, at the very least, the log file needs to be open already.

Probably best with FILE_FLAG_NO_BUFFERING and FILE_FLAG_WRITE_THROUGH, to avoid any buffering attempts. The first one requires that writes and your memory buffers are sector aligned (i.e. you need to query GetDiskFreeSpace, align your buffer by that, and write only to "multiple of sector size" file offsets, and in blocks that are multiples of sector size. I am not sure if this is necessary, or helps, but a system-wide OOM where every allocation fails is hard to simulate.

peterchen
thank you very much. I did some few tests (yet I don't know how signifacnt they are) and the MessageBox | MB_SYSTEMMODAL always showed up, even after catching several bad_allocs. For this, I used some code based on the one in my own answer, but decided to build it for x64 / Release. The bad_allocs I received with x86 were mostly out-of-virtual-address-space errors since I have 4 GB of RAM plus (now) 2 GB max page file size. With the x64 version, the system really hangs severly, even the cursor sometimes. But still, MB works and that trick with the page I mentioned, too!
DyP