I am writing a Linux program that interacts with some custom hardware
via GPIO lines. It uses a timer periodically delivering SIGALRM
in
order to manage the timing of these interactions, and there is quite
some work performed within the signal handler. The program offers a
command-line interface, and I would love to have the line-editing
convenience provided by libreadline. Unfortunately, it would seem
libreadline does not cope well with those periodic signals. I also tried
the readline compatibility mode of libedit (a.k.a. “editline”), with
no luck.
Is there an easy way to have readline capability in a program that
receives numerous SIGALRM
signals?
Symptoms
When using libreadline, if the timer is running, some of the characters typed are randomly echoed twice. For example, typing "help" may result in "heelp" being displayed on the terminal. The issue is only apparent in the echo: the program does receive the word as typed (i.e. "help").
When using libedit in readline compatibility mode, if the timer is
running, readline()
returns NULL
whenever it is interrupted by the
SIGALRM
signal.
When the timer is stopped, everything works as expected, both with libreadline and with libedit.
Environment
Ubuntu 20.04 with the latest apt packages libreadline-dev (version 8.0-4) and libedit-dev (3.1-20191231-1). The program will eventually be deployed on a Raspberry Pi running Raspberry Pi OS.
Example code
Here is an attempt at a minimal(ish), reproducible example:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <signal.h>
#include <sys/time.h>
#ifdef USE_LIBEDIT
# include <editline/readline.h>
#else
# include <readline/readline.h>
#endif
#define PERIOD_US 1000 // 1.0 ms
#define DELAY_NS 700000 // 0.7 ms
/* Timer settings. */
const struct itimerval interval_off = {
.it_value = { .tv_sec = 0, .tv_usec = 0 },
.it_interval = { .tv_sec = 0, .tv_usec = 0 }
};
const struct itimerval interval_1ms = {
.it_value = { .tv_sec = 0, .tv_usec = PERIOD_US },
.it_interval = { .tv_sec = 0, .tv_usec = PERIOD_US }
};
static void sigalrm_callback(int signum)
{
(void) signum;
// Simulate work by busy-wating for DELAY_NS.
struct timespec end, now;
clock_gettime(CLOCK_MONOTONIC, &end);
end.tv_nsec += DELAY_NS;
end.tv_sec += end.tv_nsec / 1000000000;
end.tv_nsec %= 1000000000;
do
clock_gettime(CLOCK_MONOTONIC, &now);
while (now.tv_sec < end.tv_sec
|| (now.tv_sec == end.tv_sec && now.tv_nsec < end.tv_nsec));
}
static int should_quit = 0;
/* Interpret and free line. */
void interpret(char *line)
{
if (!line) {
printf("Got NULL line\n");
} else if (line[0] == '\0') {
/* Ignore empty line. */
} else if (strcmp(line, "help") == 0) {
puts("help print this help\n"
"start start the interval timer at 1 kHz\n"
"stop stop the interval timer\n"
"quit end the program");
} else if (strcmp(line, "start") == 0) {
setitimer(ITIMER_REAL, &interval_1ms, NULL);
printf("Periodic timer started.\n");
} else if (strcmp(line, "stop") == 0) {
setitimer(ITIMER_REAL, &interval_off, NULL);
printf("Periodic timer stopped.\n");
} else if (strcmp(line, "quit") == 0) {
should_quit = 1;
} else {
printf("Unknown command \"%s\".\n", line);
}
free(line);
}
int main(void)
{
/* Catch SIGALRM. */
struct sigaction action;
action.sa_handler = sigalrm_callback;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
sigaction(SIGALRM, &action, NULL);
/* Process commands. */
while (!should_quit) {
char *line = readline("> ");
interpret(line);
}
return EXIT_SUCCESS;
}
Compile either with
gcc -O2 readline-alrm.c -lreadline -o readline-alrm
or
gcc -O2 -DUSE_LIBEDIT readline-alrm.c -ledit -o readline-alrm
Edit: I moved the command interpreter out of main()
.