Because a 'fork' is the most natural way for one process to transform itself into another process while continuing to run by itself.
I actually think it is one of the most elegant system calls in unix.
Think of all the alternative clunky ways that OS's before unix had to use to start a process at a given depth into the process. Lots of flags to make sure that you started off where you left in the 'parent', to recreate all or most of the state required for the child process. Fork passes all that state 'for free'. And copy-on-write makes it fast.
It's a bit like biology. Split the cell, then let them both specialize a bit towards what they have to become. The moment of splitting is almost 100% symmetrical, the only difference being who is the 'parent' and who is the 'child' process.
Other ways of starting new processes feel clunky in comparision, you have to specify a binary to run, you have to know all kinds of details about parameters to pass and so on.
Fork essentially abstracts the creation of a sub-process to the absolute minimum.
Fork is atomic, it's got 0 parameters and it returns only one integer (or it fails for a simple reason, no more process slots).
Needing an exact copy of the current process is an atypical use case in my experience. And the typical use case of running another binary isn't made any more elegant by fork(), it just shuffles the complexity into exec().
Here's where using fork() without an ensuing exec() is very natural ...
A server process that must reply to hundreds of network requests per second but still remain single-threaded pretty much needs fork(). The persistent parent process sets up a common configuration, accepts incoming network requests, and calls fork() (which is very fast) to do the real work for each request in a child process.
That child's work involves looking up relevant data or recording relevant data or doing a transaction that may involve only local memory, the filesystem, a DB, other processes, or other network services. The child must also reply to the network client. Doing this all in the single-threaded parent would preclude it from handling other requests and make it impossible to respond to hundreds of clients (unless the parent uses complicated asynchronous processing and the per-request work is mostly I/O and low on CPU ... but that complicates the server process). Starting a separate process per client instead (as in CreateProcess() or spawn()/fork-exec()) complicates the architecture and is very expensive because all info needed for the client reply can often be housed in the original parent process and inherited by the child (e.g. locally derived/cached DNS results). fork() leads to the simplest and most efficient architecture.
Actually I think in most cases the simplest and most efficient architecture is when your server
creates a process for each specific type of thing it needs to do and then simply delegates incoming requests. http://www.okws.org/doku.php?id=okws is a good example.
Ah, but now it's a lot harder to share anything that isn't simply coming out of a shared library; pre-cached computations, any code that isn't in a shared library (like Perl or other interpreted code), any expensive computation that must be done per-process, etc. Forking off children is virtually impossible to beat on the efficiency front; people tend to grossly overestimate the expense of a "fork".
The only way to win with the approach you suggest is if you have some sort of massively complicated server that you only ever need some small part of at any given point in time, allowing you to do a lot of swapping in and out of memory. I've never seen such a beast and can't really come up with a non-contrived use case. YMMV, but it certainly isn't a common case.
(Yes, Perl isn't exactly interpreted, but from this point of view it certainly isn't a shared binary library.)
Although I believe you're wrong about the actual utility of this kind of data sharing across processes (and also making exaggerating claims about complexity), I might change my mind if you provided a specific real-world reference example.
That's because you want to do an exec, which essentially transforms a running process into another process. I think people generally overestimate the complexity of fork on the part of the kernel, it's not all that bad. Create a new process slot, copy the VM configuration, set the 'copy-on-write' bit and the 'write-protect' bit on the pages in the memory image of the parent and return (twice!).
If all you want is a subprocess to do something interesting to the data you already have then fork is ideal. This is - or maybe was - a very common use case.
If you also need exec then why not first abstract out the fork, you need that any way, and do the exec in the child. It's one of the most elegant solutions to the problem I've seen.
Otherwise you get a whole bunch of functional duplication between system calls.
Already the 'exec' zoo is a good example of the kind of functional duplication you'd get. Adding 'forking' and 'non-forking' versions would not seem to improve matters much.
You make it sound so smooth and slick, and yes, having the child have all its state all ready and set up sounds like a good thing.
I suspect you are talking of using concurrency within the same program. The competing technology would be shared memory threads, of which I am more familiar. The only downside of this approach compared to fork seems (to me) to be the requirement to lock and signal when accessing shared resources.
Is there a downside to using fork within a process? Shirley if you've got a copy of memory (even if the actual copy is defferred) don't you also inherit the baggage of managing those resources too?
Lotsa questions there. Thanks for your original candid answer.
One thing to remember: when fork() was invented, shared-memory threads basically didn't exist. Everything that would be done with threads today, had to be done with either fork() or asynchronous event loops, because no mainstream OS had threading.
Another thing to remember: there's essentially no difference between a fork() and a thread. On Linux, they're both implemented by the same clone() syscall -- they just specify different flags about what should be shared versus what should be split and copied. Both are extremely cheap, and if you're going to implement threading, you might as well implement fork() because [on hardware with virtual memory] it's almost free.
> I suspect you are talking of using concurrency within the same program.
That's one common use case for 'oldies' :) But there are some others, such as splitting a protocol handler into two layers, all the setup including environment, privileges and so on is the same, then the two layers of the protocol split and start communicating via a pipe. Now it's technically two programs but they happen to live in a single binary image.
> The competing technology would be shared memory threads, of which I am more familiar.
When unix was first developed shared memory was not in the cards, neither were threads. Unix is old, and fork is definitely showing how old. The initial machine that unix was developed on was a PDP-7, here are the specs:
18 bit words
4 K memory standard (magnetic cores, small ferrite beads with a bunch of wires running through them for addressing and a single read/write wire (ok, that's simplified))
16 K words for the machine Unix was written on originally
(max was 64K words)
discrete parts only (no integration, just Q,D & R (and some 'C' ;) )
I'm not 100% sure if they already had 'fork' in place on that machine, but the next step up, the PDP-11 definitely had it (that's also the machine that C was born on, the first version of unix was written in assembler!).
> The only downside of this approach compared to fork seems (to me) to be the requirement to lock and signal when accessing shared resources.
They're completely different beasts I think. Fork is a way to guarantee a whole slew of things about the relationship between two processes, threads are essentially parallel executions within the same memory space (or on a single core machine a simulation of parallel execution).
Threads are both a great thing and a nightmare, depending on whether you've just solved the worst race condition bug in your career or whether you're still busy with it :)
Your 'lock and signal' sounds pretty simple, and in theory it is, in practice efficient multi-threading programs are amongst the hardest things to get right that I know of.
> Is there a downside to using fork within a process?
If you need it, you need it. If not don't use it. Like any other tool. Downsides to system calls are not really interesting, they only appear when you use the system calls in ways that they weren't meant to. What's in the core of a unix system is pretty much what needs to be there.
> don't you also inherit the baggage of managing those resources too?
Yes, absolutely. Any memory that was allocated in the parent is also allocated in the child, any file system handles that you have are present in both and so on.
In the case of file system handles there is usually some kind of convention on who gets to 'own' them but you can do interesting things by letting them be owned by both.
I actually think it is one of the most elegant system calls in unix.
Think of all the alternative clunky ways that OS's before unix had to use to start a process at a given depth into the process. Lots of flags to make sure that you started off where you left in the 'parent', to recreate all or most of the state required for the child process. Fork passes all that state 'for free'. And copy-on-write makes it fast.
It's a bit like biology. Split the cell, then let them both specialize a bit towards what they have to become. The moment of splitting is almost 100% symmetrical, the only difference being who is the 'parent' and who is the 'child' process.
Other ways of starting new processes feel clunky in comparision, you have to specify a binary to run, you have to know all kinds of details about parameters to pass and so on.
Fork essentially abstracts the creation of a sub-process to the absolute minimum.
Fork is atomic, it's got 0 parameters and it returns only one integer (or it fails for a simple reason, no more process slots).