This is why I'm interested in old, now mostly obsolete/dead operating systems. No one has really felt free or able to break out of the UNIX mould, since. And we've been using the mould for a long time. It wasn't always so uniform. There have been some craaaaazy things out there, that we simply never see today because it doesn't fit. Off the top of my head:
* ITS (of PDP-10 hacker fame) - processes could debug and introspect their child processes. The debugger was always available, basically. The operating system provided support for breakpoints, single-stepping, examining process memory, etc. Whatever part of the system manages processes, is the natural place for the debugger, really. Some of this has been brought to Unix systems recently, but you still can't trivially freeze a process, tweak a few values in its memory, and resume it as an integrated part of the operating system. Why not? It seems very basic to an operating system, now that I think about it.
* KeyKOS (developed by Tymshare for their commercial computing services in the 1970s) - A capability operating system. If everything in UNIX was a file, then everything in KeyKOS was a memory page, and capabilities (keys) to access those pages. The kernel has no state that isn't calculated from values in the virtual memory storage. The system snapshots the virtual memory state regularly. There are subtle consequences from this. Executing processes are effectively memory-mapped files that constantly rewrite themselves, with only the snapshots being written out. Snapshotting the virtual memory state of the system snapshots everything -- including the state of running processes. There's no need for a file system, just a means to map names to sets of pages, which is done by an ordinary process. After a crash, processes and their state are internally consistent, and continue running from their last snapshot. For those who are intrigued, there's a good introduction, written in 1979, by the system's designers available here: http://cap-lore.com/CapTheory/upenn/Gnosis/Gnosis.html (It was GNOSIS before being renamed KeyKOS.) And a later document written in the 90s aimed at UNIX users making the case: http://cap-lore.com/CapTheory/upenn/NanoKernel/NanoKernel.ht... Some work on capability systems continues, but it seems the lessons learned have largely been forgotten.
Ironically, Microsoft's Windows is one of the only big non-Unix operating systems out there. I say ironically, because people seldom bring up the Microsoft ecosystem as a beacon of adding to diversity.
I respect Windows for saying no to a hack that is fork. Fork is just bad design and no software should use it, and thanks to existence of Windows, lots of software avoid use of fork, which is a big plus to software ecosystem as a whole. Thanks to Windows, new operating systems like Fuchsia can also avoid fork.
The paper that sanxiyn linked is a great read. My summary is that forking and multithreading are a terrible mix. You have two bad options:
- 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.
What if the fork syscall failed if there was more than one thread? Would it be bad design then?
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?
You'd then have an explicit early-fail footgun instead of a rake in the grass, which has to be an advantage.
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.
fork() doesn't actually copy entire pages of memory. But it does have to copy the page tables (note - someone else might be able to chip in and tell me that Linux has an optimisation for that too). The actual pages are held with a copy-on-write status, so they won't be copied until they are actually modified by one of the processes.
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().
Microsoft's Unix derivative, Xenix, was a big influence on DOS 2.0, in particular the concept of directories, path name syntax, and replacing the CP/M-derived FCB API for file I/O with Unix-style file handles. That influence endures all the way from DOS 2.0 up to Windows 11.
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.)
Other influence from OpenVMS is how many of those API surfaces look like, when I looked into OpenVMS documentation for the first time on during my digitial archeology, somehow it felt familiar territory.
Correct me if i am wrong but isn't the NT design heavily based off of (some would say a rip off of) VMS, Microsoft having poached much of DEC's OS dev team including chief architect David Cuttler.
Yes, and DOS inherited things from older DEC operating systems like TOPS10 (which IIRC was the first occurrence of using /forwardslash options on the command line).
Many are under the mistaken impression that DOS got forward slash as an option character from CP/M or from IBM. CP/M denoted options using square brackets, and IBM mainframe operating systems generally indicate options using parenthesises. (The usage of square brackets appears to be a modification of IBM's approach; I'm not sure why Gary Kildall made that change – before creating CP/M, he'd used IBM CP/CMS at the Naval Postgraduate School, which likely influenced both this option style, and also drive letters.)
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.)
The Windows XP source tree contained various Unix utilities that were ported to Windows. Eg, Perl, Vim, Vi, wc, cat. Imagine if they made it into the public release.
Yeah, I'd say that the network stack on Windows is not an example of a non-Unix design. And there are other bits and pieces here and there that show some Unix influence (like the hierarchical filesystem). But on the whole, one can clearly see that Windows comes from a different heritage.
Do you have a reference for that? I remember that many TCP implementations were derived from the Reno/Tahoe tape source, but I hadn't heard until now that this included NT. Iirc that work was done by or managed by J Allard.
As I remember it (and I had no inside knowledge, I was just an application developer who read the docs), NT was supposed to have 3 distinct APIs for interfacing with the kernel: the POSIX subsystem, Win32 for compatibility with Windows and the next-generation OS/2 API.
Then IBM and MS fell out and Microsoft put all their resources into Win32.
Windows NT was originally supposed to be "OS/2 3.0". Then IBM and Microsoft broke up.
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.)
Interix wasn’t really a “replacement” - the fundamental architecture hadn’t changed, PSXSS was still there. From what I understand, Interix was just an enhanced version of the original POSIX subsystem code, not some from-scratch rewrite.
I thought that Softway replaced the Microsoft PSXSS with their own but I don't remember for sure. This would be an interesting software archaeology rabbit hole to go down.
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.
Wasn’t their updated version a modified version of Microsoft’s original code, which they’d licensed from Microsoft? If that’s true, their PSXSS was more an enhancement than a “replacement”.
That would be interesting to know. I’d love to see a copy of OpenNT. Sadly, my experience started with it when it was already Interix. I know Microsoft negotiated source access/licensing with various third-party software “manufacturers” (like w/ Citrix for WinFrame). I’d love to know more about the arrangement with Softway.
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.
notably the actual subsystem wasn't really necessary, if i recall it was some poorly implemented stubs for unix style system services. (think a backend for functions like getpwnam)
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.
The subsystem absolutely was necessary to properly support fork(). The NT kernel has always supported fork, but Win32 can’t cope with it, forking confuses CSRSS.EXE. POSIX/Interix/SFU/SUA used PSXSS.EXE instead which wasn’t upset by forked processes.
that's right, i remember now fork being an issue. wasn't that much of a deal if i recall as most programs only really used it to spawn new processes by calling exec immediately after. (so, just swap it for CreateProcess)
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.
exec() was another problem - NT doesn’t support reusing a process for a new executable; exec() on Windows actually spawns a child process and then exits the parent - which means exec() changes your PID.
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).
Posix compliance was/is often a checkbox requirement to bid for government contracts. So M$ tossed in the Posix subsystem so they didn't get excluded based on that requirement. Lots of otherwise odd features in software fall into this category (See also: Apple A/UX).
Because ITS had the debugger (DDT) as its shell. ITS wasn't the only OS for which this was true – the same was effectively true of CTSS, since in CTSS debugging was just a bunch of shell commands, not launching a separate "debugger". The same is true of IBM VM/CMS, for assembler-level debugging. (Source-level debugging is launching a separate program though.)
One interesting feature of ITS – it had an API (called "valret" or "valretting") which an application could call to run a command in the OS shell/debugger. So basically, an application could actually control the debugger debugging it. I've never seen any other system with that feature, although it is possible to implement with GDB – allocate a buffer, put a "run this debugger command" request in it, pass it to a dummy function which just returns. Have a "processed" flag in the buffer – without a debugger, calling the dummy function will not set that flag, so the app knows no debugger is listening for commands. Now write a GDB Python script which sets a breakpoint on that function, when the breakpoint is hit, it reads the command out of the buffer, runs it, optionally writes any reply back in to it, sets the processed flag, then continues execution. The app can see the processed flag is set, so it knows the command was run, and can read any reply from the buffer too.
MS-DOS COMMAND.COM did have a somewhat related feature – INT 0x2E. You could use that to tell COMMAND.COM to run a command. The difference from e.g. the C system() function, is it didn't spawn a child process COMMAND.COM to run the command, it told the parent COMMAND.COM to run it for you.
(What many people today don't understand about MS-DOS, is it was actually a pseudo-multitasking operating system – it supported having multiple processes in memory at once, unlike many earlier operating systems such as CP/M which only supported having a single process in memory at a time – but unlike an actual multi-tasking OS, only the single foreground process could actually be running, all other processes would suspended – so you could have a child process, but you'd be suspended while it ran. An ancestor process could export an API to its descendant processes by installing an interrupt handler, which is exactly what COMMAND.COM did by installing a handler for INT 0x2E.)
I think that a lot of interesting things were lost from the IBM and DEC operating systems. I spent a lot of time in VM/CMS and VAX/VMS (plus a little MVS/TSO) in the 80s and 90s and my Unix time was relatively limited for quite a while. I really liked Rexx (IBM’s scripting language which they brought into OS/2, but is now pretty much dead) and the way that command line options for VMS programs could be declared independently of the executable (plus its automatic allowance for abbreviations where you could abbreviate a command to its minimal unique prefix).
> you still can't trivially freeze a process, tweak a few values in its memory, and resume it as an integrated part of the operating system
Can't you do this with gdb? Running `gdb -p <some pid>` pauses that process, you can inspect it, set breakpoints, change values in memory, etc, and then resume execution.
* ITS (of PDP-10 hacker fame) - processes could debug and introspect their child processes. The debugger was always available, basically. The operating system provided support for breakpoints, single-stepping, examining process memory, etc. Whatever part of the system manages processes, is the natural place for the debugger, really. Some of this has been brought to Unix systems recently, but you still can't trivially freeze a process, tweak a few values in its memory, and resume it as an integrated part of the operating system. Why not? It seems very basic to an operating system, now that I think about it.
* KeyKOS (developed by Tymshare for their commercial computing services in the 1970s) - A capability operating system. If everything in UNIX was a file, then everything in KeyKOS was a memory page, and capabilities (keys) to access those pages. The kernel has no state that isn't calculated from values in the virtual memory storage. The system snapshots the virtual memory state regularly. There are subtle consequences from this. Executing processes are effectively memory-mapped files that constantly rewrite themselves, with only the snapshots being written out. Snapshotting the virtual memory state of the system snapshots everything -- including the state of running processes. There's no need for a file system, just a means to map names to sets of pages, which is done by an ordinary process. After a crash, processes and their state are internally consistent, and continue running from their last snapshot. For those who are intrigued, there's a good introduction, written in 1979, by the system's designers available here: http://cap-lore.com/CapTheory/upenn/Gnosis/Gnosis.html (It was GNOSIS before being renamed KeyKOS.) And a later document written in the 90s aimed at UNIX users making the case: http://cap-lore.com/CapTheory/upenn/NanoKernel/NanoKernel.ht... Some work on capability systems continues, but it seems the lessons learned have largely been forgotten.