Every so often an argument comes up about how in Unix you have to
fork
(create a duplicate of the running process) and then exec
(replace the running process with another program) where in Windows
that’s just one step, CreateProcess
(create a new process running a
new program).
Sure the Unix way is more flexible - it let the shell build in I/O
redirection and pipes in the early ‘70s by just manipulating the file
descriptors between fork
and exec
, no additional kernel support
needed. Then in the ‘80s when TCP/IP became a thing, that was the
logical way to write a server: accept a connection from a client, do a
fork, and the new process deals with the client while the original
goes back to listening for more. But fork
is weird - it takes no
arguments so the only thing it can do is clone the entire process with
all its memory and open files, and both processes immediately running.
That puts you in a position where it’s easy to screw up, and we
haven’t even gotten to the nightmare of combining forking and
multithreading.
Some have added new kernel calls to get around the limitations of only
having one way to fork. The BSD heresy addition vfork
halts the
parent process until the child process calls exec
or quits, and
meanwhile the child process is limited in what memory it can access.
Initially done for purposes of efficiency (no need to bother copying
those memory pages that can’t be read before they’re overwritten) it’s
stuck around as a way to avoid pitfalls that a pure fork creates.
Then POSIX introduced posix_spawn
for compatibility purposes with
non-Unix OSes implementing the API. It works atomically, more-or-less
the same way as Windows CreateProcess
, and like that call it has so
many options, it’d make Rob Pike spin in his grave if he weren’t still
alive.
But there used to be a much bigger universe than just Windows and Unix, and although those OSes may not see much use anymore there still may be something to learn from them. I started to do some research on how older OSes did this. It wasn’t easy, given the differences in terminology and the fact that everything is written in assembly language for computers discontinued before I was born - I have enough trouble with x86 and ARM. So I’m not sure I totally get what was going on, and some of this may be misrepresented, but here’s what I’ve found:
In CTSS, an ancestor to almost all other operating systems, there was one process per terminal - and back then there were only physical terminals, almost all of them typewriters. There was no way to fork outside of what we’d now call the kernel, but several calls allowed a program to “chain” with other programs. From this we get
RUNCOM
, a user-space command that ran other commands in sequence, the ancestor to shell scripting and the reason why so many configuration files still have names ending with “rc”.The Berkeley “Project Genie” OS for the SDS 940 had some influence on Unix, having been used by Ken Thompson as a student. It pioneered device files, standard input and output as file descriptors 0 and 1, and a kernel call to “fork” the current process. It appears that the call to create a fork was, in at least some versions, restricted so that only the “Executive” (shell) could create forks. Confusingly, the documentation tends to refer to system calls only by their numbers even though they had names, so maybe I’m missing something here.
Project Genie also influenced TENEX, a public domain operating system written by BBN for the PDP-10 (later made into a commercial product by DEC and renamed TOPS-20). I say “influenced” but in fact almost all of the design of TENEX came from there, including features like command completion and long filenames that only made it to Unix years later. Here the “fork” call is unrestricted and sometimes used in user processes; TENEX was very common on ARPANET and its server software forked for each client, much like TCP/IP daemons would later do on Unix. Interestingly, the fork call takes some parameters - the new fork could be either a clone of the existing one or an empty process that wouldn’t start running until the parent loaded a program into it, and there were other minor options.
While we’re at DEC - VMS was a strong influence on Windows (they were designed by many of the same people), and unsurprisingly it has a
CRTPRC
call that Windows imitated withCreateProcess
.ITS, known for originating Emacs and Scheme and the Jargon File, had an advanced job-control system but a relatively low cap on the number of processes. Apparently a parent process obtained a child process from a “user device” file and could then load a program into it with a kernel call. Each user could only have one process with a given name; if you try to create a “new” process with the same name as an existing one, you’d get the handle to the existing process of that name instead. Since ITS was never a commercial product, but a research project by weirdos at MIT, there’s little in the way of “official” documentation and the online Info files may not accurately describe how things actually worked, so I might be missing something big here.
Multics, also developed at MIT (with help from some people at Bell Labs before they dropped out and ended up becoming the Unix team), was the professional, bureaucratic rival to the weird hippiedom of ITS. This has the strangest process model I’ve seen: every command is also callable as a function in PL/I (the main programming language in which Multics was written) and executing a command is identical to dynamically loading a library. There is only one process per user session and no equivalent to
fork
orexec
. By most accounts, this didn’t work well in practice. Command functions were awkward to write in PL/I, with all the arguments needing to be strings and a command in the shell needing to explicitly populate all the arguments. Experience with this might have led to the “argv” interface in C becoming the standard way to write a program accepting arguments in Unix, and eventually everywhere else.I tried to read up on IBM mainframe OSes but got completely lost quickly. The terminology is just too different, plus there’s just a lot more material to get through before you reach the information you want - and there were a ton of different OSes that shared terminology and a command interface but were radically different internally. I did manage to learn that before a file system was implemented, IBM mainframe programs could address individual tracks and sectors of hard disks as if they were files (or, I guess, decks of punched cards)… so whatever they have instead of fork/exec is probably equally alien in origin, and has 60 years of accumulated cruft atop it.
Of these, I think the TENEX approach to forking is most interesting,
and worthy of further investigation. Today TENEX as a whole is mostly
forgotten - it’s literally a footnote in Thompson and Ritchie’s Turing
Award presentation, and there are some references buried deep in the
documentation of older Unix programs like tcsh
and alpine
, both of
which are also increadingly forgotten nowadays. It was certainly a
big deal at the time, though, and there’s much to be learned from
history.
A bigger lesson I took away from this little research session is that
the past, even the recent past, is a foreign country. People
sometimes talk about Unix inventing shell scripting or device files or
hierarchical file systems, none of which it invented. You rarely hear
about how different the Unix ld
command is from what existed before.
In other OSes there was a “load” command that loaded object files and
libraries into memory, linking them as needed, and then you needed
further commands to do something with this program. Either run it, or
inspect it with a debugger, or dump it to disk, where it could be
reloaded and run later. (In ITS this approach is pushed further - the
debugger is also the command shell and any running programs can be
immediately debugged by hitting ^Z to suspend it.) In Unix you don’t
have a “current program” in a shell session and ld
loads, links, and
immediately dumps to an executable file. That file is immediately
runnable as a command, no extra steps are needed, whereas in other
OSes a “command” was a specific kind of program that needed special
preparation - if adding external commands was possible at all. It’s a
completely different way to think of the relation between the “system”
and individual programs than we’re used to in the Windows/Unix
duopoly.
Anyway, I’m not quite sure what side of the fork debate I’m on, but I’m pretty sure I’m against doing whatever the hell Mutics did.