phresnel phresnel - 6 months ago 15
Linux Question

What happens to RAII objects after a process forks?

Under Unix / Linux, what happens to my active RAII objects upon forking? Will there be double deletions?
What is with copy construction and -assignment? How to make sure nothing bad happens?

Answer

Principally, it is no problem to use these functions in C++, but you have to be aware of what data is shared and how.

Consider that upon fork(), the new process gets a complete copy of the parent's memory (using copy-on-write). Memory is state, therefore you have two independent processes that must leave a clean state behind.

Now, as long as you stay within the bounds of the memory given to you, you should not have any problem at all:

#include <iostream>
#include <unistd.h>

class Foo {
public:
    Foo ()  { std::cout << "Foo():" << this << std::endl; }
    ~Foo()  { std::cout << "~Foo():" << this << std::endl; }

    Foo (Foo const &) {
        std::cout << "Foo::Foo():" << this << std::endl;
    }

    Foo& operator= (Foo const &) {
        std::cout << "Foo::operator=():" << this<< std::endl;
        return *this;
    }
};

int main () {
    Foo foo;
    int pid = fork();
    if (pid > 0) {
        // We are parent.
        int childExitStatus;
        waitpid(pid, &childExitStatus, 0); // wait until child exits
    } else if (pid == 0) {
        // We are the new process.
    } else {
        // fork() failed.
    }
}

Above program will print roughly:

Foo():0xbfb8b26f
~Foo():0xbfb8b26f
~Foo():0xbfb8b26f

No copy-construction or copy-assignment happens, the OS will make bitwise copies. The addresses are the same because they are not physical addresses, but pointers into each process' virtual memory space.

It becomes more difficult when the two instances share information, e.g. an opened file that must be flushed and closed before exiting:

#include <iostream>
#include <fstream>

int main () {
    std::ofstream of ("meh");
    srand(clock());
    int pid = fork();
    if (pid > 0) {
        // We are parent.
        sleep(rand()%3);
        of << "parent" << std::endl;
        int childExitStatus;
        waitpid(pid, &childExitStatus, 0); // wait until child exits
    } else if (pid == 0) {
        // We are the new process.
        sleep(rand()%3);
        of << "child" << std::endl;
    } else {
        // fork() failed.
    }
}

This may print

parent

or

child
parent

or something else.

Problem being that the two instances do not enough to coordinate their access to the same file, and you don't know the implementation details of std::ofstream.

(Possible) solutions can be found under the terms "Interprocess Communication" or "IPC", the most nearby one would be waitpid():

#include <unistd.h>
#include <sys/wait.h>

int main () {
    pid_t pid = fork();
    if (pid > 0) {
        int childExitStatus;
        waitpid(pid, &childExitStatus, 0); // wait until child exits
    } else if (pid == 0) {
        ...
    } else {
        // fork() failed.
    }
}

The most simple solution would be to ensure that each process only uses its own virtual memory, and nothing else.

The other solution is a Linux specific one: Ensure that the sub-process does no clean up. The operating system will make a raw, non-RAII cleanup of all acquired memory and close all open files without flushing them. This can be useful if you are using fork() with exec() to run another process:

#include <unistd.h>
#include <sys/wait.h>

int main () {
    pid_t pid = fork();
    if (pid > 0) {
        // We are parent.
        int childExitStatus;
        waitpid(pid, &childExitStatus, 0);
    } else if (pid == 0) {
        // We are the new process.
        execlp("echo", "echo", "hello, exec", (char*)0);
        // only here if exec failed
    } else {
        // fork() failed.
    }
}

Another way to just exit without triggering any more destructors is the exit() function. I generally advice to not use in C++, but when forking, it has its place.


References:

Comments