将信号定位到 C 中的特定线程

Targetting signal to specific thread in C

我们在我们的进程中使用 posix 间隔计时器(使用 timer_create() 创建),在计时器到期时生成 SIGALRM。生成的信号由进程中的特定线程异步处理 (sigwait),我们使用 sig_block 在所有其他线程中阻塞了该信号。 “Sig_block”在子线程生成之前在主线程中调用,因此子线程从父线程(即主线程)继承它。然而,这有一个警告,如果进程中包含的任何库在 dllmain 期间生成任何线程,信号将不会在该线程中被阻塞。此外,我们无法控制流程中包含的 DLL 的内部实现。你能建议如何处理这个问题吗?有没有其他方法可以将定时器到期信号定位到进程中的特定线程?

我勾选了选项'SIGEV_THREAD_ID'。但是文档指出它仅供线程库使用。

如果您不介意特定于 Linux,请使用 SIGEV_THREAD_ID。此外,我建议使用实时信号(SIGRTMIN+0SIGRTMAX-0,包括在内),因为它们按照发送的顺序排队和传送。

SIGEV_THREAD_ID 被记录为仅供线程库使用的原因是 Linux 线程 ID 通常不会公开;该接口不能直接用于例如线程。您将需要实现自己的 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); }

这将依赖于 Linux pthreads 不做任何愚蠢的事情,比如在保持相同 pthread_t ID 的同时切换线程 ID。

就个人而言,我建议使用不同的方法,使用辅助线程来维持超时。

让线程维护一个排序数组或超时时间戳的二进制堆,与目标线程 ID (pthread_t) 相关联。线程将在 pthread_cond_timedwait() 中等待,直到下一次超时到期,或者它收到信号,表明超时已更改(取消或添加了新的超时)。当一个或多个超时到期时,线程使用 pthread_sigqueue() 将适当的信号发送到目标线程,并将超时标识符作为有效负载。

也许粗略的简化草图有助于理解。为简单起见,假设未决超时形成一个单向链表:

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;

调用 timeout_arm() returns 指向超时的指针作为标识符,以便线程稍后可以解除它:

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;
}

注意上面的函数没有free()超时结构(除非你取消注释接近末尾的行),如果找不到超时,它returns -1elapsed 成功时删除超时时间的字段。

管理超时的线程函数相当简单:


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;    
}

请注意,我没有检查上面的拼写错误,所以可能会有一些。以上所有代码均在 CC0-1.0 下授权:你想做什么就做什么,不要因为任何错误而责怪我。

不幸的是,您想要的将计时器信号定向到特定线程的行为不可移植。

要解决您的 DLL 的天真行为——太天真了我会认为它有缺陷——您有一些可移植的选项。

exec.

之前,您可以在 SIGALRM 已被阻止的情况下调用您的程序

您的计时器可以改为指定 SIGEV_THREAD,然后该线程可以处理超时或通知您的专用线程该工作了。

您可以像 Glärbo .

一样在同步休眠线程中实现在没有信号的情况下保持自己的时间