Linux | Signals

Table of contents

Preface

1. Basic concepts of signals

1. Signs in life

2. Signals in Linux

2. Generation of signals

1. Interface introduction

2. How signals are generated

(1) Signals are generated by pressing keys on the terminal

(2) System call interface

a、kill 

b、raise 

c、abort

(3) Signals generated by software conditions

a. The SIGPIPE signal is generated by the pipeline

b. Signal generated by alarm function

(4) Signals generated by hardware abnormalities

a. Signal generated by division by zero

b. Signals generated by wild pointers

3. Core dump

3. Signal preservation

1. Concept supplement

2. Kernel structure

3、sigset_t

4. Signal set operation function

(1) sigset_t related interfaces

(2)sigprocmask

(3)sigpending

5. Test code

4. Signal processing

1. User state and kernel state

2. When to process signals

3. The entire process of signal processing


Preface

        This chapter mainly introduces the content related to Linux signals, and introduces the entire life cycle of signals in detail from three aspects: signal generation, signal storage, and signal processing.

1. Basic concepts of signals

1. Signs in life

        In daily life, there are various signals, such as our traffic lights, school bells, phone rings, etc. Then I have the following questions to introduce our topic today;

Question 1:How do we recognize these signals?

        There may be many answers to this question, it may be what our kindergarten teachers taught us, or it may be what our parents told us when we were young, etc.;

Question 2:Can we recognize these signals and process them?

        Isn’t this inevitable? Now that we know it, of course we will also process these signals. From the moment we know these signals, someone will tell us how to process these signals, or we will have our own subjective understanding of these signals and make our own judgments to tell us how to process these signals. Complete the corresponding action;

Question 3:After receiving certain signals, will we process them immediately?

        The answer is of course no. After we receive certain signals, we will not process them immediately. For example, when we are playing a game, the phone suddenly rings, and the other party tells us that the takeaway has arrived, and this may be the most important time for us to play the game. At this time, we may tell the takeaway guy to leave it at the door, that is to say , we will not process this signal immediately. As for when to process this signal, it depends on when we have the right time. This suitable time may be at the end of our game, or we may have forgotten this matter after playing the game. So that the signal is not processed;

2. Signals in Linux

        The signals in our Linux are also related to the signals in life. We answer the signals in Linux in the form of the above three questions respectively; the subject of receiving signals in life is people, that is, ourselves, and in the program, the receiving The subject of the signal is the process;

1. How does the process recognize these signals?

        You must know that the operating system is written by programmers, and the signals are of course defined by programmers. We can use kill -l to see what signals are in Linux, as shown in the figure below, where numbers 1 to 31 are our ordinary signals, and numbers 34 to 64 is a real-time signal and will not be explained in this chapter;

2. Will the process handle these signals immediately?

        Of course, it may not be possible. There may be things that need to be processed with a higher priority than processing this signal, so we need to process it at a suitable time. Since we need to process it later, we definitely need to save this signal. up, otherwise the signal will be lost; this signal will be saved in the PCB of our process, so how to save it? We have 31 signals, and we want to save them. It is not difficult to think that we can use bitmaps to save them, so that at least 31 bits are needed to save these signals. We can use 1 to indicate receipt of this signal, 0 Indicates not received;

3. What are the ways to process signals?

        Three types, divisions 默认方法 , 忽 Strategy , 用户户义

2. Generation of signals

1. Interface introduction

        Before formally introducing the generation of signals, we first understand a system call --- signal;

        The main function of this system call is to execute specified custom actions when each signal arrives; then let’s look at the parameters;

Parameter 1: Signal number (we can check this parameter through kill -l, or we can query signal through man manual No. 7). For this parameter, we can fill in the uppercase macro or directly fill in the number;

Parameter two:This parameter is a function pointer, which registers a function action for the specified signal. This parameter is the custom action to be executed;

Return value:If the call is successful, this function returns the original signal action. If it fails, it returns SIG_ERR and the error code is set;

2. How signals are generated

(1) Signals are generated by pressing keys on the terminal

        We can use the keyboard to enter a specific key combination on the terminal to generate a signal and send a signal to our current process; such as the following program;

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

using namespace std;

int main()
{
    while(true)
    {
        cout << "我正在运行..., pid: " <<  getpid() << endl;
        sleep(1);
    }
    return 0;
}

        We can find that the program is an infinite loop, but we can terminate the program through the key combination ctrl + c. In fact, ctrl + c sends a No. 2 signal to the current foreground process, which is SIGINT (interrupt); we press The key combination ctrl + \ sends signal No. 3 to the current foreground process to exit the current process;

        Let’s change the code again to make the effect look more obvious; let’s capture the No. 2 signal to make it complete the action we specified;

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

using namespace std;

void handler(int signum)
{
    cout << "signum: " << signum << endl;
}

int main()
{
    // 注册2号信号的捕捉方法
    signal(2, handler);

    while(true)
    {
        cout << "我正在运行..., pid: " <<  getpid() << endl;
        sleep(1);
    }
    return 0;
}

        We compiled and ran it and found the following results;

        When we use ctrl + c to send signal No. 2, since we have customized the action for signal No. 2 in advance, the current signal value will be printed to us every time we send it; and when we send signal No. 3, the program will exit. , because we did not capture signal No. 3 and register a custom action, so signal No. 3 completed the default action and exited the program; 

Summary:How to understand the signals generated by terminal keyboard input?

        First, we cause an interrupt through keyboard input. After our operating system gets the keyboard input, it parses the input key combination, then searches the process list to find the process currently running in the foreground. The operating system writes a specific signal in the PCB of the process. , that is, the specific bit position on the bitmap that saves the signal is set to 1;

(2) System call interface
a、kill 

        This is very simple. We have learned a command line command kill before. In fact, this command sends a specified signal to the specified process; it is still the above program;

        In fact, we can not only send signals to the process through the command line, but also through system calls. There is a system call kill with the same name, as shown below;

        We will use this system call even at a glance. The first parameter is the pid of the process, which is the process we want to send a signal to. The second parameter is the signal number; if the call is successful, it returns 0, and if the call fails, it returns -1. , the error code is set; we can encapsulate a kill command line instruction through this system call;

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <signal.h>
#include <sys/types.h>

// 我们kill使用格式:   kill pid signum
int main(int argc, char* args[])
{
    if(argc != 3)
    {
        std::cout << args[0] << " pid  signum" << std::endl;
        exit(1);
    }

    pid_t id = atoi(args[1]);
    int signum = atoi(args[2]);
    int n = kill(id, signum);
    if(n == - 1)
    {
        perror("kill");
        exit(2);
    }

    return 0;
}

b、raise 

        We can also use the raise system call to send signals. Unlike kill, raise only sends signals to the current process and cannot send signals to the specified process. The use of this system call is simpler; as shown below;

        This system call has only one parameter, which is the signal value. The return value is the same as the return value of kill. If it succeeds, it returns 0. If it fails, it returns -1. The error code is set. Next, we write a small program to make the current process send itself a message after 5 seconds. Signal No. 3;

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

using namespace std;

void handler(int signum)
{
    cout << "signum: " << signum << endl;
}

int main()
{
    // 注册2号信号的捕捉方法
    signal(2, handler);
    int count = 0;
    while(true)
    {
        cout << "我正在运行..., pid: " <<  getpid() << endl;
        sleep(1);
        count++;
        if(count == 5)
        {
            raise(3);
        }
    }
    return 0;
}

        We kept pressing ctrl + c because it was captured and a custom action was performed, so it did not exit. However, after 5 seconds, because the program sent itself signal No. 3, it exited;

c、abort

        Next, this function is similar to exit, sending signal No. 6 to our current process to make our process exit; this parameter has no parameters, and since it is always successful, there is no return value; we can change the process of the above code, and after 5 seconds Call the abort function; as shown below;

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

using namespace std;

void handler(int signum)
{
    cout << "signum: " << signum << endl;
}

int main()
{
    // 注册2号信号的捕捉方法
    signal(2, handler);
    int count = 0;
    while(true)
    {
        cout << "我正在运行..., pid: " <<  getpid() << endl;
        sleep(1);
        count++;
        if(count == 5)
        {
            //raise(3);
            abort();
        }
    }
    return 0;
}

Summary:How to understand the signals we generate through system calls

        First, the user calls a system call and executes the system call code. Then the operating system extracts the parameters of the system call, such as process pid, signal, etc., and then the operating system finds the process PCB control block and writes the corresponding signal into its bitmap;

(3) Signals generated by software conditions
a. The SIGPIPE signal is generated by the pipeline

        Earlier we did a small experiment when learning anonymous pipes, connecting both sides of the pipe; if the write-side pipe file descriptor is closed, the read-side will read to the end of the file and return 0; if the read-side pipe file descriptor is closed, the write-side pipe descriptor will be closed. The end process is terminated directly! How is the write-side process terminated? In fact, our OS sends a SIGPIPE signal to the writing end. If you don’t believe it, we can do the following experiment;

Experiment description: We capture signal No. 13 (SIGPIPE) so that it will not exit the process, but print information. We let the child process serve as the reading end and the parent process as the writing end. After 5 seconds, the child process closes the writing end pipe. file; we can check whether the parent process will perform our custom capture action;       

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

using namespace std;

void handler(int signum)
{
    cout << "pid:" << getpid() << ", 收到了信号: " << signum << std::endl;
}

int main()
{
    // 注册SIGPIPE方法
    signal(SIGPIPE, handler);

    // 创建匿名管道
    int pipefd[2];
    pipe(pipefd);
    // 创建子进程
    int id = fork();
    if(id == 0)
    {
        // 子进程(读端)
        close(pipefd[1]); // 关闭写端
        int count = 0;
        char buf[1024];
        while(true)
        {
            ssize_t sz = read(pipefd[0], buf, sizeof(buf) - 1);
            buf[sz] = '\0';
            std::cout << "from father# " << buf << " , 我的pid: " << getpid() << std::endl;
            sleep(1);
            count++;
            if(count == 5)
            {
                // 关闭子进程读端
                std::cout << "子进程要关闭管道读端啦" << std::endl;
                close(pipefd[0]);
                sleep(3); // 子进程不马上退出
                break;
            }
        }
        std::cout << "子进程退出" << std::endl;
        exit(0);
    }

    // 父进程(写端)
    close(pipefd[0]); // 关闭读端
    const char* buf = "我是父进程,我在给你发信息";
    while (true)
    {
        std::cout << "写入中...." << std::endl;
        write(pipefd[1], buf, strlen(buf));
        sleep(1);
    }
    
    // 进程等待
    wait(nullptr);

    return 0;
}

        We found that when the reader is closed, every time we want to write data into the pipe, we will receive our signal No. 13; originally signal No. 13 will cause the parent process to exit, and we custom capture the action of signal No. 13 , so there is no exit, because we keep writing data to the pipe file, so we will continue to receive signal No. 13;

b. Signal generated by alarm function

        This system call is similar to an alarm clock. It will send signal No. 14 (SIGALRM) to the current process every once in a while. The system call is declared as follows;

        This system call has only one parameter, which is the number of seconds. After a few seconds, signal No. 14 will be sent to the current process. The default action is to terminate the process; the return value of this function is generally 0. If the time for using alarm last time has not yet expired, call it again. When called, reset the alarm time and return the remaining time from the last setting; we can simply implement a scheduled task program through this system call, as shown below;

#include <iostream>
#include <vector>
#include <functional>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>

using namespace std;

// 定义一个任务类型
using task_t = std::function<void()>;
// 定义一个任务数组
std::vector<task_t> tasks;

// 数据库任务
void mysqlTask()
{
    std::cout << "正在执行数据库任务" << std::endl;
}
// 刷新磁盘
void flushDiskTask()
{
    std::cout << "正在执行刷新磁盘任务" << std::endl;
}
// 加载任务进任务数组
void load()
{
    tasks.push_back(mysqlTask);
    tasks.push_back(flushDiskTask);
}

void handler(int signum)
{
    for(auto& t : tasks)
        t();
    sleep(1);
    // 设置下次执行任务时间
    alarm(3);
}

int main()
{
    load(); // 加载任务队列

    // 对14号信号方法进行录入
    signal(SIGALRM, handler);

    alarm(5);

    // 防止主进程退出
    while(true);

    return 0;
}

        You can find that the tasks we assigned will be completed every 3 seconds; we can also write many programs like this;

Summary:How to understand the signals generated by software conditions?

        First, the operating system identifies whether a certain software condition is met or triggered. If it is met or triggered, it finds the PCB control block of the corresponding process and sets the bit position of the specific signal to 1 in its internal bitmap;

(4) Signals generated by hardware abnormalities
a. Signal generated by division by zero

        For students who are new to programming, we often hear that dividing by zero will cause a divide-by-zero error. However, we actually don’t know what a divide-by-zero error is and how it is caused. We may all think that the divide-by-zero error is caused by our program. The software error caused by the code is actually a hardware error. Next, we use the code to demonstrate the division-by-zero error. The division-by-zero error will send signal No. 8 (SIGFPE);

#include <iostream>
#include <vector>
#include <functional>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>

using namespace std;

void handler(int signum)
{
    cout << "收到信号signum: " << signum << endl;
    sleep(1);
}

int main()
{
    signal(SIGFPE, handler);

    int a = 10;
    a /= 0;

    // 防止程序退出
    while(true);
    return 0;
}

        We captured the No. 8 signal and changed its behavior, but we discovered a very magical phenomenon. We only divided by zero once, but we continued to receive the No. 8 signal; why is this? This has to do with the design of the hardware;

        First of all, what we need to understand is that our calculations are performed by the CPU. Our compiler translates our code into machine language. Our CPU will just stupidly keep fetching machine instructions to execute. The actual operation in our CPU There are two types of registers on the CPU, one is what our programmers can use such as eax, ebx, etc., and the other is the status register. When our CPU finds that there is division by zero, it will set a certain bit position of our status register to 1. Indicates that the result is abnormal. At this time, our operating system will detect the register status after the CPU completes the calculation and finds that a specific bit has been set to 1. Knowing that there is a problem with the calculation result, it finds the PCB of the current process and stores it in the PCB. The specific bit position of the signal is 1, and the signal transmission is completed; however, because the specific bit position of the status register on our CPU hardware is still 1, signal No. 8 will be continuously sent, so the above phenomenon will occur;

b. Signals generated by wild pointers

        The wild pointer error is believed to be a classic mistake for every C/C++ programmer. It is a mistake that almost every C/C++ programmer will make, but do you really understand the wild pointer error? The actual wild pointer error is also a hardware error. After our wild pointer is recognized, the operating system sends signal No. 11 (SIGSEGV) to the current process to cause the current process to exit. Next, we test it through the following code;

#include <iostream>
#include <vector>
#include <functional>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>

using namespace std;

void handler(int signum)
{
    cout << "收到信号signum: " << signum << endl;
    sleep(1);
}

int main()
{
    signal(SIGSEGV, handler);

    int* p = nullptr;
    *p = 100;

    // 防止程序退出
    while(true);
    return 0;
}

        Like our division-by-zero error, we are constantly receiving signal No. 11. What is going on? You must be smart enough to guess a little bit. This must also be related to our hardware;

        With the previous study of address space, we all know that all the addresses we see currently and the addresses seen by the CPU are virtual addresses, and we need to find the real physical space through page table mapping. In fact, we also need to use a Hardware ---- MMU, we complete the mutual conversion between virtual address and physical address through page table + MMU. As shown in the above code, when we pass in an illegal address, our MMU detects that it is an illegal address and converts this The error is recorded. When the result needs to be retrieved, the operating system detects an MMU exception, so it will find the PCB of the current process and set the position specified by the PCB to 1 to complete the signal generation; and this process will not cause our hardware to The wrong record is destroyed, so the specified signal will be continuously sent to the current process;

Signal sending summary:

        We can find that for all signal transmission, our operating system first recognizes the signal and then writes the specified signal to the PCB control block of the specified process;

3. Core dump

        We found that the default behavior of all the signals we learned above is to exit, so is there a difference between them? We query the signal through the command man 7 signal; as shown in the figure below;

        We can find the above table, where signal refers to which signal, value refers to the value corresponding to the signal, Action refers to the corresponding default action, and comment refers to the description corresponding to the signal; we are used to the Action carefully, We can find the following categories of behavior;

Term: Exit the current process

Core: Exit the current process and generate a core dump file

Ign: ignore

Stop: suspend the current process

Cont: continue running the current process

        When you see this Core dump, do you remember anything? This is a foreshadowing left by our previous explanation of process waiting. At that time, there was a core dump mark. If you are not sure, you can check the following article;

Linux | Process termination and process waiting-CSDN Blog

        You must have understood what this termination signal is by now. It is the signal that the current process exits due to. Regarding this core dump flag, we first understand what a core dump is;

Core dump:When some exception occurs in our process, the operating system dumps the core data of the current process in the memory to the disk;

core dump file:The so-called core dump file is the file transferred to the disk during the core dump process;

Note: For cloud servers, the core dump function is turned off by default. We need to turn this function on; we can check whether the core dump is turned off by ulimit -a ;

        We can see that the core file size is 0, which means that our core dump function is turned off; we set the core dump file size through ulimit -c size, and open the file after setting it; as shown below;

        We next use signal No. 3 to test the core dump function. First, we set the core file size to 0; then run the following code;

#include <iostream>
#include <vector>
#include <functional>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>

using namespace std;

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        // 子进程
        int a = 10;
        a /= 0; // OS会对子进程发送8号信号

        // 正常退出
        exit(0);
    }
    // 父进程
    int status = 0;
    waitpid(id, &status, 0);
    if(WIFEXITED(status))
    {
        // 正常退出
        cout << "exit code: " << WEXITSTATUS(status) << endl;
    }
    else
    {
        // 异常退出
        cout << "exit signal: " << (status & 0x7F) << ", core dump: " << ((status >> 7) & 1) << endl;
    }
    return 0;
}

        We found that the core dump mark is 0, and the exit signal is indeed SIGFPE---signal No. 8, which is in line with our expectations. Because the core dump file size is set to 0 by us, the core dump file will not be generated; next we will core dump Set the file size to 10240 and run it again to see the answer;

        This time our core dump flag changed to 1, and we generated a core dump file. This file is named after core. + process pid; so what is the use of this core dump file?​   

        We can use the core dump file for debugging. We add -g to the g++ compilation option and then compile again;

        We enter core-file + core dump file name in gdb to quickly locate the line of code where we made the error; then why does our cloud server turn off the core dump function by default? It is not difficult to find that our core dump file is very large, and the programs running on our server generally will not stop. Even if the program hangs, there may be an automatic restart program to restart. If our program keeps restarting , and core dump files are always generated at the same time. In the end, our disk will soon be filled up with core dump files;

3. Signal preservation

1. Concept supplement

        Before formally learning about signal preservation, we first learn a set of concepts;

Signal delivery:Actually execute the signal processing action;

Signal pending:The state between signal generation and delivery;

Signal blocking: Give a certain signal identifier, indicating that even if the signal has been generated, it is not allowed to be delivered. Only by canceling the blocking identifier can it be delivered and blocked. 's signal is pending;

Note:Blocking and ignoring are different. Signal blocking refers to a signal that cannot be delivered, while ignoring is a default action for processing signals;

2. Kernel structure

        We mentioned earlier that the signal will be saved in a bitmap in the PCB. In fact, in addition to saving the signal generation bitmap, the PCB will also save the blocking bitmap and the function pointer array of the processing action; as shown in the figure below;

        When we want to process a signal, we first traverse the pending bitmap and find that the SIGHUP signal is generated. Then we check the corresponding bits of the block bitmap and find that there is no blocking. We can then find the corresponding processing method in the handler array and find that it is Default processing action, perform default processing; the same is true for the analysis of SIGINT. First, check the pending bitmap and find that its signal is generated. Then we look at its blocking bitmap and find that the signal is also blocked, so it is not called and continues. Traverse to find the pending bitmap;

3、sigset_t

        As can be seen from the above figure, each signal can be 0 or 1 to indicate whether it is blocked and whether it occurs; therefore, we can use a bitmap to represent it, and the kernel provides us with a data type sigset_t, We use this data type to store block and pending. sigset_t is also called a signal set, and a blocking signal set is also called signal mask word; a>

4. Signal set operation function

(1) sigset_t related interfaces

#include <signal.h>

int sigemptyset(sigset_t *set);   // Set all bit positions of the signal set to 0

int sigfillset(sigset_t *set);   // Set all bit positions of the signal set to 1

int sigaddset (sigset_t *set, int signo);   // Set the corresponding bit position of a signal to 1

int sigdelset(sigset_t *set, int signo);   // Set the corresponding bit position of a signal to 0

int sigismember (const sigset_t *set, int signo); // Check whether the corresponding bit of a signal is set to 1

(2)sigprocmask

        This function can read or change the blocking signal set;

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

Parameter 1:This parameter determines what operations we want to do on the blocking signal set. There are mainly the following options;

SIG_BLOCK Add the signal we want to add to the signal mask word to the signal set of parameter two
SIG_UNBLOCK Put the signal we want to remove from the signal mask word into the signal set of parameter two
SIG_SETMASK Set the signal set we want parameter 2 into the signal mask word

Parameter two:Related to parameter one;

Parameter three:Return the original signal mask word (output parameter);

Return value:If the function call is successful, it returns 0, if it fails, it returns -1, and the error code is set;

(3)sigpending

        This system call is mainly used to read the pending signal set; it is returned through the parameter set;

int sigpending(sigset_t *set);

Parameter 1:Returns the pending signal set (output parameter);

Return value:If the function call is successful, it returns 0, if it fails, it returns -1, and the error code is set;

5. Test code

1. I want to verify that if I block and capture all semaphores, then the process cannot exit;

#include <iostream>
#include <signal.h>
#include <unistd.h>

void handler(int signum)
{
    std::cout << "signum: " << signum << std::endl;
}

// 打印未决位图
void showSignal()
{
    sigset_t pending;
    sigemptyset(&pending);
    sigpending(&pending);
    for(int i = 1; i < 32; i++)
    {
        // 捕捉
        signal(i, handler);
        // 设置屏蔽字
        if(sigismember(&pending, i))
            std::cout << "1";
        else
            std::cout << "0";
    }
    std::cout << ", pid: " << getpid() << std::endl;
}

int main()
{
    // 内存级别设置信号屏蔽字
    sigset_t block;
    sigemptyset(&block);
    for(int i = 0; i < 32; i++)
    {
        sigaddset(&block, i);
    }
    // 将信号集设置进内核
    sigprocmask(SIG_SETMASK, &block, nullptr);

    // 死循环
    while (true)
    {
        showSignal();
        sleep(1);
    }
    
    return 0;
}

        It is obvious that our No. 9 signal cannot be captured and blocked! This is an administrator signal to prevent malicious programs from preventing the process from being terminated;

        Among all the above interfaces, we don’t seem to have found an interface for modifying the pending bitmap in the kernel. You may be confused. In fact, we don’t need to modify the pending bitmap interface at all, because we can send signals to the process through interfaces such as kill. ;

4. Signal processing

        Earlier we learned the signal capture function signal. This function can change the action taken when the signal arrives; that is, the action of signal processing; but we have not figured out one question from beginning to end, when is the signal processed? ? We just mentioned a very vague concept before, at the right time, so when is the right time? Here we explore this issue;

1. User state and kernel state

        Before introducing, we have to add a set of concepts; when our operating system executes code, it actually has two states, namely user mode and kernel mode; when the operating system executes code written by the user, it usually runs in user mode. When the operating system executes kernel code, it usually runs in the kernel state; the kernel state has higher permissions than the user state;

        When we introduced the virtual address space earlier, we said that under a 32-bit machine, 1-3G belongs to the user space and 3-4G belongs to the kernel space. Here is another piece of knowledge. Our user space code and data are passed through the page table. To establish a mapping to the real physical space, this page table is called a user-level page table. Each process has its own user and page table; and our kernel space is also mapped to physical memory through the page table, only However, the page table we use is a kernel-level page table, and the kernel-level page table is shared by all processes;

        This way we can execute user code and kernel code in the same process address space;

2. When to process signals

        With the above knowledge foreshadowing, the next content will be much easier; what is the essence of our signal processing? Essentially, the process is to traverse the pending bitmap, and then check whether the corresponding signal mask word is set. If not, find the corresponding processing method in the handler array and perform a callback; these data structures are all in the PCB control block! Therefore, we process signals in the kernel,So we process signals in kernel state!

        Next we should study when we will reach the kernel state, that is, when we can process the signal! Under normal circumstances, we only enter the kernel state when using system calls, interrupts, etc.; and we generally perform signal detection and processing before exiting the kernel state;

        Some friends may have a problem. If my entire code is an infinite loop without any other operations and I don't call system calls, then will I not be able to enter the kernel state? Obviously, this is wrong. We must know that most of our current computers are time-sharing operating systems, and usually use time slice rotation to schedule processes. Once we want to schedule a process, we will inevitably enter the kernel state. So don’t worry about this issue!

3. The entire process of signal processing

        There are actually three actions for processing signals. We have mentioned them before, namely default action and Ignore and user-defined capture; in fact, of these three, the first two operating systems have Provided; as shown below;

        We can directly fill in these two parameters into parameter two of signal. Among them, we mainly focus on explaining all the signal processing processes of the third method;

        For the first two, when we use signal to register a method, once the signal arrives and is not blocked, when we enter the kernel state due to interruption, system call, etc., after we execute the kernel code, we are ready to return to the user state. Before, signal detection and processing will be performed. When we find that the signal processing action is the default action or ignored, we can directly do it and then return to the user state;

        For user-defined capture processing actions, this process may be slightly complicated; as shown in the figure below;

        ​ ​ ​ There may be some friends here who have some detailed questions. I will list a few below;

Question 1:In the third to fourth steps, can’t we directly handle the user-set processing method in the kernel state? Aren’t the permissions of kernel mode greater than that of user mode?

        Yes, the kernel state has greater permissions than the user state, so any code that can be executed by the user state can also be executed by our kernel state. But should we completely trust the signal processing methods written by the users themselves? If the processing method implemented by the user has some illegal requests, and our kernel state happens to have execution permissions, wouldn't this destroy the kernel? Therefore, we need to switch back to user mode to execute the signal processing method written by the user;

Question 2: From the fourth to the fifth step, why do we deliberately return to the kernel state instead of directly returning to the code executed before the last time we were trapped in the kernel?

        The reason why we return to the kernel mode is because we need to switch back to the code that was executed before falling into the kernel, and we cannot do it in the user mode, so we must first return to the kernel mode, and then return to the user mode from the kernel mode;

Question 3:When we have finished processing a signal and we want to return to the user mode, that is, from the fifth step to the first step, we have another signal coming. what to do? If a signal comes, does our process repeatedly switch between user mode and kernel mode to handle the signal?

        When we finish processing a signal, if other signals are brought, we will temporarily set other signals to blocking in advance, and then return to user mode. Only when we enter kernel mode next time will we process subsequent signals;

        Regarding the above figure, we can use the following method for memory;

Guess you like

Origin blog.csdn.net/Nice_W/article/details/134475247