Luxoft Training предлагает вам познакомиться с переводом статьи Роберта Сикорда «Доступ к разделяемым атомарным объектам из обработчика сигнала в C».

Роберт Сикорд, автор книги «Безопасное программирование на C и C++, 2-е издание», описывает, как доступ к разделяемым объектам в обработчиках сигнала может привести к гонкам, которые могут вызвать несогласованность данных. Исторически сложилось, что единственным подходящим способом получить доступ к разделяемым объектам из обработчика сигнала было чтение или запись в переменные типа volatile sig_atomic_t. С появлением C11 атомарные объекты стали лучшим выбором для доступа к разделяемым объектам в обработчиках сигнала.

Книга «The CERT® C Coding Standard, Second Edition: 98 Rules for Developing Safe, Reliable, and Secure Systems, Second Edition» обновлена в соответствии со стандартом C11 и правилами написания безопасного кода C ISO/IEC TS 17961. Правилом, вызвавшим наибольшее количество трудностей, было SIG31-C: «Не обращайтесь к разделяемым объектам в обработчиках сигнала». Это правило существует, так как доступ к разделяемым объектам в обработчиках сигнала может привести к гонкам, которые могут вызвать несогласованность данных. В этой статье я приведу дополнительную информацию о доступе к разделяемым объектам из обработчика сигнала. Я выйду за рамки описания правила и примеров в книге.


Это правило присутствовало в первом издании «The CERT C Secure Coding Standard», но так как темой той книги был C99 и атомарные объекты еще не были определены, то единственным подходящим способом получить доступ к разделяемому объекту из обработчика сигнала было чтение или запись в переменные типа volatile sig_atomic_t. Следующая программа устанавливает обработчик SIGINT, который определяет e_flag переменной volatile sig_atomic_t, а затем проверяет, вызывался ли обработчик перед выходом:
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>

volatile sig_atomic_t e_flag = 0;

void handler(int signum) {
  e_flag = 1;
}

int main(void) {

  if (signal(SIGINT, handler) == SIG_ERR) {
    return EXIT_FAILURE;
  }

  /* Цикл основного кода */

  if (e_flag) {
    puts("SIGINT получен.");
  }
  else {
    puts("SIGINT не получен.");
  }
  return EXIT_SUCCESS;
}


C11, 5.1.2.3, пункт 5, также позволяет обработчикам сигнала читать и записывать в неблокирующие атомарные объекты. Далее следует простой (но нестандартный) пример доступа к атомарному флагу. Тип atomic_flag обеспечивает классическую функциональность «проверить-установить». У него два состояния – set и clear, и стандарт C гарантирует, что операции на объекте типа atomic_flag не блокируются.
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#if __STDC_NO_ATOMICS__ != 1
#include <stdatomic.h>
#endif

atomic_flag e_flag = ATOMIC_FLAG_INIT;

void handler(int signum) {
  (void)atomic_flag_test_and_set(&e_flag);
}

int main(void) {

  if (signal(SIGINT, handler) == SIG_ERR) {
    return EXIT_FAILURE;
  }

  /* Цикл основного кода */

  if (atomic_flag_test_and_set(&e_flag)) {
    puts("SIGINT получен.");
  }
  else {
    puts("SIGINT не получен.");
  }
  return EXIT_SUCCESS;
}


Тип atomic_flag является единственным гарантированно неблокирующим, при условии наличия поддержки атомарных объектов. Тип atomic_flag также единственный тип, который гарантированно доступен из обработчика сигнала. Тем не менее объекты этого типа могут быть достоверно доступны только для вызовов к атомарным функциям, а такие вызовы не допускаются. Согласно стандарту C 7.14.1.1, пункт 5, неопределенное поведение возникает, если обработчик сигнала вызывает любую функцию стандартной библиотеки, за исключением функций _abort, _Exit, quick_exit и функции signal с первым аргументом, равным номеру сигнала, соответствующему сигналу, который совершил вызов обработчика.
Это ограничение существует потому, что большинство функций библиотеки C не обязаны быть безопасными для выполнения в асинхронной среде. Чтобы решить эту проблему без внесения изменений в стандарт, мы должны переписать пример с использованием другого атомарного типа, например, atomic_int:

#include <signal.h>
#include <stdlib.h>
#if __STDC_NO_ATOMICS__ != 1
#include <stdatomic.h>
#endif

atomic_int e_flag = ATOMIC_VAR_INIT(0);

void handler(int signum) {
  e_flag = 1;
}

int main(void) {
  if (signal(SIGINT, handler) == SIG_ERR) {
    return EXIT_FAILURE;
  }

  /* Цикл основного кода */

  if (e_flag) {
    puts("SIGINT получен.");
  }
  else {
    puts("SIGINT не получен.");
  }
  return EXIT_SUCCESS;
}

Это решение успешно на платформах, где тип atomic_int всегда неблокирующий. Следующий код вызывает вывод компилятором диагностического сообщения, если атомарные типы не поддерживаются или тип atomic_int не является неблокирующим:

#if __STDC_NO_ATOMICS__ == 1
#error "Атомарные типы не поддерживаются"
#elif ATOMIC_INT_LOCK_FREE == 0
#error "int не является неблокирующим"
#endif


Макро ATOMIC_INT_LOCK_FREE может иметь:
значение 0 – обозначающее, что этот тип не является неблокирующим;
значение 1 – обозначающее, что этот тип иногда неблокирующий;
значение 2 – обозначающее, что этот тип всегда неблокирующий.
Если тип иногда неблокирующий, то функция atomic_is_lock_free должна быть вызвана во время выполнения, чтобы определить, является ли тип неблокирующим:


#if ATOMIC_INT_LOCK_FREE == 1
  if (!atomic_is_lock_free(&e_flag)) {
    return EXIT_FAILURE;
  }
#endif


Атомарные типы иногда являются неблокирующими потому, что для некоторых архитектур некоторые варианты процессоров поддерживают неблокирующее сравнение с обменом, а другие не поддерживают (например, 80386 и 80486). В зависимости от варианта процессора приложение может быть связано с той или иной динамической библиотекой. Следовательно, необходимо включать динамическую проверку для реализаций, в которых ATOMIC_INT_LOCK_FREE == 1. Эта программа будет работать на реализациях, в которых тип atomic_int не блокируется:


#include <signal.h>
#include <stdlib.h>
#if __STDC_NO_ATOMICS__ != 1
#include <stdatomic.h>
#endif

#if __STDC_NO_ATOMICS__ == 1
#error "Атомарные типы не поддерживаются"
#elif ATOMIC_INT_LOCK_FREE == 0
#error "int не является неблокирующим"
#endif

atomic_int e_flag = ATOMIC_VAR_INIT(0);

void handler(int signum) {
  e_flag = 1;
}

int main(void) {
#if ATOMIC_INT_LOCK_FREE == 1
  if (!atomic_is_lock_free(&e_flag)) {
    return EXIT_FAILURE;
  }
#endif
  if (signal(SIGINT, handler) == SIG_ERR) {
    return EXIT_FAILURE;
  }

  /* Цикл основного кода */

  if (e_flag) {
    puts("SIGINT получен.");
  }
  else {
    puts("SIGINT не получен.");
  }
  return EXIT_SUCCESS;
}


Осталось обсудить, почему переменная e_flag не объявляется изменчивой (volatile). В отличие от первого примера, который использовал volatile sig_atomic_t, загрузка и хранение объектов атомарного типа выполняются с помощью семантики memory_order_seq_cst. Последовательно консистентные программы ведут себя так, как будто операции, выполняемые их составными потоками, просто чередуются, и каждое вычисление значения объекта является последним значением, хранящимся в этом чередовании. Аргументы атомарных операций определяются как volatile A *, чтобы позволить атомарным объектам быть объявленными volatile, а не потребовать этого.

Комитет по стандартам C (WG14) в целом последовал примеру комитета стандартов C++ (WG21) при определении поддержки параллельности. Целью комитета WG21 было сделать неблокирующие атомарные типы применимыми в обработчиках сигнала в C++11. К сожалению, были допущены некоторые ошибки, которые WG21 пытается исправить в C++14. Последним предложением по определению поведения обработчиков сигнала в C++ является WG21/N3910. Оно привело к добавлению следующей записи в проект международного стандарта C++14:

«Обработчик сигнала, который выполняется в результате вызова функции raise, принадлежит тому же потоку исполнения, что и вызов функции raise. В других случаях не указано, который из потоков исполнения содержит вызов обработчика сигнала».
POSIX® требует, чтобы проводилось определение, генерируется ли сигнал для процесса или для конкретного потока в процессе. Сигналы, генерируемые каким-либо действием, относящимся к конкретному потоку, например, аппаратные сбои, генерируются для потока, который вызвал генерацию сигнала. Сигналы, генерируемые в связи с ID процесса, ID группы процесса либо с асинхронным событием, таким как терминальная деятельность, генерируются для процесса.

Доступ к изменчивым объектам оценивается строго в соответствии с правилами абстрактной машины. Действия над изменчивыми объектами не могут быть оптимизированы с помощью реализации. До того, как стали доступны атомарные объекты, volatile обеспечивало самое близкое соответствие семантике, необходимой для объекта, разделяемого с обработчиком сигнала. Сейчас атомарные объекты являются лучшим выбором для доступа к разделяемым объектам в обработчиках сигнала, так как volatile не осуществляет упорядочивание областей видимости по отношению к другим потокам, сильно усложняя определение того, как он работает в разных потоках. Следовательно, volatile sig_atomic_t может быть использован для связи только с обработчиком, работающим в том же потоке.

Стандарт C не позволяет устанавливать обработчики сигнала в многопоточные программы. В частности, C11 утверждает, что использование функции signal в многопоточных программах является неопределенным поведением, так что большинство дискуссий на тему обработки сигналов в многопоточных программах являются чисто теоретическими для многопоточных программ, соответствующих семантике C.
Cледующий пример является наиболее компактной версией этой программы. Так как в этом примере используется замена типов, все должно быть известно на стадии компилирования. Этот пример использует атомарные типы, если доступность неблокирующего атомарного типа может быть определена на стадии компилирования; в противном случае он использует volatile sig_atomic_t. Следовательно, если значение ATOMIC_INT_LOCK_FREE == 1, то это рассматривается так же, как если бы оно было равно нулю.


#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#if __STDC_NO_ATOMICS__ != 1
#include <stdatomic.h>
#endif

#if __STDC_NO_ATOMICS__ == 1
  typedef volatile sig_atomic_t flag_type;
#elif ATOMIC_INT_LOCK_FREE == 0 || ATOMIC_INT_LOCK_FREE == 1
  typedef volatile sig_atomic_t flag_type;
#else
  typedef atomic_int flag_type;
#endif

flag_type e_flag;

void handler(int signum) {
  e_flag = 1;
}

int main(void) {
  if (signal(SIGINT, handler) == SIG_ERR) {
    return EXIT_FAILURE;
  }

  /* Цикл основного кода */

  if (e_flag) {
    puts("SIGINT получен.");
  }
  else {
    puts("SIGINT не получен.");
  }

  return EXIT_SUCCESS;
}


Согласно стандарту C, инициализация по умолчанию (ноль) для объектов со статической или локальной областью хранения потоков гарантированно произведет допустимое состояние. Это значит, что объект e_flag не нужно явно инициализировать в этом или любом другом примере.

Выводы


Доступ к разделяемым объектам из обработчиков сигнала в настоящее время проблематичен как для C, так и для C++ (который надеется решить эти проблемы в C++14). На данный момент мнения склоняются к внесению изменений в стандарт C, чтобы разрешить вызывать функции атомарного флага из обработчика сигнала, и такое предложение было внесено в WG14. The Austin Group работает над интеграцией C11 и POSIX для выпуска 8. Поскольку использование функции signal в многопоточной программе является неопределенным поведением, POSIX может усилить язык, обеспечив определение официально неопределенного поведения. В долгосрочной перспективе комитеты по стандартам C и C++, по-видимому, будут двигаться в направлении отказа от volatile sig_atomic_t, потому что он не поддерживает многопоточное выполнение, а также потому, что атомарные типы в настоящее время являются лучшей альтернативой.

Выражаю благодарность за вклад в создание этой статьи: Aaron Ballman, John Benito, Hans Boehm, Geoff Clare, Robin Drake, Jens Gustedt, David Keaton, Carol Lallier, Daniel Plakosh, Martin Sebor, and Douglas Walls.

Published with permission from Pearson Education.

Оригинальная статья.

Мастер-класс Роберта Сикорда пройдет 26-27 ноября в онлайн-формате и будет посвящен безопасному программированию на C и C++.

Комментарии (3)


  1. TrueBers
    18.06.2015 12:19
    +2

    А причём здесь хабы Си шарп и дотНет?


    1. PsyHaSTe
      09.07.2015 09:48

      Ну вот я чистый шарпист, статьи про плюсы иногда почитываю, чтобы быть в тренде, очень интересные по С/С++ тоже смотрю. Но вот в статью про то, как в книге раньше писалось по одному старому стандарту С, а теперь перешли на новый, заглядывать бы явно не стал. А из-за хаба добавил в закладки, а сейчас добрался… Так что такой вот грязный прием по увеличению аудитории работает весьма неплохо.


  1. skor
    19.06.2015 11:56

    Как избежать проблем, делая так, как делать не надо.
    Обрабатывать сигналы синхронно — уже большая ошибка, тем более пытаться что-то делать в их обработчиках.