Introduction

Hello, and welcome back to CS631 "Advanced Programming in the UNIX Environment". With this video segment, we will conclude our coverage of Unix signals. Recall from our last video, that signals may occur at any time. When that happens, and we do have a signal handler installed, then control is immediately transferred to the signal handler function, and when the signal handler returns, we continue wherever we left off. In other words, we get interrupted a bit rudely, and when we are allowed to continue it's possible that the actions taken within the signal handler may have changed the state of our world around us. We saw this in our simple example of signal handlers: every time we entered the signal handler, we incremented the global variable 's'. But of course we could have done anything else in the signal handler, and if you remember our discussion around fork(2) from lecture 06, segment 6, performing buffered I/O, for example, might lead to a change in the output buffer if that is not flushed.

Calling printf(3) in a signal handler

Let's interrupt our example here and illustrate what that would look like: Here, we have our signal handler, which simply prints out a message that the signal was caught. In 'main' we print something to stdout, but we are not adding a carriage-return, as we want out status message to be "Waiting for a signal... done" on a single line. To simulate the effect of being interrupted here, we allow for a pause(). Our expected execution looks like so. Now, let's illustrate what happens when we are interrupted by a signal in between our two 'printf' calls: In that case, our output buffer is not printed, because being connected to a terminal we are line buffered. Our signal handler appends its message to the output buffer, then flushes the buffer, and our output becomes interleaved. Right here. In contrast, if we use unbuffered I/O -- that is, we use the write(2) syscall instead of printf(3) library function -- then we avoid this problem. Again... send the signal... here we go. Output does not get interleaved. But there are even more severe consequences of making calls that are not async-signal-safe.

Calling getpwnam(3) in a signal handler

Let's illustrate: In this program, our signal handler iterates over the password database a few times, trying to retrieve the entry for 'root'. By calling alarm(1) from the signal handler, we are ensuring that our program gets interrupted every second. In 'main', we loop forever, trying to look up a user in our password database. As we are interrupted every second, we should see a few ways this will fail: every getpwnam(3) call requires us to open the password database, look up an entry, then close the database again. If at any time here we are interrupted and then our signal handler tries to do the same thing, we'll be operating on the same resources. That is, our program should sometimes not be able to find the user we are looking for, because the _other_ password database lookup moved the database pointer forward beyond the entry. Sometimes, we'll get a corrupted entry back, and sometimes we'll simply segfault because our datastructures are corrupted. If you run this program, you should eventually see all of these different failure modes. Here, I'll just restart the program forever to illustrate the different failures. The first time around, we immediately can't find the user we were looking for. The next time, we get a segfault. Sometimes everything works just fine for a little while before eventually failing again, but all the different corruptions are eventually triggered. This shows that you have to be quite careful about which library functions you call from within your signal handlers. Note over here: we did not find 'root', even though a couple of seconds went by without problems. And the next time, we even got a corrupted value; when we looked up the user 'jschauma', we got back 'root' as the entry. All sorts of things can go wrong when you don't call async-signal-safe functions in your signal handler.

async-signal-safe functions

Functions that are guaranteed to be async-signal-safe are listed in the manual pages, and prescribed by POSIX. Some functions are either non-interruptable, guaranteed to be atomic -- that is, the kernel will not -- cannot -- switch until the function completes. Or the function is known to be reentrant, that is, multiple invocations may safely run concurrently. Note, though, that a reentrant function is not necessarily thread-safe, and may still have certain side effects. For example, many of the reentrant functions listed here will change 'errno', so it might be wise to save the current errno in your signal handler and reset it right before exiting the signal handler again. Note also that different versions of Unix may add other syscalls or library functions here, but as usual it's best to stick to POSIX definitions for portability.

Blocking functions

So of course being interrupted is more likely to happen the longer you are running, and some of these async-signal-safe functions may block for long periods of time or even forever. read(2) will of course block if it's reading input from stdin and the user simply isn't providing any data; write(2) may block for example when writing into a pipe and there's no consumer on the other end opening a file may block on certain conditions we can of course intentionally pause, and so on and so on. So what happens when you get interrupted while blocking? There are usually three possibilities: 1) your call may return unsuccessfully and set 'errno' to EINTR to let the caller know that the call was interrupted; the caller can inspect the value of 'errno' and then repeat the call, if so desired. 2) it's possible for a call that has already begun to perform some I/O to simply return fewer bytes than requested; and (3), it's also possible for the syscall to automatically be restarted, to be tried again. Which one of these happens depends (a) on the interface in question, and (b) how the signal handler was installed. The flags passed to sigaction(2) may include SA_RESTART, which then may cause some of the syscalls such as read(2) and write(2) to be restarted if interrupted. Like so many things surrounding signals, a lot of this is OS specific, so make sure to consult your manual pages.

Restarted syscalls by example

Here, let's illustrate a restarted syscall by example. Our signal handler here only sets a global variable, so it itself is very safe. In 'main', we're setting up two signal handlers, one for SIGINT, which will be set to _not_ cause syscalls to be restarted, and one for SIGQUIT, for which we explicitly set the SA_RESTART flag. Then we read a single byte from stdin and report whether the call was interrupted or restarted. Let's run this program: If we don't send any signals, we simply enter a character, the program reads it, and prints it back to us. If we run it again and hit Ctrl+C after we provided the character, our read(2) call will return -1 and set errno to EINTR. But if we start again and send SIGQUIT, we notice that the program didn't complete; instead, our read(2) call was restarted, so blocks again until we provide the data. When we do that, it will dutifully report to us that it was restarted and echo back the data it retrieved after having been restarted.

Recap

Aaaallright, time to wrap up our lesson on signals. In this video segment we discussed reentrant and interrupted functions. We saw that we have to be careful about what functions we call from within a signal handler as only async-signal-safe functions -- those that are either uninterruptable or reentrant -- can safely be used. We know the list of such async-signal-safe functions from POSIX, but need to check with our OS manual pages for any other additions. Even for those functions, we need to be aware of any side-effects they may have, such as, for example, setting errno. And finally, when such a function is interrupted, we may observe different behavior depending on the call in question, how the signal handler was installed, and even the OS or OS version. As we noted earlier in a previous video, Signals represent a simple form of asynchronous interprocess communication. Well, perhaps not _simple_, but limited. All we can do is notify a process that a condition has occurred. Being able to communicate more complex information between processes, however, is often necessary. How we can do that will be our topic for the next two weeks. Thanks for watching, and until the next time - cheers!