Signals, Timers, and Waiting

February 8, 2022


Timers

Let's say that you're writing a program in which you need to coordinate between two threads, but want to do so in an infrequent, low-overhead manner. There are many ways that you can do this, but one of the convenient ones is to use signals. This works well if events happen pretty slowly in your program; think on the order of one second or more. For this use-case, signals offer ease-of-use and the ability to free up computing resources instead of busy-waiting on precise locks, polling file descriptors, or introducing unecessary overhead.

Here's an example: you have a thread which you want to wake up every one second, read a structure, and update the UI. In that thread, you might do something like:

  1. Define a function which should happen each second. void ui_thread_interval(int s) { ... }
  2. Set up a signal handler to call the function each time a signal is seen. struct sigaction sa; int interval_signal; interval_signal = SIGRTMIN; sa.sa_flags = 0; sa.sa_handler = ui_thread_interval; sigemptyset(&sa.sa_mask); if(sigaction(interval_signal, &sa, NULL) == -1) { fprintf(stderr, "Error creating interval signal handler. Aborting.\n"); exit(1); }
  3. Block that signal. This is so that it won't happen before we've finished setting things up. sigset_t mask; sigemptyset(&mask); sigaddset(&mask, interval_signal); if(sigprocmask(SIG_SETMASK, &mask, NULL) == -1) { fprintf(stderr, "Error blocking signal. Aborting.\n"); exit(1); }
  4. Create a realtime timer to trigger that signal. struct sigevent sev; timer_t interval_timer; sev.sigev_notify = SIGEV_THREAD_ID; sev.sigev_signo = interval_signal; sev.sigev_value.sival_ptr = &interval_timer; sev._sigev_un._tid = syscall(SYS_gettid); if(timer_create(CLOCK_REALTIME, &sev, &interval_timer) == -1) { fprintf(stderr, "Error creating timer. Aborting.\n"); exit(1); }
  5. Set the timer to occur every second. struct itimerspec its; its.it_value.tv_sec = 1; its.it_value.tv_nsec = 0; its.it_interval.tv_sec = 1; its.it_interval.tv_nsec = 0; if(timer_settime(interval_timer, 0, &its, NULL) == -1) { fprintf(stderr, "Error setting the timer. Aborting.\n"); exit(1); }
  6. Unblock the signal. if(sigprocmask(SIG_UNBLOCK, &mask, NULL) == -1) { fprintf(stderr, "Error unblocking signal. Aborting.\n"); exit(1); }

Once you're done, that thread should receive a SIGRTMIN approximately every one second, and that signal should cause ui_thread_interval to run.

Often, this code is supplemented by a timer which records when each wakeup happens. Depending on whether the wakeup happens too late or early, nanosleep and simply ignoring signals can account for the difference. This helps keep a similar low overhead while ensuring slightly more accuracy if required.

Waiting

Now let's say that you've got another thread in your program, and you'd like for it to kill the thread when the program receives SIGTERM. You can simply do something like this, to wait on SIGTERM (without busy-waiting), and perhaps break from a loop and exit the thread when it occurs:


sigset_t mask;
int sig;

sigemptyset(&mask);
sigaddset(&mask, SIGTERM);
if(sigprocmask(SIG_BLOCK, &mask, NULL) == -1) {
    fprintf(stderr, "Error blocking SIGTERM. Aborting.\n");
    exit(1);
}
while(sigwait(&mask, &sig) == 0) {
    if(sig == SIGTERM) {
        break;
    }
}