Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What happens to RAII objects after a process forks?

Tags:

c++

linux

fork

unix

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?

like image 603
Sebastian Mach Avatar asked Sep 26 '12 12:09

Sebastian Mach


People also ask

What is RAII principle?

The principle that objects own resources is also known as "resource acquisition is initialization," or RAII. When a resource-owning stack object goes out of scope, its destructor is automatically invoked. In this way, garbage collection in C++ is closely related to object lifetime, and is deterministic.

What is the use of RAII in C++ programming?

Resource Acquisition Is Initialization or RAII, is a C++ programming technique which binds the life cycle of a resource that must be acquired before use (allocated heap memory, thread of execution, open socket, open file, locked mutex, disk space, database connection—anything that exists in limited supply) to the ...

Is RAII exception safe?

Use the RAII idiom to manage resources To be exception-safe, a function must ensure that objects that it has allocated by using malloc or new are destroyed, and all resources such as file handles are closed or released even if an exception is thrown.


2 Answers

fork(2) creates a full copy of the process, including all of its memory. Yes, destructors of automatic objects will run twice - in the parent process and in the child process, in separate virtual memory spaces. Nothing "bad" happens (unless of course, you deduct money from an account in a destructor), you just need to be aware of the fact.

like image 144
Nikolai Fetissov Avatar answered Oct 24 '22 21:10

Nikolai Fetissov


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:

  • http://www.yolinux.com/TUTORIALS/ForkExecProcesses.html
  • man pages
like image 28
Sebastian Mach Avatar answered Oct 24 '22 21:10

Sebastian Mach