Handling critical errors

This is a discussion on Handling critical errors within the C Programming forums, part of the General Programming Boards category; After reading a little in this thread , I've come to the conclusion that relying on exit() to clean up ...

  1. #1
    Registered User
    Join Date
    Mar 2009
    Posts
    399

    Handling critical errors

    After reading a little in this thread, I've come to the conclusion that relying on exit() to clean up after you is generally not a good idea.

    What would you consider to be the best way to handle critical errors without some crazy C++ like exception handling system?

    I guess you could add atexit() after every memory allocation that you know is going to remain in memory during the program's entire lifespan, but how should you deal with objects with an unknown lifespan?

    This is just a very crude example, but assume that you want to terminate the program if the foo() call fails. How should you deal with freeing the previously allocated memory in a situation like this one? Using atexit here could get a little tricky since you could end up in a situation where you might've already freed some of the dynamically allocated memory. I guess one way to deal with this could be to set the memory pointer to NULL after you free it, and then check for NULL in your atexit handling.
    Code:
    memory = malloc(something);
    if (!memory)
    // Error handling
    
    .
    .
    .
    
    if (foo() == ERROR)
    // Something went wrong here, terminate program

  2. #2
    Kernel hacker
    Join Date
    Jul 2007
    Location
    Farncombe, Surrey, England
    Posts
    15,677
    Normal memory allocations, in major desktop OS's such as Windows, Linux or MacOS should be no problem - they get cleaned up on exit. Same applies for files and other such things. I'm fairly convinced those OS's also clean up any handles (files, shared memory and such things).

    On the other hand, if you have some sort of special resources (e.g. semaphores or similar) that is shared between several processes, it's possible that you could fall out of the program without cleaning up those - that could be a problem. The solution here would be to clean up such things before you exit. The easy way to solve that would be to change all calls to exit() into calls to "myExit()" or "panic()" or some such, and let that function deal with cleaning up anything that needs cleaning up - exactly how you go about figuring out what needs cleaning up is an interesting challenge of course.

    --
    Mats
    Last edited by matsp; 03-24-2009 at 11:12 AM.
    Compilers can produce warnings - make the compiler programmers happy: Use them!
    Please don't PM me for help - and no, I don't do help over instant messengers.

  3. #3
    spurious conceit MK27's Avatar
    Join Date
    Jul 2008
    Location
    segmentation fault
    Posts
    8,300
    Quote Originally Posted by Memloop View Post
    I guess you could add atexit() after every memory allocation that you know is going to remain in memory during the program's entire lifespan, but how should you deal with objects with an unknown lifespan?

    This is just a very crude example, but assume that you want to terminate the program if the foo() call fails. How should you deal with freeing the previously allocated memory in a situation like this one?
    If a critical error causes the whole process to end, which is what atexit() is for, you do not have to free() anything. The entire thing is freed by the operating system, which is why the process is finished. atexit() is more for taking care of things the OS won't, such as saving the current state of a data file, dealing with network connections appropriately, etc.

    free() is for freeing memory while the process is running, not when it is finished. Consider this loop:
    Code:
    char *tmpbuffer;
    while (1) {
            tmpbuffer=malloc(sizeof(input));
            [work with tmpbuffer]
            if (condition) break;
            free(tmpbuffer);
    }
    If the free call were not there, at each iteration of the loop *tmpbuffer gets reassigned to a new block of memory -- but the last one is still held and protected. Since you don't have a pointer to it, it's useless, so for every iteration of the while loop you will leak sizeof(input) bytes.

    ps. even more useless -- the new cboard code box!!! BOO BOO BOO!!!!
    C programming resources:
    GNU C Function and Macro Index -- glibc reference manual
    The C Book -- nice online learner guide
    Current ISO draft standard
    CCAN -- new CPAN like open source library repository
    3 (different) GNU debugger tutorials: #1 -- #2 -- #3
    cpwiki -- our wiki on sourceforge

  4. #4
    Captain Crash brewbuck's Avatar
    Join Date
    Mar 2007
    Location
    Portland, OR
    Posts
    7,243
    Quote Originally Posted by MK27 View Post
    If a critical error causes the whole process to end, which is what atexit() is for, you do not have to free() anything. The entire thing is freed by the operating system, which is why the process is finished. atexit() is more for taking care of things the OS won't, such as saving the current state of a data file, dealing with network connections appropriately, etc.
    It's not defined, though, what the OS will and will not "take care of." You can assume that memory is freed, files are closed, etc. on an OS like Windows or Linux, but take your code to some embedded environment and this may not be true.

    To address the initial question, exception-handling systems don't have to be "crazy." In many cases they are the right method. I'm not talking about hacks using setjmp/longjmp, but some kind of structured unwinding mechanism can be very useful.
    Code:
    //try
    //{
    	if (a) do { f( b); } while(1);
    	else   do { f(!b); } while(1);
    //}

  5. #5
    Registered User
    Join Date
    Mar 2009
    Posts
    399
    Quote Originally Posted by brewbuck View Post
    You can assume that memory is freed, files are closed, etc. on an OS like Windows or Linux, but take your code to some embedded environment and this may not be true.
    I'm majoring in robotics, so I suspect I will be dealing with some rather esoteric systems.

  6. #6
    Captain Crash brewbuck's Avatar
    Join Date
    Mar 2007
    Location
    Portland, OR
    Posts
    7,243
    So, to give a bit more inspiration, I've created cleanup handler system in the past that work like this:

    A global (or thread-specific) pointer points to the head of a linked list of cleanup handler frames. The frames act as containers for one or more cleanup handlers. A frame either suceeds or fails. If it succeeds, no cleanup handlers are called. If it fails, all the handlers are called.

    When you enter an operation that allocates resources, you create a cleanup frame. As you allocate resources, you push cleanup handlers onto this frame. If you reach the end of your function successfully, you discard the cleanup frame. If you fail, you invoke it, which autocleans the allocated resources.

    You can mark frames with a flag indicating that they should always execute. Into these frames you can dump your global cleanup.
    Code:
    //try
    //{
    	if (a) do { f( b); } while(1);
    	else   do { f(!b); } while(1);
    //}

  7. #7
    Registered User
    Join Date
    Mar 2009
    Posts
    399
    Thanks, that's pretty much what I had in mind. Is this something that you've had to use often?

  8. #8
    Captain Crash brewbuck's Avatar
    Join Date
    Mar 2007
    Location
    Portland, OR
    Posts
    7,243
    Quote Originally Posted by Memloop View Post
    Thanks, that's pretty much what I had in mind. Is this something that you've had to use often?
    Not so often these days since I mostly code C++ now. The last serious project where I used such a thing was a special-purpose shell. When launching pipelines there are a lot of things which have to happen -- opening pipes, dup'ing file descriptors, forking things, etc. Without a structured way of dealing with failures it would have been almost impossible to keep everything straight and avoid resource leaks.
    Code:
    //try
    //{
    	if (a) do { f( b); } while(1);
    	else   do { f(!b); } while(1);
    //}

  9. #9
    Captain Crash brewbuck's Avatar
    Join Date
    Mar 2007
    Location
    Portland, OR
    Posts
    7,243
    And as an implementation hint... What I've described (a linked list of frames, each of which points to a linked list of cleanup handlers) is easy to code, but may not be suitable for an embedded environment. You don't necessarily want to call malloc()/free() a bunch of times as you build and then tear down the frames.

    As well, if you are encountering memory-related conditions that require cleanup, you probably don't want to be calling dynamic memory functions while handling that.

    So imagine a fixed-size array of cleanup handlers:

    Code:
    struct cleanup_handler
    {
        void (*cleanup)(void *arg);
        void *arg;
    };
    
    struct cleanup_handler cleanup_stack[MAX_CLEANUP_STACK_SIZE];
    unsigned int cleanup_stack_ptr = 0;
    It is possible to represent not just the handlers but the frames using such a structure. To open a frame, you put a marker on the stack first (i.e. use NULL for both cleanup and arg). Then you push handlers as usual. When you go to pop the frame, you simply invoke handlers until you reach the marker, and remove it.
    Code:
    //try
    //{
    	if (a) do { f( b); } while(1);
    	else   do { f(!b); } while(1);
    //}

  10. #10
    Registered User
    Join Date
    Mar 2009
    Posts
    399
    That's a clever way of doing it. Thanks!

    How would you handle the unlikely situation where cleanup_stack_ptr >= MAX_CLEANUP_STACK_SIZE?

  11. #11
    Kernel hacker
    Join Date
    Jul 2007
    Location
    Farncombe, Surrey, England
    Posts
    15,677
    Quote Originally Posted by Memloop View Post
    That's a clever way of doing it. Thanks!

    How would you handle the unlikely situation where cleanup_stack_ptr >= MAX_CLEANUP_STACK_SIZE?
    Measure the high-water-mark in testing, and make sure that it's a good deal bigger (e.g. double or 4x higher).

    --
    Mats
    Compilers can produce warnings - make the compiler programmers happy: Use them!
    Please don't PM me for help - and no, I don't do help over instant messengers.

  12. #12
    Maz
    Maz is offline
    Registered User Maz's Avatar
    Join Date
    Nov 2005
    Location
    Finland
    Posts
    194
    Actually I'm pretty sure Linux does not free shared memory when program exits. At least in my code, the memory took with shm_open() will be visible under /dev/shm/ after the program closes. Eg:

    After running
    Code:
    #include <sys/mman.h>
    #include <sys/stat.h>        /* For mode constants */
    #include <fcntl.h>           /* For O_* constants */
    
    int main()
    {
        return shm_open("FOO-SHM", O_RDWR|O_CREAT|O_TRUNC, 777);
    }
    I'll see

    Code:
    [Matti@home ~]$ ls /dev/shm/
    FOO-SHM  pulse-shm-1007351291

  13. #13
    Captain Crash brewbuck's Avatar
    Join Date
    Mar 2007
    Location
    Portland, OR
    Posts
    7,243
    Quote Originally Posted by Maz View Post
    Actually I'm pretty sure Linux does not free shared memory when program exits. At least in my code, the memory took with shm_open() will be visible under /dev/shm/ after the program closes. Eg:
    Yes, IPC objects are one of the (few) resources that don't get freed, but that makes sense, since a shared resource can't be destroyed just because a single process exits.
    Code:
    //try
    //{
    	if (a) do { f( b); } while(1);
    	else   do { f(!b); } while(1);
    //}

  14. #14
    Registered User
    Join Date
    Mar 2009
    Posts
    399
    This is just an example of how it might work. Any constructive criticism? I did not feel that typedefs added anything to the readability of the code.

    Is it even possible to declare and use a generic function pointer with a variable number of arguments of different types?

    Code:
    #include <stdio.h>
    #include <stdlib.h>
    
    #define MAX_CLEANUP_STACK_SIZE 200
    
    /*
    ** The stack for our cleanup implementation.
    **
    ** The stack pointer points to the top of the
    ** stack, i.e. the last element that got pushed onto the stack is
    ** cleanup_stack_ptr - 1.
    */
    struct cleanup_handler
    {
        void (*func)(void *arg);
        void *arg;
    };
    struct cleanup_handler cleanup_stack[MAX_CLEANUP_STACK_SIZE];
    unsigned int cleanup_stack_ptr = 0;
    
    /*
    ** Push a cleanup frame onto the stack.
    **
    ** A frame begins by setting the member variable func of the cleanup_handler
    ** struct to NULL.
    */
    void cleanup_push_frame(void)
    {
    	cleanup_stack[cleanup_stack_ptr].func = NULL;
    	cleanup_stack[cleanup_stack_ptr].arg = NULL;
    	
    	if (cleanup_stack_ptr+1 > MAX_CLEANUP_STACK_SIZE) {
    		fprintf(stderr, "Warning: cleanup_stack is full\n");
    	} else {
    		++cleanup_stack_ptr;
    	}
    }
    
    /*
    ** Push a frame onto the stack.
    */
    void cleanup_push_func(void (*func)(void *arg), void *arg)
    {
    	cleanup_stack[cleanup_stack_ptr].func =func;
    	cleanup_stack[cleanup_stack_ptr].arg = arg;
    	
    	/* This should NEVER happen! */
    	if (cleanup_stack_ptr+1 > MAX_CLEANUP_STACK_SIZE) {
    		fprintf(stderr, "Warning: cleanup_stack is full\n");
    	} else {
    		++cleanup_stack_ptr;
    	}
    }
    
    /*
    ** Call all cleanup handlers in the current stack frame, and then remove the
    ** frame from the stack.
    */
    void cleanup_pop_frame(void)
    {
    	if (cleanup_stack_ptr != 0) {
    		--cleanup_stack_ptr;
    	}
    	
    	while (cleanup_stack_ptr > 0 &&
    		cleanup_stack[cleanup_stack_ptr].func != NULL) {
    		cleanup_stack[cleanup_stack_ptr].func(
    			cleanup_stack[cleanup_stack_ptr].arg);
    		--cleanup_stack_ptr;
    	}
    	
    }
    
    /*
    ** Pop all frames from the stack.
    */
    void cleanup_pop_all(void)
    {
    	while (cleanup_stack_ptr > 0) {
    		cleanup_pop_frame();
    	}
    }
    
    void foo(int *i)
    {
    	printf("%d\n", *i);
    }
    
    void bar(float *f)
    {
    	printf("%f\n", *f);
    }
    
    int main(void)
    {
    	int i = 1;
    	float b = 2;
    	
    	cleanup_push_frame();
    	cleanup_push_func((void *)foo, &i);
    	cleanup_push_func((void *)bar, &b);
    	cleanup_pop_all();
    	
    	return(EXIT_SUCCESS);
    }

  15. #15
    Complete Beginner
    Join Date
    Feb 2009
    Posts
    312
    I like brewbuck's approach for modules that implement their own memory management, but for a program as a whole, there's another approach which I consider to be slightly more elegant: design your program to have only one call to exit(), probably the last line of main(). Every function does its own cleanup and may return some value that indicates the caller to do the same. This way, resource allocation and deallocation are next to each other and there's no need to introduce another layer of abstraction/complexity. This has the additional benefit that you will get a warm feeling if Edsger Dijkstra is watching over you shoulder while you're coding.

    Of course, this approach doesn't work with fork() and SIGTERM, because they introduce new exit points.

    Greets,
    Philip
    Last edited by Snafuist; 03-26-2009 at 07:52 AM.
    All things begin as source code.
    Source code begins with an empty file.
    -- Tao Te Chip

Popular pages Recent additions subscribe to a feed

Similar Threads

  1. global namespace errors
    By stubaan in forum C++ Programming
    Replies: 9
    Last Post: 04-02-2008, 03:11 PM
  2. Ten Errors
    By AverageSoftware in forum Contests Board
    Replies: 0
    Last Post: 07-20-2007, 10:50 AM
  3. Header File Errors...
    By Junior89 in forum C++ Programming
    Replies: 5
    Last Post: 07-08-2007, 12:28 AM
  4. executing errors
    By s0ul2squeeze in forum C++ Programming
    Replies: 3
    Last Post: 03-26-2002, 12:43 PM

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21