Asynchronous Signal Handling#

My First Signal Handler#

#include <unistd.h>
#include <signal.h>
#include <print>


static void handler(int signal)
{
    static const char msg[] = "signal handler\n";
    write(STDOUT_FILENO, msg, sizeof(msg));            // <-- async-signal-safe
}

int main()
{
    std::println("PID={}", getpid());

    struct sigaction sa = { 0 };
    sa.sa_handler = handler;

    int rv = sigaction(SIGTERM, &sa, nullptr);         // <-- establish handler
    if (rv == -1) {
        perror("sigaction");
        return 1;
    }

    for(;;);
    return 0;
}

Ignoring Signals#

  • Handler for SIGTERM does nothing (suppresses termination)

  • ⟶ Could just as well ignore

#include <unistd.h>
#include <signal.h>
#include <print>


int main()
{
    std::println("PID={}", getpid());

    struct sigaction sa = { 0 };
    sa.sa_handler = SIG_IGN;                           // <-- special pointer value for "ignore"

    int rv = sigaction(SIGTERM, &sa, nullptr);
    if (rv == -1) {
        perror("sigaction");
        return 1;
    }

    for(;;);
    return 0;
}

SIGKILL Cannot Be Handled/Ignored#

  • Ignore both SIGTERM and SIGINT

  • ⟶ No way to nicely request termination

  • Send SIGSEGV that we already know from the introduction

  • Send SIGKILL: cannot be handled/ignored

#include <unistd.h>
#include <signal.h>
#include <print>


int main()
{
    std::println("PID={}", getpid());

    struct sigaction sa = { 0 };
    sa.sa_handler = SIG_IGN;

    int rv = sigaction(SIGTERM, &sa, nullptr);
    if (rv == -1) {
        perror("sigaction(SIGTERM)");
        return 1;
    }
    rv = sigaction(SIGINT, &sa, nullptr);
    if (rv == -1) {
        perror("sigaction(SIGINT)");
        return 1;
    }

    for(;;);
    return 0;
}

Interrupting System Calls#

  • Process context (as opposed to code in signal handlers) may want to react upon changes made by a signal handler

  • ⟶ Long-sleeping system calls are interrupted

  • EINTR/”Interrupted system call”

  • Teacher: replace for(;;); with pause() (⟶ more efficient 😳)

#include <unistd.h>
#include <signal.h>
#include <print>


static void handler(int signal)
{
    static const char msg[] = "signal handler\n";
    write(STDOUT_FILENO, msg, sizeof(msg));            // <-- async-signal-safe
}

int main()
{
    std::println("PID={}", getpid());

    struct sigaction sa = { 0 };
    sa.sa_handler = handler;

    int rv = sigaction(SIGTERM, &sa, nullptr);
    if (rv == -1) {
        perror("sigaction");
        return 1;
    }

    rv = pause();                                      // <-- blocks a long time
    if (rv == -1) {                                    // <-- errno == EINTR
        perror("pause");                               // <-- "Interrupted system call"
        return 1;
    }
    return 0;
}

Long-Sleeping System Calls: What Is That?#

Legalese from man -s 7 signal:

•  read(2), readv(2), write(2), writev(2), and ioctl(2) calls on  "slow"
   devices.   A "slow" device is one where the I/O call may block for an
   indefinite time, for example, a terminal, pipe, or socket.  If an I/O
   call on a slow device has already transferred some data by  the  time
   it  is  interrupted  by a signal handler, then the call will return a
   success status (normally, the number  of  bytes  transferred).   Note
   that  a  (local)  disk is not a slow device according to this defini‐
   tion; I/O operations on disk devices are not interrupted by signals.

•  open(2), if it can block (e.g., when opening a FIFO; see fifo(7)).

•  wait(2), wait3(2), wait4(2), waitid(2), and waitpid(2).

•  Socket  interfaces:  accept(2),  connect(2),  recv(2),   recvfrom(2),
   recvmmsg(2), recvmsg(2), send(2), sendto(2), and sendmsg(2), unless a
   timeout has been set on the socket (see below).

•  File  locking  interfaces: flock(2) and the F_SETLKW and F_OFD_SETLKW
   operations of fcntl(2)

•  POSIX message queue  interfaces:  mq_receive(3),  mq_timedreceive(3),
   mq_send(3), and mq_timedsend(3).

•  futex(2)  FUTEX_WAIT  (since  Linux 2.6.22; beforehand, always failed
   with EINTR).

•  getrandom(2).

•  pthread_mutex_lock(3), pthread_cond_wait(3), and related APIs.

•  futex(2) FUTEX_WAIT_BITSET.

•  POSIX semaphore interfaces: sem_wait(3) and  sem_timedwait(3)  (since
   Linux 2.6.22; beforehand, always failed with EINTR).

•  read(2)  from an inotify(7) file descriptor (since Linux 3.8; before‐
   hand, always failed with EINTR).

Long-Sleeping: Waiting For Keypress#

  • Replace pause() with read(STDIN_FILENO, ...)

  • ⟶ Wait for keypress

#include <unistd.h>
#include <signal.h>
#include <print>


static void handler(int signal)
{
    static const char msg[] = "signal handler\n";
    write(STDOUT_FILENO, msg, sizeof(msg));
}

int main()
{
    std::println("PID={}", getpid());

    struct sigaction sa = { 0 };
    sa.sa_handler = handler;

    int rv = sigaction(SIGTERM, &sa, nullptr);
    if (rv == -1) {
        perror("sigaction");
        return 1;
    }

    char c;
    rv = read(STDIN_FILENO, &c, 1);                    // <-- blocks until keypress (possibly long)
    if (rv == -1) {
        perror("read");
        return 1;
    }
    return 0;
}

Handling EINTR Sanely? Manually Restarting Interrupted Call?#

  • Bigger (single-threaded) applications rarely block deep inside a call chain

  • Event driven: main event loop blocks (and nothing else) ⟶ central event/EINTR handling

  • If EINTR is seen somewhere inside a call chain ⟶ restart what’s been interrupted

  • ⟶ Dirty though!

#include <unistd.h>
#include <signal.h>
#include <print>


static void handler(int signal)
{
    static const char msg[] = "signal handler\n";
    write(STDOUT_FILENO, msg, sizeof(msg));            // <-- async-signal-safe
}

char read_one_byte() {
    while (true) {
        char c;
        int rv = read(STDIN_FILENO, &c, 1);
        if (rv == -1) {
            if (errno == EINTR)
                continue;                              // <-- restart
            else
                ;                                      // <-- and now? return error?
        }
        return c;
    }
}

int main()
{
    std::println("PID={}", getpid());

    struct sigaction sa = { 0 };
    sa.sa_handler = handler;

    int rv = sigaction(SIGTERM, &sa, nullptr);
    if (rv == -1) {
        perror("sigaction");
        return 1;
    }

    read_one_byte();
    return 0;
}

Automatic Restart Interrupted Call#

  • sigaction.sa_flags |= SA_RESTART

  • ⟶ Automatic restart of system calls

  • Complicated, not well defined ⟶ avoid! (Teacher’s opinion)

#include <unistd.h>
#include <signal.h>
#include <print>


static void handler(int signal)
{
    static const char msg[] = "signal handler\n";
    write(STDOUT_FILENO, msg, sizeof(msg));
}

int main()
{
    std::println("PID={}", getpid());

    struct sigaction sa = { 0 };
    sa.sa_handler = handler;
    sa.sa_flags = SA_RESTART;                          // <-- automatic restart

    int rv = sigaction(SIGTERM, &sa, nullptr);
    if (rv == -1) {
        perror("sigaction");
        return 1;
    }

    char c;
    rv = read(STDIN_FILENO, &c, 1);                    // <-- restarted
    if (rv == -1) {
        perror("read");
        return 1;
    }
    return 0;
}

Async-Signal-Safety#

  • Signal handler runs in a context where nothing is allowed

  • No malloc(), no standard IO, no nothing

  • No pthread

  • Basically: only system calls (see here)

  • Possible solution: blocking signals when operating on shared data (see below)

  • ⟶ Complicated and error prone, keep away!

#include <unistd.h>
#include <signal.h>
#include <list>
#include <print>


static std::list<int> my_numbers;

static void handler(int signal)
{
    for (int i=0; i<10; i++)
        my_numbers.push_back(i);
}

int main()
{
    std::println("PID={}", getpid());

    struct sigaction sa = { 0 };
    sa.sa_handler = handler;

    int rv = sigaction(SIGUSR1, &sa, nullptr);
    if (rv == -1) {
        perror("sigaction");
        return 1;
    }

    while (true) {
        if (my_numbers.size() > 100)
            my_numbers.clear();
        for (int i=0; i<10; i++)
            my_numbers.push_back(i);
        pause();
    }

    return 0;
}

Blocking Asynchronous Signal Delivery: Signal Mask#

  • Signal mask: set of signals that are blocked from asynchronous delivery

  • A signal that is blocked becomes pending on arrival

  • Single threaded programs use sigprocmask(): unspecified in multithreaded programs!

  • Multithreaded programs use pthread_sigmask()

From man -s 2 sigprocmask:

SIG_BLOCK

The set of blocked signals is the union of the current set and the set argument.

SIG_UNBLOCK

The signals in set are removed from the current set of blocked signals. It is permissible to attempt to unblock a signal which is not blocked.

SIG_SETMASK

The set of blocked signals is set to the argument set.

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


static void handler(int signal)
{
    static const char msg[] = "signal handler\n";
    write(STDOUT_FILENO, msg, sizeof(msg));
}

int main()
{
    std::println("PID={}", getpid());

    struct sigaction sa = { 0 };
    sa.sa_handler = handler;

    int rv = sigaction(SIGUSR1, &sa, nullptr);
    if (rv == -1) {
        perror("sigaction");
        return 1;
    }

    sigset_t blocked;
    sigemptyset(&blocked);
    sigaddset(&blocked, SIGUSR1);

    rv = sigprocmask(SIG_BLOCK, &blocked, nullptr);    // <-- block SIGUSR1
    if (rv == -1) {
        perror("sigprocmask(SIG_BLOCK)");
        return 1;
    }
    std::println("blocked SIGUSR1, <RETURN> to continue");

    char c;
    rv = read(STDIN_FILENO, &c, 1);                    // <-- send SIGUSR1 from another terminal
                                                       //     - *not* delivered
                                                       //     - read() continues uninterrupted
    if (rv == -1) {
        perror("read");
        return 1;
    }

    rv = sigprocmask(SIG_UNBLOCK, &blocked, nullptr);  // <-- unblock SIGUSR1 -> delivered *immediately*
                                                       //     on return to userspace
    if (rv == -1) {
        perror("sigprocmask(SIG_UNBLOCK)");
        return 1;
    }

    std::println("done unblocking SIGUSR1");

    return 0;
}

Asking For Pending Signals#

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


static void handler(int signal)
{
    static const char msg[] = "signal handler\n";
    write(STDOUT_FILENO, msg, sizeof(msg));
}

int main()
{
    std::println("PID={}", getpid());

    struct sigaction sa = { 0 };
    sa.sa_handler = handler;

    int rv = sigaction(SIGUSR1, &sa, nullptr);
    if (rv == -1) {
        perror("sigaction");
        return 1;
    }

    sigset_t blocked;
    sigemptyset(&blocked);
    sigaddset(&blocked, SIGUSR1);

    rv = sigprocmask(SIG_BLOCK, &blocked, nullptr);
    if (rv == -1) {
        perror("sigprocmask(SIG_BLOCK)");
        return 1;
    }

    std::println("send me a SIGUSR1, and press <return> when done");
    char c;
    rv = read(STDIN_FILENO, &c, 1);
    if (rv == -1) {
        perror("read");
        return 1;
    }

    sigset_t pending;
    rv = sigpending(&pending);                         // <-- ask kernel what's pending
    if (rv == -1) {
        perror("sigpending");
        return 1;
    }
    if (sigismember(&pending, SIGUSR1))                // <-- it's SIGUSR1
        std::println("SIGUSR1 pending");
    else 
        std::println("Gosh!!");

    rv = sigprocmask(SIG_UNBLOCK, &blocked, nullptr);
    if (rv == -1) {
        perror("sigprocmask(SIG_UNBLOCK)");
        return 1;
    }

    std::println("done unblocking SIGUSR1");

    return 0;
}

Realtime Signals#

  • Traditional signals are not queued

  • Realtime signals are

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


static void handler(int signal)
{
    static const char msg[] = "signal handler\n";
    write(STDOUT_FILENO, msg, sizeof(msg));
}

int main()
{
    std::println("PID={}", getpid());

    struct sigaction sa = { 0 };
    sa.sa_handler = handler;

    int rv = sigaction(SIGRTMIN+5, &sa, nullptr);
    if (rv == -1) {
        perror("sigaction");
        return 1;
    }

    sigset_t blocked;
    sigemptyset(&blocked);
    sigaddset(&blocked, SIGRTMIN+5);

    rv = sigprocmask(SIG_BLOCK, &blocked, nullptr);
    if (rv == -1) {
        perror("sigprocmask(SIG_BLOCK)");
        return 1;
    }
    std::println("blocked SIGRTMIN+5 ({}), "           // <-- output its number so we know
                 "<RETURN> to continue", SIGRTMIN+5);

    char c;
    rv = read(STDIN_FILENO, &c, 1);
    if (rv == -1) {
        perror("read");
        return 1;
    }

    sigset_t pending;
    rv = sigpending(&pending);
    if (rv == -1) {
        perror("sigpending");
        return 1;
    }
    if (sigismember(&pending, SIGRTMIN+5))
        std::println("SIGRTMIN+5 pending");

    rv = sigprocmask(SIG_UNBLOCK, &blocked, nullptr);  // <-- handler is called as many times 
                                                       //     as signal instances are pending
    if (rv == -1) {
        perror("sigprocmask(SIG_UNBLOCK)");
        return 1;
    }
    std::println("unblocked SIGRTMIN+5, <RETURN> to continue");

    rv = read(STDIN_FILENO, &c, 1);
    if (rv == -1) {
        perror("read");
        return 1;
    }

    return 0;
}