4

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

编码:

#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”,但是如果有人可以确认或澄清这一点,我会很感激的。他还询问如何解决(比赛条件),而不是寻找完整的答案,但任何提示都将不胜感激。

4

2 回答 2

7

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

查看man 7 signal手册页,特别是Async-signal safe functions下的部分:

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

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

在一般情况下,解决方案很重要,因为没有异步信号安全锁定原语(除了sem_post(),这本身还不够,还有文件锁,必须在所有printf()调用中使用)。通用的、可移植的解决方案是使用pipe()from创建一个管道unistd.h,并使用 将输出写入管道write(),并让主程序从管道中读取并“转发”内容。POSIX quarantees 写的比PIPE_BUF原子更短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)__atomic_fetch_add(variable,1,__ATOMIC_SEQ_CST)__sync_synchronize()__atomic_signal_fence(__ATOMIC_SEQ_CST)

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

于 2012-08-05T12:36:34.683 回答
5

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

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

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

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

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

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

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

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

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

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

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


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

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

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

这意味着 POSIX.1 打算write()在信号处理程序中安全使用,并且errno也可以对其进行操作以避免被中断的线程在errno.

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

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

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

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

我希望你发现这个信息。

于 2012-08-06T20:24:55.493 回答