[Linux] In-depth exploration of Linux signals

Table of contents

words written in front

what is a signal

signs in life

Signals under Linux

 Linux common signals 

 Core core dump        

how the signal is generated

keyboard combination

1. How to understand that the signal is saved by the process

2. How to understand the essence of signal transmission

Send a signal to a process via a system call

kill()

Manually implement the kill command 

 raise()

abort()[non-system call]

How to understand sending signals through system calls

Software condition generates signal

alarm()

How to Understand Software Conditions Generate Signals 

hardware exception signal

Summarize

Introduction to Signal Capture

blocking signal

Signal some related concepts

The representation of the signal in the kernel (how the signal is saved)

sigset_t signal set

Signal Set Manipulation Functions

sigpending

sigprocmask

use of functions

signal capture

Understanding of user mode and kernel mode

Signal capture process

sigaction()

reentrant function

volatile


words written in front

        This article will take readers to understand in detail the Linux inter-process communication method - signal , which is divided into three parts as a whole:

  • what is a signal
  • why should there be a signal
  • How to use the signal★

        The first two parts mainly deal with related issues before signal generation : such as the concept of signal and how to generate it , etc.        

       The last part is dealing with related issues after the signal is generated : such as the sending process and processing process of the signal , etc. This part accounts for a large proportion, so it will focus on explaining it.

        This article covers almost all the content of the signal, with a cumulative word count of 1.5w+. After reading it carefully, you will definitely gain something!

        Let's start a pleasant journey of exploring signals


what is a signal

signs in life

        Signals are everywhere in our lives, such as traffic lights, alarm clocks, car turn signals, etc. Take traffic lights as an example, why do we know that we can leave when the light is green? This is because we remember the signals in the corresponding scene + Subsequent actions , because we have always said that red lights stop and green lights go, so you recognize the green light signal and execute the passing action.

        Assuming that it is a red light at this time, we still know that we can only pass when it is a green light, that is, if a specific signal is not generated, but we still know how to deal with this signal.

        Suppose the light is green at this time, but it happens that a classmate greets you behind you at this time, so you do not pass, but you also greet this classmate, and do not pass immediately, that is, after receiving this signal , This signal may not be processed immediately .

        After you say hello, you still remember that the light is green, so you hurry over. Of course, it doesn't matter if you look at the traffic lights haha, maybe the example is a bit far-fetched. This means that the signal itself, when we cannot process it immediately, must be temporarily saved.


Signals under Linux

        The essence of the signal under Linux is a |notification| mechanism. The user or the operating system sends a specified signal to tell the process that a certain time has occurred and needs to be processed accordingly.

        Combining the above examples in life, we draw the following conclusions about the signal:

a.  To process signals, a process must have the ability to "recognize" signals (see + process actions)

b.  Why can a process "recognize" a signal? The programmer has built-in code related to the action of handling the signal in the process.

c.  The generation time of the signal is random, and the process may be executing something with a higher priority when it is generated, so the processing of the signal may not be immediate!

d. The process will temporarily record the corresponding signal to facilitate subsequent processing

e. When will it be processed? This can only be answered tentatively when it is appropriate.

g. Generally speaking, the generation of signals is asynchronous to the process , which will be explained in detail later.

The concept of asynchronous can be understood temporarily now, it is relatively easy to understand, compared with synchronous :

Synchronous (Synchronous) means that when performing an operation, you must wait for the operation to complete before proceeding to the next operation. In other words, synchronous operations are performed sequentially in sequence, with the completion of each operation dependent on the result of the previous operation.

Asynchronous (Asynchronous) means that when an operation is performed, other operations can be continued without waiting for the completion of the current operation. Asynchronous operations typically return immediately, and then process the results after the operation completes, through notifications, callback functions, or polling.

To give a simple example to illustrate the difference between the two, suppose there is an operation to download a file:

  • Synchronization method : After starting the download, the program will wait for the file download to complete before performing other operations. That is to say, the program must perform operations such as downloading, processing, and saving in sequence.
  • Asynchronous mode : After the download starts, the program can perform other operations immediately without waiting for the download to complete. When the download is complete, the program will obtain the download result through a callback function or notification, and perform corresponding processing operations.

 Linux common signals 

        Under Linux, you can use kill -l to view all signals.

         We usually use very little real-time signals, so I don’t want to explain too much. Generally, they are only used in some special industries. For example, some smart cars will use operating systems, such as stepping on the brakes. This action needs to be executed immediately or processed as soon as possible. It cannot be delayed, and real-time signals can be used at this time.

        So we only need to understand a few commonly used common signals.

        We can use man 7 signal to view the default actions and reasons for the first 31 signals.

 where the default actions are:

        1. Term (Terminate termination): The process will terminate immediately after receiving the signal

        2. Ign (Ignore ignore): After the process receives the signal, it will ignore it and do nothing.

        3. Core : The process will terminate after receiving the signal and generate a core dump file containing the current state of the process .

Just know these three.

        The following are some common signals, default actions and descriptions:

     1. SIGHUP (Hangup): The default action is to terminate the process. Usually sent to the controlling process when the terminal hangs up or the network connection is disconnected.

     2. SIGINT (Interrupt): The default action is to terminate the process. Usually sent to the foreground process group by the user pressing Ctrl+C on the terminal.

     9. SIGKILL (Kill): The default action is to terminate the process immediately. Cannot be caught, ignored or blocked, is the signal to "force kill" the process.

     10.SIGUSR1 (User-defined signal 1): The default action can be to terminate the process or a user-defined operation. User-defined signal 1.

     12.SIGUSR2 (User-defined signal 2): ​​The default action can be to terminate the process or a user-defined operation. User-defined signal 2

     14. SIGALRM (Alarm clock): The default action can be to terminate the process or a user-defined operation. for timer events


 Core core dump        

        The default action of the above signal has a Core, so it is necessary to tell you about this.

In Linux, a core dump (core dump) refers to dumping the relevant core data image in the memory of the process when it crashes or terminates abnormally into a file for subsequent debugging and analysis

        Do you still remember that when we were waiting to explain waitpid in the process, there was the following picture:

         This code dump flag is used to mark whether the program has a core dump.

        For example, we randomly run an executable program, and then use signal 3 to kill it, and the default action of signal 3 is Core.

         When we send signal No. 3 to the process, the process is terminated, and at the same time there will be an extra file:

         The suffix is ​​exactly the pid of the process.

        The prerequisite for using core dump is to open it:

        You can use the ulimit -c unlimited command to open the core dump, and then use the core dump function.


Verify the core dump flag in process waiting

        Looking at the picture above, we know that if the child process that the parent process is waiting for is killed by a signal , then the last 7 digits are the signal number that killed the process, the 8th digit is the core dump , and 1/0 represents whether there is a core dump

        We can first use fork() to create a child process, and then the child process performs a division by 0 error, the parent process waitpid waits for the child process, and then outputs the last 7 digits of the result (&0x7F), and the 8th digit ((>>7)&1) .

        code show as below:

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //子进程进行除0
        int a = 100;
        a /= 0;
        exit(0);
    }
    int status = 0;
    waitpid(id,&status,0);
    cout << "父进程: " << getpid() << " 子进程: " << id \
     << "exit sig: "<< (status&0x7F) << " is core " << ((status >> 7) &1) <<endl; 
    return 0;
}

Then we run this program:

        It can be found that the signal is signal No. 8 (SIGFPE floating-point number error), and then a core dump occurs. Looking at the above picture, you can also see that when the default action of signal No. 8 is Core, a core dump occurs.

how the signal is generated

keyboard combination

        When we use linux, we usually want to end a process and use the ctrl + c shortcut key to terminate a process. Its essence is to send the No. 2 signal to the target process through the keyboard combination .

        How does the process handle this signal? There are three common ways:

        a. Default (the process comes with it, that is, executes the logic written by the programmer)

        b. Ignore (do not do any processing on the signal, it is also a way of processing the signal)

       c. Custom actions (capture signals, which will be mentioned later).

        So this process executes the default action No. 2 signal, and No. 2 signal is a signal that makes the process exit, so it will end.

        So how to understand that the key combination becomes a signal ?

        Before talking about this question, the following two questions need to be explained first:

1. How to understand that the signal is saved by the process

        The signal saved by the process needs to know these two pieces of information:

        a. what signal

        b. Whether to generate

        There are many types of signals, how do we manage them? Since the signal has only two states, we can use a bitmap to save them. We can create an unsigned int type, each bit represents the corresponding signal, 1 means that the signal is generated, and 0 means that the signal is not generated.

This signal bitmap field is saved         inside the process PCB structure .


2. How to understand the essence of signal transmission

        As I just said, the signal is stored in the bitmap, so if you want to send a signal, you only need to modify the corresponding bit in the signal bitmap to 1.

        The signal bitmap is in the PCB (task_strcut), and only the OS can access the data structure in the PCB, so the OS is actually modifying the corresponding signal bitmap to complete the " send" process of the signal .

        Knowing the answers to these two questions, let's go back to the previous question:

How to understand that the key combination becomes a signal?        

        The working method of the keyboard is carried out by means of interruption, of course, it can also recognize the key combination ctrl + c

        The specific process is: OS interprets the key combination --> finds the process list --> finds the process running in the foreground --> OS writes the corresponding signal.


Send a signal to a process via a system call

kill()

We can send a specific signal to the specified process through the kill() function. The function prototype is as follows:

       int kill(pid_t pid, int sig);

        Among them, the first parameter is the process that received the signal

        The second parameter is the signal to send

return value :

 Returns 0 if successful, otherwise returns -1.


Manually implement the kill command 

        The kill command we usually use actually calls the kill() system call internally, so we can also implement a kill manually.

        First of all, we can use command line parameters to obtain user input. What we want is command + option + process id , so if the number of command line parameters is not equal to 3, then output the manual. Then advance the options and process id respectively, and pass them into the kill() function, thus completing an implementation of kill.

        code show as below:

static void Usage(string proc)
{
    cout << "Usage:\r\n\t" << proc << " Signumber pid" << endl;
}
//./mykill 2 pid
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    int signumber = atoi(argv[1]);
    int processid = atoi(argv[2]);

    kill(processid,signumber);
}

ps: If you don't understand the command line parameters, you can refer to my article: the last part of the portal  .

Then after we compile, we can use the kill we wrote normally. My file name is mykill, so


 raise()

kill() is to send a signal to the specified process , and raise is to let the operating system send itself a signal.

Function prototype:

       int raise(int sig);

The first parameter is the signal number sent to its own process.

return value:

 It can be seen that the sending is successful and returns 0, and the failure returns non-0.


abort()[non-system call]

        This is a function provided by the C language library . The function of this function is to terminate the current process and make the process generate a core dump file (provided that the core dump is turned on).

         When abort() is called, a signal is automatically sent to the current process SIGABRT, so there is no need to pass in any parameters.

    void abort(void);

  It can also be used as exit(), the effect is the same. It's just that abort() can generate a core dump file,


How to understand sending signals through system calls

        Through the above, we can understand it as follows:

The user calls the system interface --> executes the corresponding system call code of the OS --> OS extracts parameters, or sets a specific value --> OS writes a signal to the target process --> modifies the flag bit of the corresponding signal --> process subsequent processing Signal --> perform the corresponding operation.


Software condition generates signal

        We have learned about pipes before, and we know that the parent and child processes read through the pipe, and the other process writes.

        If the read end is closed at this time, but the write end has been writing, writing at this time is meaningless! Therefore, the operating system will automatically terminate the corresponding write-end process by sending signal No. 13 SIGPIPE.


alarm()

   alarm() function is a function that sets a timer that sends  a signal to the calling process after a specified time interval . This function can be used to implement some timing operations or perform timeout processing. SIGALRM

        The function prototype is as follows:

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

The parameter seconds is the time interval of the timer in seconds.

        The return value of the function is the remaining time of the previous timer, or 0 if no timer has been set before.

        That is to say, suppose I first set a timer for 10 seconds. At this time, because the timer has not been set before, it returns 0, and then after 5 seconds, another timer is set. At this time, the return value of this timer is 5. Because there are 5 seconds left on the previous timer.

        For example, we can calculate how many times a while loop can be executed in one second:

int main(int argc, char* argv[])
{
    alarm(1);
    int count = 0;

    while(true)
    {
        cout << "count: " << count++ << endl;    
    }
}

 When we run this program, we can find that it is executed 30,000+ times in one second. Of course, due to network and device reasons, the number of times will definitely be different.

 In fact, the execution speed of the CPU is very fast, and it can perform hundreds of millions of operations per second. The reason why it was executed more than 3w times in the end is because a large amount of IO consumes a lot of time.


How to Understand Software Conditions Generate Signals 

a. The OS first recognizes that a certain software condition is triggered or not met. b.OS builds the signal and sends it to the specified process.

This signal is not triggered by hardware or operating system events, but is generated by the program itself based on logical judgment .


hardware exception signal

        A hardware exception is somehow detected by the hardware and notified to the kernel, which then sends the appropriate signal to the current process. For example, if the current process executes the instruction of dividing by 0, the operation unit of the CPU will generate an exception, and the kernel will interpret this exception as a SIGFPE signal and send it to the process.

         Why is the division by 0 error a hardware exception?

        1. First of all, we need to know that the calculation is performed by the CPU, which is a piece of hardware.

        2. There are registers inside the CPU, and the status register (bitmap) has corresponding status bit marks, such as overflow or other errors, and the OS will automatically detect after the calculation is completed . If the overflow flag bit is 1, the OS recognizes There is an overflow problem in it, immediately just find who is currently running, extract the pid of the process, then the OS sends a signal to the process, and the process will handle it at the right time.

        3. Once a hardware exception occurs, the process may not necessarily exit , and our general default action is to exit. But if we don't quit, there's nothing we can do

        Knowing this, let's look at the following phenomenon:

void catchSig(int signum)
{
    sleep(1);
    cout << "进程捕捉到了一个信号,正在处理中:" << signum << " Pid: "<< getpid() << endl;
}
int main(int argc, char* argv[])
{
    signal(SIGFPE,catchSig);
    int a = 100;
    a /= 0;

    while(true)
    {
        sleep(1);
    }
}

A signal capture signal is used here, which will be explained soon. Now we know that it means that when the corresponding signal is received, the default action of the signal is not executed, but the function we specify is executed.

        We see that there is a/=0, which will trigger the SIGFPE signal, and then execute our corresponding function. The following process keeps running in an endless loop. Then we run to see the result:

 We found that it has been executing this, but in theory shouldn't it send a signal once and execute it once ?

        This is because the exception in the CPU register has not been resolved , and normally, the OS sees the exception. A signal will be sent to terminate the process, but this signal is caught by us, and the process will no longer exit , and the exception inside will always be in the register.        

        When the process is scheduled , the exception in this register will also be taken away as the context. The next time the process is scheduled again, the exception still exists, and then the OS continues to send signals, which are caught by us again but do not exit. This is how cause of this phenomenon.


Similarly, how do we understand wild pointers or out-of-bounds issues?

First of all, we have to find the target location through the address, and the address in the language is a virtual address, the virtual address needs to be converted into a physical address, and the conversion needs to be converted through the page table + MMU , so that the illegal address will definitely report an error when performing MMU conversion. !


Summarize

All signals have their sources, but they are finally recognized, interpreted, and sent by the OS! 


Introduction to Signal Capture

        Signal handling (Signal handling) refers to the corresponding processing operation on the received signal in the program. When a process receives a signal, it can specify a specific processing behavior through the signal catching mechanism , such as executing a processing function or taking a specific operation.

        To sum up, it is not to let a specific signal perform the default action, but to specify the operation we specified.

        Here you need to use the signal() function, the function prototype is as follows:

       #include <signal.h>

       typedef void (*sighandler_t)(int);//函数指针

       sighandler_t signal(int signum, sighandler_t handler);

        The function accepts two parameters: signum and handler.

        The parameter signum specifies the number of the signal to capture.

        handler is a function pointer pointing to a custom function that handles the signal .        

For example, we want to modify the default behavior of signal 2:

#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<sys/types.h>

using namespace std;
//
void catchSig(int signum)
{
    cout << "进程捕捉到了一个信号,正在处理中: " << signum << "Pid: "<< getpid() << endl;
}
int main()
{
    // signal(2,fun); 使用信号对应的数字也可以,但一般还是写名字比较好

    signal(SIGINT,catchSig);//特定信号的处理动作,一般只有一个
    //signal函数,仅仅是修改进程对特定信号的后续动作,不是直接调用对应的处理动作
    //如果后续没有任何的SIGINT信号产生,catchSig就永远不会被调用

    //...
    while(true)
    {
        cout << "I am a process, I am running, pid: " << getpid() << endl; 
        sleep(1);
    }

    return 0;
}

        Here we change the action of the No. 2 signal to the action performed by the catchSig function, so that when we receive the No. 2 signal again, the original termination operation will not be executed, but catchSig.

        Let's compile and run:

         We found that every time we press ctrl+c, the program will no longer terminate, but execute the function operation we specified.

blocking signal

Signal some related concepts

  • The actual execution of the signal processing action is called the signal delivery (Delivery)
  • The state between signal generation and delivery is called signal pending (Pending).
  • A process can choose to block (Block) a signal.
  • When a blocked signal is generated, it will remain in a pending state until the process unblocks the signal before performing the delivery action.
  • Note that blocking and ignoring are different . As long as the signal is blocked , it will not be delivered , and ignoring is an optional processing action after delivery

The representation of the signal in the kernel (how the signal is saved)

        Those concepts mentioned above, in order to realize it, the operating system built three tables in the kernel, as follows:

The first table block represents whether the signal is blocked .

        The second table represents whether the corresponding signal is received .

        The third table represents the processing action corresponding to the signal .

Therefore, the above diagram can be interpreted as follows:

Each signal has two flags indicating blocking (block) and pending (pending) , and a function pointer indicating processing actions . When a signal is generated, the kernel sets the pending flag of the signal in the process control block, and the flag is not cleared until the signal is delivered. In the example above,

The SIGHUP signal is neither blocked nor generated, and the default processing action is performed when it is delivered.
The SIGINT signal has been generated, but is being blocked, so it cannot be delivered temporarily . Although its processing action is to ignore, this signal cannot be ignored before unblocking, because the process still has the opportunity to change the processing action before unblocking.
The SIGQUIT signal has never been generated. Once the SIGQUIT signal is generated, it will be blocked, and its processing action is the user-defined function sighandler.

Like pending, block is a bitmap, and the content in the block bitmap means whether the corresponding signal is blocked .

        handler is an array of function pointers , and each element points to the corresponding function, that is, the processing action of the signal.

        So what is the process of a signal being processed?

        First, the OS sends a signal, that is, changes 0 to 1 at the corresponding position in pending, and then instead of directly executing the corresponding handler method at this time, it first checks whether the corresponding block is 1, and if not, executes the corresponding method , otherwise blocked.

So the process is:


sigset_t signal set

        With the above understanding, how do we use these pending and blocks specifically, and how is this graph specifically used and realized?

   sigset_t Is a data type in the C language, used to represent a signal set (signal set). A signal set is a collection of signals used to manage and process signals.

   sigset_t Can be viewed as a bit vector, where each bit represents a signal . You can control the state of signals in a signal set by setting, clearing, and querying the state of bits.

        This type can represent the "valid" or "invalid" status of each signal. The meaning of "valid" and "invalid" in the blocking signal set ( block) is whether the signal is blocked, and in the pending signal set (pending) The meaning of "valid" and "invalid" in is whether the signal is pending or not.

        Each signal has only one bit pending flag, which is either 0 or 1, and does not record how many times the signal has been generated

Signal Set Manipulation Functions

        The sigset_t type uses a bit for each signal to represent the "valid" or "invalid" state. As for how to store these bits inside this type, it depends on the system implementation. From the user's point of view, it is not necessary to care. The user can only call the following functions To operate the sigset_t variable , and should not make any interpretation of its internal data, such as printing the sigset_t variable directly with printf is meaningless.

There are several functions in total:

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);//signo为要在set中添加的信号
int sigdelset(sigset_t *set, int signo);//signo为要在set中删除的信号
int sigismember(const sigset_t *set, int signo);//判断signo有没有在set中,如果有返回1,否则返回0
  • The function sigemptyset initializes the signal set pointed to by set, and clears the corresponding bits of all signals in it , indicating that the signal set does not contain any valid signals.
  • The function sigfillset initializes the signal set pointed to by set, and sets the corresponding bits of all signals to 1 , indicating that the effective signals of this signal set include all signals supported by the system.
  • Note that before using the variable of type sigset_t, be sure to call sigemptyset or sigfillset for initialization, so that the signal set is in a definite state.
  • After initializing the sigset_t variable, you can call sigaddset and sigdelset to add or delete a valid signal in the signal set .
  • sigismember is a Boolean function used to judge whether a certain signal is included in the effective signal of a signal set
    , if it is included, it will return 1, if it is not included, it will return 0, and if it fails, it will return -1

        The use of these functions is similar to bitmap, how to use it. Later we will code to implement and use these functions. 


sigpending

sigpending The function is used to get the pending signal set of the current process.

The prototype of this function is as follows:

       #include <signal.h>

       int sigpending(sigset_t *set);

This parameter is an output parameter used to receive the current pending signal set

return value

It can be seen that when the acquisition is successful, it will return 0, responsible for returning -1, and setting the error code.


sigprocmask

        Calling the function sigprocmask can read or change the signal mask word (blocking signal set) of the process.

        The function prototype is as follows:

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

        The first parameter is how, and the second parameter is set. They are used in conjunction with the following three options:

 Here is a brief explanation:

SIG_BLOCK: Indicates that the new signal in the set is added to the current signal mask word (block table)

SIG_UNBLOCK: Indicates that the signal in the set is released from the current signal mask word

SIG_SETMASK: directly set the current signal mask word to set, which is equivalent to completely replacing

The last parameter is the output parameter oldset , which obtains the value of the signal mask word (block table) before modification .

return value

Similarly, it returns 0 if successful, otherwise returns -1.


use of functions

        There are so many functions mentioned above, now we are going to use these functions.

        For example, we first block the No. 2 signal, and then send a No. 2 signal. At this time, because the No. 2 signal is blocked, it will always be in the pending bitmap (the corresponding bit is 1)

        At the same time, in the while loop, after 10 seconds, we unblock the No. 2 signal. At this time, the No. 2 signal should be executed, and then the process is terminated. In order to make the phenomenon more obvious, we capture the No. 2 signal, output it once, and then continue to output pending, in which the bit corresponding to the No. 2 signal is 0.

        code show as below:

static void handler(int signum)
{
    cout << "捕捉到信号: " << signum << endl;

}

//打印pending位图中的信息
static void showPending(sigset_t& pending)
{
    for(int sig = 1; sig <= 31; sig++)
    {    
        //如果pending中对应的位为1,则输出1,否则输出0
        if(sigismember(&pending,sig)) cout << "1 ";
        else cout << "0 ";
    }
    cout << endl;
}
int main()
{
    //0.方便测试,捕捉2号信号,使其不要退出
    signal(2,handler);
    //1.定义信号集
    sigset_t bset, obset;
    sigset_t pending;
    //2.初始化
    sigemptyset(&bset);
    sigemptyset(&obset);
    sigemptyset(&pending);
    //3.添加要进行屏蔽的信号
    sigaddset(&bset,2);
    //4.设置set到内核中(默认情况进程不会任何信号blcok)
    int n = sigprocmask(SIG_BLOCK,&bset,&obset);
    assert(n == 0);
    cout << "block 2 号成功...,pid: " << getpid() << endl;
    int count = 0;
    //5.重复打印当前进程的pending信号集
    while(true)
    {
        //5.1获取当前pending信号集
        sigpending(&pending);
        //5.2显示pending信号集中没有被递达的信号
        showPending(pending);

        count++;
        sleep(1);
        if(count == 10)
        {
            sigprocmask(SIG_SETMASK,&obset,nullptr);
            cout << "解除对于2号信号的block" << endl;
        }
    }
    return 0;
}

           You can take a closer look at this code, I have commented on each part, and then pay attention to the usage of the function.

Let's take a look at the renderings:

There are two more problems here:

        1. If we custom capture all signals --- then have we written a process that will not be killed by exceptions or users?

        The answer is no, signal number 9 is not caught by custom , it is an admin signal. The same goes for signal number 19.

        2. Similarly, if we block (block) all signals --- then have we written a process that will not be killed by exceptions or users?

        The answer is no, signal No. 9 will still not be blocked , and there is still No. 19, but other signals will be blocked and cannot be executed normally.

        It can be verified with the following code:

//阻塞特定的信号
void blockSig(int sig)
{
    sigset_t bset;
    sigemptyset(&bset);
    sigaddset(&bset,sig);

    int n = sigprocmask(SIG_BLOCK,&bset,nullptr);
}
int main()
{
    //循环遍历,分别将1-31号信号阻塞
    for(int sig = 1; sig <= 31; sig++)
    {
        blockSig(sig);
    }
    sigset_t pending;
    //不断获取pending位图,并且打印
    while(true)
    {
        sigpending(&pending);
        showPending(pending);
        sleep(1);
    }
    return 0;
}

Then the effect picture:

        When we send signal number 9, the process is killed without being blocked by the block. 


signal capture

Understanding of user mode and kernel mode

        We also mentioned a conclusion before: after the signal is generated, the signal may not be processed immediately, but will be processed at an appropriate time, so when is this appropriate time ?

        We know that the signal-related data fields are all inside the process PCB , which belongs to the kernel category , so it must be executed by the OS, that is, the kernel state , and most of our code execution is in the user state , so we want to process signals , you must first enter the kernel state before you can process it.

        In the kernel state, when returning to the user state from the kernel state , the signal detection and processing will be performed!

 But how to enter the kernel mode?

        Usually in the system call, defect trap exception, etc. will enter the kernel state.

 So what exactly are the user mode and kernel mode?

User mode: Execute the user's own code, and the state the system is in is called user mode. Userland is a supervised state.

Kernel mode: In the code we write, calling the system interface is actually calling the kernel-level code. At this time, kernel-level permissions are required. Kernel mode is usually used to execute os code, which is a very high-privileged state.

        When we are executing our own code, we will execute the code and data in the user space. When we call the system interface and enter the kernel state, we will need to execute the kernel-level code, so how to find the kernel-level corresponding What about the code ?

 

        The operating system is also a kind of software. When it is turned on, it will load various codes and data of the kernel into the physical memory, and then it can be seen and shared by the address space of each process. The kernel space saves the virtual address of the kernel . address, the code and data of the operating system kernel in the memory can be found through the kernel page table mapping.

        Of course, since it is the code of the operating system, we users cannot access it , only in the kernel state , but how does the OS know whether we are in the user state or the kernel state?

        The code is loaded into the CPU to run, and there is a cr3 register in the CPU, which is used to identify the execution state of the current CPU , such as 1 is the kernel mode, 3 is the user mode, etc. In this way, we can switch between the user and the kernel state, and execute the corresponding level of code.


Signal capture process

        We generally enter the kernel state through a system call , and then when the kernel is ready to return to the user state after processing , it will first process the signal of the current process. The state is transformed into the user state to execute the custom handler method, and then returns to the kernel state , and then ends the kernel state and then returns to the user state to execute the code before execution.

        If the signal action is default and ignored, you can directly continue to execute the kernel code, and then return to the user mode to continue executing the following code.

        What I said above may be more abstract based on text alone. Let me use a picture to illustrate the whole process.

 Then it can be simplified to the following:

 

  Each ring represents a state transition, and the arrow represents the direction of the process. As long as you remember this picture, the process of signal capture will be done.

sigaction()

         The function of this function is also signal capture, but the function of signal is more powerful.

         Let's first look at the prototype of this function:

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  • signum The parameter specifies the signal to capture . such as  SIGINT等,即Ctrl+C).

  • act The parameter is a  struct sigaction pointer to a structure that sets how the signal should be handled . Signal handlers and processing options can be defined by configuring the members of this structure. Commonly used structure members include:

    • sa_handler: Specify the function pointer of the signal handler , which is used to customize the processing of the signal . You can set the handler to a function or use to  SIG_IGN ignore the signal, or use to  SIG_DFL use the system's default processing method.

    • sa_mask: The data type is sigset_t, indicating whether to enable signal shielding.

    • sa_flags: This member is used to set options for signal processing, such as whether to enable signal restart ( SA_RESTART)

  • oldact The argument is a  struct sigaction pointer to a structure to get the previous signal handler and processing options. Equivalent to an output parameter .

return value

        Returns 0 on success, -1 on failure and the error code is set.

let's use it

void handler(int signum)
{
    cout << "获取了一个信号: " << signum << endl;
}
int main()
{
    // 内核数据结构,用户栈定义的
    struct sigaction act, oact;
    //初始化
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    act.sa_handler = handler;

    //设置进调用进程的PCB中
    sigaction(2,&act,&oact);

    while (true)
    {
        sleep(1);
    }
    return 0;
}

        In this way, when we execute, the No. 2 signal sent by ctrl + c is captured, and the custom action is executed. 

        When a signal processing function is called, the kernel automatically adds the current signal to the signal mask word of the process , and automatically restores the original signal mask word when the signal processing function returns, thus ensuring that when a signal is processed, if this If this signal is generated again, it will be blocked until the current processing ends. Then execute the blocked signal.

        If you want to automatically mask some other signals besides the current signal when calling the signal processing function, use the sa_mask field to indicate these signals that need to be additionally masked, and automatically restore the original signal mask word when the signal processing function returns And execute the signal that was blocked just now.

For example, when we want to execute custom processing actions on signal 2, other signals 3, 4, 5, and 6 are not executed, that is, they are also shielded, just set sa_mask, as follows:

 


reentrant function

        First, let's look at such a scene:

        In the main function, we executed the insert function, but when the insert function was executed halfway, it was scheduled by the process, and then there happened to be a signal at this time, and this signal happened to execute a custom capture action, The insert function is also executed, so that the insert function is executed once by the custom action of the signal, and then returns to the main main process, continues to execute the insert, and executes the insert function again, so the insertion error of the linked list is caused due to the timing problem . Because a function is entered twice at the same time.

        The above is a general understanding, the following is a detailed description of the above picture:

The main function calls the insert function to insert the node node1 into the head of a linked list. The insertion operation is divided into two steps. When the first step is just completed, the process is switched to the kernel because of a hardware interrupt. Before returning to the user mode, it is checked that there is a signal to be processed. , so switch to the sighandler function, sighandler also calls the insert function to insert node node2 into the head of the same linked list, after the two steps of the insertion operation are completed, return to the kernel state from sighandler, and return to the user state again to call the insert function from the main function Continue to execute in the middle, and was interrupted after doing the first step before, and now continue to complete the second step. The result is that the main function and sighandler insert two nodes into the linked list successively, and only one node is actually inserted into the linked list at the end.

As in the above example, the insert function is called by different control flows, and it is possible to enter the function again before the first call returns. This is called reentrancy. The insert function accesses a global linked list, which may cause reentrancy. causing confusion, a function like this is called a non-reentrant function , and vice versa

A reentrant function is a function that works correctly in multiple instances of execution. It only accesses its own local variables or parameters and can be called multiple times concurrently without incorrect results or race conditions.

volatile

        In fact, this keyword was also known in the C language before, which is to prevent the compiler from optimizing some variables. Now let's re-understand this keyword from the perspective of signals .

First, let's look at this piece of code:

int flag = 0;
void handler(int signum)
{
    cout << "pid :" << getpid () << " get a signal" << signum << endl;
    flag = 1;
}
int main()
{
    signal(2,handler);
    while(!flag);
    cout << "Quit" << endl;
    return 0;
}

        This code captures the No. 2 signal, then modifies the value of the global variable flag to 1, and then continues to execute the code after main. At this time, since the flag is 1 and !flag = 0, it will not enter an infinite loop at this time , then output "Quit",

 It is indeed as we thought, but if we change the compiler optimization level to O3 (the default is O1 or O2).

g++ -O3 mysignal.cc -o mysignal 

At this time, we found that we have fallen into an infinite loop. Why is this, isn't the flag already 1?

 This is because when the compiler executes the main function, it finds that there is no statement to modify the flag variable, and the flag is a global variable, so it directly fills flag=0 into the register, and the next time it is fetched directly from the register , the speed is fast .

        So at this time, we modify flag=1 only to modify the value in memory , and the compiler only reads from the register every time, which causes an error.

        In order to avoid this situation, we need to add the volatile keyword in front of this global variable to tell the compiler not to optimize the variable, and only fetch it from memory every time instead of putting it in a register.

int volatile flag = 0;

At this point, we run it again, and it ends normally.

In short, volatile it is a keyword used to identify that a variable may change unexpectedly, and it is used to tell the compiler not to optimize the variable, so as to ensure the visibility and correctness of reading and writing the variable under certain circumstances.

        So far, the whole content of signal is over. Thank you very much for reading. This chapter has explained most of the relevant knowledge of signal in depth. I believe that after reading it, you will have a new understanding of signal.

Guess you like

Origin blog.csdn.net/weixin_47257473/article/details/132144003