SIGUSR1: An Under-Appreciated IPC Mechanism
Polling sucks, but inter-process communication (IPC) is hard. Sometimes you have two programs that need to coordinate, and you just want a simple way for one to tell the other “hey, new information is ready, go look at it”. Here’s a trick I picked up from xidle(1)… I’m sure other programs have used it, but this is where I got the idea.
You may be familiar with UNIX signals (see
signal(3) on OpenBSD, and
signal(7) on Linux),
which allow one program to interrupt another. Most often, this is in the form
of a SIGKILL
or SIGSTOP
to halt a misbehaving program. What we’re
interested in today is SIGUSR1
, which is one of two signals set aside for
user-defined use.
We can catch SIGUSR1 like any other signal, by creating an appropriate signal handler:
void handler(int signo) {
printf("Caught signal %i at ", signo);
print_time();
printf("\\n");
}
And then registering it using signal()
:
if (signal(SIGUSR1, handler) == SIG_ERR) {
err(1, "Failed to register signal handler");
}
In this trivial example, we just print out a timestamp, but in a real program,
it might cause you to go read a file, FIFO, socket, or some other medium by
which you can gather more information. Or in the case of xidle(1)
, if your
need for IPC is simple enough, you may not need any further information. Lets
look at a fully working example:
#define _GNU_SOURCE
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <err.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>
/**
* @brief Displays the current local time on standard out.
*/
void print_time(void) {
/* get the current time and convert to local time */
time_t t;
struct tm* l;
time (&t);
l = localtime(&t);
/* generate a timestamp string, and remove the trailing newline */
char* ts;
asprintf(&ts, "%s", asctime(l));
ts[strlen(ts)-1] = '\\0';
/* display the timestamp */
printf("%s", ts);
free(ts);
}
/**
* @brief Handler for SIGUSR1
*
* @param signo
*/
void handler(int signo) {
printf("Caught signal %i at ", signo);
print_time();
printf("\\n");
}
int main(void) {
/* register the signal handler */
if (signal(SIGUSR1, handler) == SIG_ERR) {
err(1, "Failed to register signal handler");
}
/* wait forever, to give us time to send the signal */
printf("Beginning wait at ");
print_time();
printf("\\n");
while(1) {
sleep(10);
printf("Waiting... (");
print_time();
printf(")\\n");
}
}
We can go ahead and compile and run this program:
$ cc main.c
$ ./a.out
And now from another terminal, run:
$ pkill -USR1 a.out
Back in the first terminal, we should see something like:
Beginning wait at Mon Jun 22 20:15:17 2020
Waiting... (Mon Jun 22 20:15:27 2020)
Caught signal 10 atMon Jun 22 20:15:35 2020
After publishing the original version of this article, my friend Joshua
Nelson reminded me that the example presented above
exhibits undefined behavior: printf()
is not
signal-safe.
Fortunately, eliminating this undefined behavior is quite easy: we can simply have a global flag which the handler sets, with the main function simply sitting in an idle loop until the flag is changed by the signal handler. We could simply poll in a hot loop, or perhaps add a short delay to avoid burning too many CPU cycles, but fortunately there is a function to wait until a signal is received: pause(2). This leads us to a slightly modified variation of our previous program which does not contain any undefined behavior…
#define _GNU_SOURCE
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <err.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>
int flag_sigusr1;
/**
* @brief Displays the current local time on standard out.
*/
void print_time(void) {
/* get the current time and convert to local time */
time_t t;
struct tm* l;
time (&t);
l = localtime(&t);
/* generate a timestamp string, and remove the trailing newline */
char* ts;
asprintf(&ts, "%s", asctime(l));
ts[strlen(ts)-1] = '\\0';
/* display the timestamp */
printf("%s", ts);
free(ts);
}
/**
* @brief Handler for SIGUSR1
*
* @param signo
*/
void handler(int signo) {
flag_sigusr1 = 1;
}
int main(void) {
flag_sigusr1 = 0;
/* register the signal handler */
if (signal(SIGUSR1, handler) == SIG_ERR) {
err(1, "Failed to register signal handler");
}
/* wait forever, to give us time to send the signal */
printf("Beginning wait at ");
print_time();
printf("\\n");
while(1) {
/* wait for some signal to be received by the program */
pause();
if (flag_sigusr1 != 0) {
printf("Caught signal SIGUSR1 at: ");
print_time();
printf("\\n");
flag_sigusr1 = 0;
}
}
}
This trick is super easy to implement (just look up how to write a signal handler in your language of choice), and has a few benefits:
- No need to set up any network service, or indeed even touch the network at all.
- Enables synchronous push-based communication, rather than polling.
- Simple to implement in nearly all languages.
The real sell though is that this method can be used to easily trigger code
running as a normal user from the root user, without any of the environment
variable or X11-related shenanigans of sudo
. This is why it’s so handy in
xidle
– you can run xidle
as your own user account, and your ACPI handler
can, as root, run pkill -SIGUSR1 xidle
and cause it to lock the screen, the
xidle
instance having been launched from your ~/.xsession
.
I use this method from a pair of shell scripts to handle my monitor settings.
dpmsman
automatically sets my monitor’s DPMS settings based on the laptop’s dock state
and a config file.
displayman
updates my xrandr
settings as needed based on what monitors are connected.
Both scripts are triggered from scripts that run as root in response to udev
and ACPI events, and would otherwise not have a convenient or elegant way to
run code as me that needs to interact with the running X session.