⬆️ ⬇️

Signal Practice

I want to capture a little experience with signals in Linux. Below are examples of the use of the most significant structures in this area. I will try to put everything into separate shelves so that it is always easy to look and remember what to use and how.



Important facts about signals:



I will not delve into the theory of signals, where from why and where. I am primarily interested in the mechanism of working with them. Therefore, SIGUSR1 and SIGUSR2 will be used as the signals used, these are the only two signals put at the user's disposal. And also I will try to pay more attention to the inter-stream interaction of signals.

So let's go.



Function signal handler



This function is called when a process (or thread) receives a non-blocking signal. The default handler terminates our process (thread). But we can define the handlers for the signals of interest to us. It should be very careful about writing a signal handler; it is not just a function that runs on a callback; the current execution flow is interrupted without any preparatory work, so global objects can be in a non-consistent state. The author does not undertake to give a set of rules, since he does not know them himself, and calls for Kobolog to follow the advice (I hope he doesn’t mind that I refer to him) and to study at least this FAQ material.

void hdl(int sig) { exit = true; } 


You can install a new signal handler with two functions.

 sighandler_t signal(int signum, sighandler_t handler); 


Which accepts a signal number, a pointer to a handler function (or SIG_IGN (ignore signal) or SIG_DFL (default handler)), and returns the old handler. SIGKILL and SIGSTOP signals cannot be "intercepted" or ignored. Using this feature is highly discouraged because:



These shortcomings have no function.

 int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); 


Which also accepts a signal number (except SIGKILL and SIGSTOP). The second argument is the new description for the signal; the third value returns the old value. The struct sigaction structure has the following fields of interest



Using this function looks completely simple.

 struct sigaction act; memset(&act, 0, sizeof(act)); act.sa_handler = hdl; sigset_t set; sigemptyset(&set); sigaddset(&set, SIGUSR1); sigaddset(&set, SIGUSR2); act.sa_mask = set; sigaction(SIGUSR1, &act, 0); sigaction(SIGUSR2, &act, 0); 


Here we set up our handler for SIGUSR1 and SUGUSR2 signals, and also indicated that it is necessary to block the same signals while the handler is running.

With a signal handler there is one not very convenient moment, it is installed on the whole process and all generated threads at once. We do not have the ability for each thread to set its own signal handler.

But it should be understood that when a signal is addressed to a process, the handler is invoked for the main thread (representing the process). If the signal is addressed to a thread, then the handler is called from the context of this thread. See example 1 .



Signal blocking



In order to block some signals for the process, you need to add them to the signal mask of this process. To do this, use the function

 int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); 


We can add new signals (SIG_BLOCK) to the already existing signal mask, we can remove some signals from this mask (SIG_UNBLOCK), and also set our signals mask completely (SIG_SETMASK).

To work with the signal mask inside the thread, the function is used.

 int pthread_sigmask(int how, const sigset_t *set, sigset_t *oset); 


which allows you to do everything too, but for each thread individually.

It is not possible to block SIGKILL or SIGSTOP signals using these functions. Attempts to do this will be ignored.

')

sigwait



This function allows you to pause a process (or thread) until the desired signal (or one of the signal masks) is received. A feature of this function is that when a signal is received, the signal handler function will not be called. See example 2 .



Signal send



Two functions can be used to send a signal to a process.

 int kill(pid_t pid, int sig); int raise(int sig); 


From the first, everything is clear. The second is needed in order to send a signal to itself, and in essence is equivalent to kill (getpid (), signal). The getpid () function returns the PID of the current process.

To send a separate thread signal, use the function

 int pthread_kill(pthread_t thread, int sig); 




An example of using signals



Everything that I described above does not answer the question "Why should I use signals?" Now I would like to give a real example of the use of signals, and where they simply can not do without.

Imagine that you want to read or write some data to a device, but this can lead to blocking. Well, for example, reading in the case of working with sockets. Or maybe an entry in the pipe. You can put it in a separate thread, so as not to block the main work. But what to do when you need to complete the application? How to interrupt the blocking operation IO correctly? It would be possible to set a timeout, but this is not a very good solution. There are more convenient means for this: the pselect and ppoll functions. The difference between them is only in usability, their behavior is the same. First of all, these functions are needed for multiplexing work with IO (select / poll). The prefix 'p' at the beginning of the function indicates that this function can be correctly interrupted by a signal.



So, we formulate the requirement:

It is necessary to develop an application that opens a socket (for UDP simplicity) and performs a read operation in the stream. This application should be completed without any delay, at the request of the user.

The thread function looks like this.

 void* blocking_read(void* arg) { if(stop) { //   ,     ? std::cout << "Thread was aborted\n"; pthread_exit((void *)0); } //   SIGINT sigset_t set, orig; sigemptyset(&set); sigaddset(&set, SIGINT); sigemptyset(&orig); pthread_sigmask(SIG_BLOCK, &set, &orig); if(stop) { //         //       std::cout << "Thread was aborted\n"; pthread_sigmask(SIG_SETMASK, &orig, 0); pthread_exit((void *)0); } //       SIGINT std::cout << "Start thread to blocking read\n"; // ... // ,  ,    ppoll ppoll((struct pollfd*) &clients, 1, NULL, &orig); if(stop) { //      std::cout << "Thread was aborted\n"; close(sockfd); pthread_sigmask(SIG_SETMASK, &orig, 0); //   SIGINT    } //    ,    .     //        " " close(sockfd); pthread_exit((void *)0); } 


stop is a global boolean flag which is set to true by our handler, which tells the thread to terminate.

The logic of work is as follows:



Here is the main function

 int main() { ... struct sigaction act; memset(&act, 0, sizeof(act)); act.sa_handler = hdl; sigemptyset(&act.sa_mask); sigaddset(&act.sa_mask, SIGINT); sigaction(SIGINT, &act, 0); ... pthread_kill(th1, SIGINT); ... } 


We install our handler for SIGINT, and when we need to terminate the child stream, send it a signal.

Full listing, see example 3 .



In my opinion, the disadvantage of this method is that in the case of several threads we can complete them all at once. There is no possibility to set your own signal handler for each thread. Thus, it is not possible to implement a full interflow flow through signals. Linux way it does not provide.



Ps. The source codes are placed on the PasteBin service (I do not give the link, otherwise they will consider it as an advertisement).

Pps. Please forgive me for the abundance of errors. Language, my weak side. Thanks to everyone who helped fix them.






This article does not claim to have a full (and deep) description of working with signals and is aimed primarily at those who have not come across the concept of "signal" up to this point. For a more in-depth understanding of the operation of signals, the author calls to contact more competent sources and to get acquainted with constructive criticism in the comments.

Source: https://habr.com/ru/post/141206/



All Articles