Безопасно ли вызывать fflush из обработчика сигналов?

Ну, название говорит о многом.

Скажем, мое приложение регистрируется в stdout/file. Однако при завершении он не всегда полностью сбрасывается. Одним из решений может быть сброс после каждого действия регистрации, однако это неприемлемо замедляет работу программы.

Итак, вопрос в том, безопасно ли вызывать fflush(file) из обработчика сигнала, может быть, даже fflush(NULL)?

Если нет, подскажите, почему. Есть ли другие решения этой проблемы? Может быть, это безопасно, если я знаю, что не работаю с файлами?


person heinrich5991    schedule 30.01.2014    source источник
comment
Нет, потому что список авторизованных функций на странице руководства говорит о вызове эта функция внутри обработчика сигнала безопасна. не включает fflush().   -  person Grijesh Chauhan    schedule 30.01.2014


Ответы (2)


Вы можете использовать асинхронно-безопасные функции только из обработчика сигналов, и это не включает библиотеку stdio. Стандарт(ы) POSIX определяет набор функций, безопасных для асинхронности. Вы не указали платформу, и я не знаю общего решения.

Если вы храните файловый дескриптор, поддерживающий ФАЙЛ — используя fileno() — в системе POSIX/BSD, вы можете спасти что-то, используя асинхронно-безопасные функции: write(), lseek() (flush), fsync() и т. д. Это, конечно, победит. не поможет, если stdio использует собственные буферы.

(некоторые рекомендации Cert-C)

person Brett Hale    schedule 30.01.2014
comment
Я только что нашел это: gnu.org/software/libc/manual/html_node /Nonreentrancy.html Однако, если вы знаете, что поток, используемый обработчиком, не может быть использован программой в то время, когда могут поступать сигналы, то вы в безопасности. Это не проблема, если программа использует какой-то другой поток. Итак, если бы я мог гарантировать, что никакая другая функция не записывает данные в определенный поток, теоретически вызов fflush(that_object) должен быть безопасным, не так ли? - person heinrich5991; 31.01.2014

@ 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 (из putchar main) еще не завершилась. В зависимости от реализации стандартной библиотеки это может привести к тому, что буфер 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, на которую вы ссылаетесь (подраздел «Обработка сигналов и функции без повторного входа»), говорится:

Если вы вызываете функцию в обработчике, убедитесь, что она является реентерабельной по отношению к сигналам, или убедитесь, что сигнал не может прервать вызов связанной функции.

Мы можем получить некоторое представление из части «или иначе» в приведенном выше предложении о прерывании вызова «связанной функции». Какие функции в нашем основном коде связаны с fflush(stdout)? Наш основной код вызывает только две функции (стандартная библиотека), getchar() и putchar(). Разумно предположить, что getchar() касается только stdin и поэтому не будет вызывать fflush(stodut) или какие-либо подчиненные функции. Таким образом, «связанная» функция здесь — putchar(), поскольку, как упоминалось ранее, она периодически запускает fflush(stdout) или подобные вещи.

Следуя рекомендации библиотеки GNU C, мы можем заставить обработчик сделать следующее:

  • Если обработчик вызывается внутри 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;
}
person GDGP    schedule 09.10.2018
comment
Вызов fflush() перед возвратом из main() не требуется. Возврат из main() аналогичен вызову exit(), который сбрасывает все потоки. - person Andrew Henle; 09.10.2018