💾 Archived View for cdaniels.net › posts › sigusr.gmi captured on 2023-01-29 at 15:26:10. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2022-01-08)
-=-=-=-=-=-=-
Published 2020-06-23
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)[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)[2] on OpenBSD, and signal(7)[3] 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[4] reminded me that the example presented above exhibits undefined behavior: printf() is not signal-safe[5].
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)[6]. 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:
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[7] automatically sets my monitor's DPMS settings based on the laptop's dock state and a config file. displayman[8] 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.
Copyright 2020 Charles Daniels.