Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to handle malloc failing and returning NULL?

I'm a bit confused on how to check if a memory allocation failed in order to prevent any undefined behaviours caused by a dereferenced NULL pointer. I know that malloc (and similiar functions) can fail and return NULL, and that for this reason the address returned should always be checked before proceeding with the rest of the program. What I don't get is what's the best way to handle these kind of cases. In other words: what is a program supposed to do when a malloc call returns NULL?

I was working on this implementation of a doubly linked list when this doubt raised.

struct ListNode {

    struct ListNode* previous;
    struct ListNode* next;
    void* object;
};

struct ListNode* newListNode(void* object) {

    struct ListNode* self = malloc(sizeof(*self));

    if(self != NULL) {

        self->next = NULL;
        self->previous = NULL;
        self->object = object;
    }

    return self;
}

The initialization of a node happens only if its pointer was correctly allocated. If this didn't happen, this constructor function returns NULL.

I've also written a function that creates a new node (calling the newListNode function) starting from an already existing node and then returns it.

struct ListNode* createNextNode(struct ListNode* self, void* object) {

    struct ListNode* newNext = newListNode(object);

    if(newNext != NULL) {

        newNext->previous = self;

        struct ListNode* oldNext = self->next;

        self->next = newNext;

        if(oldNext != NULL) {

            newNext->next = oldNext;
            oldNext->previous = self->next;
        }
    }

    return newNext;
}

If newListNode returns NULL, createNextNode as well returns NULL and the node passed to the function doesn't get touched.

Then the ListNode struct is used to implement the actual linked list.

struct LinkedList {

    struct ListNode* first;
    struct ListNode* last;
    unsigned int length;
};

_Bool addToLinkedList(struct LinkedList* self, void* object) {

    struct ListNode* newNode;

    if(self->length == 0) {

        newNode = newListNode(object);
        self->first = newNode;
    }
    else {

        newNode = createNextNode(self->last, object);
    }

    if(newNode != NULL) {

        self->last = newNode;
        self->length++;
    }

    return newNode != NULL;
}

if the creation of a new node fails, the addToLinkedList function returns 0 and the linked list itself is left untouched.

Finally, let's consider this last function which adds all the elements of a linked list to another linked list.

void addAllToLinkedList(struct LinkedList* self, const struct LinkedList* other) {

    struct ListNode* node = other->first;

    while(node != NULL) {

        addToLinkedList(self, node->object);
        node = node->next;
    }
}

How should I handle the possibility that addToLinkedList might return 0? For what I've gathered, malloc fails when its no longer possible to allocate memory, so I assume that subsequent calls after an allocation failure would fail as well, am I right? So, if 0 is returned, should the loop immediately stop since it won't be possible to add any new elements to the list anyway? Also, is it correct to stack all of these checks one over another the way I did it? Isn't it redundant? Would it be wrong to just immediately terminate the program as soon as malloc fails? I read that it would be problematic for multi-threaded programs and also that in some istances a program might be able to continue to run without any further allocation of memory, so it would be wrong to treat this as a fatal error in any possible case. Is this right?

Sorry for the really long post and thank you for your help!

like image 1000
Gian Avatar asked Jan 25 '23 05:01

Gian


1 Answers

It depends on the broader circumstances. For some programs, simply aborting is the right thing to do.

For some applications, the right thing to do is to shrink caches and try the malloc again. For some multithreaded programs, just waiting (to give other threads a chance to free memory) and retrying will work.

For applications that need to be highly reliable, you need an application level solution. One solution that I've used and battle tested is this:

  1. Have an emergency pool of memory allocated at startup.
  2. If malloc fails, free some of the emergency pool.
  3. For calls that can't sanely handle a NULL response, sleep and retry.
  4. Have a service thread that tries to refill the emergency pool.
  5. Have code that uses caching respond to a non-full emergency pool by reducing memory consumption.
  6. If you have the ability to shed load, for example, by shifting load to other instances, do so if the emergency pool isn't full.
  7. For discretionary actions that require allocating a lot of memory, check the level of the emergency pool and don't do the action if it's not full or close to it.
  8. If the emergency pool gets empty, abort.
like image 58
David Schwartz Avatar answered Jan 29 '23 07:01

David Schwartz