6

We had a lecture last week that involved how the OS (in this case Linux, and in this particular case our school server uses SUSE Linux 11) handles interrupts. One thing of note was that for most signals, you can catch the interrupt and define your own signal handler to run instead of the default. We used an example to illustrate this, and I found what at first seemed to me as interesting behavior. Here's the code:

#include <stdio.h>
#include <signal.h>

#define INPUTLEN 100

main(int ac, char *av[])

{
  void inthandler (int);
  void quithandler (int);
  char input[INPUTLEN];
  int nchars;

  signal(SIGINT, inthandler);
  signal(SIGQUIT, quithandler);

  do {
    printf("\nType a message\n");
    nchars = read(0, input, (INPUTLEN - 1));
    if ( nchars == -1)
      perror("read returned an error");
    else {
      input[nchars] = '\0';
      printf("You typed: %s", input);
    }
  }
  while(strncmp(input, "quit" , 4) != 0); 
}

void inthandler(int s)
{
  printf(" Received Signal %d ....waiting\n", s);
  int i = 0;
  for(int i; i<3; ++i){
    sleep(1);
    printf("inth=%d\n",i);
  }
  printf(" Leaving inthandler \n");
}

void quithandler(int s)
{
  printf(" Received Signal %d ....waiting\n", s);
  for(int i; i<7; ++i){
    sleep(1);
    printf("quith=%d\n",i);
  }  printf(" Leaving quithandler \n");
}

So, when running this code, I expected something like this:

  1. Running code.... ^C
  2. Enter inthandler, executing loop, hit ^\
  3. Exit inthandler, go into quithandler, execute quithandler loop
  4. ^C back to inthandler. If I execute ^C again while I'm in the inthandler, ignore successive inthandler signals until the current inthandler is done processing.

I found something that, based on observation, seems like a nested, 2-queue-depth "scheduling" of the signals. If, for example, I enter the following interrupts in quick succession:

  • ^C, ^\, ^C, ^\, ^\, ^C

I'll receive the following behavior/output from the code:

^CReceived signal 2 ....waiting
^\Received Signal 3 ....waiting
^C^\^\^C quith=0
quith=1
quith=2
quith=3
quith=4
quith=5
quith=6
quith=7
Leaving quithandler
Received Signal 3 ....waiting
quith=1
quith=2
quith=3
quith=4
quith=5
quith=6
quith=7
Leaving quithandler
inth=0
inth=1
inth=2
inth=3
Leaving inthandler
Received Signal 2 ....waiting
inth=0
inth=1
inth=2
inth=3
Leaving inthandler

In other words, it appears to be processed like this:

  1. Receive first ^C signal
  2. Receive ^\ signal, "delay" the inthandler and go into quithandler
  3. Receive next ^C signal, but because we are "nested" in a inthandler already, put it at the back of the inthandler "queue"
  4. Receive quithandler, place at back of quithandler queue.
  5. Execute quithandler until queue is empty. Ignore the third quithandler because it seems to only have a queue depth of 2.
  6. Leave quithandler, and execute the 2 remaining inthandlers. Ignore the final inthandler because queue-depth of 2.

I showed the behavior to my professor, and he seems to agree that the "nested 2 queue depth" behavior is what's happening, but we're not 100% sure why (he comes from a hardware background and has only just started teaching this class). I wanted to post to SO to see if anybody could shed some light on why/how Linux processes these signals, as we weren't quite expecting some of the behavior i.e. nesting.

I think the test case I wrote out should be enough to illustrate what's going on, but here are a bunch of screenshots of additional test cases:

https://i.stack.imgur.com/2k6dH.png

I wanted to leave the additional test cases as a link as they're kind of large screenshots.

Thank you!

caf
  • 233,326
  • 40
  • 323
  • 462
karasaj
  • 63
  • 5
  • 1
    for best results, the function prototypes should be outside of any function. – user3629249 Apr 20 '15 at 03:49
  • You should show the code you ran, not an approximation to it. Your 'quit' handler code prints 'inth' but the record shows something printing 'quith'. – Jonathan Leffler Apr 20 '15 at 03:50
  • 1
    See [How to avoid using `printf()` in a signal handler?](http://stackoverflow.com/questions/16891019/how-to-avoid-using-printf-in-a-signal-handler) Also, you have limited control over what happens with `signal()` compared with `sigaction()`. See [What is the difference between `signal()` and `sigaction()`?](http://stackoverflow.com/questions/231912/what-is-the-difference-between-sigaction-and-signal/) – Jonathan Leffler Apr 20 '15 at 03:53
  • the posted code fails to compile cleanly. for numerous reasons, starting with the two unused parameters to main: ac and av. Suggest compiling with all warnings enabled (or at least '-Wall -Wextra -pedantic') then fix the warnings, then re-post the code. As a start, the code is missing a couple header files string.h and unistd.h – user3629249 Apr 20 '15 at 03:55
  • @JonathanLeffler sorry - I was having trouble getting the VPN to work on my desktop (where I typed this post, possibly because im on W10TP) so I manually wrote some parts of it when I was rewriting the question - I accidentally wrote inth instead of quith. I've fixed that. As for printf(), that wasn't what the original example came with (see that here: http://pastebin.com/k1CFRzPx I just wanted to be able to see output to be sure what it was doing. The result is actually the same if I only use sleep. Could printf have something to do with how it's working? – karasaj Apr 20 '15 at 04:00
  • @user3629249 (see above) I'm actually having a bit of trouble with VPN right now for some reason, so I can't fix it right now. I can re-fix it tomorrow, at least. The code was taken straight from our lecture, so I (mostly) didn't have a hand in writing it. – karasaj Apr 20 '15 at 04:03

2 Answers2

11

The rules (for non-realtime signals, such as the SIGQUIT and SIGINT you're using) are:

  1. By default, a signal is masked when its handler is entered, and unmasked when the handler exits;
  2. If a signal is raised while it is masked, it is left pending, and will be delivered if/when that signal is unmasked.

The pending state is binary - a signal is either pending or not pending. If a signal is raised multiple times while masked, it will still only be delivered once when unmasked.

So what happens in your example is:

  1. SIGINT is raised and the inthandler() signal handler starts executing, with SIGINT masked.
  2. SIGQUIT is raised, and the quithandler() signal handler starts executing (interrupting inthandler()) with SIGQUIT masked.
  3. SIGINT is raised, adding SIGINT to the set of pending signals (because it is masked).
  4. SIGQUIT is raised, adding SIGQUIT to the set of pending signals (because it is masked).
  5. SIGQUIT is raised, but nothing happens because SIGQUIT is already pending.
  6. SIGINT is raised, but nothing happens because SIGINT is already pending.
  7. quithandler() finishes executing, and SIGQUIT is unmasked. Because SIGQUIT is pending, it is then delivered, and quithandler() starts executing again (with SIGQUIT masked again).
  8. quithandler() finishes executing for the second time, and SIGQUIT is unmasked. SIGQUIT is not pending, so inthandler() then resumes executing (with SIGINT still masked).
  9. inthandler() finishes executing, and SIGINT is unmasked. Because SIGINT is pending, it is then delivered, and inthandler() starts executing again (with SIGINT masked again).
  10. inthandler() finishes executing for the second time, and SIGINT is unmasked. The main function then resumes executing.

On Linux, you can see the current set of masked and pending signals for a process by examining /proc/<PID>/status. The masked signals are shown in the SigBlk: bitmask and the pending signals in the SigPnd: bitmask.

If you install your signal handlers with sigaction() rather than signal(), you can specify the SA_NODEFER flag to request that the signal isn't masked while its handler executes. You could try this in your program - with one or both signals - and try to predict what the output will look like.

caf
  • 233,326
  • 40
  • 323
  • 462
  • Thank you very much! This explained it pretty much perfectly. One follow up question: Is there a particular reason they decided on a binary "pending/not pending and ignore everything else" behavior? Was it some convention or ease of design? If a program relied on sending a SIGINT to another process for some reason, but that process was already pending and the signal was never generated, how would that first program/process respond? – karasaj Apr 21 '15 at 03:17
  • @karasaj: This design dates back to fairly early in the history of UNIX. I presume it is to do with ease of implementation, together with resource handling issues - (if you have a queue of signals, you have to decide what to do when that queue overflows). The "pending" design is sufficient to write race-free handlers, anyway - a signal is treated as a notification that an event of interest has occured, and when you handle a signal, you handle *all* instances of that event that have occured since you last checked, not just one. This is also how hardware interrupts are generally handled, too. – caf Apr 21 '15 at 05:45
  • As an observation, signals between SIGRTMIN and SIGRTMAX (real time signals) behave differently, as they actually get queued up and as many as were received will be handled (for these you don't have the binary pending vs non pending flag) – Paul Stelian Apr 14 '19 at 06:53
3

I found this in the manpage signal (7) which seems relevant:

Real-time signals are delivered in a guaranteed order. Multiple real-time signals of the same type are delivered in the order they were sent. If different real-time signals are sent to a process, they are delivered starting with the lowest-numbered signal. (I.e., low-numbered signals have highest priority.) By contrast, if multiple standard signals are pending for a process, the order in which they are delivered is unspecified.

Looking at sigprocmask and sigpending documentation, in addition to signal (7), should enhance your understanding of the guarantees regarding pending signals.

To move from the weak "unspecified" guarantee to what actually happens on your version of OpenSUSE, you'd probably need to inspect the signal delivery code in the kernel.

Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • 1
    All true, but in this case the order is well-understood because the OP never has two unmasked signals pending at any time. – pilcrow Apr 20 '15 at 16:12