Luxoft Training suggests you to get acquainted with the translation of the article by Robert Sikord “Access to Atomic Objects from the Signal Handler to C”.

Robert Seacord, author of Secure Programming in C and C ++, 2nd Edition, describes how access to shared objects in signal handlers can lead to races that can cause data inconsistency. Historically, the only suitable way to access shared objects from a signal handler was to read or write to
volatile variables of type
sig_atomic_t . With the advent of C11, atomic objects have become the best choice for accessing shared objects in signal handlers.
The book "The CERT Coding Standard, Second Edition: 98 Rules for Developing Safe, Reliable, and Secure Systems, Second Edition" has been updated in accordance with the C11 standard and the rules for writing the ISO / IEC 17961 C safe code. , was SIG31-C: "Do not access shared objects in signal handlers." This rule exists because access to shared objects in signal handlers can lead to races that can cause data inconsistency. In this article, I will provide additional information about accessing shared objects from a signal handler. I will go beyond the description of the rules and examples in the book.')
This rule was present in the first edition of The CERT C Secure Coding Standard, but since the subject of that book was C99 and atomic objects were not yet defined, the only suitable way to access a shared object from a signal handler was to read or write to variables
volatile sig_atomic_t . The following program installs the SIGINT handler, which defines the
e_flag of the variable
volatile sig_atomic_t , and then checks if the handler was called before exiting:
#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, clause 5, also allows signal handlers to read and write to non-blocking atomic objects. The following is a simple (but non-standard) example of accessing an atomic flag. The
atomic_flag type provides the classic check-install functionality. It has two states, set and clear, and the C standard ensures that operations on an object like
atomic_flag are not blocked.
#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; }
The
atomic_flag type is the only guaranteed non-blocking type, provided there is support for atomic objects. The
atomic_flag type
is also the only type that is guaranteed to be available from the signal handler. However, objects of this type can only be reliably available for calls to atomic functions, and such calls are not allowed. According to standard C 7.14.1.1, clause 5, undefined behavior occurs if a signal handler calls any function of the standard library, except for the _
abort, _Exit, quick_exit functions and the
signal function with the first argument equal to the signal number corresponding to the signal that made the call .
This limitation exists because most of the C library functions do not have to be safe to run in an asynchronous environment. To solve this problem without making changes to the standard, we must rewrite the example using a different atomic type, for example,
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; }
This solution is successful on platforms where the
atomic_int type
is always non-blocking. The following code causes the compiler to output a diagnostic message if atomic types are not supported or the
atomic_int type
is not non-blocking:
#if __STDC_NO_ATOMICS__ == 1 #error " " #elif ATOMIC_INT_LOCK_FREE == 0 #error "int " #endif
Macro
ATOMIC_INT_LOCK_FREE can have:
a value of 0 means that this type is not non-blocking;
a value of 1 means that this type is sometimes non-blocking;
a value of 2 means that this type is always non-blocking.
If the type is sometimes non-blocking, the
atomic_is_lock_free function must be called at runtime to determine if the type is non-blocking:
#if ATOMIC_INT_LOCK_FREE == 1 if (!atomic_is_lock_free(&e_flag)) { return EXIT_FAILURE; } #endif
Atomic types are sometimes non-blocking because, for some architectures, some processor variants support non-blocking comparison with exchange, while others do not (for example, 80386 and 80486). Depending on the processor variant, the application may be associated with one or another dynamic library. Therefore, it is necessary to enable dynamic checking for implementations in which
ATOMIC_INT_LOCK_FREE == 1 . This program will work on implementations in which the
atomic_int type
is not blocked:
#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; }
It remains to discuss why the variable
e_flag is not declared volatile. Unlike the first example, which was used by
volatile sig_atomic_t , loading and storing objects of an atomic type are performed using the semantics
memory_order_seq_cst . Consistently consistent programs behave as if the operations performed by their component streams simply alternate, and each calculation of the value of an object is the last value stored in this alternation. Arguments of atomic operations are defined as volatile A * to allow atomic objects to be declared
volatile , rather than requiring it.
The C standards committee (WG14) generally followed the lead of the C ++ standards committee (WG21) in determining support for concurrency. The goal of the WG21 committee was to make non-blocking atomic types applicable in signal handlers in C ++ 11. Unfortunately, some mistakes were made that WG21 is trying to fix in C ++ 14. The final proposal for defining the behavior of signal handlers in C ++ is WG21 / N3910. It led to the addition of the following entry in the draft international standard C ++ 14:
“The signal handler that is executed as a result of a call to the raise function belongs to the same execution thread as the call to the raise function. In other cases, it is not indicated which of the execution threads contains a call to a signal handler. ”
POSIX requires that a determination be made whether a signal is generated for a process or for a specific flow in a process. The signals generated by an action related to a particular stream, such as hardware failures, are generated for the stream that caused the generation of the signal. Signals generated in connection with a process ID, a process group ID, or an asynchronous event, such as a terminal activity, are generated for the process.
Access to changeable objects is evaluated strictly in accordance with the rules of the abstract machine. Actions on mutable objects cannot be optimized through implementation. Before atomic objects became available,
volatile provided the closest match to the semantics needed for an object shared with a signal handler. Now atomic objects are the best choice for accessing shared objects in signal handlers, since
volatile does not order the scopes with respect to other streams, making it very difficult to determine how it works in different streams. Therefore,
volatile sig_atomic_t can only be used to communicate with a handler running on the same thread.
Standard C does not allow signal handlers to be installed in multi-threaded programs. In particular, C11 argues that the use of the
signal function in multi-threaded programs is uncertain behavior, so most of the discussions on signal processing in multi-threaded programs are purely theoretical for multi-threaded programs corresponding to the semantics of C.
The following example is the most compact version of this program. Since this example uses type substitution, everything must be known at the compilation stage. This example uses atomic types if the availability of a non-blocking atomic type can be determined at the compiling stage; otherwise, it uses
volatile sig_atomic_t. Therefore, if
ATOMIC_INT_LOCK_FREE == 1 , then it is treated the same as if it were zero.
#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; }
According to the C standard, the default initialization (zero) for objects with a static or local thread storage area is guaranteed to produce a valid state. This means that the
e_flag object
does not need to be explicitly initialized in this or any other example.
findings
Access to shared objects from signal handlers is currently problematic for both C and C ++ (which hopes to solve these problems in C ++ 14). At the moment, opinions are inclined to make changes to the C standard in order to allow calling the functions of the atomic flag from the signal handler, and such a proposal has been made to WG14. The Austin Group is working on the integration of C11 and POSIX for release 8. Since the use of the
signal function in a multi-threaded program is an undefined behavior, POSIX can enhance the language by providing a definition of officially undefined behavior. In the long run, the C and C ++ standards committees are likely to move towards abandoning
volatile sig_atomic_t , because it does not support multi-threaded execution, and also because atomic types are currently the best alternative.
Thanks for contributing to the creation of this article: 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.Original article .
Robert Sikord's master class will be held
on November 26-27 in the online format and will be dedicated to safe programming in C and C ++.