"The received wisdom suggests that Unix’s unusual combination of fork() and exec() for process creation was an
inspired design. In this paper, we argue that fork was a clever
hack for machines and programs of the 1970s that has long
outlived its usefulness and is now a liability. We catalog the
ways in which fork is a terrible abstraction for the modern programmer to use, describe how it compromises OS
implementations, and propose alternatives."<p>from <i>A Fork in the Road</i>, <<a href="https://www.microsoft.com/en-us/research/uploads/prod/2019/04/fork-hotos19.pdf" rel="nofollow">https://www.microsoft.com/en-us/research/uploads/prod/2019/0...</a>>
If you mix fork with threads, you're going to have a [undefined behavior] time. It seems like if you link with the sqlite that comes with macOS, you're using threads whether you like it or not. I think ending up at "you shouldn't use fork() at all" is a bit of an extreme conclusion, though.<p>BTW, article title needs a (2016). It appears that the relevant Python bug has long since been closed, by avoiding linking with the system sqlite on macOS.
The dense fog lifts, tree branches part, a ray of light shines down on the ruins of a moss-covered pedestal revealing the hidden intentions of the ancients. A plaque states "The operational semantics of the most basic primitives of your operating system are optimized to simplify the implementation of command line shells." You look upon the pedestal, pause in respect, then turn away disappointed but unsurprised. As you walk you shake your head trying to evict the after image of a beam of light illuminating a turd.
fish shell uses posix_spawn sometimes because of its performance benefits. We can't use it in the following cases:<p>1. No analog to tcsetpgrp, so it's no good if job control is enabled<p>2. No analog to fchdir, meaning you have to synchronize with fchdir elsewhere in the progarm<p>3. Error codes do not convey enough information for good error messages (e.g. if a file doesn't exist, posix_spawn doesn't tell you which file)<p>4. Inconsistent behavior around dup2 fd redirections and CLO_EXEC.<p>5. Inconsistent behavior for shebangless scripts<p>These are basically deal-breakers so fish also supports a fork/exec path. However the performance benefits of posix_spawn are too real to ignore so fish uses posix_spawn when it can, and fork/exec when it must.
Another danger using fork is it duplicates the internal state of pseudo random number generators. It's a great way to accidentally take the same random samples in every process, utterly trashing any statistics you were intending to do. Bonus: the python multiprocessing module silently uses fork by default. Person A writes a "make multiprocessing convenient" library, Person B writes a sampling library, you put them together and... <i>whoops!</i>.
See also this recent 340 (!) comment thread about the issues of fork <a href="https://news.ycombinator.com/item?id=30502392" rel="nofollow">https://news.ycombinator.com/item?id=30502392</a>
FWIW this is the same reason you can't implement implement a portable Unix shell in portable Go. (And similar issues with an init daemon)<p>Go only exports os.ForkExec() -- there is no os.Fork() or os.Exec(), because the things you can do between the calls could break Go's threaded runtime. (Goroutines are implemented with OS threads.)<p>Some elaboration on that: <a href="https://lobste.rs/s/hj3np3/mvdan_sh_posix_shell_go#c_qszuer" rel="nofollow">https://lobste.rs/s/hj3np3/mvdan_sh_posix_shell_go#c_qszuer</a><p>That is, the space between fork and exec is where pipelines are implemented, but also entire subinterpreters/subshells. The shell actually uses copy-on-write usefully. (And yes I'm aware that there's a good argument that the shell is almost the ONLY program that needs fork() !)<p>----<p>A lot of people have asked me why not implement Oil in Go and various other languages, so I wrote this page:<p><a href="https://github.com/oilshell/oil/wiki/FAQ:-Why-Not-Write-Oil-in-X%3F" rel="nofollow">https://github.com/oilshell/oil/wiki/FAQ:-Why-Not-Write-Oil-...</a><p>So the funny thing is that Python is a lower level language than Go for this particular problem. It doesn't do anything weird with regard to syscalls. I'm still looking for help on this (and donations to pay people other than me):<p><i>Oil Is Being Implemented "Middle Out"</i> <a href="https://www.oilshell.org/blog/2022/03/middle-out.html" rel="nofollow">https://www.oilshell.org/blog/2022/03/middle-out.html</a>
The devil, as they say, is in the details.<p>But to be fair, the only times I can recall using fork() without exec() were forking network servers, and that was mostly me learning about doing network stuff, and a forking server was the easiest to implement manually.<p>Oh yeah, and that one time I accidentally wrote a fork bomb trying to stress test a DNS server. At least I learned something from my mistake. ;-)<p>EDIT: To me, using fork() without exec() is kind of like operator overloading - there are cases where it absolutely is the right tool, but these aren't very numerous, so one should exercise caution. A lot.
Curious why there isn’t an interface in which all required handles and resources could be passed to a child process explicitly. E.g.:<p><pre><code> execvpehm(
...,
int *handles, size_t,
void **pages, size_t,
/* etc */
);
</code></pre>
Would remove so many headaches with concurrency and accidental inheritance.
The suggestions here aren't really great. What you should do is already written in the fork(2) manpage <a href="https://man7.org/linux/man-pages/man2/fork.2.html" rel="nofollow">https://man7.org/linux/man-pages/man2/fork.2.html</a><p>"After a fork() in a multithreaded program, the child can safely call only async-signal-safe functions (see signal-safety(7)) until such time as it calls execve(2)."<p>So just use only async-singal-safe function
<a href="https://man7.org/linux/man-pages/man7/signal-safety.7.html" rel="nofollow">https://man7.org/linux/man-pages/man7/signal-safety.7.html</a><p>I don't know why so many people still hit this issue when it already told you what you can do and not do in the document. I've done this sort of things without any issue.
Discussed at the time:<p><i>Fork() without exec() is dangerous in large programs</i> - <a href="https://news.ycombinator.com/item?id=12302539" rel="nofollow">https://news.ycombinator.com/item?id=12302539</a> - Aug 2016 (101 comments)
I asked this a year or so ago. Interesting to read this article in light of that discussion.<p><a href="https://news.ycombinator.com/item?id=863871" rel="nofollow">https://news.ycombinator.com/item?id=863871</a>
(13 years? Yikes!)
The problems with fork in the face of threads are caused by threads, not by fork. Fork was there first, and it is part of a system that is designed and integrated well.<p>Threads were bolted onto Unix in a hamfisted way, breaking more than just fork. For instance, threads broke relative paths, requiring "at" functions like <i>openat</i> to be invented, an ugly stop-gap measure. Threads were badly integrated with signal handling too, another example.<p>Blaming those existing mechanisms is purely an emotional argument, from the perspective of being infatuated with threads.<p>The design of threads (coming from various efforts that became POSIX threads) came from such an infatuation: the desire to get any kinds of threads working at any cost, while ignoring the global state that exists in a Unix process, and the need to make a lot of it thread-local, or at least optionally so.<p>A thread-local working directory or signal mask would have caused difficulties in hack thread implementations that used user space scheduling or M:N (M user space threads to N kernel tasks).<p>The situation we have today largely comes from the initial reluctance to accept the fact that each thread has to be an entity known to the kernel; the belief that user space threads are viable into the long-term future.
> Only use fork in toy programs. The challenge is that successful toy programs grow into large ones, and large programs eventually use threads. It might be best just to not bother.<p>How do you create a new process and pipe it data in a fast fashion without using fork, exec or posix_spawn ?
One other option is to fork all the threads too.<p>Since you probably don't know what all the other threads in your process are up to, your only option is to attach a debugger to all of them, halt them all, and copy all their state into brand new threads in the child process.<p>Do it all correctly and you end up with a multi-threaded-fork.<p>You still need to fix up signal handlers, interrupted syscalls, various notification API's that no longer work, memory mapped temp files used for IPC, pipes and sockets, and a bunch of other things.<p>But a fork of a complex process is possible. It just isn't easy.
fork() also presents performance issues for programs with a large virtual space. Here vfork() helps, but it has even more pitfalls than fork(). I had written a small doc about converting the recollindex Recoll indexer from fork() to vfork() a while ago: <a href="https://www.lesbonscomptes.com/recoll/pages/idxthreads/forkingRecoll.html" rel="nofollow">https://www.lesbonscomptes.com/recoll/pages/idxthreads/forki...</a>
New programming language implementations should maybe make fork() and multithreading be mutually exclusive at link time by default, and only allow them together in an unsafe-I-know-what-I’m-doing mode (if at all).
It's only dangerous if you use libraries without fully understanding what they're doing. And most well designed libraries will avoid creating threads, and will do so only when you make it explicit that you want it to happen.<p>I also find that libraries that absolutely need to make their own threads are better off being their own process. Then you can use proper communication methods to pass data.
> When I ran into this problem, I was just trying to run all of Bluecore's unit tests on my Mac laptop. We use nose's multiprocess mode, which uses Python's multiprocessing module to utilize multiple CPUs. Unfortunately, the tests hung, even though they passed on our Linux test server.<p>There will never be a time at which you can reliably expect any program developed on one system to "just work" on a different system. This person wasted a lot of time tracking down what was essentially a portability bug. Did they <i>need</i> this to be portable? Was this time well spent generating business value?<p>Pick one system for development through production, stick to it. There will be portability bugs hiding in your code, but you will never have to fix them. You will be upset for a minute that you can't use a different system, but you will get over it.