Comment by wongarsu
4 years ago
fork() and exec() is a reflection of the unix philosophy: small tools for separate tasks, and as a result you have great composability. You can set up right etc for your new process by calling any number of system calls between fork() and exec().
Windows is a great example of the alternative (following "Windows philosophy"): There are about three different API calls for creating a new process, each with a heap of complicated optional arguments. The API becomes more complex, less composable, less extensible and less powerful. But also easier to reason about and easier for the kernel to provide, and arguably with fewer footguns.
Windows approach is not the only alternative. Simply provide API to create a process in a suspended state, then adjust its properties based on pid/handle and then start the process execution.
A huge number of tricky thread problems go away if the child thread is blocked at startup, and allowed to run only after the parent allows it. To retrofit this, it is easiest to lock a mutex before spawning and have the child block on that. Then the parent unlocks it to let the child run.
I think shared libraries can spawn threads on their load/init phase that you don't know about. Then you are hosed but you only know about it due to sporadic weird problems, that if you restart on failure, e.g. a pre-forked worker scenario, you might never even really care about.
I always felt that the way to create new processes and new threads should initially start the same. A new process is simply a thread that after creation does a syscall to isolate itself from the other thread and receive a unique copy of all resources.
This could also solve the issue with forking from multithreaded programs since we can ensure we own all shared resources when we isolate our thread, to effectively thus become a new process.
So instead of fork we have clone/isolate.
A new thread can also of course immediately suspend itself, allow other threads to work on it's data in some way, who then give it a signal to resume itself and then execute if need be.
fork() is not a small tool.
It is a MASSIVE, unwieldy tool that is difficult to use correctly. It happens to have a small interface.
...is it?
In it is original implementation, fork() was pretty trivial. All it did was create a new process entry in the kernel table, with all the pages and capabilities and such copied from the original process. Then mark all pages as copy-on-write, and return to the caller. Maybe not trivial, but much less complicated than loading an executable file from disk.
My understanding of Linux internals is maybe 20 years out of date, so I am legitimately curious what makes fork() so complicated these days.
fork() is not trivial now. Processes are huge now -- they have huge heaps among other things. Copying all that is expensive. In the 80s we tried COW, but that turns out to be very slow as well. What operating systems do now is immediately copy the resident set, then do COW for the rest of writable memory, but in large, multi-threaded processes, this is still too slow.
Use vfork() or posix_spawn().
17 replies →
> Then mark all pages as copy-on-write, and return to the caller.
Unix actually copied the memory over initially.
It was simple when processes were simple, but requirements got serious.
Fork is not a reflection of unix philosophy! It is a beautiful hack tho.
Has Linux gained syscalls equivalent to those Windows API calls yet? Or is linux too different from the windows kernel to make that happen? (In that case, what does WINE do?)
On linux, fork libc function(s) is(are) a wrapper to clone system call which is more flexible (they added some stuff to make Wine work better):
https://lwn.net/Articles/826313/
Clone is also used to start threads, IIRC:
https://stackoverflow.com/questions/4856255/the-difference-b...