Lab 4 Solutions

These questions were written by Jerry Cain and Ryan Eberhardt.

Before the end of lab, be sure to fill out the lab checkoff sheet here!

Problem 1: Incorrect Output File Redirection

The publish user program takes an arbitrary number of filenames as arguments and attempts to publish the date and time (via the date executable that ships with all versions of Unix and Linux). publish is built from the following source:

static void publish(const char *name) {
   printf("Publishing date and time to file named \"%s\".\n", name);
   int outfile = open(name, O_WRONLY | O_CREAT | O_TRUNC, 0644);
   dup2(outfile, STDOUT_FILENO);
   close(outfile);
   if (fork() > 0) return;
   char *argv[] = { "date", NULL };
   execvp(argv[0], argv);
}
 
int main(int argc, char *argv[]) {
   for (size_t i = 1; i < argc; i++) publish(argv[i]);
   return 0;
}

Someone with a fractured understanding of processes, descriptors, and file redirection might expect the program to have printed something like this:

$ ./publish a.txt b.txt c.txt d.txt
Publishing date and time to file named "a.txt".
Publishing date and time to file named "b.txt".
Publishing date and time to file named "c.txt".
Publishing date and time to file named "d.txt".

However, that’s not what happens. You should step through this cplayground to visualize what is happening.

Problem 2: Implementing timeout

timeout is a helpful program that will run a command, killing it with SIGKILL if it exceeds a specified duration. The timeout program exits with the same return code as the child, or 128 + terminatingSignalNum if the child was killed by a signal:

🍉 ./timeout 2 sleep 1
🍉 echo $?
0
🍉 ./timeout 2 sleep 3
👾 echo $?
137
# ^ 128 + 9 (SIGKILL)

Note: the real timeout that ships with Linux sends SIGTERM by default and exits with status 124 if the child timed out, but for simplicity, we’ll use the specification above.

In this problem, you’ll implement your own version of timeout. Take a look at the starter code in this cplayground. The starter code parses a duration and NULL-terminated childArgv from the command line arguments. Note that command line arguments are set in the settings sidebar (click the gear icon in the top left).

Your job is to:

  1. Start a child process running childArgv
  2. Right after spawning the child, use the alarm syscall to tell the OS to send you SIGALRM at a time duration seconds in the future
  3. If SIGALRM comes in before the child process exits, terminate it with SIGKILL
  4. Wait for the child to exit, and make sure the parent exits with the same status as the child (or 128 + signalNum if the child was killed by a signal – use WIFSIGNALED and WTERMSIG)

You can assume the child won’t stop/continue, so you don’t need to think about WUNTRACED or WCONTINUED, and you don’t need to worry about handling multiple SIGCHLD signals.

Solution: (cplayground here)

int main(int argc, char *argv[]) {
    if (argc < 3) {
        cerr << "Usage: " << argv[0] << " DURATION COMMAND [ARG]..." << endl;
        exit(1);
    }
    
    int duration = stoi(argv[1]);
    const int childArgc = argc - 2;
    char *childArgv[childArgc + 1]; // leave room for NULL terminator
    memcpy(childArgv, argv + 2, childArgc * sizeof(char*));
    childArgv[childArgc] = NULL;
    
    sigset_t signals;
    sigemptyset(&signals);
    sigaddset(&signals, SIGCHLD);
    sigaddset(&signals, SIGALRM);
    sigprocmask(SIG_BLOCK, &signals, NULL);
    
    pid_t pid = fork();
    if (pid == 0) {
        // Make sure the signals aren't blocked in the child
        sigprocmask(SIG_UNBLOCK, &signals, NULL);

        execvp(childArgv[0], childArgv);
        cerr << childArgv[0] << ": Command not found" << endl;
        exit(1);
    }
    
    alarm(duration);
    int received;
    sigwait(&signals, &received);
    if (received == SIGALRM) {
        kill(pid, SIGKILL);
    }

    int status;
    waitpid(pid, &status, 0);
    return WIFEXITED(status) ? WEXITSTATUS(status) : 128 + WTERMSIG(status);
}
Note that SIGCHLD and SIGALRM must be blocked before fork() or alarm() so that if we are pulled off the CPU immediately after fork() or alarm() and one of those signals happens to come in, we don’t miss the signal. The signals must be unblocked in the child before execvp so that the program being run isn’t blocked from receiving SIGCHLD or SIGALRM.