我正在做一些课程,我们已经看到了以下代码。一些问题询问代码的各个行是什么,这很好,我理解,但曲线球是“这个程序包含竞争条件。它出现在哪里以及为什么出现?”
代码:
#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”将在信号到达时打印,但如果有人可以确认或澄清这一点,我非常感激。他还询问如何修复(比赛条件),不寻找完整的答案,但任何提示将不胜感激。
答案 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;我认为这很愚蠢和/或懒惰。
我希望你能找到这个信息。