from A Fork in the Road, <https://www.microsoft.com/en-us/research/uploads/prod/2019/0...>
from A Fork in the Road, <https://www.microsoft.com/en-us/research/uploads/prod/2019/0...>
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.
It is a MASSIVE, unwieldy tool that is difficult to use correctly. It happens to have a small interface.
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.
Use vfork() or posix_spawn().
I could see it being worthwhile to immediately copy a few pages, like the top of the stack, but copying the whole resident set seems excessive. Especially since some of that data might not even be written to.
As well there's the cost of all those page faults that the two processes are likely to take to do the copying.
And lastly there's all sorts of complexity involving multiple parent threads calling fork(), or the child calling fork() again (or vfork()) before calling exec.
It's just much easier to copy the resident set and mark the address space as being CoW, because now you only have to worry about page faults for pages that are not in core anyways and so were going to fault anyways, and that means you don't have to worry about TLB shootdowns either (if a page is not in core, it's not referenced by any TLB either). You still have the multi-fork issues, but now you can use an atomic reference count on the address space.
With what you describe, you'd share nothing because at the point you've loaded it up, all the data you just loaded is resident. :-(
The posix_spawn() and posix_spawnp() functions provide the
functionality of a combined fork(2) and exec(3), with some
optional housekeeping steps in the child process before the
exec(3). These functions are not meant to replace the fork(2)
and execve(2) system calls. In fact, they provide only a subset
of the functionality that can be achieved by using the system
calls.
Also, there's no way to set resource limits in the child process, nor switch user or group ID, using posix_spawn().Windows defaults to CLOEXEC semantics and you have to opt-in to child process inheriting open file handles, and that has caused problems.
Unix defaults to not-CLOEXEC sematincs, and that too has caused problems.
The Unix default can cause unsolvable problems because of races between threads.
You should use CLOEXEC everywhere. Except you can't because you are using libraries.
A common hack I've had to add is an argument FD that is not to be closed because. e.g., an flock is held on it.
Unix actually copied the memory over initially.
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.
https://lwn.net/Articles/826313/
Clone is also used to start threads, IIRC:
https://stackoverflow.com/questions/4856255/the-difference-b...
Windows doesn't have fork(). It has a real, fully mature thread and process model. In Windows NT, every process consists of a handle that is a "Process", which in turn points to a structure containing a list of "Threads". A process is done when its main thread exits or all threads exit, whichever is defined by the main process. Fork/Exec is replaced with CreateProcess (or ShellExecute, your choice).
For a very zen-like example of the fork/exec and pipe management that you'd do on a POSIX system done in Windows, the [MSDN Docs](https://docs.microsoft.com/en-us/windows/win32/procthread/cr...) are quite informative.
From my perspective watching the various techniques used by a multitude of operating systems over many decades, the Windows-style process+thread model seems to be winning out over the UNIX fork() model.
For example, PostgreSQL seems to suffer greatly from the "forked process per connection" model, necessitating front-ends that do connection pooling. Database engine after database engine seems to go through this phase and then "upgrade" to either a single-process thread pool model, or start using async IO in some way. (Web servers also.)
For reference, Microsoft SQL Server back in the 1990s could on Windows NT 4 could handle more connections than PostgreSQL in 2020...
It does (or maybe did, not sure if it still works) have the literal equivalent of fork() - NtCreateProcess() with NULL SectionHandle argument creates a new process which is a clone of the caller. However, it never really worked. The Win32 API did not support forking, and so any process which forked itself and then tried to invoke any Win32 API calls would soon crash or fail mysteriously. In theory forking did work for pure NT API applications, but those are rather limited in their abilities.
https://hn.algolia.com/?q=https%3A%2F%2Fwww.microsoft.com%2F...
and also here: