使用异步信号保证互斥锁安全

6

首先,我知道互斥锁通常不被认为是异步安全的。这个问题涉及使用 sigprocmask 在带有异步信号和信号处理程序的多线程程序中使互斥锁安全。

我有一些类似以下概念的代码:

struct { int a, b; } gvars;

void sigfoo_handler(int signo, siginfo_t *info, void *context) {
    if(gvars.a == 42 || gvars.b == 13) {
        /* run a chained signal handler */
    }
}

/* called from normal code */
void update_gvars(int a, int b) {
    gvars.a = a;
    gvars.b = b;
}

gvars 是一个全局变量,它太大了,无法适应单个 sig_atomic_t。它由普通代码更新并从信号处理程序中读取。受控代码是链接的信号处理程序,因此必须在信号处理程序上下文中运行(它可以使用 infocontext)。因此,所有对 gvars 的访问都必须通过某种同步机制进行控制。更加复杂的是,该程序是多线程的,任何线程都可能接收到 SIGFOO

问题: 通过结合 sigprocmask(或 pthread_sigmask)和 pthread_mutex_t,是否可以使用以下代码来保证同步?

struct { int a, b; } gvars;
pthread_mutex_t gvars_mutex;

void sigfoo_handler(int signo, siginfo_t *info, void *context) {
    /* Assume SIGFOO's handler does not have NODEFER set, i.e. it is automatically blocked upon entry */
    pthread_mutex_lock(&gvars_mutex);
    int cond = gvars.a == 42 || gvars.b == 13;
    pthread_mutex_unlock(&gvars_mutex);

    if(cond) {
        /* run a chained signal handler */
    }
}

/* called from normal code */
void update_gvars(int a, int b) {
    sigset_t set, oset;
    sigemptyset(&set);
    sigaddset(&set, SIGFOO);
    pthread_sigmask(SIG_BLOCK, &set, &oset);
    pthread_mutex_lock(&gvars_mutex);
    gvars.a = a;
    gvars.b = b;
    pthread_mutex_unlock(&gvars_mutex);
    pthread_sigmask(SIG_SETMASK, &oset, NULL);
}

逻辑如下:在sigfoo_handler中,SIGFOO被阻塞,因此它不能中断pthread_mutex_lock。在update_gvars中,在pthread_sigmask保护的临界区域中,当前线程不会引发SIGFOO,因此它也无法中断pthread_mutex_lock。假设没有任何其他信号(我们总是可以阻止任何可能有问题的其他信号),锁定/解锁应该始终以正常、不可中断的方式在当前线程上进行,并且锁定/解锁的使用应确保其他线程不会干扰。我说得对吗?还是应该避免这种方法?
2个回答

2
我发现了这篇论文https://www.cs.purdue.edu/homes/rego/cs543/threads/signals.pdf,它讨论了如何安全地在信号处理程序中运行AS-unsafe代码,其方法是:
  1. 在正常上下文代码的AS-unsafe块中屏蔽信号(效率较低)或
  2. 使用全局sig_atomic volatile标志来保护正常上下文代码中的AS-unsafe块,如果设置该标志,则防止在处理程序中进入AS-unsafe代码(效率更高)
此方法满足POSIX标准的一部分,即只有当sighandler中断AS-unsafe函数时,调用AS-unsafe函数才被认为是不安全的(http://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_04_03_03:请向下滚动至功能列表后的第一段落)。
我认为你在这里尝试的基本上是这个想法的更细粒度版本,因为你不是试图防止
pthread_mutex_lock(&gvars_mutex);
int cond = gvars.a == 42 || gvars.b == 13;
pthread_mutex_unlock(&gvars_mutex);

在处理这个互斥锁和这些变量时,从sig-handler运行与任何AS-unsafe代码冲突而不是与其他AS-unsafe代码冲突很重要。

遗憾的是,POSIX只有对信号安全性的代码概念:一个函数是否安全,而不考虑其参数。

然而,我认为,信号量/互斥锁没有理由操作除传递的mutex/semaphore中包含的那些数据或操作系统句柄之外的任何数据或操作系统句柄,因此,如果保证从未与同一互斥锁的sem_wait/pthread_mutex_lock发生冲突,即使POSIX标准在技术上表示它不安全,我认为从信号处理程序调用sem_wait(&sem)/pthread_mutex_lock(&mx)应该是安全的(欢迎提出反驳意见)。


1
我的观点是你是正确的,更多或更少地是“我们在生产中使用它,没有出现任何问题”:D。但是,当然,任何正确性都可能归因于运气(例如,内核恰好表现正确的方式,libc 表现正确的方式等),而不是严格按照标准正确。 - nneonneo

1
你显然知道提到sig_atomic_t时会进入未定义的行为领域。话虽如此,我能想到这个确切的例子在现代类Unix系统上无法工作的唯一方法是如果使用了SA_NODEFER设置信号。
互斥锁足以确保不同线程(包括在另一个线程中运行的信号处理程序)之间的正确同步,而sigmask将防止此线程中的信号处理程序递归互斥锁。
话虽如此,在信号处理程序中使用锁可能会带来麻烦。一个信号处理程序可能足够安全,但如果有两个信号处理程序使用不同的锁执行相同的技巧,则会出现锁定顺序死锁。这可以通过应用进程sigmask而不是线程sigmask来部分缓解。例如,在信号处理程序中进行简单的调试fprintf肯定会违反锁定顺序。
我会放弃并重新设计我的应用程序,因为在信号处理程序中使用此类东西表明它变得过于复杂且容易出错。由于获取其他任何正确的东西的爆炸性复杂性,信号处理程序仅涉及一个sig_atomic_t是C标准中定义的唯一事物。

如果使用 SA_NODEFER 设置信号,那么我就可以在处理程序中以同样的方式使用 sigprocmask,对吗? - nneonneo
1
此外,处理 sigmask 是不可行的,因为在两个线程同时使用 sigprocmask 绝对是不行的。我怀疑最好的方法是,如果有多个信号处理程序,则在线程 sigmask 中阻止所有信号。 - nneonneo
如果可以的话,我肯定会重新设计这个应用程序,但在我的情况下,有非常好的理由需要这样做。 - nneonneo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接