2

EDIT:
I wrote this kind of hurriedly and right after I was first introduced to signal.h so my initial question was kind of all over the place & boggled down by a lot of messier parts, which likely made it more tiresome/difficult to answer. I also seem to have misunderstood the "masking" part of signals – I seem to have thought that the masking meant that the signal would just be discarded, but in reality, I think what happens is that the handler isn't getting triggered, but the signal is transferred and is "on hold" (with repeated signals being coalseced) until returning from the handler.
Rephrasing here to highlight those parts of my original question that still confuse me (and the original remains below so as not to leave existing comments etc without clear context):

As far as I know, signal handlers are triggered upon kernel mode to user mode transitions. Citation:
man 7 signal excerpt
(In case the image ever breaks, for whatever reason – it's a quote from man 7 signal: "Whenever there is a transition from kernel-mode to user-mode execution [...] the kernel checks whether there is a pending unblocked signal for which the process has established a signal handler.")

I scribbled the following code:

const char* ON_SIGTSTP_ENTRY = "Got SIGTSTP.\n";
const char* ON_SIGTSTP_EXIT = "Exiting SIGTSTP handler.\n";
const char* ON_SIGINT_ENTRY = "Got SIGINT.\n";
const char* ON_SIGINT_EXIT = "Exiting SIGINT handler.\n";

void ctrlZHandler(int sig_num) {
    write(1, ON_SIGTSTP_ENTRY, strlen(ON_SIGTSTP_ENTRY));
    kill(getpid(), SIGINT);
    write(1, ON_SIGTSTP_EXIT, strlen(ON_SIGTSTP_EXIT));
}

void ctrlCHandler(int sig_num) {
    write(1, ON_SIGINT_ENTRY, strlen(ON_SIGINT_ENTRY));
    kill(getpid(), SIGTSTP);
    write(1, ON_SIGINT_EXIT, strlen(ON_SIGINT_EXIT));
}

int main() {
    if(signal(SIGTSTP, ctrlZHandler) == SIG_ERR) {
        cout << "Failed to set SIGTSTP handler." << endl;
    }

    if(signal(SIGINT, ctrlCHandler) == SIG_ERR) {
        cout << "Failed to set SIGINT handler." << endl;
    }

    kill(getpid(), SIGTSTP);

    cout << "Returning from main." << endl;
    return 0;
}

The output infinitely followed the pattern of:

Got SIGTSTP.
Got SIGINT.
Exiting SIGINT handler. 
Exiting SIGTSTP handler.
Got SIGTSTP.
Got SIGINT.
Exiting SIGINT handler. 
Exiting SIGTSTP handler.

This is a little strange to me, since this implies that what happens is that we enter the SIGTSTP handler, and then within it the SIGINT handler, then we exit both – and then instantly get both triggered again, without making any progress with the rest of main().
I'd have guessed that what would happen is more similar to that we get one of those sigtstp-into-sigint cycles, then reach the printing in main, which in turns triggers a kernel mode-user mode transition, which triggers another signal-cycle, after this point we return from main (perhaps with another few cycles as more system calls start firing after main's end).
However, it seems like the printing in main is never actually reached: kind of like the next signal handler is fired right after we return from the signal handler, but as far as I understand it, returning from a function should not generally trigger a user mode to kernel mode transition (though maybe signal handlers are different?).

Would appreciate an explanation regarding this phenomenon and my misunderstandings about it.


ORIGINAL:

To my understanding, until we return from a certain signal handler, signals of the same type that invoked it are masked; we won't get that same signal handler triggered another time.
Based on this, I attempted something like the following:
I registered handlers to two different signals (SIGFPE and SIGINT, if that matters), and in each of those two, sent the other signal to the process. That is to say, something like:

void fpe_handler (int signum) {
    kill(getpid(), SIGINT);
    printf("fpe\n");
}

void int_handler (int signum) {
    kill(getpid(), SIGFPE);
    printf("int\n");
}

After this point, I got the signal triggered by using sleep(3);, so that we have some system call being triggered in the background to initiate a kernel-mode-to-user-mode switch and trigger available handlers, and then I opened another bash instance and sent a SIGFPE to my process.
What happened at this point is something I can't quite explain to myself: my process had entered an infinite loop.
The reason this is confusing to me is the "understanding" I'd opened this question with. As I was trying to predict what would happen before running my code, I thought: I trigger SIGFPE's handler, fpe_handler; at this point, kill is called to trigger SIGINT's handler, int_handler; this once again sends a SIGFPE, but fpe_handler's run hadn't yet concluded, so this will not, at this point, have an effect – at which point int_handler will return, passing the execution over to fpe_handler's printf, and then back to main. Maybe there'd have been another syscall or two due to libc shenanigans, but even then, just a finite amount of prints.
I then thought: maybe what would actually happen is that instead of being returned to fpe_handler's printf, I'll be returned to fpe_handler's kill – thus triggering an infinite loop of int_handlers, because fpe_handler never concludes and so can never be called again, but nothing stops us from entering-and-quitting int_handler again and again.

However, neither happened: the output was not a repeating int message, but repeating alternatinv fpe and int messages, meaning fpe_handler does indeed get triggered again and again due to int_handler's kill calls.

Why does this happen? What am I misunderstanding?

Shay
  • 587
  • 4
  • 13
  • 3
    _Side note:_ `printf` is _not_ async-signal-safe. Using it in a signal handler can cause your program to have undefined behavior. See: `man signal-safety`. You _can_ use `write`, so try (e.g.) `void signal_safe_puts(const char *msg) { int sverr = errno; write(1,msg,strlen(msg)); errno = sverr; }` Then, you can call from within your signal handler. And, I'd put the call _above_ the `kill` call [or you may never get any output]. – Craig Estey May 09 '23 at 23:57
  • The behavior is _not_ a bug [in libc]. [Obviously], your initial understanding must be incorrect. The fact that you get a message out, even though the `printf` occurs _after_ you do the `kill` indicates that the signal isn't caught _instantaneously_. Otherwise, you'd get an infinite loop, but _no_ messages. It isn't clear whether you're using `signal` or `sigaction` to set the handler, but, with the latter you can control masking of _other_ signals within the handler. – Craig Estey May 10 '23 at 00:18
  • Doing `kill` tells the OS kernel to add the signal to a "pending" mask. Depending upon what state the _kernel_ thinks the process is in, it may issue the signal immediately. Or, it will issue it to the process when it thinks it's appropriate (earliest possible moment). It may or may not allow signals to be "stacked". Then, the "next" signal is triggered _after_ the "current" signal handler returns. To prevent the infinite loop, add (e.g.) `static int done = 0; if (done) return; done = 1;` to the front of each handler. This makes it a one-shot. – Craig Estey May 10 '23 at 00:25
  • Please read [How to avoid using `printf()` in a signal handler?](https://stackoverflow.com/q/16891019/15168) for a discussion of what functions are allowed in signal handlers — hint: `printf()` is not one of the allowed functions. – Jonathan Leffler May 10 '23 at 03:37
  • Sorry for the late reply: thanks a lot for your help, guys – very good to know. I added a modified version of the code without any unsafe functions. The same thing happens, however. Regarding 'the "next" signal is triggered after the "current" signal handler returns' – could you please clarify why? To my understanding, returning from a signal handler should not invoke a user mode/kernel mode transition (should it?), and according to `man 7 signal`, such transitions are when signals are triggered. – Shay Jun 19 '23 at 16:42

1 Answers1

5

Signals that aren't ignored can be blocked while the signal handler is executing, but they're no longer blocked after the signal handler returns.

Such signals remain pending while they're blocked (note, though that multiple signals of the same type are subject to being coalesced into a single signal), and will be delivered to the process after they are no longer blocked.

Per 2.4.1 Signal Generation and Delivery:

...

During the time between the generation of a signal and its delivery or acceptance, the signal is said to be "pending". Ordinarily, this interval cannot be detected by an application. However, a signal can be "blocked" from delivery to a thread. If the action associated with a blocked signal is anything other than to ignore the signal, and if that signal is generated for the thread, the signal shall remain pending until it is unblocked, ...

Once the pending signal is unblocked, it will be delivered.

Andrew Henle
  • 32,625
  • 3
  • 24
  • 56
  • Hey Andrew – thanks a lot for the answer and sorry for the very delayed response. If you wouldn't mind expanding: as far as I can tell (not very far, presumably), I understand the points that you've described. What's been confusing me, though, is the exact timing indicated by "once the pending signal is unblocked". As you state, it should happen at some point after the function returns; but to my understanding, signals are generally delivered when moving from kernel mode to user mode, and to my understanding, returning from a function shouldn't cause this (is a signal handler different?). – Shay Jun 19 '23 at 15:49
  • Would really appreciate if you could help me understand where my misunderstanding(s) lie :) – Shay Jun 19 '23 at 15:49