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

Saf*_*Saf 3 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;
Run Code Online (Sandbox Code Playgroud)

}

我在想,竞争条件是没有办法知道什么顺序"这是主要的"或"这是SIGUSR1"将在信号到达时打印,但如果有人可以确认或澄清这一点我我非常感激.他还询问如何修复(比赛条件),不寻找完整的答案,但任何提示将不胜感激.

Nom*_*mal 6

真的没有竞争条件; 它比那更糟糕.根据POSIX标准,程序的行为是不确定的(如果信号在适当的时刻传递).

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

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

注意,printf()绝对不是异步信号安全功能 ; 因此行为是不确定的.

在一般情况下,解决方案是非常重要的,因为没有异步信号安全锁定原语(除了sem_post(),它本身不足以满足这一要求,而且文件锁定必须在所有printf()调用中使用).通用的便携式解决方案是使用pipe()from 创建管道unistd.h,并使用输出将输出写入管道write(),并从管道读取主程序并"转发"内容.POSIX保证写入时间比PIPE_BUF原子短PIPE_BUF,至少为512(Linux中man 7 pipe为4096) - 详情请参阅- 因此,在实际可行的便携式代码中,这也仅限于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)__atomic_fetch_add(variable,1,__ATOMIC_SEQ_CST)在信号处理程序中捕获每一个;两者都先写入__sync_synchronize()__atomic_signal_fence(__ATOMIC_SEQ_CST)调用make确保更改立即对另一方有效/可见.但在这种情况下,您无需担心原子操作.)

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


Nom*_*mal 5

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

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

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

    (还有其他接收信号的方法,例如使用设置全局标志的信号处理程序,以及循环检查.大多数方法导致忙等待,运行无操作循环,无用地烧掉CPU时间:a非常糟糕的解决方案.我在这里描述的方式没有浪费CPU时间:内核会在调用sigwait()/ 时将线程置于休眠状态sigwaitinfo(),并且仅在信号到达时将其唤醒.如果你想限制睡眠的持续时间,你可以sigtimedwait()改为使用.)

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

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

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

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

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

就个人而言,我发现从原始问题跳到"正确的解决方案" - 如果我所描述的确实是正确的解决方案; 我相信它是 - 有点拉伸.当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;
}
Run Code Online (Sandbox Code Playgroud)

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

在对这个答案的评论中,有一些讨论errno和混淆是否write(2)修改errno使其不适合信号处理程序.

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

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

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

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

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

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

我希望你发现这个信息丰富.