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>
-> terminatedkill 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 handlerStart with printf(), promising the worst
Installed as handler for
SIGTERM
Using
signal()
, knowing that it is bad.Still only
pause()
, in linear flow, no loopTerminates -> why? Show fallthrough, cout after pause
Discuss
errno
,EINTR
, and error handling in generalIntroduce loop around pause
Install as
SIGINT
. No second terminal necessary tokill <pid>
, but ratherControl-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. Usesigaction()
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
saysEINTR
on pipeman alarm
says delivered to calling processSignal 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!