我正在做一些课程,我们已经看到了以下代码.一些问题询问代码的各个行是什么,这很好,我理解,但曲线球是"这个程序包含竞争条件.它出现在哪里以及为什么出现?"
代码:
#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"将在信号到达时打印,但如果有人可以确认或澄清这一点我我非常感激.他还询问如何修复(比赛条件),不寻找完整的答案,但任何提示将不胜感激.
真的没有竞争条件; 它比那更糟糕.根据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,默认情况下未被阻止.除非处理,否则会导致进程终止.因此,根据继承的信号掩码,SIGUSR1在sigset()呼叫之前传递的信号被阻止,或导致进程终止.(但是,如果set包含SIGUSR1; SIGUSR1即被阻止,那么就会出现竞争条件,除非sigprocmask()之前被调用过sigset().但是,因为它set是空的,sigset()最好在之前调用sigprocmask().)
显然,课程的目的是修改代码
使用单独的线程,它接收调用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的基本原理似乎是"它通常有效".我个人非常讨厌,因为例如暴力也"通常有效".我认为这很愚蠢和/或懒惰.
我希望你发现这个信息丰富.