C信号处理程序中的竞争条件

时间:2012-08-05 11:37:06

标签: c linux signals

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

代码:

#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”将在信号到达时打印,但如果有人可以确认或澄清这一点,我非常感激。他还询问如何修复(比赛条件),不寻找完整的答案,但任何提示将不胜感激。

2 个答案:

答案 0 :(得分:6)

确实没有竞争条件;它比那更糟糕。根据POSIX标准,程序的行为未定义(如果信号在适当的时刻发送)。

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

  

自处理以来,信号处理函数必须非常小心   其他地方可能会在执行中的某个任意点中断   该计划。 POSIX具有“安全功能”的概念。如果一个   信号中断执行不安全的函数和处理程序   调用一个不安全的函数,然后程序的行为是   未定义。

请注意,printf()绝对不是异步信号安全功能;因此行为未定义。

在一般情况下,解决方案是非常重要的,因为没有异步信号安全锁定原语(sem_post()除了它本身是不够的)和文件锁定,这必须是用于所有printf()次调用)。通用的可移植解决方案是使用pipe()中的unistd.h创建管道,并使用write()将输出写入管道,并从管道读取主程序并“转发”内容。 POSIX保证短于PIPE_BUF的写入是原子的,PIPE_BUF至少为512(Linux中为4096) - 详见man 7 pipe - 所以这也限于512-实际上可移植代码的字节或更短的消息。

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

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

关于sigset()sigprocmask(),还有一个有趣的角落案例 - 不是竞争条件。进程从其父进程继承其信号掩码,默认情况下SIGUSR1未被阻止。除非处理,否则会导致进程终止。因此,根据继承的信号掩码,SIGUSR1调用之前传递的sigset()信号将被阻止,或导致进程终止。 (但是,如果set包含SIGUSR1;即SIGUSR1被阻止,则成为竞争条件,除非在sigprocmask()之前调用sigset() {1}}。但是,由于set为空,sigset()最好在sigprocmask()之前调用。)

答案 1 :(得分:5)

显然,课程的目的是将代码修改为

  • 使用单独的线程,该线程接收循环中调用sigwait()sigwaitinfo()的信号。必须阻止信号(首先,对于所有线程,一直是这样),或者未指定操作(或者将信号传递给另一个线程)。

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

    (还有其他接收信号的方法,例如使用设置全局标志的信号处理程序,以及循环检查。大多数方法导致忙等待,运行一个 - 没有循环,无用地耗费CPU时间:一个非常糟糕的解决方案。我在这里描述的方式没有浪费CPU时间:内核会在调用sigwait() / sigwaitinfo()时让线程进入休眠状态,并唤醒它仅在信号到达时才启动。如果您想限制睡眠持续时间,可以改用sigtimedwait()。)

  • printf()等人。不保证是线程安全的,您应该使用pthread_mutex_t来保护输出到标准输出 - 换句话说,这样两个线程不会尝试在完全相同的时间输出。

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

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

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

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

就个人而言,我发现从原始问题跳到了正确的解决方案&#34; - 如果确实我所描述的是正确的解决方案;我相信它是 - 有点拉伸。当Saf提到正确的解决方案预计会使用pthread时,这种方法才对我产生了影响。

我希望你能找到这个信息丰富,但不是一个剧透。


编辑2013-03-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)个字节。 {I}也可以在I / O密集型应用程序中使用writefd()将信号(如果使用sigqueue(),相关值,整数或指针引发)转换为套接字或管道输出(数据),使多路复用I / O流和信号处理更加容易。变体(具有标记为close-on-exec的额外文件描述符)通常用于轻松检测子进程是执行另一个进程还是失败;否则很难检测到哪个进程 - 原始子进程或已执行进程退出。

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

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

这意味着POSIX.1希望write()在信号处理程序中安全使用,并且errno也可以被操纵以避免被中断的线程在errno中看到意外的变化

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

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

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

遗憾的是,C中的几乎所有信号处理程序示例都使用stdio.h printf()等等。不仅在许多级别上都是错误的 - 从非异步安全到缓存问题,可能是非FILE字段的非原子访问,如果被中断的代码同时也在进行I / O操作 - ,但使用unistd.h正确的解决方案与此编辑中的示例类似,同样简单。在信号处理程序中使用stdio.h I / O的基本原理似乎是&#34;它通常可以工作&#34;。我个人非常厌恶,因为例如暴力也常常起作用#34;我认为这很愚蠢和/或懒惰。

我希望你能找到这个信息。