【问题标题】:Is fflush safe to be called from a signal handler?从信号处理程序调用 fflush 是否安全?
【发布时间】:2014-01-30 15:29:51
【问题描述】:

嗯,标题说明了大部分内容。

假设我的应用程序正在记录到标准输出/文件。但是,当终止时,它并不总是完全刷新。一种解决方案是在每次日志记录操作后刷新,但这会大大降低程序速度。

所以,问题是,从信号处理程序调用fflush(file) 是否安全,甚至可能是fflush(NULL)

如果不是,请说明原因。这个问题还有其他解决方案吗?如果我知道我不在文件处理例程中,这可能安全吗?

【问题讨论】:

  • 不,因为manual page 中的授权函数列表告诉在信号处理程序中调用此函数是安全的。不包括fflush()

标签: c signals fflush


【解决方案1】:

您只能在信号处理程序中使用异步安全函数,并且不包括 stdio 库。 POSIX 标准定义了一组异步安全的函数。你没有指定平台,我不知道通用的解决方案。

如果您在 POSIX / BSD 系统上存储支持 FILE 的文件描述符 - 使用 fileno() - 您可以使用异步安全函数来挽救一些东西:write(), lseek() (flush), @ 987654326@ 等。当然,如果 stdio 使用自己的缓冲区,这将无济于事。

(some Cert-C guidelines)

【讨论】:

  • 我刚刚发现了这个:gnu.org/software/libc/manual/html_node/Nonreentrancy.html "但是,如果您知道处理程序使用的流在信号可以到达的时候不可能被程序使用,那么您是安全的。它如果程序使用其他流,则没有问题。”所以如果我能保证没有其他函数写入某个流,理论上调用fflush(that_object)应该是安全的,不是吗?
【解决方案2】:

@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 的刷新(来自mainputchar)还没有完成。根据标准库的实现,这可能会导致 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 库的建议,我们可以让处理程序这样做:

  • 如果在main() 中的putchar() 调用中调用处理程序,则不会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;
}

【讨论】:

  • 不需要在从main() 返回之前调用fflush()。从main() 返回与调用exit() 相同,会刷新所有流。
猜你喜欢
  • 2019-07-20
  • 1970-01-01
  • 2016-03-31
  • 1970-01-01
  • 1970-01-01
  • 2023-03-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多