I'm having some trouble grasping how to correctly handle creating a child process from a multithreaded program that uses Boost Asio in a multithreaded fashion.
If I understand correctly, the way to launch a child process in the Unix world is to call fork()
followed by an exec*()
. Also, if I understand correctly, calling fork()
will duplicate all file descriptors and so on and these need to be closed in the child process unless marked as FD_CLOEXEC
(and thereby being atomically closed when calling exec*()
).
Boost Asio requires to be notified when fork()
is called in order to operate correctly by calling notify_fork()
. However, in a multithreaded program this creates several issues:
Sockets are by default inherited by child processes if I understand correctly. They can be set to
SOCK_CLOEXEC
- but not directly at creation*, thus leading to a timing window if a child process is being created from another thread.notify_fork()
requires that no other thread calls any otherio_service
function, nor any function on any other I/O object associated with theio_service
. This does not really seem to be feasible - after all the program is multithreaded for a reason.If I understand correctly, any function call made between
fork()
andexec*()
needs to be async signal safe (seefork()
documentation). There is no documentation of thenotify_fork()
call being async signal safe. In fact, if I look at the source code for Boost Asio (at least in version 1.54), there may be calls to pthread_mutex_lock, which is not async signal safe if I understand correctly (see Signal Concepts, there are also other calls being made that are not on the white list).
Issue #1 I can probably work around by separating creation of child processes and sockets + files so that I can ensure that no child process is being created in the window between a socket being created and setting SOCK_CLOEXEC
. Issue #2 is trickier, I would probably need to make sure that all asio handler threads are stopped, do the fork and then recreate them again, which is tideous at best, and really really bad at worst (what about my pending timers??). Issue #3 seems to make it entirely impossible to use this correctly.
How do I correctly use Boost Asio in a multithreaded program together with fork()
+ exec*()
?
... or am I "forked"?
Please let me know if I have misunderstood any fundamental concepts (I am raised on Windows programming, not *nix...).
Edit:
* - Actually it is possible to create sockets with SOCK_CLOEXEC
set directly on Linux, available since 2.6.27 (see socket()
documentation). On Windows, the corresponding flag WSA_FLAG_NO_HANDLE_INHERIT
is available since Windows 7 SP 1 / Windows Server 2008 R2 SP 1 (see WSASocket()
documentation). OS X does not seem to support this though.
Consider the following:
fork()
creates only one thread in the child process. You would need to recreate the other threads.fork()
. Callbacks registered withpthread_atfork()
could release the mutexes, but majority of libraries never bother usingpthread_atfork()
. In other words, you child process could hang forever when callingmalloc()
ornew
because the standard heap allocator does use mutexes.In the light of the above, the only robust option in a multi-threaded process is to call
fork()
and thenexec()
.Note that your parent process is not affected by
fork()
as long aspthread_atfork()
handlers are not used.Regarding forking and
boost::asio
, there isio_service::notify_fork()
function that needs to be called before forking in the parent and after forking in both parent and child. What it does ultimately depends on the reactor being used. For Linux/UNIX reactorsselect_reactor
,epoll_reactor
,dev_poll_reactor
,kqueue_reactor
this function does not do anything to the parent before of after fork, but in the child it recreates the reactor state and re-registers the file descriptors. I am not sure what it does on Windows, though.An example of its usage can be found in process_per_connection.cpp, you can just copy it:
In a multi-threaded program,
io_service::notify_fork()
is not safe to invoke in the child. Yet, Boost.Asio expects it to be called based on thefork()
support, as this is when the child closes the parent's previous internal file descriptors and creates new ones. While Boost.Asio explicitly list the pre-conditions for invokingio_service::notify_fork()
, guaranteeing the state of its internal components during thefork()
, a brief glance at the implementation indicates thatstd::vector::push_back()
may allocate memory from the free store, and the allocation is not guaranteed to be async-signal-safe.With that said, one solution that may be worth considering is
fork()
the process when it is still single threaded. The child process will remain single threaded and performfork()
andexec()
when it is told to do so by the parent process via inter-process communication. This separation simplifies the problem by removing the need to manage the state of multiple threads while performingfork()
andexec()
.Here is a complete example demonstrating this approach, where the multi-threaded server will receive filenames via UDP and a child process will perform
fork()
andexec()
to run/usr/bin/touch
on the filename. In hopes of making the example slightly more readable, I have opted to use stackful coroutines.Terminal 1:
Terminal 2: