- Duplicate all threads. No one does this because it would be total chaos. If another thread was writing to a file or a socket, parts of those writes might happen twice, etc.
- Duplicate only the calling thread. This isn't total chaos, so everyone does this, but this also sucks. Any locks that were held by any other threads will never be released in the new process, so you need to make sure not to touch any locks at all post-fork. But that's a huge restriction, because e.g. malloc() touches locks sometimes. Code that runs post-fork ends up being as restricted as code that runs in signal handlers.
I guess the advantage of fork relative to other hypothetical methods of creating child processes is essentially just getting a copy of all the parent's state before the fork. So maybe the question is how often getting a copy of all the parent's state is actually useful?
However, fork() is still not necessarily a good design, and the reason for that is that the vast majority of the time the reason you want to run fork() is to immediately run exec() afterwards because you're trying to launch a separate process. So, the fork() does all the hard work of duplicating the entire address space of the process, and then exec() throws it all away again.
One thought is, if the OS/libc was designed so that exec() always created a child process, then you could get the same functionality as fork() by creating a child process using exec() and passing along just the info that the child process needed, possibly using IPC. This seems like better encapsulation, for the same reason you wouldn't write a function which required a ton of arguments it didn't use. It also makes the issue of forking a multithreaded process irrelevant.
But I could imagine this approach being slow if there's a lot of state you want to pass, if copying entire pages of memory (the way fork does?) is significantly faster than than sending the same data via IPC.
I wonder what the most common non-exec() use of fork() is.
There are quite a lot of things that survive exec(). Not least of which is the set of open files. This is how a shell will pass on the definitions of stdin/stdout/stderr to a command that you want it to run, for example. Also, environment variables survive exec().
Does this change depending on whether the environment variable definition was prepended with 'export'?
My own reconstruction of what I think happened (this was all before I was born, I'm just making some inferences based on what I know about the topic) – Microsoft was heavily influenced by DEC operating systems (Bill Gates and Paul Allen wrote the first version of Microsoft Basic on a PDP-10). Microsoft didn't like CP/M's native option syntax, so for their development tools for CP/M, they adopted a DEC-style option syntax instead. Since many ISVs used Microsoft's development tools, a lot of third party software on CP/M ended up using the same option syntax too. Tim Paterson at Seattle Computer Products was influenced a lot by Microsoft (even before he worked for them), so QDOS/86-DOS/PC-DOS/MS-DOS ended up with the DEC/Microsoft-style forward slash as an option character. When DOS 2.0 came around, Microsoft wanted to switch it to dash/minus, for better Unix/Xenix compatibility. In support of this, they added a CONFIG.SYS setting SWITCHCHAR= to let you choose between forward slash and dash/minus, and an API (INT 21,37) to get/set that config setting at runtime. However, it was decided that the backward compatibility cost of that change was too great, so they pulled the feature from the documentation (although it still existed in the code; in later DOS versions, they disabled the ability to change it to anything other than forward slash, but the undocumented API to get the setting was never removed.)
People talk about Windows NT being influenced by OpenVMS, via Dave Cutler – the influence is very obvious in some areas (e.g. asynchronous IO), but path name syntax is one area in which Windows is much more influenced by Unix than by OpenVMS (even considering warts such as drive letters, support for backslash instead of forward slash, and reserved file names such as NUL and CON.)
the sockets api was modeled after berkeley and the libc itself is posix compliant to this day, i believe.
Then IBM and MS fell out and Microsoft put all their resources into Win32.
However, Windows NT did actually have those three APIs – the Win32 subsystem (which also incorporated Win16/DOS support), the POSIX subsystem, and the OS/2 subsystem. But the POSIX and OS/2 subsystems were always rather half-baked, and never saw that much use. The OS/2 subsystem was removed in Windows 2000; it only ever supported OS/2 1.x apps, and only character mode unless you purchased a separate graphics mode add-on from Microsoft. The POSIX subsystem was not removed until Windows 8.1, although by then it had changed its name to "Subsystem for Unix-based Applications", and had grown a lot compared to the original NT 3.x POSIX subsystem. WSL is effectively the replacement for the POSIX subsystem, however its implementation is very different (which is true of both WSL1 and WSL2, despite the fact that they are also very different in implementation from each other.)
the important part was the posix api functions that made porting unix daemons that had been written in c/c++ to nt pretty easy. there were some quirks around shared memory/mmap, getpwnam, the sockets api and threading support, but beyond that doing a port was pretty easy.
the work there was plugging into the service control and event logging apis. and doing an installshield, that was no fun.
if a program did use fork as part of its concurrency model (like say, preforking apache) then making use of actually functional nt threading support and gratuitous ifdefs was usually the answer.
Both Cygwin and POSIX/Interix/SFU/SUA use the same workaround - maintain a separate Unix PID for each process. exec() changes the NT PID but leaves the Unix PID unchanged. So exec() creates a new process, but to Unix apps it looks like the same process.
You are right that for many apps, rather than rely on emulated fork/exec, it was better to switch to native Windows facilities. Still, there was some value in being able to recompile Unix software with minimal changes (as Cygwin demonstrates). Even WSL now confirms that, albeit in a rather different way. Also, it was necessary to pass the POSIX test suite, which Microsoft briefly was concerned about (until they successfully lobbied the US government to drop POSIX certification as a procurement requirement).
Interix 2.2[0] shipped with its own PSXSS.EXE. I don't have a contemporaneous version of the PSXSS.EXE that shipped with Windows at that time to compare handy.
Finding a copy of OpenNT prior to the Microsoft acquisition is proving to be difficult.
So much history is slipping away.
However (putting aside the legalities of downloading "abandonware"), they won't let you download anything unless you first upload something they don't have. Heck, I probably do have something they don't have in my box of old floppy disks (which I keep on telling myself I will image one of these days), but my curiosity about this topic isn't strong enough to motivate me to do that.