2

How would I set a cap in RAM, heap, or stack usage in my C (or, in principle but not in this case, C++) program? I am using Visual Studio on Windows 10.

I have a fully-working program (well, library, and a small program to run basic tests and demo it to someone I'm tutoring), and I want to show what happens when memory allocation fails. (And I'm not just doing that with a stupidly-large allocation because it's linked lists, and I want to show memory allocation failure in that context.) So: how can I limit the amount of memory that my program is allowed to use, and where would I do that? Would I do something in the OS to tell it "this application I'm about to run can only use X bytes of RAM" (or maybe even tell it to limit heap or stack size), would there be something I would do in the compiler arguments, the linker arguments, or what?

And the code I've written HAS GUARDS that prevent illegal memory accesses, and subsequently crashing, when malloc (or, in only a small number of places, calloc) returns NULL! So don't worry about illegal memory accesses and stuff, I have a fairly good idea of what I'm doing.

Here's what the library header, singleLinkList.h, looks like:

#ifndef SINGLELINKEDLIST_H
#define SINGLELINKEDLIST_H

#ifndef KIND_OF_DATA
#define KIND_OF_DATA 3
#endif // !KIND_OF_DATA





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

typedef long long LL_t;


#if KIND_OF_DATA == 1

typedef float data_t;
#define DATA_FORM "%f"

#elif KIND_OF_DATA == 2

typedef double data_t;
#define DATA_FORM "%lf"

#elif KIND_OF_DATA == 3

typedef LL_t data_t;
#define DATA_FORM "%lld"

#else

typedef int data_t;
#define DATA_FORM "%d"

#endif // KIND_OF_DATA == 1, 2, etc...


struct listStruct;


// equivalent to `list_t*` within the .c file
typedef struct listStruct* LS_p;

// equivalent to `const list_t* const` within the .c file
typedef const struct listStruct* const LS_cpc;

typedef struct listStruct* const LS_pc;



int showSizes(void);
size_t queryNodeSize(void);

// returns NULL on failure
LS_p newList(void);

// returns NULL on failure (in memory alloc, at any point), or if given the NULL pointer
LS_p mkListCopy(LS_cpc);

// copies one list into another; leaves the destination unmodified upon failure
//returns a value indicating success/type of failure; returns 0 on success, 
//  various `true` values on failure depending on type
// 1 indicates simple allocation failure
// -1 indicates that you gave the NULL pointer
int copyList(LS_pc dst, LS_cpc src);


//destroys (frees) the given singly-linked list (the list_t* given, and all the list of nodes whose head it holds)
void destroyList(LS_p);

// destroys the list pointed to, then sets it to NULL
//inline void strongDestroyList(LS_p* listP) {
inline void strongDestroyList(struct listStruct** listP) {
    destroyList(*listP);
    *listP = NULL;
}

// Takes a pointer to a list_t
// returns how many elements it has (runs in O(n) time)
//  If you don't understand what `O(n) time` means, go look up "Big O Notation"
size_t len_list(LS_cpc);


//prints a list; returns characters printed
int print_list(LS_cpc);


// gets the data at the specified index of the list; sets the output parameter on failure
data_t indexToData(LS_pc, const size_t ind, int* const err);

// will write the data at ind to the output parameter
//returns a value indicating success/type of failure; returns 0 on success, 
//  various `true` values on failure depending on type
// 1 indicates simple allocation failure
// -1 indicates that you gave the NULL pointer
int copyToPointer(LS_pc, const size_t ind, data_t* const out);


// gets the data at the specified index and removes it from the list; sets output param on failure
data_t popFromInd(LS_pc, const size_t ind, int* const errFlag);

// pops the first item of the list; sets the output param on failure
data_t popFromTop(LS_pc, int* const errFlag);

//returns a value indicating success/type of failure; returns 0 on success, 
//  various `true` values on failure depending on type
// 1 indicates simple allocation failure
// -1 indicates that you gave the NULL pointer
int assignToIndex(LS_pc, const size_t ind, const data_t value);



//returns a value indicating success/type of failure; returns 0 on success, 
//  various `true` values on failure depending on type
// 1 indicates simple allocation failure
// 2 indicates inability to reach the specified index, because it's not that long.
// -1 indicates that you gave the NULL pointer
int insertAfterInd(LS_pc, const size_t ind, const data_t value);


//returns a value indicating success/type of failure; returns 0 on success, 
//  various `true` values on failure depending on type
// 1 indicates simple allocation failure
// -1 indicates that you gave the NULL pointer
int appendToEnd(LS_pc, const data_t value);


//returns a value indicating success/type of failure; returns 0 on success, 
//  various `true` values on failure depending on type
// 1 indicates simple allocation failure
// -1 indicates that you gave the NULL pointer
int insertAtStart(LS_pc list, const data_t value);


#endif // !SINGLELINKEDLIST_H

And here's what main.c, which runs the demo/tests, looks like:

#ifdef __INTEL_COMPILER
#pragma warning disable 1786
#else
#ifdef _MSC_VER
#define _CRT_SECURE_NO_WARNINGS 1
#endif // _MSC_VER

#endif // __INTEL_COMPILER


#include "singleLinkList.h"

#include <stdio.h>
#include <string.h>



void cleanInputBuffer(void) {
    char c;
    do {
        scanf("%c", &c);
    } while (c != '\n');
}


void fill_avail_memory(void) {
    size_t count = 0;
    LS_p list = NULL;
    size_t length;
    data_t fin;
    int err = 0;
    const size_t nSize = queryNodeSize();
    printf("nSize: %zu\n", nSize);
    int last = -5;
    printf("Do you wish to run the test that involves filling up available memory? "
        "(only 'y' will be interpreted as an affirmative) => ");
    char ans;
    scanf("%c", &ans);
    cleanInputBuffer();
    if ((ans != 'y') && (ans != 'Y')) {
        printf("Okay. Terminating function...\n");
        return;
    }
    printf("Alright! Proceeding...\n");
    list = newList();
    if (list == NULL) {
        printf("Wow, memory allocation failure already. Terminating...\n");
        return;
    }
    print_list(list);
    while (!(last = insertAtStart(list, (data_t)count))) {
        ++count;
    }
    length = len_list(list);
    if (length < 5) {
        print_list(list);
    }
    fin = indexToData(list, 0, &err);
    strongDestroyList(&list);
    printf("Last return value: %d\n", last);
    if (!err) {
        printf("Last inserted value: " DATA_FORM "\n", fin);
    }
    printf("Count, which was incremented on each successfull insert, reached: %zu\n", count);
    printf("Length, which was calculated using len_list, was: %zu\n", length);
}



int main() {
    printf("Hello world!\n");
    showSizes();
    LS_p list = newList();
    print_list(list);

    printf("Printing the list: "); print_list(list);
    printf("Appending 5, inserting 1987 after it...\n");
    appendToEnd(list, 5);
    insertAfterInd(list, 0, 1987);
    printf("Printing the list: "); print_list(list);
    printf("Inserting 15 after index 0...\n");
    insertAfterInd(list, 0, 15);
    printf("Printing the list: "); print_list(list);
    printf("Appending 45 to the list\n");
    appendToEnd(list, 45);
    printf("Printing the list: "); print_list(list);
    //destroyList(list);
    //list = NULL;
    printf("Value of pointer-variable `list` is 0x%p\n", list);
    printf("Destroying list...\n");
    strongDestroyList(&list);
    printf("Value of pointer-variable `list` is 0x%p\n", list);

    printf("\n\n\n");
    fill_avail_memory();

    return 0;
}

(The __INTEL_COMPILER and _MSC_VER stuff are to suppress the nonsense about the usage of scanf.

So:

  • Is it possible to set memory usage caps?
  • If so, can it be Heap vs Stack-specific?
  • If not, is there a way to make it only use physical memory?
  • If memory caps can be set, where do I do it (in the OS, in compiler options, in linker options, or even somewhere else) and how do I do it?

And I would compile from the terminal (rather than just 'run code' since it's a Visual Studio project) as follows:

cl singleLinkList.c -c
cl main.c /Zp4 /link singleLinkList.obj

Any help, or advice on where to look, would be much appreciated! Thank you! Update: people have suggested Job Objects. That looks to be a C++ thing. Would it work in plain C? (If not, then while MAYBE it'll suffice, it's not quite what I'm looking/hoping for.)

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Poke
  • 61
  • 7
  • "How to set memory caps for your programs" - that will depend on what OS you are running your program on. DOS, QNX, AiX, Solaris, FreeBSD, Linux, Windows, VMS, Z/OS etc, are all going to have different ways to do that. – Jesper Juhl Apr 25 '20 at 18:23
  • 2
    According to this [answer](https://stackoverflow.com/a/193596/10732434) something like [Job Objects](https://learn.microsoft.com/en-gb/windows/win32/procthread/job-objects?redirectedfrom=MSDN) might be useful for that sort of task. But I don't know how to use them and if Windows 10 or any 64-bit application can be managed by such method. – sanitizedUser Apr 25 '20 at 18:33
  • You can't do that as far as I know. On Linux (maybe Windows too) `malloc` is designed never to fail and your program will be killed or will crash instead. – S.S. Anne Apr 25 '20 at 18:36
  • I don't know about Windows 10 but Windows 7 will not allocate more than about 1.7Gb to an executable compiled with my version of MSVC anyway. – Weather Vane Apr 25 '20 at 18:37
  • Aside, you don't need to check for Windows compiler before `#define _CRT_SECURE_NO_WARNINGS` (which doesn't need any value anyway). It would probably be be a harmless definition on other systems. – Weather Vane Apr 25 '20 at 18:39
  • One approach ([to solve problem X](http://meta.stackexchange.com/questions/66377/what-is-the-xy-problem)) is to write wrappers for `malloc` and `free`, and have the `malloc` wrapper return NULL as appropriate. – user3386109 Apr 25 '20 at 18:40
  • Doesn't Windows have something similar to cgroups in Linux? – Spidey Apr 25 '20 at 18:46
  • 2
    It looks like [as mentioned] "job objects" might help. It says: The system tracks the value of PeakProcessMemoryUsed and PeakJobMemoryUsed constantly. This allows you know the peak memory usage of each job. You can use this information to establish a memory limit using the JOB_OBJECT_LIMIT_PROCESS_MEMORY or JOB_OBJECT_LIMIT_JOB_MEMORY value. – Craig Estey Apr 25 '20 at 18:48
  • @Jesper Juhl - I said I was on Windows. – Poke Apr 25 '20 at 18:58
  • @sanitizedUser - 1) ...okay... that looks promising... but are you CERTAIN that it works for plain C, and isn't some C# thing? 2) I _can_ do it in 32-bit mode instead; that part doesn't really matter. – Poke Apr 25 '20 at 19:00
  • @WeatherVane On your MSVC: ...okay, but when tested earlier, it was perfectly happy to eat my RAM and lag my machine. On the macro: I only check for it because when I'm compiling with the ICC compiler (which uses a bunch of MSVC's stuff to help it, especially headers), having that macro defined doesn't suppress the warnings/errors. – Poke Apr 25 '20 at 19:02
  • @user3386109 - ideally, I want to demonstrate that a program can hit externally-imposed memory limits, not just internally-imposed ones. – Poke Apr 25 '20 at 19:03
  • @Poke That notion seems a little outdated. [Here's a question from 2009](https://stackoverflow.com/questions/1655650/linux-optimistic-malloc-will-new-always-throw-when-out-of-memory). That question is specifically about linux, not windows. So the real question here is whether windows 10 will ever return NULL from a `malloc` call. – user3386109 Apr 25 '20 at 19:31
  • @user3386109 ...what - or rather, which - notion seems outdated? (Also, the person I wish to demonstrate this stuff to is on a Mac, so if [neither a MSVC-compiled C program nor an Intel C Compiler-compiled C program will ever have malloc/calloc/realloc return NULL on Windows 10], then could they with GCC on an older Mac with the current version of OS X? (Or very recent if they haven't happened to update their computer.) I'm perfectly happy to demo it either on my system or on theirs, but I can test things on my own on mine.) – Poke Apr 26 '20 at 13:53
  • To my knowledge, there's no way to limit the amount of RAM a program can use on Windows. Then again, that wouldn't be very useful either. – IInspectable Apr 26 '20 at 15:10
  • @IInspectable Not very useful, in a practical manner? Sure. For my purposes? Yes, it would be useful. – Poke Apr 27 '20 at 00:01
  • @pok: No. Limiting RAM does not cause your allocation routines to fail. Limiting RAM slows down your program. That's not very useful, and most certainly doesn't solve the issue you set out to solve. – IInspectable Apr 27 '20 at 00:57
  • Does this answer your question? [Set Windows process (or user) memory limit](https://stackoverflow.com/questions/192876/set-windows-process-or-user-memory-limit) – IInspectable Apr 27 '20 at 06:32
  • @IInspectable Why wouldn't they fail? The program isn't allowed more memory, so they aren't allowed to get more. Or is it an issue of limiting RAM doesn't limit how much virtual memory they go use anyway? (And again, my name is "Poke", not "pok".) – Poke Apr 27 '20 at 15:24
  • @pok: Limiting RAM doesn't limit the amount of address space a process has access to. – IInspectable Apr 27 '20 at 15:52

2 Answers2

1

If you want to do it at the user/runtime level (and have control over the code that is being tested), you could implement your own safe_malloc(), safe_calloc(), safe_realloc(), and safe_free() that would act as frontends to their system-supplied counterparts, and would increase or decrease a numberOfBytesUsed counter appropriately, but would fail if numberOfBytesUsed was going to become greater than a fixed maximum value.

(note that doing this is made a little bit tricky due to the fact that free() doesn't include a number-of-bytes-to-free argument, so you have to "hide" that information in the allocated buffers returned by safe_calloc() and friends -- usually by allocating an extra 4 bytes above what the calling code requested, placing the allocation-size value in the first 4 bytes of the allocation, and then returning a pointer to the first byte after the allocation-size field)

If you don't have control over the code that is being tested (i.e. that code will call malloc() and free() directly and you can't rewrite the code to call your functions instead, then you might be able to do nasty some preprocessor magic (e.g. #define calloc safe_calloc in a header file that you know will be included) to trick the tested code into doing the right thing.

As for limiting the amount of stack-space used, I'm not aware of any elegant way to enforce that at the code level. If there's a way to enforce it with a compiler-flag, you could at least get the program to reliably crash on a stack-overflow condition, but that's not quite the same as a controlled/handled failure.

Jeremy Friesner
  • 70,199
  • 15
  • 131
  • 234
  • Okay... the stack part is fine; I don't _really_ need to limit stack usage. And while I have complete, or near-complete, control of what's being tested and such (though REALLY, I just care about how much heap space my linked list library is using), _ideally_ it should _run out_, as a means of demonstrating memory allocation failure because, again, I want to demonstrate that happening in the context of linked lists. (Also, it'd need to be 8 bytes, not 4 - or dependent on 32-bit or 64-bit mode.) – Poke Apr 25 '20 at 18:56
  • Agreed, the byte-counts should properly be of type size_t. – Jeremy Friesner Apr 25 '20 at 18:58
  • And if I were to follow this approach (artificial return of NULL), I could let it use a little more memory to track all allocated void pointers and the size allocated for each, using another linked list and searching it. Would that be efficient if they allocate many different blocks of memory? Hell no. But it'd probably _work_. But... still not quite what I want. And DO NOT worry about limiting the stack. – Poke Apr 26 '20 at 14:12
  • The [system provides](https://stackoverflow.com/questions/61430428/how-to-set-memory-caps-for-your-programs#comment108669652_61430428) what your crude attempt at counting bytes is trying to accomplish. But the OP was asking about limiting RAM, not address space. – IInspectable Apr 26 '20 at 23:15
  • 1
    @IInspectable I look forward to reading your answer that details a better way to accomplish the questioner's goals. – Jeremy Friesner Apr 26 '20 at 23:31
  • @IInspectable Jeremy's answer provides an adequate way to _artificially_ - _highly_ so - limit RAM usage. Though it doesn't quite reach what I want, because I wish to demonstrate that _those builtin functions can fail_, and _how they indicate this failure_ (or are supposed to, per the C standard). – Poke Apr 27 '20 at 00:04
  • @pok: As I pointed out before, I'm not aware of a way to limit RAM usage for a Windows program. I also pointed out, that that would be rather pointless. There are many ways to slow down a program, and limiting RAM is probably one of the harder ones to implement. – IInspectable Apr 27 '20 at 00:54
  • @jer: As I commented, I'm not aware of a way to limit RAM usage of a process in Windows. And neither are you, apparently. If you are looking forward to a solution to that question, don't hold your breath, I cannot provide an answer to that. – IInspectable Apr 27 '20 at 00:56
  • @IInspectable 1) at us with our full names, jeez. (Autocomplete. It wants to do that for me.) 2) What? I don't need to slow it down; I need it to run out of memory. – Poke Apr 27 '20 at 15:22
  • @pok: RAM is a performance optimization. Limiting RAM causes your process to run slower. And nothing else. – IInspectable Apr 27 '20 at 15:25
  • How is it a performance optimization? RAM is where a process holds the work it's doing - like a desk or other workspace for us humans. The HDD/SSD is like a filing cabinet, where you store "how to do things" along with data. (Virtual Memory is like sticking some of your work in the filing cabinet when you're running low on space.) If the application were NOT to go using virtual memory, then wouldn't it hit an allocation failure at some point, simply because there wasn't any space to give it? (Sure, with virtual memory, it'd slow down - but WITHOUT IT!) – Poke Apr 27 '20 at 15:31
  • @pok: I'm not aware of a way to make the system forego [memory management](https://learn.microsoft.com/en-us/windows/win32/memory/memory-management) for a process. – IInspectable Apr 27 '20 at 15:51
  • @IInspectable 1) It's Poke, not pok. ("Poke" as in Pokemon, not "pok" that'd be said the same as the singular of "pox", or as part of the word for marks resulting from a pox) 2) **sigh** alright, thank you. That, at least, is helpful. – Poke Apr 28 '20 at 01:14
  • @pok: It's [Pokémon](https://en.wikipedia.org/wiki/Pok%C3%A9mon), not *"Pokemon"*. – IInspectable Apr 28 '20 at 07:16
  • And I don't have a convenient way to type that character. Now scram. – Poke May 14 '20 at 00:28
0

With Visual Studio, and for demo purposes, you could use a _DEBUG build and hook into the CRT memory management with _CrtSetAllocHook. This would allow the program to monitor the memory allocations and trigger failures as it sees fit.

dxiv
  • 16,984
  • 2
  • 27
  • 49
  • Okay... More details, please? – Poke May 14 '20 at 00:28
  • @Poke There is some [sample code](https://github.com/Microsoft/VCSamples/tree/master/VC2010Samples/crt/crt_dbg2) linked from that page. Modify their [`MyAllocHook`](https://github.com/microsoft/VCSamples/blob/master/VC2010Samples/crt/crt_dbg2/crt_dbg2.c) to `return FALSE;` to trigger allocation failures. You could base that on the [`_CrtMemState::lHighWaterCount`](https://docs.microsoft.com/en-us/visualstudio/debugger/crt-debug-heap-details?view=vs-2019) returned by [`_CrtMemCheckpoint`](https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/crtmemcheckpoint?view=vs-2019) for example – dxiv May 14 '20 at 00:51
  • ...or you could maintain a running count and/or total size of allocations in the hook function itself, and decide when to fail based on those. – dxiv May 14 '20 at 00:51