Introduction

Hello, and welcome back to CS631 "Advanced Programming in the UNIX Environment". When we spent some time talking about job control in our last video, we saw several ways that the shell can communicate with the background and foreground process groups by way of the controlling terminal. We also saw several examples of processes being able to send a signal to each other, a very simple way of asynchronous interprocess communications, whereby one process sends the other a very small message, something along the lines of "please suspend yourself" or "please continue running". So in this video, we will pick up from where we left off there and talk in much more detail about Unix signals. Here's this video segment in a single slide. Basically, signals are a way for the computer to tell your process that something happened, and most of the time that's not good news. In fact, more often than not your process will be terminated as a result of receiving a signal.

Signals summarized

But not always, so let's perhaps take a closer look. Signals are a way for a process to be notified of an event. The single most important aspect of signals is that they are _asynchronous_, and thus unpredictable. You don't know when they will occur, nor even _that_ they will occur; they _can_ occur at any time or not at all. We've seen a number of examples, and at the very least you all have been using the SIGINT signal countless times yourself. This signal, like several others we've already seen, are generated by the terminal driver when a certain keyboard combination is pressed. Amongst those, we count, for example, SIGTSTP, which we talked about in our last video. We also saw some signals being generated by processes without our help, such as when a background process wanted to perform I/O on the terminal. Other things that may lead to a signal being generated are a timer goes off, a user disconnects from the controlling leader, causing SIGHUP to be delivered to the session leader, or a user resizing a window, requiring the visual editor to redraw the screen, and many others. You can find a more complete list of signals in the signal manual page. Depending on your version of Unix, the details may be described in different sections, however. So other than keyboard combinations, there are also certain hardware exceptions, such as the ever popular segmentation fault SIGSEGV, or a divide by zero, say. or some software conditions, such as when you try to write into a pipe where the reading consumer process has been terminated. We can deliver any signal to any of our processes via the kill(1) command, which you will not be surprised to find out is implemented using the kill(2) system call, which... ...looks like so.

kill(2)

The usage of this syscall is trivial if you are passing in a valid PID, but you may also wish to send a signal to multiple processes, such as to all processes in your current process group. To do that, pass in a PID of 0. Now what happens if you pass in a process ID of negative one? POSIX doesn't define this, but both BSD derived systems and Linux implement this such that if you're the superuser, then the signal is delivered to all processes except for certain system processes (init, swapper, pagedaemon) and the current process. That is, this allows you to bring the system into a state where you can debug or re-bootstrap the system without being logged out or terminating the process. if you're not root, then the signal is delivered to all of your processes, except for the calling process. Now Linux also supports another special behavior: if you pass in a negative number less than -1, then the signal is delivered to all processes in the process group of that positive PID. Note that this is obviously not portable. Finally, it's also worth noting that you can pass 0 as the signal number, in which case the kill(2) syscall will simply return 0 if the process exists, and -1 otherwise. That is, you can easily check whether a given process exists in this way, without actually delivering a signal to the process.

Handling signals

So when such a signal is delivered to your process, what can you do? Well, here's the lazy way out. Don't do anything in your code, and you'll get whatever the default behavior is for the given signal. Note that in most cases the default action is to terminate the current process, so that may not always be what you want. Remember, for example, our simple shell from our first lecture -- because we didn't do anything about signal handling, if the user hit Ctrl+C and delivered SIGINT to the shell, it would be terminated. So a better solution may be to explicitly instruct your code to ignore the signal altogether. That is, ignoring a signal requires an explicit action. But you may also decide that you'd rather do something else whenever the signal occurs. To do that, you specify a function to call. That is known as "installing a signal handler". Finally, you also have the option to say "not now" by blocking the signal from being delivered. This is different from ignoring the signal in that you can then unblock the signal at a later point and then see whether any such signal did get delivered and then have it take either of the above three actions when you're ready. We'll see some example of this in a little bit. So how do we tell our process what we want to do about a given signal?

signal(3)

Well, we call the appropriately named "signal(3)" function, which has this prototype shown here. By the way, if you ever write "I know C" on your resume, you can bet that somebody is going to ask you to explain this function prototype in an interview. Can you tell what this function returns and what its arguments are? If not, and even if you just want to simplify things a bit, you can use this variation: That is, we typedef a "sig_t" to be a pointer to a function that takes an 'int' as an argument and that returns void. With that typedef, you can then write the signal(3) function prototype as shown here. That is, the signal(3) function takes as arguments an integer as well as a sig_t and returns a sig_t. Or it's a function that takes as arguments an integer as well as a pointer to a function that takes an integer as its argument and that returns void, while signal(3) itself returns such a pointer to a function taking an in integer and returning void. Specifically, what it returns is the previous function handler.

A simple signal(3) example

Here, let's take a look at a simple example to illusrtate how you'd call the signal(3) function. Here we see a function that will be our signal handler -- sig_usr. In 'main' we install this signal handler for the SIGUSR1, SIGUSR2, and SIGHUP signals, print our current PID, and then pause(3) forever, waiting for signals to be sent to us. Before we run this program, let's create a second window so we can observe the program while sending signals to it. Ok, so now we have our program with PID 1021. Let's send it SIGUSR1 using the kill(1) command. And there we go. Now SIGUSR2. That works, too. What happens if we send it a signal that we didn't set up a signal handler for? Well, then the default action takes place. In this case, we're lucky -- the default action for SIGINFO is to do nothing. If we send SIGHUP, then our signal handler will terminate the program. So this was a simple example of using the signal(3) function. But you may have noticed that signal(3) is a library function, not a syscall, which means that there's a different syscall that can be used to implement this function.

sigaction(2)

And that syscall is... ...sigaction(2). The sigaction(2) syscall allows for more comprehensive handling of signals, which is necessary since signals, due to their asynchronous nature, can be a bit weird. For example, while you're in a signal handler, the same signal that triggered this handler is being blocked, but another signal might come in, which means you're being transferred out of the current signal handler and into the signal handler for the new signal. If any of the signals that are currently blocked are delivered, you can then inspect those and see if that was the case. After you're done with your signal handler, the signal that triggered this handler will be unblocked automatically. If you did receive the same signal while you were in the handler, it is now delivered after you've returned, kicking you right back into that handler. If multiples of the same signal are triggered while the signal is blocked, then you won't receive each one of them one at a time after you're returning; instead, they are merged into a single signal, which is then delivered. Seeing how we can change how our process handles signals, we also need to think about what happens when we fork a new process or exec(3) a new program. Since fork(2) creates a full copy of the current process, all established signal handlers or other dispositions are of course inherited by the child, but if we exec(3) a new program, then the established handlers are reset to the default action, while signals that are explicitly being ignored continue to be ignored as well.

Code examples

All of these considerations may be a bit confusing when you first hear about them, so let's take a look at some examples, which I hope you will recreate yourself as well: Our first program, signals1.c, has two signal handlers: sig_quit and sig_int. sig_quit will print a message, then go to sleep, then print another message, each time showing the value of global variable. sig_int simply prints a message and returns. In 'main', we establish the signal handlers, then go to sleep, allowing the user to send us a signal of some sort. When we run this program, we hit Ctrl+\ to generate SIGQUIT and see us enter the sig_quite signal handler. After that signal handler returns, we hit Ctrl+\ again, again enter the signal handler, and then try to hit Ctrl+\ again. This time, though, nothing happens, because SIGQUIT is currently being blocked. But when our sig_quite function returns, it will immediately have another SIGQUIT signal delivered and thus re-enters the signal handler right away. If multiple such signals are generated, we still only re-enter the handler once. Now, let's run it again. Again, we enter the sig_quit handler, but this time, _while we're executing the sig_quit handler, we hit Ctrl+C, causing SIGINT to be delivered, which interrupts our sig_quit handler. After sig_int returns, sig_quit prints its message and returns. If we re-enter the sig_quit handler and then deliver another SIGQUIT signal, we know that will be blocked and later delivered, but if we then hit Ctrl+C, we again immediately jump into the sig_int handler, return, but _now_ immediately re-enter the sig_quit handler, as the blocked signal was now delivered. After all that, we exit. So this illustrated that we can be interrupted within a signal handler as well as that multiple blocked signals are merged into one and then delivered when unblocked. Now, let's look at signals2.c. This program is almost identical, only here we emulate the old behavior of a signal handler always resetting the disposition to the default upon each trigger. Everything else remains the same. Running it, we again enter sig_quit, and then, after we return from that signal handler, we again deliver the same signal. But since our signal handler had changed the handler to the default, the second SIGQUIT leads to the default action for SIGQUIT, which is to abort with a core dump. Groovy. Ok, let's try again. Enter sig_quit once more, now interrupt that handler with Ctrl+C, and again the next SIGQUIT will abort the program. Now a third try: Enter sig_quit, as before. Now generate a few more SIGQUITs that are being blocked. But once the signal handler returns, the merged signals get delivered, but at this time our signal handler had already reset the disposition to the default, so the single merged signal being delivered again triggers the default action. This shows that this old behavior is quite inconvenient, and fortunately nowadays the signal handler remains installed after the signal was delivered. Finally, let's take a look at a code example using sigaction(2): Our signal handlers themselves remain the same, but now we initialize a new signal mask and add SIGQUIT to it. Then we explicitly block the signal using sigprocmask(2). We allow for an alternate behavior based on argc to ignore SIGQUIT, then check whether any QUIT signals were delivered, and finally unblock the signal again. Let's run this program. Now, if we hit Ctrl+\, nothing happens. The QUIT signal generated by the terminal driver is being blocked. After our call to sleep(3), we then saw that the signal in question _was_ delivered, so upon unblocking, we enter the signal handler, which now again behaves just as before. Note, though, that we entered the signal handler immediately after we unblocked the signal and _before_ we printed that we unblocked the signal. What happens if a signal is delivered while being blocked, but then - _after_ we checked for pending signals we change the disposition to ignore this signal? As before, entering Ctrl+\ does nothing, as the signal is being blocked. After it is unblocked, however, we see that no signals were found to be pending! That is, changing the signal disposition to SIG_IGN removed the pending signals from the queue to be delivered!

Recap

Make sure to replay the code examples yourself, but for now, let's recap real quick. Signals are asynchronous and unpredictable. They may be delivered at any time or never. We can allow the default action to take place, to ignore the signal, to install a signal handler, or to block the signal. We saw that multiple arriving signals may be merged, but also that our signal handler itself can be interrupted by another signal. But what kind of stuff can we do inside the signal handler? If we can be interrupted there, we probably have to be a bit careful about what we can and can't do. Well, we'll discuss _that_ in our next video. Until then, thanks for watching! Cheers.