Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Difference between a process signal mask, blocked signal set, and a blocked signal?

Learning about signals, and I was wondering about the subtle differences between the process signal mask, a blocked signal set, a signal handler, and a blocked signal.

The questions involve (on Debian):

  • sigprocmask(2)
  • sigsetops(3) related functions

Each process has it's own signal mask (a long which contains the signals being blocked). And a signal set can be obtained by calling sigprocmask(2) with a NULL argument for the *set variable, will result in the old process mask to be put into *oldset, unchanged:

#include <string.h>
#include <signal.h>

void show_signals(const sigset_t exmask)
{

    int exsignals[43];

    exsignals[0] = SIGABRT;
    exsignals[1] = SIGALRM;
    exsignals[2] = SIGBUS;
    exsignals[3] = SIGCHLD;
    exsignals[4] = SIGCONT;
#ifdef SIGEMT
    exsignals[5] = SIGEMT;
#else
    exsignals[5] = -1;
#endif

    exsignals[6] = SIGFPE;

#ifdef SIGFREEZE
    exsignals[7] = SIGFREEZE;
#else
    exsignals[7] = -1;
#endif

    exsignals[8] = SIGHUP;
    exsignals[9] = SIGILL;
#ifdef SIGINFO
    exsignals[10] = SIGINFO;
#else
    exsignals[10] = -1;
#endif

    exsignals[11] = SIGINT;
    exsignals[12] = SIGIO;
    exsignals[13] = SIGIOT;

#ifdef SIGJVM1
    exsignals[14] = SIGJVM1;
#else
    exsignals[14] = -1;
#endif
#ifdef SIGJVM2
    exsignals[15] = SIGJVM2;
#else
    exsignals[15] = -1;
#endif

    exsignals[16] = SIGKILL;
#ifdef SIGLOST
    exsignals[17] = SIGLOST;
#else
    exsignals[17] = -1;
#endif

#ifdef SIGLWP
    exsignals[18] = SIGLWP;
#else
    exsignals[18] = -1;
#endif

    exsignals[19] = SIGPIPE;
    exsignals[20] = SIGPOLL;
    exsignals[21] = SIGPROF;
    exsignals[22] = SIGPWR;
    exsignals[23] = SIGQUIT;
    exsignals[24] = SIGSEGV;
    exsignals[25] = SIGSTKFLT;
    exsignals[26] = SIGSTOP;
    exsignals[27] = SIGSYS;
    exsignals[28] = SIGTERM;
#ifdef SIGTHAW
    exsignals[29] = SIGTHAW;
#else
    exsignals[29] = -1;
#endif
#ifdef SIGTHR
    exsignals[30] = SIGTHR;
#else
    exsignals[30] = -1;
#endif
    exsignals[31] = SIGTRAP;
    exsignals[32] = SIGTSTP;
    exsignals[33] = SIGTTIN;
    exsignals[34] = SIGTTOU;
    exsignals[35] = SIGURG;
    exsignals[36] = SIGUSR1;
    exsignals[37] = SIGUSR2;
    exsignals[38] = SIGVTALRM;
#ifdef SIGWAITING
    exsignals[39] = SIGWAITING;
#else
    exsignals[39] = -1;
#endif

    exsignals[40] = SIGWINCH;
    exsignals[41] = SIGXCPU;
    exsignals[42] = SIGXFSZ;
#ifdef SIGXRES
    exsignals[43] = SIGXRES;
#else
    exsignals[43] = -1;
#endif

    int exsignals_n = 0;

    for (;exsignals_n < 43; exsignals_n++) {
        if (exsignals[exsignals_n] == -1) continue;
        static char *exsignal_name;
        exsignal_name = strsignal(exsignals[exsignals_n]);
        switch(sigismember(&exmask, exsignals[exsignals_n]))
        {
        case 0: break;
        case 1: printf("YES %s\n", exsignal_name); break;
        case -1: printf("could not obtain signal\n"); break;
        default: printf("UNEXPECTED for %s return\n", exsignal_name); break;
        }
    }
}
const sigset_t getmask(void)
{
        static sigset_t retmask;
        if ((sigprocmask(SIG_SETMASK, NULL, &retmask)) == -1)
                printf("could not obtain process signal mask\n");

        return retmask;
}

At the beginning of my program, I realize that the process signal mask, has not blocked any signals. I then place a signal handler into the program.

static void sig_abrt(int signo)
{
    printf("Caught SIGABRT\n");
}

int main(void)
{
    show_signals(getmask());

    signal(SIGABRT, sig_abrt);

    show_signals(getmask());

    return 0;
}

So now there is a signal handler for SIGABRT, but if I were to call sigprocmask(2) again, as above, SIGABRT will not be in the process signal mask. I tried checking with sigismember(3), but the process signal mask will only be modified once I have called sigaddset(3) or another function which modifies the signal mask.

If I block SIGABRT with sigaddset(3), will the signal handler sig_abrt not receive the call when the SIGABRT is delivered? Does it mean that the signal mask affects which signals are delivered? What is the difference?

Also, is there a way to block a signal in a process without using the sigsetops(3) and sigprocmask(2) functions?

like image 771
seanlum Avatar asked Jan 23 '18 19:01

seanlum


1 Answers

Each process has it's [sic] own signal mask (a long which contains the signals being blocked)

Well, no. The signal mask is actually thread-specific. (In a multithreaded program, you must use pthread_sigmask() to manipulate the signal mask for the current thread; in a single-threaded program, you can use sigprocmask().)

Also, it's not "a long". It is of type sigset_t, which might be an array, structure, or union type. In any case, one should consider it simply as an unordered bit set, one bit per signal.

So now there is a signal handler for SIGABRT, but SIGABRT will not be in the process signal mask.

Correct. Whether or not you have assigned a signal handler or not, does not affect the signal mask at all.

If I block SIGABRT with sigaddset(3), will the signal handler sig_abrt not receive the call when the SIGABRT is delivered? Does it mean that the signal mask affects which signals are delivered? What is the difference?

If all your threads block SIGABRT, it will not be delivered until either the signal is unblocked (removed from the signal mask). If the signal is consumed using sigwait(), sigwaitinfo(), or sigtimedwait(), the signal handler will not be invoked at all.

A short summary:

  • Signals can be directed to a process group (kill() with pid == 0 or pid == -pgid), a specific process (pid), or a specific thread in a specific process (pthread_kill() within the same process, tgkill system call in Linux in general).

  • If a signal is directed to a process group, each process in that group receives "a copy" of the signal.

  • The signal mask defines whether signals are blocked, or delivered immediately.

  • In each process, each signal

    • can have a signal handler, or

    • be ignored (SIG_IGN "handler"), or

    • have the default disposition (ignored (Ign), terminates the process with (Core) or without (Term) a core dump; or it can stop (Stop) or continue (Cont) the execution of the target thread or process). See man 7 signal for details.

  • If some, but not all threads, block a signal, and the signal is not targeted to a specific thread, the kernel directs the signal to one of the threads that are not blocking the signal (at random).

  • There are two ways of catching a signal:

    1. Using a signal handler. The signal gets delivered to a signal handler only when the signal is not blocked. If the signal is blocked, the delivery of the signal is pending until not blocked (or caught by the other option below).

    2. sigwait(), sigwaitinfo(), or sigtimedwait(). These functions check if any signals are pending, and if so, "catch" it. The set of signals they check is defined by a function parameter of sigset_t type.

When the kernel sends/forwards a signal to a process, it first checks if the process has a thread that is not blocking that signal. If there is such a thread, it delivers it via that thread. (If the signal has a signal handler, that signal handler gets invoked in that thread; otherwise, the effect is dictated by the signal disposition.)

If the signal is blocked, the kernel leaves it pending for the process.

If the process calls sigwait(), sigwaitinfo(), or sigtimedwait() with the pending signal in the specified signals set, it receives the information on that signal, and the signal is caught. (It will no longer be pending, and it will not cause a signal handler to be invoked; it is "consumed".)

If the process changes its signal mask, so that the pending signal becomes unblocked, it is delivered by the kernel (just as if it was sent at that point in time).

Also, is there a way to block a signal in a process without using the sigsetops(3) and sigprocmask(2) functions?

No. (You can implement your own sigsetops() and a syscall wrapper for sigprocmask(), but that's about it.)


Here is an example program, example.c, you can use for exploring signal handlers, catching signals, and the signal mask, in a single-threaded process:

#define  _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <stdio.h>
#include <errno.h>

/* Async-signal safe write-to-standard error function.
   Keeps errno unchanged. Do not use stderr otherwise!
*/
static int wrerrpp(const char *ptr, const char *end)
{
    const int  saved_errno = errno;
    ssize_t    chars;

    while (ptr < end) {
        chars = write(STDERR_FILENO, ptr, (size_t)(end - ptr));
        if (chars > 0)
            ptr += chars;
        else
        if (chars != -1) {
            errno = saved_errno;
            return EIO;
        } else
        if (errno != EINTR) {
            const int  retval = errno;
            errno = saved_errno;
            return retval;
        }
    }

    errno = saved_errno;
    return 0;
}

/* Write the supplied string to standard error.
   Async-signal safe. Keeps errno unchanged.
   Do not mix with stderr!
*/
static int wrerr(const char *ptr)
{
    if (!ptr)
        return 0;
    else {
        const char *end = ptr;
        /* strlen() is not async-signal safe, so
           find the end of the string the hard way. */
        while (*end)
            end++;
        return wrerrpp(ptr, end);
    }
}

/* Write the supplied long to standard error.
   Async-signal safe. Keeps errno unchanged.
   Do not mix with stderr!
*/
static int wrerrnum(const long  value)
{
    unsigned long  u = (value < 0) ? (unsigned long)-value : (unsigned long)value;
    char           buf[40];
    char          *ptr = buf + sizeof buf;
    char *const    end = buf + sizeof buf;

    do {
        *(--ptr) = '0' + (u % 10uL);
        u /= 10uL;
    } while (u > 0uL);

    if (value < 0)
        *(--ptr) = '-';

    return wrerrpp(ptr, end);
}

/* Async-signal safe variant of strsignal().
   Only covers a small subset of all signals.
   Returns NULL if the signal name is not known. */
static const char *signal_name(const int signum)
{
    switch (signum) {
    case SIGHUP:    return "HUP";
    case SIGINT:    return "INT";
    case SIGQUIT:   return "QUIT";
    case SIGKILL:   return "KILL";
    case SIGSEGV:   return "SEGV";
    case SIGTERM:   return "TERM";
    case SIGUSR1:   return "USR1";
    case SIGUSR2:   return "USR2";
    case SIGCHLD:   return "CHLD";
    case SIGCONT:   return "CONT";
    case SIGSTOP:   return "STOP";
    default:        return NULL;
    }
}

/* Signal handler that reports its delivery immediately,
   but does nothing else.
*/
static void report_signal(int signum, siginfo_t *info, void *ctx)
{
    const char *sname = signal_name(signum);

    wrerr("report_signal(): Received signal ");
    if (sname)
        wrerr(sname);
    else
        wrerrnum(signum);

    if (info->si_pid) {
        wrerr(" from process ");
        wrerrnum(info->si_pid);
        wrerr(".\n");
    } else
        wrerr(" from kernel or terminal.\n");

}

/* Install report_signal() handler.
*/
static int install_report_signal(const int signum)
{
    struct sigaction  act;

    memset(&act, 0, sizeof act);

    sigemptyset(&act.sa_mask);

    act.sa_sigaction = report_signal;
    act.sa_flags = SA_SIGINFO;

    if (sigaction(signum, &act, NULL) == -1)
        return errno;

    return 0;
}


int main(void)
{
    sigset_t    mask;
    siginfo_t   info;
    const char *name;
    int         signum;

    if (install_report_signal(SIGINT) ||
        install_report_signal(SIGCONT)) {
        const char *errmsg = strerror(errno);
        wrerr("Cannot install signal handlers: ");
        wrerr(errmsg);
        wrerr(".\n");
        return EXIT_FAILURE;
    }

    sigemptyset(&mask);
    sigaddset(&mask, SIGUSR1);
    sigaddset(&mask, SIGUSR2);
    sigaddset(&mask, SIGHUP);
    sigaddset(&mask, SIGTERM);
    sigprocmask(SIG_SETMASK, &mask, NULL);

    printf("Process %ld is ready to receive signals! Run\n", (long)getpid());
    printf("\tkill -USR1 %ld\n", (long)getpid());
    printf("\tkill -USR2 %ld\n", (long)getpid());
    printf("\tkill -HUP  %ld\n", (long)getpid());
    printf("\tkill -TERM %ld\n", (long)getpid());
    printf("in another terminal; press Ctrl+C in this terminal; or press Ctrl+Z and run\n");
    printf("\tfg\n");
    printf("in this terminal.\n");
    fflush(stdout);

    /* Almost same as blocked mask, just without SIGUSR1 and SIGUSR2. */
    sigemptyset(&mask);
    sigaddset(&mask, SIGHUP);
    sigaddset(&mask, SIGTERM);

    do {
        do {
            signum = sigwaitinfo(&mask, &info);
        } while (signum == -1 && errno == EINTR);
        if (signum == -1) {
            const char *errmsg = strerror(errno);
            wrerr("sigwaitinfo(): ");
            wrerr(errmsg);
            wrerr(".\n");
            return EXIT_FAILURE;
        }

        name = signal_name(signum);
        if (name)
            printf("main(): Received signal %s from ", name);
        else
            printf("main(): Received signal %d from ", signum);

        if (info.si_pid == 0)
            printf("kernel or terminal.\n");
        else
            printf("process %ld.\n", (long)info.si_pid);
        fflush(stdout);

    } while (signum != SIGTERM);

    return EXIT_SUCCESS;
}

Compile it using for example

gcc -Wall -O2 example.c -o example

I suggest you prepare two terminals. In one terminal, run the compiled program, using

./example

and observe its output. It will be something like

Process 843 is ready to receive signals! Run
    kill -USR1 843
    kill -USR2 843
    kill -HUP 843
    kill -TERM 843
in another terminal; press Ctrl+C in this terminal; or press Ctrl+Z and run
    fg
in this terminal.

The KILL and STOP signals cannot be caught. KILL will always kill the process, and STOP will always stop ("pause") the process.

If you press Ctrl+C in that terminal, the kernel will send an INT signal to the process. (This will be delivered via the report_signal() signal handler.)

If you press Ctrl+Z in that terminal, the kernel will send a STOP signal to the process. The shell detects this, pushing ./example under job control, and lets you input new shell commands. The fg command brings ./example back to foreground, with the shell sending it the CONT signal, so that ./example will continue execution.

USR1 and USR2 signals are blocked, so they are never delivered to the report_signal() signal handler.

HUP and TERM signals are also blocked, but they are received by the main thread via sigwaitinfo().

The program exits, when it receives a TERM signal.

like image 139
Nominal Animal Avatar answered Sep 30 '22 15:09

Nominal Animal