Skip to content

Overview: generator.py

This tool, generator.py, allows the user to create little C programs that exercise fork in different ways so as to gain better understanding of how fork works.

A sample usage is just as follows:

prompt> ./generator.py -n 1 -s 0

The output you will see when you run this is a randomly generated C program. In this case, you will see something like this:

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <unistd.h>

void wait_or_die() {
    int rc = wait(NULL);
    assert(rc > 0);
}

int fork_or_die() {
    int rc = fork();
    assert(rc >= 0);
    return rc;
}

int main(int argc, char *argv[]) {
    // process a
    if (fork_or_die() == 0) {
        sleep(6);
        // process b
        exit(0);
    }
    wait_or_die();
    return 0;
}

Let’s understand this code a bit. The first part (from the top, up to the beginning of main()) will be included in every generated C program. The two pieces of code, wait_or_die() and fork_or_die(), are just simple wrappers to the wait and fork system calls, and either succeed (as they usually will) or detect an error (by checking the return code, stored in rc) and exit, via the assert() call. The wrappers are useful when it’s OK to simply exit upon failure (which it is here, but not always), and make the code in main() easier to read.

Aside: assert(), if you’re not familiar with it, is a macro that simply checks the truth of the expression you pass to it. If the assertion is true, assert() simply returns and the program continues. If it is false, the process will exit.

The interesting part of the code, which changes with different random seeds, is found in main(). Here we see the main process, which we will refer to as “process a” (or just “a” for short) start, call fork_or_die() to create another process, and then wait for that process to complete (by calling wait_or_die()).

The child process (called “b”) just sleeps for some period of time (here, 4 seconds) and then exits.

The challenge for you, then, is to predict what the output will look like when this program runs. As usual, we can get the result simply by using the -c flag:

prompt> ./generator.py -n 1 -s 0 -c
  0 a+
  0 a->b
  6      b+
  6      b-
  6 a<-b
prompt> 

The way to read the output is as follows. The first column shows the time when certain events take place. In this case, there are two events that happen at time 0. First, process a starts running (shown by a+); then, a forks and creates b (shown by a->b).

Then, b starts running and immediately sleeps for 6 seconds, as shown in the code. Once this sleep is done, b prints that it has been created (b+), but it doesn’t do much; in fact, it just exits, which is shown as well (b-). These are shown to both happen at time 6 in the output; however, in reality, we know that b+ happens just before b-.

Finally, once b has exited, the wait_or_die() call in its parent process, a, returns, and then a final print out takes place (a<-b) to indicate this has happened.

A number of flags control the randomly generated code that gets created. They are: * -s SEED - different random seeds yield different programs * -n NUM_ACTIONS - how many actions (fork, wait) a program should include * -f FORK_CHANCE - the chances, from 1-99 percent, that a fork() will be added * -w WAIT_CHANCE - same, but a wait() (of course, there must be an outstanding fork for this to be called) * -e EXIT_CHANCE - same, but the chances the process will exit * -S MAX_SLEEP_TIME - the max sleep time that is chosen when adding sleeps into the code

There are also a few flags that control which C files get created for the code: * -r READABLE - this is the file shown to you (and optimized for readability) * -R RUNNABLE - this is the file that will be compiled and run; it is identical to the above but adds print statements and such

Finally, there is one other flag, -A, that lets you specify a program exactly. For example:

prompt> ./generator.py -A "fork b,1 {} wait"

The resulting C code:

int main(int argc, char *argv[]) {
    // process a
    if (fork_or_die() == 0) {
        sleep(1);
        // process b
        exit(0);
    }
    wait_or_die();
    return 0;
}

This command creates the default process (“a”), which then creates “b” which sleeps for 1 but doesn’t do anything else; in the meanwhile, “a” then waits for “b” to complete.

More complex examples can be created. For example: * -A "fork b,1 {} fork c,2 {} wait wait" - process “a” creates two processes, “b” and “c”, and then waits for both * -A "fork b,1 {fork c,2 {} fork d,3 {} wait wait} wait" - process “a” creates “b” and then waits for it to complete; “b” creates “c” and “d” and waits for them to complete.

Read through and do the homework questions to gain a fuller understanding of fork.