Search code examples
clinuxmultithreadinggccsignals

Targetting signal to specific thread in C


We use posix interval timer (created using timer_create()) in our process that generates SIGALRM on timer expiration. The generated signal is handled asynchronously (sigwait) by a specific thread in the process and we have blocked the signal in all other threads using sig_block. ‘Sig_block’ is invoked in the main thread before the child threads are spawned and so child threads inherit it from parent (i.e., main). However this comes with a caveat that if any of the libraries included in the process spawn any thread during dllmain, the signal will not get blocked in that thread. Also we don't have control over the internal implementation of the DLLs that we include in the process. Can you suggest how to handle this problem? Is there any other way to target the timer expiration signal to specific thread in the process?

I checked the option 'SIGEV_THREAD_ID'. However the documentation states that it is intended only for use by threading libraries.


Solution

  • If you do not mind being Linux-specific, use SIGEV_THREAD_ID. Also, I recommend using a realtime signal (SIGRTMIN+0 through SIGRTMAX-0, inclusive), since these are queued and delivered in the order they were sent.

    The reason SIGEV_THREAD_ID is documented as intended for use only by threading libraries is that Linux thread IDs are not normally exposed; this interface is not directly usable with e.g. pthreads. You will need to implement your own gettid():

    #define  _GNU_SOURCE
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/syscall.h>
    
    static inline pid_t gettid(void) { return syscall(SYS_gettid); }
    

    That will rely on Linux pthreads not doing anything silly, like switching thread-ids while keeping the same pthread_t ID.

    Personally, I suggest a different approach, using a helper thread to maintain the timeouts.

    Have a thread maintain a sorted array or a binary heap of timeout timestamps, associated with the target thread ID (pthread_t). The thread will wait in pthread_cond_timedwait() until next timeout expires, or it is signaled, indicating that the timeouts have changed (cancelled or new ones added). When one or more timeouts expire, the thread uses pthread_sigqueue() to send the appropriate signal to the target thread, with the timeout identifier as a payload.

    Perhaps a rough simplified sketch helps understand. For simplicity, let's say the pending timeouts form a singly linked list:

    struct timeout {
        struct timeout  *next;
        struct timespec  when;    /* Absolute CLOCK_REALTIME time */
        double           repeat;  /* Refire time in seconds, 0 if single-shot */
        pthread_id       thread;
        int              elapsed;
    };
    
    pthread_mutex_t      timeout_lock = PTHREAD_MUTEX_INITIALIZER;
    pthread_cond_t       timeout_wait = PTHREAD_COND_INITIALIZER;
    struct timeout      *timeout_pending = NULL;
    int                  timeout_quit = 0;
    
    static inline int timespec_cmp(const struct timespec t1, const struct timespec t2)
    {
        return (t1.tv_sec < t2.tv_sec) ? -1 :
               (t1.tv_sec > t2.tv_sec) ? +1 :
               (t1.tv_nsec < t2.tv_nsec) ? -1 :
               (t1.tv_nsec > t2.tv_nsec) ? +1 : 0;
    }
    
    static inline void timespec_add(struct timespec *const ts, const double seconds)
    {
        if (seconds > 0.0) {
            ts->tv_sec += (long)seconds;
            ts->tv_nsec += (long)(1000000000.0*(double)(seconds - (long)seconds));
            if (ts->tv_nsec < 0)
                ts->tv_nsec = 0;
            if (ts->tv_nsec >= 1000000000) {
                ts->tv_sec += ts->tv_nsec / 1000000000;
                ts->tv_nsec = ts->tv_nsec % 1000000000;
            }
        }
    }
    
    struct timeout *timeout_arm(double seconds, double repeat)
    {
        struct timeout *mark;
    
        mark = malloc(sizeof (timeout));
        if (!mark) {
            errno = ENOMEM;
            return NULL;
        }
    
        mark->thread = pthread_self();
        mark->elapsed = 0;
        clock_gettime(CLOCK_REALTIME, &(mark->when));
        timespec_add(&(mark->when), seconds);
        mark->repeat = repeat;
    
        pthread_mutex_lock(&timeout_lock);
    
        mark->next = timeout_pending;
        timeout_pending = mark;
    
        pthread_cond_signal(&timeout_wait);
        pthread_mutex_unlock(&timeout_lock);
    
        return mark;
    

    A call to timeout_arm() returns a pointer to the timeout as an identifier, so that the thread can disarm it later:

    int timeout_disarm(struct timeout *mark)
    {
        int  result = -1;
    
        pthread_mutex_lock(&timeout_lock);
    
        if (timeout_pending == mark) {
            timeout_pending = mark->next;
            mark->next = NULL;
            result = mark->elapsed;
        } else {
            struct timeout  *list = timeout_pending;
            for (; list->next != NULL; list = list->next) {
                if (list->next == mark) {
                    list->next = mark->next;
                    mark->next = NULL;
                    result = mark->elapsed;
                    break;
                }
            }
        }
    
        /* if (result != -1) free(mark); */
    
        pthread_mutex_unlock(&timeout_lock);
        return result;
    }
    

    Note that the above function does not free() the timeout structure (unless you uncomment the line near the end), and it returns -1 if the timeout cannot be found, and the elapsed field at the time when the timeout was removed if successful.

    The thread function managing the timeouts is rather simple:

    
    void *timeout_worker(void *unused)
    {
        struct timespec  when, now;
        struct timeout  *list;
    
        pthread_mutex_lock(&timeout_lock);
        while (!timeout_quit) {
            clock_gettime(CLOCK_REALTIME, &now);
    
            /* Let's limit sleeps to, say, one minute in length. */
            when = now;
            when.tv_sec += 60;
    
            /* Act upon all elapsed timeouts. */
            for (list = timeout_pending; list != NULL; list = list->next) {
                if (timespec_cmp(now, list->when) >= 0) {
                    if (!list->elapsed || list->repeat > 0) {
                        const union sigval  value = { .sival_ptr = list };
                        list->elapsed++;
                        pthread_sigqueue(list->thread, TIMEOUT_SIGNAL, value);
                        timespec_add(&(list->when), list->repeat);
                    }
                } else
                if (timespec_cmp(when, list->when) < 0) {
                    when = list->when;
                }
            }
    
            pthread_cond_timedwait(&timeout_wait, &timeout_lock, &when);
        }
    
        /* TODO: Clean up timeouts_pending list. */
    
        return NULL;    
    }
    

    Note that I haven't checked the above for typos, so there might be some. All code above is licensed under CC0-1.0: do whatever you want, just don't blame me for any errors.