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 [7]. Оно привело к добавлению следующей записи в проект международного стандарта C++14:
«Обработчик сигнала, который выполняется в результате вызова функции raise, принадлежит тому же потоку исполнения, что и вызов функции raise. В других случаях не указано, который из потоков исполнения содержит вызов обработчика сигнала».
POSIX® [8] требует, чтобы проводилось определение, генерируется ли сигнал для процесса или для конкретного потока в процессе. Сигналы, генерируемые каким-либо действием, относящимся к конкретному потоку, например, аппаратные сбои, генерируются для потока, который вызвал генерацию сигнала. Сигналы, генерируемые в связи с 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++.
ссылка на оригинал статьи http://habrahabr.ru/post/260509/
Добавить комментарий