Fork() without exec() is dangerous in large programs
evanjones.ca
evanjones.ca
When you are embedding a language like Go in a C program, its runtime becomes a library. But that's the example the proves the rule: there are (or were) well-known problems with that. Libraries shouldn't be doing GC.
Boehm GC disagrees.
Applications can then instantiate their own threads and pass them to the library (i.e. dependency injection.)
So if you have 10 different concurrent libraries in an application, then you don't end up with 10 different threading paradigms or 10 different thread pools. This is a matter of application architecture -- libraries shouldn't bake in a specific policy.
...this applies not just to threading, but other things that are not in the domain of the library such as memory allocation strategy or communication mechanism.
Libraries should be small, focused and decoupled. This is the way to proper re-usability.
I suppose another rule should be that there libraries should encapsulate a sufficient amount of complexity to make their existence worthwhile.... unlike left-pad.
True, but they're more often used as a crutch where a select-style loop and/or state machine would be more suitable, leaving race conditions in their wake.
The thing is that any thread you launch is a big piece of machinery and your attempt to hide it is inevitably going to be a leaky abstraction.
That's not true at all. Threads aren't that complicated, and there's lots of ways of using them that aren't complicated and don't leak anything. As a trivial example, if I want to process a bunch of data, it might be faster to process it in parallel, and I can use a thread to do that without causing any observable difference in behavior (beyond potentially turning a single-threaded program into a multithreaded one). And if I'm on a platform like OS X or iOS, I can use higher-level threading APIs like libdispatch that make it even simpler.
And your example of a big calculation in a library is particurly bad, because the user can threadify that herself if she wants to.
#include <thread.h>
#include <yourlib.h>
#include "my_eventloop.h"
void launch_da_ting()
{
std::thread ting_doer([]{
Result rez = yourlib::do_da_ting();
my_event_loop::post([rez](){get_rez(rez);});
});
ting_doer.detach();
}
Now it's true those five lines would become a single function call if your library forced the user to user a particular dispatching library. But I think that would be poor seperation of concerns.And no, the user can't threadify that themselves. Your sample code is putting the library call on a thread. That's not at all what I was talking about. I was talking about a library using multiple threads (or, more likely, some abstraction over a thread pool that handles scheduling chunks of work on threads for you, such as libdispatch on Apple platforms) in order to process a bunch of data in parallel, data that only the library knows about. The user makes a single call into the library, the library processes a bunch of data in parallel, and then returns to the user. From the user's perspective it's a synchronous call, but it runs in a fraction of the time it would if the library wasn't allowed to use threading internally.
That't not what makes a framework. It's all about inversion of control, you call a library and a framework calls you.
http://stackoverflow.com/questions/3057526/framework-vs-tool...
I might use it to start out an application, since it requires very little code for dumb parallelism. But if I were making a real library, I would try to abstract the concurrency policy away so the user can control it.
This should be trivial by layering the library. Just provide a bunch of single-threaded functions, and then some OpenMP wrappers. Then user can call the single-threaded functions if she wants to do something different.
What I don't want is OpenMP baked deeply into the program logic. I think it shares the problem I was talking about. If you have 10 different libraries using OpenMP in an app, and the app is ITSELF multi-threaded, it seems like you have a mess of threads that you have no control over.
This isn't an issue on non-UNIX OSes.
Hence why fork() is just kind-of implemented on their POSIX compatibility layers.
In UNIX it is only an issue, because IEEE doesn't want to set in stone in POSIX how threads, signals and processes should interact, due to the way threads were added to UNIX.
Then there is pipe() and probably some more similar ones.
For example, what happens to the interactions of signal handlers semantics and thread scheduling.
Or what happens to the threads when the process does a fork().
You mean this? http://pubs.opengroup.org/onlinepubs/009695399/functions/for...
A process shall be created with a single thread. If a multi-threaded process
calls fork(), the new process shall contain a replica of the calling thread
and its entire address space, possibly including the states of mutexes and
other resources.
Granted, it's SUSv3.Just to get a glimpse of what POSIX says and what each UNIX actually does.
EDIT: Not talking about you, rather in general.
POSIX defines a few mechanisms for IPC.
What is optional and what is mandatory in pthreads doesn't concern me as i don't even like pthreads. (will still read what you said, later)
CSP stile concurrency over pipes (be they named or not), shared memory and synchronizations over it. POSIX defines mechanisms that work as expected over UNIX platforms.
I wonder how many (standards compliant) programs work differently between linux, *bsd, solaris, etc.
Wrong, you can fork() and only the thread issuing the fork () call survives the fork(), for example.
There are quite a few other differences.
> POSIX defines mechanisms that work as expected over UNIX platforms.
Not really, there are a few semantic differences and corner cases specially outside the GNU/LINUX, *BSD ones.
Thinking about it, it makes perfect sense.
Threads are processes that share the same memory space. When a thread is created (via pthread_whatever() or clone()) it starts executing some code that is practically in a $RANDOM place. So how would one go about copying the state of a process that has threads ? Freeze all the threads and copy everything ? That way the thread, that is probably doing some job at the moment, would end up being cloned without it knowing. So you would get two processes (threads are processes) that are doing exactly the same thing. Until something external changes their (internal) state, that is.
Non-determinism should always be avoided if possible.
This still does not say that one couldn't write portable, threaded, UNIX compliant code. Nor that those undefined behaviors from the specs would be fixable by rewriting the spec (by adding atomic fork+exec, that seems to me to be the context of this whole thing).
For many years now I have the rule that new code must not use fork() and I am living a happier life. It's a bad call and it has no use if you have good threading support in the language. There are many programming languages that do not support fork() andy more or heavily restrict the use.
Here are the details of how it works:
https://github.com/erlang/otp/blob/a5256e5221aff30f6d2cc7fab...
They also claim a 3-5x speedup for launching external commands because of it. So there is a nice performance boost as well.
So basically can add 4th strategy -- fork once a small program at the start, then fork from there from then on.
Unfortunately once copy-on-write was implemented, the crude hack was retained for backwards compatibility....
Anyway, I didn't explain it well, but this was what I was trying to get at with "fork a worker at the beginning of your program, before there can be other threads."
In places where you need the speed, threads are useful, but they're harder to use than forks.
Annoyingly, threads and forks don't work well together. zzzcpan already talked about how to fork threaded code. As for the problem of libraries using threads, if a library you're using is using threads, and it's documented (it probably is), and you didn't know about it, I'll pencil that in as your fault.
Other comments in this thread have dismissed fork(2) as a bad job entirely, but I don't think I agree. It's an effective way to do simple multiprocessing, and it's a lot simpler than threads in many contexts.
Btw, Redis reminds me, one really awesome usage of fork. To make a snapshot of itself Redis forks the main process. After that the child simply goes through tuples and write them to a files with out any worries that somebody will modify a records. Copy-on-write mechanism simply prevents it.
Redis save snapshot code: https://github.com/antirez/redis/blob/unstable/src/rdb.c#L99...
So if you control very closely the libs you link with and what they do, as well as the threads you use yourself, it is possible to use fork() in a reliable way.
the apple way of just throwing its hands in the air and abort()ing sound either like giving up because the apple devs think it is too hard, or because they feel the need to impose their way in crippling code that uses fork() without calling exec()
in my opinion if you are going to write a library that uses threads and is supposed to be thread safe then remember you need to handle the issue of fork(); don't be lazy, do the job properly, even if doing it right it is hard and ugly.
EDIT: Never mind, seems like initgroups() is also nonstandard but generally available on Unix-like systems.
I see two implementation challenges with that suggestion:
1.) implementing that transaction mechanism as a kernel feature: When entering a CS mark all pages CoW, upon leaving the CS merge modified pages (problem: Whole pages are then mutual exclusive, dealing with this is the challenge)
2.) battling with user space implemented locks that use atomics.
-----
An immediate mitigation I see is, that fork() itself is a CS on _all_ the locks of a process. If we consider that only the standard locking mechanisms are used, then whenever a CS is entered (which includes the creation process of a locking primitive) it raises/posts a global fork-lock semaphore. And upon leaving that semaphore is lowered.
This still leaves the DIY-locking primitives problem open. But it should be more or less straightforward to add this to the system libc/pthread libraries' locking primitive implementation and fork() syscall wrappers.
Or did I miss something essential here? Talk is cheap, so if nobody has any obvious objections I'd actually go ahead implement it.
EDIT: Okay, one immediate problem I see is, that this would pose a challenge for calling fork inside a CS. Technically this is a situation where thread recursive locks would help, but as we all know, recursive locks are highly problematic.
Some more problems to consider:
1. how do you roll back IO that happens in a critical section? 2. your global semaphore will be highly contended and basically destroy scalability
re 2) the fork global lock satisfies every requirement for a multiple-readers/single-writer, with fork() being the only writer. Unless the program does a lot of fork()-ing it should hardly ever run into contention. The moment fork() waits on the lock, further attempts on read-lock are delayed until after fork() completes and the existing reads are waited for, before fork()-ing.
Even fork _with_ exec can be real trouble. This is one of my bugaboos at work: due to poor life choices and high pain tolerance, I own the infrastructure we use to spawn subprocesses (carefully.) For various reasons (security most notably) we have to do some very tricky things in and to the forked child before exec(), and pretty much all of this code is a disaster waiting to happen. Every so often I get feature requests for more stupid pet tricks people would like out of subprocesses, and they're always surprised by what their "simple" change would entail.
I'd like it if Linux had native support for posix_spawn, but even that would require a lot of extensions to be useful.
Don't get me started on the teams that want to break forking rules and thus ask me how to guarantee a process has no non-main threads. There are few ways you can make me more upset than by building software that breaks if some one else happens to call pthread_create and doesn't tell you.
Re: multiprocessing in 3.4: I think it is unfortunate that due to backwards compatibility, the "forkserver" mode can't become the default on Unix, since it would help avoid this particular issue.
What exactly is simple and elegant about it? Have you looked into how much tooling is necessary everywhere in unix to make it work? It's insane. It has a huge footprint and it does not provide standardized APIs to make it work for non covered cases (the best we have is pthread_atfork which is not portable).
Me too. I found it simple: Create new child process, copy the address space, copy the instruction pointer, let it run.
If you want to improve performance via copy-on-write, it gets more complex to implement. If you combine it with other features like threads, mmap, limits, etc use becomes complex, but the mechanism itself is simple.
You might be able to design an easy variant of fork, which handles threads, mmap, limits, etc in a sane way, but that would not be simple anymore.
Fork is actually a good example, why simple and easy are different concepts and why it is really hard to achieve both.
Even in the total absence of threads (which you never have on a modern OS) you already fucked up. Because you did not account for file handles, signal handling, pid management etc.
fork() even on the simplest real world operating system is a massive pain for the OS.
Spawn-style APIs have to take a billion parameters that are mostly set to defaults, for things like working directory, environment variables, user ID, controlling terminal. Whereas fork+exec means you can express these things in a more compositional, buildery style: fork, change the two things you actually need to change, then exec.
Python says explicit is better than implicit. I agree in this case.
Global vars set wherever in a program are preferable to explicit params when creating a new process? And there's no way to have default params or anything?
The outside world is always going to be implicit global mutable state; spawning an arbitrary process to do who-knows-what is inherently that kind of problem. If you could express it as an actual function you wouldn't need to run a separate process.
> And there's no way to have default params or anything?
Well not in C. You can sort-of do it by having a struct with fields that you fill in where all-bits-zero is the default but that's generally more trouble than it's worth.
C easily allows you to have multiple functions. exec_with_current_state or something. And a struct is a way, too.
If you care about purity you don't use spawn-like functionality at all.
> C easily allows you to have multiple functions. exec_with_current_state or something.
That doesn't help. You'd need to have one variant where you just want to change the working directory, one variant where you just wanted to set an environment variable, one variant where you want to do both, one variant where you want to run as a different UID and change working directory but not touch the environment variables...
Typically calls such as malloc(), printf() etc. are strictly forbidden in the child after a fork().
This post does the best job explaining the issue: http://www.linuxprogrammingblog.com/threads-and-fork-think-t...
Do this when you must, but it is easy to screw up.
4. Stop writing broken multithreaded code altogether or if you must at least run an event loop per core/thread and use a wrapper for fork() to put the system into a fork()able state before forking. It's nice and reliable.
Multithreading by itself is just not a high-level concept to be used reliably by programs.
If you want a series of worker threads that are effectively the same as the master/initial thread. The proper way to do this clone(2) not fork, or fork/exec.
When used properly clone allows your group of threads to share a single PID, and TGID (Thread Group IDentifier). This cuts down on kernel resources your process(es) are using. They can natively share file descriptors and memory between each other (improving cache coherence). Also their signals are handled globally for all threads at once, not each thread managing it's own signals like Fork/Exec will result in.
man: No entry for clone in the manual.
As for the kernel resources for the process, you'd be surprised how large RAM has gotten these days...
...And why would you want that?
All your applications share the same virtual memory space. So you have a much higher hit rate in your TLB, then without. It greatly lowers the chance that shared memory will leave L3 cache.Cache hit rates are very important...
The advantage for fork(2) over clone is that you CAN'T
share those datastructures, making it harder to write
code resulting a deadlock
Dead locking has nothing to do with how RAM is accessed or how virtual memory is partitioned, it has to do with how you are managing your locking. Modern memory/instruction re-ordering is stupidly fast. If you are locking memory/data structure locations you are likely doing something wrong. Concurrent Memory fences are around 3 orders of magnitude faster then locks. As for the kernel resources for the process, you'd be
surprised how large RAM has gotten these days...
TLB/L2/L3 cache is still premium real-estate. RAM size is awesome! I love waiting 100,000cycles to get the page I requested. >If you are locking memory/data structure
locations you are likely doing something wrong.
Oh, concurrent memory fences! Well, that solves all my problems. Let's see. All I have to do is give up all hope of my code ever being portable to mutiple architectures, and than dig around in asm to add support for them to my environment, which can range from mildly annoying (C), to near-impossible (JVM). Or I can use fork(2), and do what programmers have been doing for decades: trading performance for simplicity, and productivity.That'll be a really hard decision.
All I have to do is give up all hope of my code ever
being portable to multiple architectures
You know nothing about modern compiler atomics.Post 2011 compilers (LLVM, GCC, MSVC, ICC) standardized generic memory fences for C++ and C. These are fully portable as the compiler itself determines how the layout of Acquire/Releases needs to be changed on platform you are compiling too.
Atomics are supported on ARM, x64, MIPS64, SPARC64, POWER8, POWER9...
So they're very portable. The compiler even manages removing unneeded fences. Like if you add an acquire fence after a CAS load. The CAS load is an acquire fence on x64, but not on POWER8.
Are you just a professional contrarian?
Why are you concerned about system calls in a HLL?
Why are you concerned with concurrency guarantees in a HLL?
You care about none of these, you care about your run time. This is why you are working in a HLL.
Are you just a professional contrarian?
No. Why are you concerned about system calls in a HLL?
Because in many HLLs, fork(2) is the best (or sometimes the only) option for concurrency. As in C, it is certainly the simplest.Many things written in HLLs use fork. Unicorn is perhaps the most famous example.
Why are you concerned with concurrency guarantees in a HLL?
Because if I have multiple threads of execution, I want them to run in parallel if possible, and I want to minimize the risk of deadlocks. fork(2) does both. You care about none of these, you care about your run time. This is why you are working in a HLL.
No. I care about them, as demonstrated above. And I don't know what you mean by care about my runtime. I would go so far as to claim that the above is an insult to me and everybody else who uses an HLL.I care about getting stuff done. If C is the right tool for that, I'll use it. If an HLL is the right tool, I'll use it. And if syscalls are the right tool for the job, I'll use them, too.