Lecture 9: Signals

Note: Reading these lecture notes is not a substitute for watching the lecture. I frequently go off script, and you are responsible for understanding everything I talk about in lecture unless I specify otherwise.

Pipes

Sending data between processes: what if the amount of data is variable?

Last lecture, we showed an example of the parent sending a fixed number of bytes to the child. The parent called write with exactly 6 bytes, and the child called read with exactly 6 bytes.

In practice, we often need to send data where we don’t know in advance how big the message will be. To do this, it is very common for the receiving process to read in a while (true) loop until all of the data has been read. We rely on two important properties of read:

With this in mind, we can repeatedly call read until we see it return 0, then break out of the loop (because there must not be any more input coming).

Closing file descriptors

Previously, we explained how close() is similar to free() in that it frees system resources (file descriptors occupy kernel memory), and you should avoid leaking file descriptors as much as possible. However, leaking file descriptors can be much worse than leaking memory, because if any write ends of a pipe accidentally remain open, that will cause deadlock (i.e. the program will hang, waiting forever).

This program is exactly the same as above, but with a close() call commented out such that the child process fails to close its write end of the pipe. Even though the parent calls close(), the child does not, so from the operating system’s point of view, it’s theoretically still possible for more data to go into the pipe in the future. Therefore, read() blocks, waiting for input, and does not return 0. The child process gets stuck on the read() call waiting for more data, and the parent process gets stuck on the waitpid() call waiting for the child process to exit. You can try running the program to see what the file descriptor tables look like.

Implementing subprocess

Last time, we implemented a library function called system that launches an executable in a child process and waits for it to complete. What if we want to run an executable, but we want to interact with it while it is running (e.g. feed it input, process its output, etc)?

Similar to system, subprocess launches an executable in a child process. However, instead of waiting for the child process to exit, it returns immediately, so that the parent can communicate with the child while it runs. It returns the PID of the child, as well as a file descriptor; if the parent writes to this file descriptor, the child will be able to read that data.

We can write some code that is almost exactly the same as system, but without the waitpid call, and with the addition of a pipe:

But we have a problem: what file descriptor is the executable (run in the child process) supposed to read from? In our test program, we end up with file descriptor 3 as the read end of the pipe, but if the sample test program calls open() for some unrelated file before calling subprocess(), then we will end up as file descriptor 4 as the read end of the pipe. We have no guarantees on what the pipe file descriptors will end up being, and since the executable is going to run the same code every time, we need some deterministic file descriptor to read from.

This is what “standard in,” “standard out,” and “standard error” are. Stdin is a hardcoded file descriptor (0) that a process reads input from, as standard practice. Stdout is a hardcoded file descriptor (1) that a process writes output to, as standard practice. Usually, all three of these file descriptors point to the terminal, but in situations like these, we can make them point elsewhere (e.g. stdin can point to the read end of the pipe, so that when the executable reads from file descriptor 0 as it’s hardcoded to do, that comes from the pipe).

dup2

The dup2 syscall rewires a file descriptor to point somewhere else:

int dup2(int source, int destination);

You can think of the parameter names like this:

int dup2(
    int fd_pointing_to_the_thing_we_want_to_point_to,
    int fd_we_want_to_change_to_point_to_the_first_argument);

Updating subprocess

We can update subprocess so that the child process’s stdin points to the read end of the pipe:

subprocess_t subprocess(const char *command) {
    int fds[2];
    pipe(fds);
    subprocess_t process = { fork(), fds[1] };
    if (process.pid == 0) {
        close(fds[1]);  // The child isn't writing to the pipe, so we can close
                        // the write end
        dup2(fds[0], STDIN_FILENO); // Rewires fd 0 to point to the read end of
                        // the pipe. The read end of the pipe now has 2 file
                        // descriptors pointing to it.
        close(fds[0]);  // Now that STDIN_FILENO points to the read end of the
                        // pipe, we can close this extra file descriptor.

        // Start the target exectuable:
        char *argv[] = {"/bin/sh", "-c", (char *) command, NULL};
        execvp(argv[0], argv);
    }

    // The parent isn't reading from the pipe, so we can close the read end:
    close(fds[0]);

    return process;
}

Finished and working version on (cplayground).

Recap

System calls:

Library functions:

If you’re curious, I taught another lecture in a different class about common pitfalls in multiprocessing, and why I think you should use higher-level functions like system and subprocess instead of calling fork whenever possible: video, slides

Signals

A signal is a way to tell a process that something has happened (e.g. the user has pressed CTRL+C, a segfault has happened, a new file was created on disk, etc.). Signals are very primitive in that there is no associated data attached to a signal, and you can’t even tell who sent the signal – you can only tell that the thing has happened.

When a signal comes in, that runs a signal handler, which is just a function to react to the thing that happened.

Common signals

Signal handlers

Signal handlers are ordinary functions that we ask the operating system to run whenever a signal comes in. The signal function registers a signal handler, asking the OS to run our function for a specified signal. Signal handlers take a single argument, which is used to indicate which signal it is handling.

For example:

This is incredibly cool and useful, as it gives us the power to do multiple things concurrently within a single process. We can be in the middle of doing something, then have a signal come in, hop over and take care of whatever happened, then go back to what we were doing. Imagine working on your homework, and then someone calls you, so you hop over to take the call, then go back to what you were doing: that sort of thing is enabled by signal handlers such as these.

Perils of traditional signal handling

Unfortunately, signal handlers are extremely easy to misuse, and the consequences can be severe. Many regard signals to be one of the worst parts of Unix’s design; for interesting reading, the Ghosts of Unix Past series is an excellent read. You can also check out a lecture video I gave for a different class.

The main challenge of using signal handlers is preventing them from touching variables that are currently being used by the main body of execution.

To give you an analogy, once, I was cutting my hair with electric hair clippers when my little brother knocked on the door and wanted to see what I was doing. I showed him how the clippers and detachable guards worked, then went back to cutting my hair. But when I was talking to him, I removed the guard, and I went back to cutting my hair without remembering that, so I cut off all my hair…

Something similar can happen with signal handlers:

vector<string> stringsToProcess;

void sigintHandler(int sig) {
    // Clear out stringsToProcess so we can shut down
    stringsToProcess.clear();
}

int main() {
    stringsToProcess = ...
    signal(SIGINT, sigintHandler);  // call sigintHandler when SIGINT is received

    for (string &s : stringsToProcess) {
        // DANGER: what if sigintHandler were to run in the middle of this
        // loop? We would end up trying to access deleted memory, which is
        // undefined behavior
        processString(s);
    }
}

Signal handlers are especially dangerous because you can’t clearly see where all these conflicts are. This code is spectacularly unsafe, and could theoretically cause anything to happen (from a segfault to deletion of all files on disk) if you get unlucky:

void sigintHandler(int sig) {
    printf("Got SIGINT!\n");
}

int main() {
    signal(SIGINT, sigintHandler);

    while (true) {
        printf("Sleeping...\n");
        sleep(1);
    }
}

Here’s a screen recording of an extended version of this program going terribly wrong.

You can’t tell that anything is wrong just from looking at this, but if you know how printf is implemented, then you know that it:

Expanded, the above code looks more like this:

char outputBuffer[OUT_BUF_SIZE];

void sigintHandler(int sig) {
    append "Got SIGINT!\n" to output buffer
    if newline in buffer:
        flush to stdout
}

int main() {
    signal(SIGINT, sigintHandler);

    while (true) {
        append "Sleeping...\n" to output buffer
        if newline in buffer:
            flush to stdout
        sleep(1);
    }
}

This has a similar problem to the previous stringsToProcess example, where the signal handler clashes with the main body of execution if the signal handler ends up running at an unfortunate time.

There are tools to control when signal handlers execute in order to avoid these “race conditions,” but these functions are tricky to use, filled with nuance, and they are completely ineffective when using external libraries. You cannot call printf, malloc, or free from a signal handler, and that means you also can’t use anything from the C++ standard library such as std::string or std::vector. The things you can safely do in a signal handler function are so limited that I don’t think it’s worth writing them at all.

Synchronous signal handling

To safely handle signals, I strongly feel the best option is to avoid the concurrency you get when using signal handling functions, and instead focus on one thing at a time. Keep one logical train of thought without jumping around due to interruptions. Write code like this:

main:
    do my homework
    wait for my partner to finish the problems
    check answers

Or like this:

while true:
    do some work
    handle any email notifications that came in

We can accomplish this in three steps.

  1. First, we create a set of signals that we are interested in handling:
    // Create a set of signals:
    sigset_t signals;
    // Initialize it to the empty set. This is IMPORTANT, otherwise the set will
    // contain random signals based on whatever garbage stack memory happened to
    // be at &signals before
    sigemptyset(&signals);
    // Add the signals of interest:
    sigaddset(&signals, SIGINT);
    sigaddset(&signals, SIGTSTP);
    
  2. Next, we tell the operating system to hold off on normal delivery of these signals. We don’t want the default behavior of the signals (e.g. by default, SIGINT, will terminate the process, and we don’t want that), and we don’t want to run any signal handling functions… We just want the operating system to hang onto these when they come in so that we can deal with them later.
    sigprocmask(SIG_BLOCK, &signals, NULL);
    
  3. Finally, we can wait for a signal to come in by using sigwait. This waits for any signal in our set to be received, and it returns the received signal via the second parameter:
    int received;
    sigwait(&signals, &received);
    printf("Received signal %d\n", received);
    

You can try out this simple demo, clicking the terminal and pressing ctrl+c and ctrl+z to send SIGINT and SIGTSTP.