【问题标题】:Race condition in C Signal handlerC 信号处理程序中的竞争条件
【发布时间】:2012-08-05 11:37:06
【问题描述】:

我正在做一些课程作业,我们看到了以下代码。有些问题问代码的不同行是做什么的,这很好,我理解,但曲线球是“这个程序包含一个竞争条件。它在哪里以及为什么出现?”

代码:

#include <stdio.h>
#include <signal.h>

static void handler(int signo) { 
    printf("This is the SIGUSR1 signal handler!\n");
}

int main(void) { 
    sigset_t set;
    sigemptyset(&set);
    sigset(SIGUSR1, handler);
    sigprocmask(SIG_SETMASK, &set, NULL);

    while(1) {
        printf("This is main()!\n");
    }
    return 0;

}

我认为竞争条件是没有办法知道信号到达时将打印什么顺序“这是主要”或“这是 SIGUSR1”,但如果有人可以确认或澄清这一点,我将不胜感激。他还询问如何解决它(比赛条件),而不是寻找完整的答案,但任何提示都将不胜感激。

【问题讨论】:

  • 提示:两个printf 语句可能会发生更糟糕的事情......
  • 为了保证完全没有竞争,让系统在调用信号处理程序之前为我们设置信号屏蔽。如果我们使用 sigaction() 系统调用来定义信号处理函数和执行处理程序时要使用的信号掩码,就可以做到这一点。既然您已经熟悉了信号处理的各种概念,您可能可以自己阅读 sigaction() 的手册页。在旧系统上,您不会找到此系统调用,但您仍然可能会发现 sigvec() 调用启用了类似的功能。 jcomeau_ictx 在其他地方发布,这可以在这里工作吗?

标签: c linux signals


【解决方案1】:

确实没有竞争条件;比那更糟。根据 POSIX 标准,程序的行为是未定义的(如果信号在正确的时刻传递)。

查看man 7 signal 手册页,特别是异步信号安全功能下的部分

信号处理函数必须非常小心,因为处理 其他地方可能会在执行中的某个任意点被中断 的程序。 POSIX 有“安全功能”的概念。如果一个 信号中断不安全函数的执行,并且处理程序 调用一个不安全的函数,那么程序的行为是 未定义。

注意printf() 绝对不是一个异步信号安全函数;因此行为未定义。

在一般情况下,解决方案是不平凡的,因为没有异步信号安全锁定原语(sem_post() 之外,这本身是不够的,文件锁,必须在周围使用所有printf() 电话)。通用的、可移植的解决方案是使用unistd.h 中的pipe() 创建一个管道,并使用write() 将输出写入管道,然后让主程序从管道中读取并“转发”内容。写比PIPE_BUF 更短的POSIX 隔离是原子的,PIPE_BUF 至少为 512(Linux 中为 4096)——有关详细信息,请参阅man 7 pipe——因此在实践中这也仅限于 512 字节或更短的消息用于可移植代码。

通常,在这种特殊情况下,将信号处理程序中的printf() 替换为设置全局volatile sigatomic_t 变量就足够了。然后主循环可以简单地检查(并清除)全局变量并输出消息本身。

虽然标志变量方法可能会丢失快速重复的SIGUSR1 信号,但这无关紧要,因为您可以总是丢失快速重复的SIGUSR1 信号:一次只能等待一个,因此重复在第一个信号和处理信号之间发生的信号根本不会传递! (如果您要使用像 SIGRTMIN+0 这样排队的实时信号,您可以通过在主循环中使用 __sync_fetch_and_and(variable,0)__atomic_exchange_n(variable,0,__ATOMIC_SEQ_CST) 等原子内置函数以及 __sync_fetch_and_add(variable,1) 或 @ 来确保捕获每一个信号信号处理程序中的 987654340@;两者之前都有一个 __sync_synchronize()__atomic_signal_fence(__ATOMIC_SEQ_CST) 调用,以确保更改立即生效/对对方可见。但在这种情况下您无需担心原子操作。)

对于sigset()sigprocmask(),还有一个有趣的极端情况——不是竞争条件。进程从其父进程继承其信号掩码,默认情况下不阻止SIGUSR1。除非处理,否则它会导致进程终止。因此,根据继承的信号掩码,在sigset() 调用之前传递的SIGUSR1 信号要么被阻塞,要么导致进程终止。 (但是,如果 set 包含 SIGUSR1;即 SIGUSR1 被阻止,则出现竞争条件,除非在 sigset() 之前调用了 sigprocmask()。但是,由于 @987654353 @ 为空,sigset() 最好在 sigprocmask() 之前调用。)

【讨论】:

  • 非常感谢您提供如此详细的答复。看来我们必须使用 posix pthreads 来实现解决方案,但我读过的所有内容都表明将两者(信号和 pthreads)混合是一个糟糕的主意。对此有何建议?我会尝试解决它,如果我不能让它工作,可能会发布代码。
  • @Saf:有很多混合信号和 pthread 的技术。您确实必须小心,但一些简单的技术可以使整个事情变得简单。例如,由于 write (NOT fwrite) 是异步安全的,您可以通过管道或 Unix 域套接字从信号处理程序发送消息。
  • 我在 Linux 中没有遇到任何重大问题;我使用man 7 signal 作为参考。信号处理程序是为每个进程分配的,因此所有线程将始终具有相同的处理程序,但可以将信号定向到特定线程。如果要将信号传递给特定线程,您可能需要使用 pthread_sigmask() 阻止信号;否则内核将“随机”选择哪个线程将接收信号。在复杂的情况下,您可以使用反射器,一个将信号(将信号编号更改为 SIGRTMIN+1)转发到使用 pthread_sigqueue() 的线程的处理程序。
  • @Saf:在这种情况下,为整个进程(所有线程)阻塞SIGUSR1,并在专用线程中循环调用sigwait()以接收信号。 GNU C pthread 是线程安全的,因此您可以使用 printf() 等从所有线程输出到标准输出。使用fflush(stdout); 确保printf()s 没有被C 库缓存。如果您想炫耀,请安装一个SIGINT 处理程序,它将全局volatile sig_atomic_t 变量设置为零。将其初始化为 1,并让线程循环直到变为零,然后清理并退出。然后,Ctrl+C干净地停止你的程序。
【解决方案2】:

显然课程中的意图是将代码修改为

  • 使用单独的线程,在循环中接收调用sigwait()sigwaitinfo() 的信号。信号必须被阻塞(首先,并且一直,对于所有线程),或者操作未指定(或者信号被传递到另一个线程)。

    这种方式本身没有信号处理函数,这将仅限于异步信号安全函数。调用sigwait()/sigwaitinfo() 的线程是完全正常的线程,不受任何与信号或信号处理程序相关的限制。

    (还有其他接收信号的方法,例如使用设置全局标志的信号处理程序,并进行循环检查。大多数会导致 busy-waiting,运行 do -nothing 循环,无用地消耗 CPU 时间:一个非常糟糕的解决方案。我在这里描述的方式不会浪费任何 CPU 时间:内核将在调用 sigwait()/sigwaitinfo() 时将线程置于睡眠状态,并且仅在信号到达。如果要限制睡眠时长,可以使用sigtimedwait()代替。)

  • 自从printf() 等。不保证是线程安全的,您可能应该使用pthread_mutex_t 来保护输出到标准输出——换句话说,这样两个线程就不会尝试同时输出。

    在 Linux 中这不是必需的,因为 GNU C printf()_unlocked() 版本除外)是线程安全的;对这些函数的每次调用都已经使用了一个内部互斥体。

    注意C库可能会缓存输出,所以要确保输出数据,需要调用fflush(stdout);

    如果您想以原子方式使用多个printf()fputs() 或类似调用,而其他线程无法在其间注入输出,则互斥锁特别有用。因此,建议使用互斥锁,即使在简单情况下在 Linux 上不需要它。 (是的,您确实想在持有互斥锁时也执行fflush(),尽管如果输出阻塞可能会导致互斥锁被持有很长时间。)

我个人会以完全不同的方式解决整个问题——我会在信号处理程序中使用write(STDERR_FILENO,) 输出到标准错误,并将主程序输出到标准输出;没有线程或任何特殊需要,只是信号处理程序中的一个简单的低级写循环。严格来说,我的程序的行为会有所不同,但对于最终用户来说,结果看起来非常相似。 (除了可以将输出重定向到不同的终端窗口,并并排查看它们;或将它们重定向到辅助脚本/程序,这些脚本/程序将纳秒挂钟时间戳添加到每个输入行;以及在调查时有用的其他类似技巧东西。)

就个人而言,我发现了从原始问题到“正确解决方案”的跳跃——如果我所描述的确实是正确的解决方案;我确实认为这有点牵强。当 Saf 提到正确的解决方案应该使用 pthreads 时,我才意识到这种方法。

我希望您能从中找到信息,但不要剧透。


2013 年 3 月 13 日编辑:

这是我用来安全地将数据从信号处理程序写入描述符的writefd() 函数。我还包括了包装函数wrout()wrerr(),您可以使用它们分别将字符串写入标准输出或标准错误。

#include <unistd.h>
#include <string.h>
#include <errno.h>

/**
 * writefd() - A variant of write(2)
 *
 * This function returns 0 if the write was successful, and the nonzero
 * errno code otherwise, with errno itself kept unchanged.
 * This function is safe to use in a signal handler;
 * it is async-signal-safe, and keeps errno unchanged.
 *
 * Interrupts due to signal delivery are ignored.
 * This function does work with non-blocking sockets,
 * but it does a very inefficient busy-wait loop to do so.
*/
int writefd(const int descriptor, const void *const data, const size_t size)
{
    const char       *head = (const char *)data;
    const char *const tail = (const char *)data + size;
    ssize_t           bytes;
    int               saved_errno, retval;

    /* File descriptor -1 is always invalid. */
    if (descriptor == -1)
        return EINVAL;

    /* If there is nothing to write, return immediately. */
    if (size == 0)
        return 0;

    /* Save errno, so that it can be restored later on.
     * errno is a thread-local variable, meaning its value is
     * local to each thread, and is accessible only from the same thread.
     * If this function is called in an interrupt handler, this stores
     * the value of errno for the thread that was interrupted by the
     * signal delivery. If we restore the value before returning from
     * this function, all changes this function may do to errno
     * will be undetectable outside this function, due to thread-locality.
    */
    saved_errno = errno;

    while (head < tail) {

        bytes = write(descriptor, head, (size_t)(tail - head));

        if (bytes > (ssize_t)0) {
            head += bytes;

        } else
        if (bytes != (ssize_t)-1) {
            errno = saved_errno;
            return EIO;

        } else
        if (errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK) {
            /* EINTR, EAGAIN and EWOULDBLOCK cause the write to be
             * immediately retried. Everything else is an error. */
            retval = errno;
            errno = saved_errno;
            return retval;
        }
    }

    errno = saved_errno;
    return 0;
}

/**
 * wrout() - An async-signal-safe alternative to fputs(string, stdout)
 *
 * This function will write the specified string to standard output,
 * and return 0 if successful, or a nonzero errno error code otherwise.
 * errno itself is kept unchanged.
 *
 * You should not mix output to stdout and this function,
 * unless stdout is set to unbuffered.
 *
 * Unless standard output is a pipe and the string is at most PIPE_BUF
 * bytes long (PIPE_BUF >= 512), the write is not atomic.
 * This means that if you use this function in a signal handler,
 * or in multiple threads, the writes may be interspersed with each other.
*/
int wrout(const char *const string)
{
    if (string)
        return writefd(STDOUT_FILENO, string, strlen(string));
    else
        return 0;
}

/**
 * wrerr() - An async-signal-safe alternative to fputs(string, stderr)
 *
 * This function will write the specified string to standard error,
 * and return 0 if successful, or a nonzero errno error code otherwise.
 * errno itself is kept unchanged.
 *
 * You should not mix output to stderr and this function,
 * unless stderr is set to unbuffered.
 *
 * Unless standard error is a pipe and the string is at most PIPE_BUF
 * bytes long (PIPE_BUF >= 512), the write is not atomic.
 * This means that if you use this function in a signal handler,
 * or in multiple threads, the writes may be interspersed with each other.
*/
int wrerr(const char *const string)
{
    if (string)
        return writefd(STDERR_FILENO, string, strlen(string));
    else
        return 0;
}

如果文件描述符引用管道,writefd() 可用于以原子方式写入最多PIPE_BUF(至少 512)字节。 writefd() 也可用于 I/O 密集型应用程序,以将信号(如果使用 sigqueue() 引发,则相关值、整数或指针)转换为套接字或管道输出(数据),使其更容易多路复用多个 I/O 流和信号处理。变体(带有标记为 close-on-exec 的额外文件描述符)通常用于轻松检测子进程是执行了另一个进程还是失败了;否则很难检测出哪个进程——原来的子进程,还是执行的进程——退出了。

在此答案的 cmets 中,有一些关于 errno 的讨论,以及 write(2) 修改 errno 是否使其不适合信号处理程序这一事实感到困惑。

首先,POSIX.1-2008(及更早版本)将async-signal-safe 函数定义为可以从信号处理程序安全调用的函数。 2.4.3 Signal actions 章节包括此类函数的列表,包括 write()。请注意,它还明确指出 “获取 errno 值的操作和为 errno 分配值的操作应是异步信号安全的。”

这意味着 POSIX.1 打算将 write() 安全地用于信号处理程序中,并且还可以对 errno 进行操作以避免被中断的线程看到 errno 中的意外变化。

因为errno是线程局部变量,所以每个线程都有自己的errno。传递信号时,它总是会中断进程中现有的线程之一。信号可以定向到特定线程,但通常内核决定哪个线程获得进程范围的信号;它因系统而异。如果只有一个线程,即初始线程或主线程,那么显然是被中断的线程。所有这一切意味着,如果信号处理程序保存它最初看到的errno 的值,并在它返回之前恢复它,那么对errno 的更改在信号处理程序之外是不可见的。

有一种方法可以检测它,但是,在 POSIX.1-2008 中也通过谨慎的措辞暗示:

从技术上讲,&amp;errno 几乎总是有效的(取决于所应用的系统、编译器和标准),并产生包含当前线程错误代码的 int 变量的地址。因此,另一个线程可以监视另一个线程的错误代码,是的,这个线程会在信号处理程序中看到对它的更改。但是,不能保证其他线程能够原子地访问错误代码(尽管它在许多架构上是原子的):这种“监视”无论如何都只会提供信息。

遗憾的是,几乎所有 C 中的信号处理程序示例都使用 stdio.h printf() 等等。不仅在许多层面上都是错误的——从非异步安全到缓存问题,可能是对FILE 字段的非原子访问,如果被中断的代码同时也在执行 I/O——,而且使用unistd.h正确 解决方案与我在此编辑中的示例类似,同样简单。在信号处理程序中使用 stdio.h I/O 的基本原理似乎是“它通常有效”。我个人讨厌这一点,因为例如暴力也“通常有效”。我认为它愚蠢和/或懒惰。

我希望你发现了这些信息。

【讨论】:

  • 这是一个很好的解释,并给了我很多关于我仍然需要做的其他相关工作的信息 - 非常感谢。完成后我会链接或发布解决方案。
  • @JulienPalard:Poppycock,当然可以。 errno 是每个线程的变量。您需要做的就是在任何write() 调用之前保存errno 的值(以及您可能使用的任何其他修改errno 的异步信号安全调用),然后在从信号处理程序返回之前恢复它。哦,感谢您基于不正确的信息/假设而投反对票。
  • @NominalAnimal 如果可以的话,我会拒绝你两次。线程安全与信号安全不同,这里的问题是关于信号,而不是线程,请查看 POSIX 第 3.3.1.3 章。请停止基于错误假设传播错误信息。
  • @JulienPalard:你要么是巨魔,要么是白痴。 write() 是一个 async-signal-safe 函数,明确列出在信号处理程序中使用是安全的。如果发生错误,write() 会修改 errno。因为errno 是线程局部变量,所以该更改仅对信号传递中断的线程可见。将信号处理程序中的errno 恢复到它在信号处理程序开始时的值会使对errno 的任何更改在信号处理程序之外都无法检测到。阅读 IEEE 1003.1-2008 3.2.7 Async-signal-safe 和 2.4.3 Signal actions,然后意识到你完全错了。
  • @NominalAnimal 好的,写入的 TIL 列在“POSIX.1-2004”中的异步信号安全功能列表中,再次为您投票,对不起!
猜你喜欢
  • 2012-04-15
  • 1970-01-01
  • 2011-09-23
  • 2014-09-23
  • 1970-01-01
  • 2011-12-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多