好吧,标题说明了大部分内容。
假设我的应用程序正在记录到标准输出/文件。但是,当终止时,它并不总是完全刷新。一种解决方案是在每次记录操作后刷新,但是这会减慢程序的速度,令人无法接受。
所以,问题是,fflush(file)
从信号处理程序调用是否安全,甚至可能fflush(NULL)
?
如果不是,请说明原因。这个问题还有其他解决方案吗?如果我知道我不在文件处理程序中,这可能安全吗?
您只能在信号处理程序中使用异步安全函数,并且不包括 stdio 库。POSIX 标准定义了一组异步安全的函数。您还没有指定平台,我不知道通用的解决方案。
如果您将支持 FILE 的文件描述符存储在fileno()
POSIX / BSD 系统上,则可以使用异步安全函数来挽救某些东西:write()
, lseek()
(flush),fsync()
等。当然,如果 stdio 没有帮助正在使用自己的缓冲区。
@heinrich5991:根据程序,可能有一种信号安全的执行方式fflush()
。如果事情做得很仔细,你最后一个问题的答案(在你对布雷特黑尔答案的评论中)应该是肯定的。但是,它可能会变得复杂。
为了说明,让我们从一个简单的cat
程序版本开始:
int main(void)
{
int c;
while (1) {
c = getchar();
if (c < 0 || c > UCHAR_MAX) /* Includes EOF */
break;
putchar(c);
}
fflush(stdout);
return 0;
}
getchar()
程序在返回时优雅地终止EOF
。
EOF
当此程序从可能永远不会发送的流中获取输入时(例如,处于原始模式的终端),可能需要使用信号。对于这种情况,我们可以尝试通过它捕获的信号来实现优雅终止,比如SIGUSR1
:
void sig_handler(int signo)
{
if (signo == SIGUSR1)
_exit(0);
}
int main(void)
{
/* Code for installing SIGUSR1 signal handler here */
/* Remaining code here as before */
}
这个处理程序是安全的,因为_exit()
异步信号是安全的。问题是SIGUSR1
信号会在不刷新输出缓冲区的情况下终止程序,这可能会导致输出丢失。我们可以尝试通过强制刷新来解决这个问题,如下所示:
void sig_handler(int signo)
{
if (signo == SIGUSR1) {
fflush(stdout);
_exit(0);
}
}
但是它不再是信号安全的,因为fflush()
不是。库putchar()
函数定期刷新stdout
。信号可能在调用的中间到达putchar()
,这可能在冲洗中间stdout
。然后,我们的处理程序再次调用fflush(stdout)
,而stdout
(from main
's putchar
) 的先前刷新尚未完成。根据标准库的实现,这可能会导致stdout
缓冲区覆盖自身的一部分(修改输出),或者stdout
缓冲区的剩余部分被复制。
一种可能的方法是让处理程序只设置一个全局标志变量,然后让主程序处理刷新:
volatile int gotsignal = 0;
void sig_handler(int signo)
{
if (signo == SIGUSR1)
gotsignal = 1;
}
int main()
{
int c;
/* Code for installing signal handler here */
while (1) {
c = getchar();
if (c < 0 || c > UCHAR_MAX)
break;
putchar(c);
if (gotsig == 1)
break;
}
fflush(stdout);
return 0;
}
这再次变得信号安全,但引入了死锁。假设程序已经接收到它将要获得的所有输入,我们发送一个SIGUSR1
信号来优雅地终止它。当进程在读调用中被阻塞时,信号到达getchar()
。信号将由处理程序处理,gotsig
将设置为 1,但控制权将返回到被阻塞的getchar()
调用,因此它将永远阻塞,永远不会(优雅地)终止。我们陷入了僵局。
一个解法:
您引用的相同 GNU C 库文档(“信号处理和不可重入函数”小节)说:
如果您在处理程序中调用函数,请确保它对于信号是可重入的,否则请确保信号不会中断对相关函数的调用。
我们可以从上面句子的“or else”部分得到一些想法,关于中断对“相关函数”的调用。我们主要代码中的哪些函数与fflush(stdout)
?我们的主要代码只调用了两个函数(标准库),getchar()
并且putchar()
. 可以合理地假设 getchar() 仅触及stdin
,因此不会调用fflush(stodut)
或任何从属函数。所以这里的“相关”功能是putchar()
,因为如前所述,它会定期触发fflush(stdout)
,或相关的东西。
按照 GNU C Libarary 的建议,我们可以让处理程序这样做:
如果处理程序在调用putchar()
中被调用main()
,它不会fflush(stdout)
;只需返回并main()
冲洗即可
fflush(stdout)
否则,从处理程序中取出应该是安全的
这解决了上述所有问题(刷新/不安全/死锁)。
为了实现这一点,我们使用另一个全局标志unsafe_to_flush
.
下面的最终代码应该是信号安全的并且没有死锁,但比上面的早期尝试更复杂。
volatile int unsafe_to_flush = 0;
volatile int gotsig = 0;
void sig_handler(int signo)
{
if (signo == SIGUSR1) {
gotsig = 1; /* Caller can flush using this flag */
if (unsafe_to_flush) {
/* We got called from putchar()! */
return;
}
else {
/* Safe to call fflush(stdout) */
fflush(stdout);
_exit(0);
}
}
}
int main(void)
{
int c;
/* Code for installing signal handler here */
while (1) {
c = getchar();
if (c < 0 || c > UCHAR_MAX)
break;
/* Critical region */
unsafe_to_flush = 1;
putchar(c); /* non-reentrant fflush related */
unsafe_to_flush = 0;
if (gotsig == 1)
break;
}
fflush(stdout);
return 0;
}