Recitation 10: Starfork

In this example, we will study the starfork program. You can obtain the skeleton code on CLAC:

git clone ~j-hui/cs3157-pub/examples/starfork

When you run make in here, it will build nine different version of starfork, named starfork-s1 through starfork-s9. You should try to first predict the output of each part before running the executable to validate (or disprove) your hypothesis. Keep in mind that some of these parts are actually unpredictable, so you may need to run them multiple times to see different behavior.

Note that parts 8 and 9 are optional; signals and signal handlers will not appear on exams. However, you may find working through these parts helpful for understanding signal handlers, which you will need to use for your labs.

Part 1

For starters, let’s make sure you understand the skeleton code:

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

void star(int numstar) {
    if (numstar >= 100)
        exit(1);

    char star = '*';

    if (numstar < 0) {
        numstar = -numstar;
        star = '@';
    }

    char line[100];

    line[numstar] = 0;

    while (0 <= --numstar)
        line[numstar] = star;

    printf("%s\n", line);
}

int main(int argc, char **argv) 
{
    assert(argc == 2);
    int n = atoi(argv[1]);

    for (int i = 1; i <= n; i++) {

        // You can enable each code block below by defining S1, S2, etc.
        // at the preprocessing stage using -D option in gcc. For example,
        //
        //     gcc -Wall -g -D S1 starfork.c && ./a.out 3 
        //
        // will run the program with the code in S1 block. 

        // BEGIN MOD BLOCK

        star(i);

        // END MOD BLOCK
    }
}

How does this code behave? How many arguments does this program expect, and what do those arguments do?

In subsequent parts, we will modify the “mod block”, the portion of the code between // BEGIN MOD BLOCK and // END MOD BLOCK.

Part 2

Change the mod block to:

star(i);
fork();

Run it with different command line arguments, 1, 2, 3, etc.

  • Can you predict the output? How many lines are printed, and how many stars are printed?
  • Is the output always the same? And if not, are there any kinds of patterns you can find among possible outputs?

  • Is the following output possible:

    *
    **
    ***
    **
    ***
    ***
    ***
    
  • And what about this output:

    *
    ***
    **
    ***
    **
    ***
    ***
    
  • Sometimes you’ll see output that looks like this:

    $ ./starfork-s2 3
    *
    **
    **
    ***
    $ ***
    ***
    ***
    

    Note that stars were printed even after the shell prompt $ was shown. Why might this happen? (Hint: what is the parent process of starfork?)

Part 3

What about the following modification?

star(i);
fork();
star(i);
  • Try to predict the output with command line argument 1; identify which process prints each line.

  • What about with arguments 2 or 3? Are they predictable?

Part 4

Let’s add some synchronization by having the parent process wait for its child. What would be the output if you change the modification block to the following?

star(i);
pid_t pid = fork();
if (pid == 0) { // Child process
    star(i);
    exit(0);
}
waitpid(pid, NULL, 0); // No status, no options
  • Is the output predictable?
  • Could you rewrite this modification block to produce the same output, without using fork() and waitpid()?

Part 5

Now what about this version?

star(i);
pid_t pid = fork();
if (pid > 0) { // Parent process
    waitpid(pid, NULL, 0); // No status, no options
    star(i);
    exit(0);
}
  • Is the output predictable?
  • Explain the output of this program using a fork diagram or process tree. Which process prints each line?

Part 6

Now let’s understand what exec() does. How would the following block behave?

star(i);
sleep(1);
char *a[] = { argv[0], argv[1], NULL };
execv(*a, a);
printf("%s\n", "A STAR IS BORN");
exit(0);
  • Is A STAR IS BORN ever printed?
  • Are any new processes ever created?
  • How does the command line argument affect the behavior of the program, if at all?

Part 7

Now let’s see if you really understood exec(). What would be the output for the following block when you run starfork with arguments 2, 10, and 50?

star(n);
pid_t pid = fork();
if (pid == 0) { // Child process
    char buf[100];
    sprintf(buf, "%d", 2 * n);
    char *a[] = { argv[0], buf, NULL };
    execv(*a, a);
}
waitpid(pid, NULL, 0); // No status, no options
star(n);
exit(0);

Some hints and guiding questions:

  • How many loop iterations does each process execute?
  • Note that we call star() with n instead of i!
  • Try to justify your explanation with a fork diagram; when starfork executes itself, make a note of what argument it is called with.

Part 8 (optional)

Let’s install some signal handlers to detect when stars die. We will use the supernova() function as our SIGCHLD handler, and use setup_supernova() to install it.

void supernova(int sig) {
    int wstatus;
    while (waitpid(-1, &wstatus, WNOHANG) > 0)
        for (int i = WEXITSTATUS(wstatus); i > 0; i--)
            star(-i);
}

void setup_supernova(int sa_restart) {
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sigemptyset(&sa.sa_mask);
    sa.sa_handler = supernova;
    if (sa_restart)
        sa.sa_flags = SA_RESTART;
    sigaction(SIGCHLD, &sa, NULL);
}

In the beginning of our main() function, before the loop, we call setup_supernova(1) to install our supernova() handler (with SA_RESTART).

Then, in our mod block, we have the following code:

star(i);
pid_t pid = fork();
if (pid == 0) // Child process
    exit(i);

char c;
// Block until we can read a byte from stdin (fd=0)
read(0, &c, sizeof(char));

read() tells the parent process to block until it receives input from standard input; we can advance through each loop iteration by pressing enter. What is the output?

What happens if you type some extra characters before hitting enter? Keep in mind that your terminal will buffer characters before sending them to the standard input of the starfork program.

Part 9 (optional)

Now, we do the same thing as part 8, but we do not use the SA_RESTART flag by calling setup_supernova(0). The mod block is the same (aside from comments):

star(i);
pid_t pid = fork();
if (pid == 0) // Child process
    exit(i);

char c;
// Block until we can read a byte from stdin (fd=0)
// or we receive and handle a signal
read(0, &c, sizeof(char));

Then, SIGCHLD will interrupt the blocking read(). How does the behavior compare with the part 8?