January 19, 2024

operating systems very different from ours

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 with CreateProcess.

  • 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 or exec. 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.

© 2020-24 Bronx River Software

Powered by Hugo & Kiss.