Screenplay: Sysprog: Signals

Barebones Naive Program

  • pause(): sit and wait for something to happen. A signal for example.

  • Output PID for convenience (getpid())

  • Discuss “Default actions”, see man 7 signal.

    • kill TERM <pid> -> terminated

    • kill SEGV <pid> -> core (discuss)

    • Show exit status != 0

  • Discuss core (post mortem debugging)

    $ cat /proc/sys/kernel/core_pattern
    |/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %h
    

    Hmm. Don’t want them to send that core home.

    # echo core > /proc/sys/kernel/core_pattern
    

    Better yet, to prevent conflicts (many processes dumping to core simultaneously)

    # echo core.%p > /proc/sys/kernel/core_pattern
    
#include <iostream>
#include <unistd.h>

using std::cout;
using std::endl;


int main(void)
{
    cout << getpid() << endl;
    pause();
    return 0;
}

Signal Handler

  • termination_handler(): signal handler

  • Start with printf(), promising the worst

  • Installed as handler for SIGTERM

    • Using signal(), knowing that it is bad.

    • Still only pause(), in linear flow, no loop

    • Terminates -> why? Show fallthrough, cout after pause

    • Discuss errno, EINTR, and error handling in general

    • Introduce loop around pause

    • Install as SIGINT. No second terminal necessary to kill <pid>, but rather Control-C in controlling terminal.

  • Termination

    • bool quit -> NO! sig_atomic_t

    • while (!quit) ... pause ...

  • Fix crap

    • cout in signal handler context. Jump through hoops for simple output on STDOUT_FILENO. See man 7 signal-safety.

    • sig_atomic_t quit

    • Error handling. Fail when trying to comprehend bloody signal() return value. Use sigaction() from here on.

    • sigaction(): why is complicated better than simple?

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <assert.h>

using std::cout;
using std::endl;


static sig_atomic_t quit;
static void termination_handler(int signal)
{
    char buffer[64];
    sprintf(buffer, "handler called, signal=%d\n", signal);
    ssize_t nwritten = write(STDOUT_FILENO, buffer, strlen(buffer));
    assert(nwritten > 0);
    assert((size_t)nwritten == strlen(buffer));

    quit = true;
}

int main(void)
{
    cout << getpid() << endl;

    struct sigaction term_action;
    memset(&term_action, 0, sizeof(term_action));
    term_action.sa_handler = termination_handler;

    int error;
    error = sigaction(SIGTERM, &term_action, NULL);
    assert(!error);
    error = sigaction(SIGINT, &term_action, NULL);
    assert(!error);

    while (!quit) {
        int error = pause();
        if (error)
            cout << "pause: error; errno=" << errno << '(' << strerror(errno) << ')' << endl;
    }
    return 0;
}

Alarm

  • Add alarm() periodic handler (i.e. re-arm in signal handler)

  • See how pause() is still interrupted

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <assert.h>

using std::cout;
using std::endl;


static sig_atomic_t quit;
static void termination_handler(int signal)
{
    char buffer[64];
    sprintf(buffer, "handler called, signal=%d\n", signal);
    ssize_t nwritten = write(STDOUT_FILENO, buffer, strlen(buffer));
    assert(nwritten > 0);
    assert((size_t)nwritten == strlen(buffer));

    quit = true;
}

static void alarm_handler(int)
{
    char msg[] = "alarm\n";
    ssize_t nwritten = write(STDOUT_FILENO, msg, sizeof(msg));
    assert(nwritten>0);
    int error = alarm(3);
    assert(!error);
}

int main(void)
{
    cout << getpid() << endl;

    struct sigaction term_action;
    memset(&term_action, 0, sizeof(term_action));
    term_action.sa_handler = termination_handler;

    int error;
    error = sigaction(SIGTERM, &term_action, NULL);
    assert(!error);
    error = sigaction(SIGINT, &term_action, NULL);
    assert(!error);

    struct sigaction alarm_action;
    memset(&alarm_action, 0, sizeof(alarm_action));
    alarm_action.sa_handler = alarm_handler;

    error = sigaction(SIGALRM, &alarm_action, NULL);
    assert(!error);

    alarm(3);

    while (!quit) {
        int error = pause();
        if (error)
            cout << "pause: error; errno=" << errno << '(' << strerror(errno) << ')' << endl;
    }
    return 0;
}

Alarm (Louder)

  • Dangerous man signal-safety

  • See below for threading issues

Synchronous Delivery

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


int main(void)
{
    int error;

    // setup set of signals that are meant to terminate us
    sigset_t termination_signals;
    sigemptyset(&termination_signals);
    sigaddset(&termination_signals, SIGTERM);
    sigaddset(&termination_signals, SIGINT);
    sigaddset(&termination_signals, SIGQUIT);

    // block asynchronous delivery for those
    error = sigprocmask(SIG_BLOCK, &termination_signals, NULL);
    if (error) {
        perror("sigprocmask(SIGTERM|SIGINT|SIGQUIT)");
        exit(1);
    }

    // wait for one of these signals to arrive. EINTR handling is
    // always good to have in larger programs. for example, libraries
    // might make use of signals in their own weird way - thereby
    // disturbing their users most impolitely by interrupting every
    // operation they synchronously wait for.
    while (1) {
        int sig;
        error = sigwait(&termination_signals, &sig);
        if (error && errno == EINTR) {
            perror("sigwait");
            continue;
        }
        
        printf("received termination signal %d\n", sig);
        break;
    }

    return 0;
}

Multithreading

Multithreading and signals: There Be Dragons. Hmm. How.

Innocent Multithreaded Program

Consumes from n pipes, in n threads, and writes to stdout.

#include <thread>
#include <vector>
#include <iostream>
#include <string>

#include <assert.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

using std::thread;
using std::vector;
using std::cout;
using std::endl;
using std::string;


static void consume_pipe(std::string name)
{
    while (true) {
        int fd = open(name.c_str(), O_RDONLY);
        if (fd == -1) {
            perror("open");
            continue;
        }
        
        char buffer[64];
        ssize_t nread, nwritten;

        nread = read(fd, buffer, sizeof(buffer));
        if (nread == -1) {
            perror("read");
            goto out;
        }
        if (nread == 0) {
            cout << "not expecting eof because I read only once" << endl;
            goto out;
        }

        nwritten = write(STDOUT_FILENO, buffer, nread);
        if (nwritten == -1) {
            perror("write");
            goto out;
        }
        if (nwritten == 0) {
            assert(!"writing 0 bytes?");
            goto out;
        }
        assert(nwritten == nread);

    out:
        close(fd);
    }
}


int main(int argc, char** argv)
{
    std::vector<thread> threads;
    for (int i=1; i<argc; i++) {
        string pipename = argv[i];
        threads.push_back(thread([pipename](){consume_pipe(pipename);}));
    }

    for (auto& t: threads)
        t.join();

    return 0;
}

Add SIGALRM

Add alarm handling to that. Be puzzled why system calls are not interrupted in pipe threads as one would expect.

#include <thread>
#include <vector>
#include <iostream>
#include <string>

#include <assert.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>

using std::thread;
using std::vector;
using std::cout;
using std::endl;
using std::string;


static void alarm_handler(int)
{
    char msg[] = "alarm\n";
    ssize_t nwritten = write(STDOUT_FILENO, msg, sizeof(msg));
    assert(nwritten>0);
    int error = alarm(3);
    if (error)
        perror("alarm");
}

static void consume_pipe(std::string name)
{
    while (true) {
        cout << "open pipe " << name << endl;
        int fd = open(name.c_str(), O_RDONLY);
        cout << "(done) open pipe " << name << endl;
        if (fd == -1) {
            perror("open");
            continue;
        }
        
        char buffer[64];
        ssize_t nread, nwritten;

        nread = read(fd, buffer, sizeof(buffer));
        if (nread == -1) {
            perror("read");
            goto out;
        }
        if (nread == 0) {
            cout << "not expecting eof because I read only once" << endl;
            goto out;
        }

        nwritten = write(STDOUT_FILENO, buffer, nread);
        if (nwritten == -1) {
            perror("write");
            goto out;
        }
        if (nwritten == 0) {
            assert(!"writing 0 bytes?");
            goto out;
        }
        assert(nwritten == nread);

    out:
        close(fd);
    }
}

int main(int argc, char** argv)
{
    cout << getpid() << endl;

    struct sigaction alarm_action;
    memset(&alarm_action, 0, sizeof(alarm_action));
    alarm_action.sa_handler = alarm_handler;

    int error = sigaction(SIGALRM, &alarm_action, NULL);
    assert(!error);

    alarm(3);

    std::vector<thread> threads;
    for (int i=1; i<argc; i++) {
        string pipename = argv[i];
        threads.push_back(thread([pipename](){consume_pipe(pipename);}));
    }

    for (auto& t: threads)
        t.join();

    return 0;
}

Write a standalone single-threaded program and see system call interrupted.

#include <iostream>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#include <assert.h>
#include <string.h>

using std::cout;
using std::endl;

static void alarm_handler(int)
{
    char msg[] = "alarm\n";
    ssize_t nwritten = write(STDOUT_FILENO, msg, sizeof(msg));
    assert(nwritten>0);
    int error = alarm(3);
    if (error)
        perror("alarm");
}

int main(int, char** argv)
{
    cout << getpid() << endl;

    struct sigaction alarm_action;
    memset(&alarm_action, 0, sizeof(alarm_action));
    alarm_action.sa_handler = alarm_handler;

    int error = sigaction(SIGALRM, &alarm_action, NULL);
    assert(!error);

    alarm(3);

    int fd = open(argv[1], O_RDONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }
    return 0;
}

Discuss

  • man open says EINTR on pipe

  • man alarm says delivered to calling process

  • Signal delivery changes significantly when threads are thrown in. Much of the semantics seems to be undefined. See for example man sigprocmask, where they say,

    “sigprocmask() is used to fetch and/or change the signal mask of the calling thread.”

    but then,

    “The use of sigprocmask() is unspecified in a multithreaded process; see pthread_sigmask(3).”

Danger

So WTF? Stay away!