Asynchronous Signal Handling#
My First Signal Handler#
Discuss sigaction() vs. signal (deprecated)
Discuss “async-signal-safety” ⟶ use
write(STDOUT_FILENO, ...)
#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
andSIGINT
⟶ No way to nicely request termination
Send
SIGSEGV
that we already know from the introductionSend
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(;;);
withpause()
(⟶ 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()
withread(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
handlingIf
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 nothingNo
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:
|
The set of blocked signals is the union of the current set and the set argument. |
|
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. |
|
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;
}