30

I heard recently that the process model in very early variants of UNIX was quite a bit different to the fork/exec model used nowadays.

How did it differ from the current state?

2

2 Answers 2

36

If you search for the seminal 1979 paper from Dennis Ritchie, entitled The Evolution of the Unix Time-Sharing System, it covers this (amongst a few other things like incredibly difficult-to-use file-system links, only being able to create directories at boot time, and why the password file has a GECOS field).

First, we'll recap the current model. A process is one type of execution unit within UNIX while a program is a runnable item that lives within a process (when it's running). That distinction is important.

A running process that wants to start a new process will call fork and this gives you two nearly identical processes running the same program (at the same point), where only one existed before.

At that point, one of them (usually the child) may choose to exec a new program to perform some other work - this exec basically replaces the program in the current process with a whole new program.

Should the original process wish to wait until the child exits, it can call wait to do so.


The old model was a little similar but it only ever had a limited number of processes, one for each of the terminals hooked up to the machine. These processes were created at boot time and there was therefore no fork. A shell ran in each of these processes, interacting with the user on the given terminal.

When the user specified a program to run, the shell would:

  1. Create a link to the file in the current directory (this has to do with the "incredibly difficult-to-use file-system links" mentioned earlier).

  2. Open the file.

  3. Remove the link.

  4. Copy a small bootstrap program to the top of memory and jump to it.

  5. This bootstrap program would read in the already-open file over the current shell code, then jump to the first location of the command (exec).

  6. After the command had done its work, it called exit. But this isn't the exit we know and love nowadays. What this exit did was simply to reload the shell program into the process in much the same way as the shell had loaded the program in the first place.

At that point, you would be back in the shell, ready to type in another command. As you may imagine, this had no support for pipeslines/filters but, interestingly, had I/O redirection from a very early stage - all the shell had to do was connect the standard handles to specific files rather than the terminal device.

6
  • IIRC, though not in the initial release, pipes were trivial to add; trivial as in one person spending an afternoon coding (or something like that). Nov 22, 2018 at 7:22
  • ^^^ Citation needed. If anyone has a primary source to back up my assertion, kindly post below. Nov 22, 2018 at 7:39
  • 2
    @Alex, the paper states that: "Some time later, thanks to McIlroy’s persistence, pipes were finally installed in the operating system (a relatively simple job)". Not the bit about it taking an afternoon and presumably we had to wait until we had a proper fork (to get more than one process per terminal), but it says it was easy.
    – user6464
    Nov 22, 2018 at 8:19
  • Yea, I saw that mention in the paper. The bit about it taking an afternoon (or overnight) is I think from an oral history interview. Nov 22, 2018 at 8:28
  • 15
    Found it: "The basic redirectability of input-output made it easy to put pipes in when Doug McIlroy finally persuaded Ken Thompson to do it. In one feverish night Ken wrote and installed the pipe system call, added pipes to the shell, and modified several utilities, such as pr and ov … to be usable as filters. The next day saw an unforgettable orgy of one-liners as everybody joined in the excitement of plumbing." A Research UNIX Reader: Annotated Excerpts from the Programmer’s Manual, 1971-1986, M. Douglas McIlroy, p. 9 Nov 22, 2018 at 15:16
8

If you're interested in the early history of various features of Unix, you cannot go past the article The Evolution of the Unix Time-Sharing System by Dennis Ritchie, presented at a 1979 conference in Australia, and subsequently published by AT&T.

It covers a great many things like:

  • the absence of paths, and the use of "link gymnastics" required to get around this limitation;
  • the subsequent fact that there was no / path separator;
  • limited processes as discussed below;
  • the fact that there was no mkdir and why certain commands (such as chdir) have to be incorporated into the shell;
  • why the restarted shell was responsible for closing a program's file handles after exit (and re-opening its own).
  • lots of other trivia of interest to uber-geeks.

Now on to the specific question: the fork system call was not in the original (PDP-7) UNIX, though it didn't take that long to appear.

In the earliest UNIX, there were a fixed number of processes created at start-up time, one per terminal device that was attached to the machine.

You can discern this from the very early source code for Unix over at The Unix Heritage Society, specifically the source code archives and, in particular, the first edition.

Keep in mind this is likely the first edition for the PDP-11, which would have had much more functionality than the one described in Dennis Ritchie's paper. The terminal/process start-up code seems to be functionally identical but, if you look closely, it includes the system calls for fork and wait (hence this answer is more based on the paper than the source code).

This (slightly modified for readability) init.s file from that first edition shows how the fixed number of shell processes were created:

    mov    $itab, r1        / Put address of table into r1.
1b:
    mov    (r1)+, r0        / Get next entry from itab.
    beq    1f               / Finish loop if table end (0).

    movb   r0, ttyx+8       / Put symbol in ttyx.
    jsr    pc, dfork        / Call to make new init for this ttyx.
    mov    r0, (r1)+        / Save child id in word for '0, '1, etc.
    br     1b               / Go back for next itab entry.

1f:
    ...                     / All terminal processes now set up.

itab:
    '0; ..
    '1; ..
    0

There you can see the snippet which creates the processes for each of the two connected terminals. Note that these are the days of hard-coded values, no auto detection of terminal quantity was involved.

The zero-terminated table at itab is used to create a number of processes and hopefully the comments from the code explain how. The crux of the code shown simply processes the table, calling dfork to:

  • create a process for each terminal; and
  • start getty, the login program, in that process, connected to the related terminal.

The getty program, in turn, eventually started the shell (see below).


Once the shell was running in the process space for a specific terminal, the basic idea was that it would accept a command from the user and then the following would happen (simplified a little, but you should get the idea):

  • The shell would locate the program file.
  • It would then load a small bootstrap program into high memory and jump to it, passing enough details for the bootstrap code to load the program file.
  • The bootstrap code would load the program into low memory, overwriting the shell, and then jump to it.

There was no new process started at all, this is more akin to the exec function rather than fork.

When the program was finished, it would call exit much like modern programs. This exit call would perform the same steps as the shell did when it replaced itself with the new program (except it would, of course, replace itself with the shell).

Hence running programs was a continuous "swap between shell and other program, then swap back when done" method although there were ways to load another program directly without returning to the shell first. It's likely that's how the getty log-in program started a shell for you once it had accepted and verified your login details.


From this two-way exec between shell and another program in a single process, it was actually not that big a leap to adding fork/wait) as a process duplicator/reaper to work in conjunction. The Dennis Ritchie paper cited below states that "the PDP-7’s fork call required precisely 27 lines of assembly code", a truly astonishing achievement.

While many systems combine fork/exec into a single call (such as CreateProcess in Windows) , it's this "just add what's needed" method which is responsible for the separation of duties between fork and exec in modern UNIX (it also resulted in a very simple fork function).


4
  • When was getty introduced? In the very beginning, I believe init forked login. Getty seems superfluous with a couple of hardwired local teletypes. Or do I have that wrong?
    – dave
    Apr 30, 2022 at 15:24
  • 1
    After reading the code, it's clearly getty in the 1st ed. Nevermind.
    – dave
    Apr 30, 2022 at 18:28
  • @another-dave: yes, it's listed in the init.s as well though, interestingly, in /etc rather than /bin. However, keep in mind that's the code for the initial PDP-11 release, not the earlier PDP-7 one.
    – paxdiablo
    May 1, 2022 at 0:56
  • 1
    The PDP-7 version of init did 'login' itself and then forked the shell. No inbetweens.
    – dave
    May 1, 2022 at 2:21

You must log in to answer this question.