~charles/cdaniels.net

ref: 6ba7392104bb665fa6968d949d315d595c6bc869 cdaniels.net/src/sigusr.md -rw-r--r-- 4.6 KiB
6ba73921Charles Daniels add siguser draft 2 months ago

#SIGUSR1: An Under-Appreciated IPC Mechanism

Published DRAFT

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

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.