[Linux] Signal--First understanding of signal/How to generate signal/Save of signal

1. Preliminary understanding of signals

1. Signals from a life perspective

There are many signals in our lives. For example, when the battery of our mobile phone is less than 20%, it will remind us that the battery is low, traffic lights, and QQ message reminders, etc. Let's take the traffic light as an example. We can recognize the traffic light because we know what the traffic light is and should produce the corresponding behavior, such as stop on red light and go on green light. So why can we recognize traffic lights? This is because someone has educated you and allowed you to remember the attributes or behaviors of the corresponding traffic lights in your brain. When the light turns green, we don't necessarily have to cross the road, because we may be saying goodbye to friends and other more important things at this time, so when the signal light comes, we don't necessarily process the signal immediately. The signal can be generated at any time (asynchronously ), we might be doing something more important. The period between the arrival of the signal and the signal being processed is called the time window, but during this period, we must remember the signal. When the green light is on, we can cross the road, or we can dance on the side of the road before crossing the road. It is also possible that you are not waiting for the green light at all, and will ignore the green light at this time, so for signals, we There are three processing methods: default action, custom action and ignore action

2. Signals from the perspective of technical application

Now we migrate the above concepts to the operating system. How does the process recognize the signal? Awareness + action. The process itself is a collection of properties and logic written by programmers that is coded by programmers. When the process receives the signal, the process may be executing more important code, so the signal may not be processed immediately, so the process itself must have the ability to save the signal. When a process handles a signal, it generally has three actions: default, custom, and ignore.

If a signal is sent to a process and the process needs to save it, where should it be saved? The answer is in task_struct. So how to save it? The structure contains a field of the signal.

struct task_struct
{
    
    
    ......
    unsigned int signal;
}

We use the kill -l command to view all signals:

Insert image description here

Among them [1,31] are ordinary signals, [34,64] are real-time signals

So how does an unsigned int guarantee 31 signals? The answer is to use a bitmap, using 31 bits to represent 31 signals. The position of the bit represents the signal number, and the content of the bit represents whether the signal has been received. 0 means no, and 1 means yes.

The essence of generating a signal is to modify the signal bitmap in the PCB. PCB is a data structure object maintained by the kernel. The manager of PCB is the OS, so only the OS has the right to modify the contents of the PCB. Therefore, no matter which way a signal is generated, the essence is to send a signal to the target process through the OS. Therefore, the OS must provide relevant system calls for sending signal processing signals, so the kill command we used before must have called the corresponding system call at the bottom layer.

2. How signals are generated

1. Generate signals through terminal keys

When our program is running, we can usectl + c to interrupt the process

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

using namespace std;

int main()
{
    
    
    // 1. 通过键盘发送信号
    while (true)
    {
    
    
        cout << "hello world" << endl;
        sleep(1);
    }
    return 0;
}

Insert image description here

ctl + c is a key combination, and the operating system interprets ctl + c as signal number 2 – SIGINT. The default processing action of SIGINT is to terminate the process, and the default processing action of SIGQUIT is to terminate the process.

Precautions:

1. The signal generated by Ctrl-C can only be sent to the foreground process. Adding an & after a command can run it in the background, so that the Shell can accept new commands and start new processes without waiting for the process to end.

2. Shell can run a foreground process and any number of background processes at the same time. Only the foreground process can receive signals generated by control keys like Ctrl-C.

3. When the foreground process is running, the user may press Ctrl-C at any time to generate a signal. That is to say, the user space code of the process may receive the SIGINT signal and terminate wherever it is executed, so the signal is relative to the process. The control flow is asynchronous.

We can also use the kill command to terminate the program

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

// 我写了一个将来会一直运行的程序,用来进行后续的测试
int main()
{
    
    
    while (true)
    {
    
    
        std::cout << "我是一个正在执行的进程,pid:" << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}

Insert image description here

2. Call system functions to send signals to the process

1. The kill function can send any signal to any process

#include <signal.h>
int kill(pid_t pid, int signo);
成功返回0,错误返回-1
#include <iostream>
#include <unistd.h>
#include <signal.h>

using namespace std;

static void Usage(const string &proc)
{
    
    
    cout << "\nUsage:"
         << proc << "pid signo\n"
         << endl;
}

// ./mysignal pid signo
int main(int argc, char *argv[])
{
    
    
    if (argc != 3)
    {
    
    
        Usage(argv[0]);
        exit(1);
    }

    pid_t pid = atoi(argv[1]);
    int signo = atoi(argv[2]);

    int n = kill(pid, signo);
    if (n != 0)
    {
    
    
        perror("kill");
    }
    return 0;
}

Insert image description here

In this way we can use one process to send a signal to another process

2.raise() sends any signal to yourself kill(getpid(), any signal)

#include <signal.h>
int raise(int signo);
成功返回0,错误返回-1
#include <iostream>
#include <unistd.h>
#include <signal.h>

using namespace std;

int main()
{
    
    
    // 2. 系统调用向目标进程发送信号
    int cnt = 0;
    while (cnt <= 10)
    {
    
    
        cout << "cnt:" << cnt++ << "pid" << getpid() << endl;
        sleep(1);
        if (cnt >= 5)
        {
    
    
            raise(9);
        }
    }
    
    return 0;
}

Insert image description here

3.abort() sends the specified signal SIGABRT to itself, kill(getpid(), SIGABRT)

#include <stdlib.h>
void abort(void);
就像exit函数一样,abort函数总是会成功的,所以没有返回值。
#include <iostream>
#include <unistd.h>
#include <signal.h>

using namespace std;

int main()
{
    // 2. 系统调用向目标进程发送信号
    int cnt = 0;
    while (cnt <= 10)
    {
        cout << "cnt:" << cnt++ << "pid" << getpid() << endl;
        sleep(1);
        if (cnt >= 5)
        {
            abort();
        }
    }
    
    return 0;
}

Insert image description here

Precautions:

The kill command is implemented by calling the kill function. The kill function can send a specified signal to a specified process. The raise function can send a specified signal to the current process (send a signal to yourself)

Understanding of signal processing behavior: In many cases, the process receives most of the signals, and the default processing action is to terminate the process
The meaning of the signal: the difference of the signal, Represents different events, but the processing actions after the event can be the same!

3. Hardware exception generates signal

Hardware exceptions are somehow detected by the hardware and notified to the kernel, which then sends appropriate signals to the current process. For example, if the current process executes the instruction of dividing by 0, the CPU's arithmetic unit will generate an exception, and the kernel will interpret this exception as a SIGFPE signal and send it to the process. For another example, if the current process accesses an illegal memory address, the MMU will generate an exception, and the kernel will interpret this exception as a SIGSEGV signal and send it to the process.

We know that most of the processing results are to terminate the program, but we can also automatically control the OS's processing behavior after receiving a certain signal.

Signals can be customized to capture, and the siganl function is used to capture signals. Next we introduce the signal function

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
函数功能:改变OS接收到信号之后的行为
参数
signum:信号的编号
handler:处理方法的函数指针
返回值:signal() 返回信号处理程序的先前值,如果出错则返回 SIG_ERR。在发生错误的情况下,会设置errno以指示错误的原因。

1. Divide by 0 error

We know that once a divide-by-zero error occurs in the program, the program will crash directly, causing the process to exit. So, why divide by 0 error terminates the program, the answer is that the current process receives a signal from the operating system – SIGFPE

int main()
{
    
    
    while (true)
    {
    
    
        std::cout << "我在运行中...." << std::endl;
        sleep(1);
        int a = 10;
        a /= 0;
    }
}

Insert image description here

Now we make a custom capture of the signal

void catchSig(int signo)
{
    
    
    cout << "获取到一个信号,信号编号是: " << signo << endl;
    sleep(1);
}
int main()
{
    
    
	signal(SIGFPE, catchSig);
    while (true)
    {
    
    
        std::cout << "我在运行中...." << std::endl;
        sleep(1);
        int a = 10;
        a /= 0;
    }
}

Insert image description here

How does the OS know that it should send signal No. 8 to the current process? How does the OS know that I have divided by 0?

This is because there is a status register in the CPU, which contains an overflow flag bit. When we divide by 0, the data overflows, and an operation exception occurs in the CPU. At this time, the overflow flag bit is set to 1, because the operation The system is the manager of software and hardware resources. When the operating system detects an operation abnormality, it will send signal No. 8 to the corresponding process.

Let's look at the following code. We put the divide by 0 outside the loop:

void catchSig(int signo)
{
    
    
    cout << "获取到一个信号,信号编号是: " << signo << endl;
    sleep(1);
}
int main()
{
    
    
	signal(SIGFPE, catchSig);
	int a = 10;
    a /= 0;
    while (true)
    {
    
    
        std::cout << "我在运行中...." << std::endl;
        sleep(1);
    }
}

Insert image description here

We found that we obviously only divided by 0 once, why "obtained a signal, the signal number is 8" was still printed in a loop. This is because the following code has not ended yet, and the program continues to execute, but the The overflow flag bit is still 1, so it will continue to receive signal No. 8, so it will keep printing.

Receiving a signal does not necessarily cause the process to exit - if it does not exit, it may still be scheduled. There is only one copy of the register inside the CPU, but the contents of the register belong to the context of the current process! , you have no ability or action to correct this problem. When the process is switched, there are countless processes of saving and restoring the status register. Therefore, every time it is restored, the OS recognizes the overflow flag in the status register inside the CPU. it's 1

2. Wild pointer

int main()
{
    
    
    signal(11, catchSig);
    while (true)
    {
    
    
        std::cout << "我在运行中...." << std::endl;
        sleep(1);
        int *p = nullptr;
        *p = 100;
    }
}

Insert image description here

Why do wild pointers crash? Because the OS will send the specified signal No. 11 to the current process

An exception occurred in the MMU due to out-of-bounds access. After notifying the operating system, the OS system generates signal No. 11 for the specified process.

4. Signals generated by software conditions

1. Pipeline

When we close the read end of the pipe, the OS system sends the SIGPIPE signal to the writing process, and the writing end stops writing. This is triggered by software conditions.

2.alarm function and SIGALRM signal

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动
作是终止当前进程。

The return value of this function is 0 or the number of seconds remaining in the previously set alarm time. For example, someone wants to take a nap and sets the alarm clock to sound in 30 minutes. He is woken up 20 minutes later and wants to sleep a little longer, so he resets the alarm clock to sound in 15 minutes. "Previously set "The remaining time on the alarm clock" is 10 minutes. If the seconds value is 0, it means canceling the previously set alarm clock. The return value of the function is still the number of seconds remaining in the previously set alarm time.

int main()
{
    
    
    alarm(1);
    while(true)
    {
    
    
        cout << "我在运行: " << getpid() <<endl;
    }
}

Insert image description here

Here we can only use alarm to calculate how many times our computer can accumulate the data!

int cnt = 0;
int main()
{
    
    
    alarm(1);
    while (true)
    {
    
    
        cout << "cnt:" << cnt++<< endl;
    }
    return 0;
}

Insert image description here

nt cnt = 0;

void catchSig(int signo)
{
    
    
    cout << "cnt: " << cnt << endl;
    exit(1);
}

int main()
{
    
    
    signal(SIGALRM, catchSig);

    alarm(1);
    while (true)
    {
    
    
        cnt++;
    }
    return 0;
}

Insert image description here

We can see from the above comparison that IO is actually very slow.

Any process can set an alarm clock in the kernel through the alarm system call. There may be many alarm clocks in the OS. So should the OS system manage these alarm clocks? The answer is yes. The management method is to describe first and then organize.

The operating system will define a data structure similar to the following for the alarm clock

struct alarm
{
    
    
    uint64_t when;//未来的超时时间
    int type;//闹钟的类型,一次性的还是周期性的
    task_struct *p;
    struct alarm* next;
};

In this way, we can use a linked list to link the alarm clock data structure to manage the alarm clock. It becomes the management of linked lists. The OS system will periodically detect these alarm clocks, and when it times out, it will send SIGALARM to the corresponding process. It can also use more efficient data structures for management, such as priority queues.

Summarize:

All the signals mentioned above must be ultimately executed by the OS. Why? OS is the manager of processes

Is the signal processing immediate? at the right time

If the signal is not processed immediately, does the signal need to be temporarily recorded by the process? Where is the best place to record it?

When a process does not receive a signal, can it know what it should do with legal signals?

How to understand the OS sending signals to the process? Can you describe the complete sending process?

5. Core dump problem when process exits

Each signal has a number and a macro definition name. These macro definitions can be found in signal.h. For example, there is a definition #define SIGINT 2.

The conditions under which these signals are generated and what the default processing actions are are detailed in signal(7): man 7 signal

Insert image description here

Signals No. 2 and No. 3 both terminate the process, but their actions are Term and Core.

Term will directly terminate the process, but after Core terminates the process, all user space memory data will be saved to the disk. On the cloud server, if the process exits by core by default, we will not see the phenomenon of no data being saved for the time being, because the cloud server turns off the core file option by default. If you need to view it, you need to turn on the core file option of the cloud server.

We can use the ulimit -a option to view

ulimit -a

Insert image description here

We can set it using ulimit -c 1024

ulimit -c size
int main()
{
    
    
    while (true)
    {
    
    
        std::cout << "我在运行中...." << std::endl;
        sleep(1);
        int *p = nullptr;
        *p = 100;
    }
}

Insert image description here

Core dumped is called core dump: when an exception occurs in the process, we dump the valid data in the memory at the corresponding moment of the process to the disk.

First explain what Core Dump is. When a process is about to terminate abnormally, you can choose to save all the user space memory data of the process to the disk. The file name is usually core. This is called Core Dump. Abnormal termination of the process is usually due to bugs, such as illegal memory access leading to segmentation faults. You can use a debugger to check the core file afterwards to find out the cause of the error. This is called Post-mortem Debug. How large a core file a process is allowed to generate depends on the Resource Limit of the process (this information is stored in the PCB). By default, core files are not allowed to be generated because core files may contain sensitive information such as user passwords and are not safe. During the development and debugging phase, you can use the ulimit command to change this limit and allow core files to be generated. First, use the ulimit command to change the Resource Limit of the Shell process to allow the core file to be up to 1024K: $ ulimit -c1024

Insert image description here

The number in the file suffix is ​​the pid of the process

The saved data can support us in debugging

Insert image description here

3. Preservation of signals

1. Other common concepts related to signals

The actual execution of the signal processing action is called signal delivery (Delivery)

The state between signal generation and delivery is called signal pending.

A process can choose to block a signal.

The blocked signal will remain in the pending state when it is generated, and the delivery action will not be executed until the process unblocks the signal.

Note that blocking and ignoring are different. As long as the signal is blocked, it will not be delivered, while ignoring is an optional processing action after delivery.

2. Representation of signals in the kernel

Insert image description here

Insert image description here

The kernel sets up a pending bitmap and a block bitmap for signals. For the pending bitmap, the position of the bit represents the number of the signal, and the content of the bit indicates whether the corresponding signal has been received. For the block bitmap, the bit The position of the bit indicates the number of the signal, and the content of the bit indicates whether the signal is blocked. Secondly, an array of function pointers is maintained. The subscript of the array indicates the number of the signal. The content corresponding to the subscript of the array indicates the processing method of the corresponding signal. .

It should be noted that if a signal is not generated, it does not prevent it from being blocked first.

Each signal has two flag bits indicating blocking and pending, and a function pointer indicating processing action. When a signal is generated, the kernel sets the pending flag of the signal in the process control block and does not clear the flag 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 and then unblock.

The SIGQUIT signal has never been generated. Once the SIGQUIT signal is generated, it will be blocked. Its processing action is the user-defined function singhandler. What happens if a signal is generated multiple times before the process unblocks it? POSIX.1 allows the system to deliver the signal one or more times. Linux implements this: regular signals that are generated multiple times before delivery are counted only once, while real-time signals that are generated multiple times before delivery can be placed in a queue in sequence.

3.sigset_t

From the above figure, each signal has only one bit of pending flag, which is either 0 or 1. It does not record how many times the signal has been generated. The blocking flag is also expressed in this way. Therefore, pending and blocked flags can be stored with the same data type sigset_t. sigset_t is called a signal set. This type can represent the "valid" or "invalid" status of each signal, and "valid" and "invalid" in the blocking signal set. " means whether the signal is blocked, and "valid" and "invalid" in the pending signal set mean whether the signal is in a pending state. The blocking signal set is also called the signal mask of the current process. The "mask" here should be understood as blocking rather than ignoring.

4. Signal set operation function

The sigset_t type uses a bit to represent the "valid" or "invalid" status for each signal. How to store these bits internally in this type depends on the system implementation. From the user's perspective, it does not need to be concerned. The user can only call the following functions. To operate the sigset_t variable, there should be no explanation of its internal data. For example, it makes no sense to use printf to directly print the sigset_t variable.

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。
函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。
注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1

sigprocmask

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

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 
返回值:若成功则为0,若出错则为-1

If oset is a non-null pointer, the current signal mask of the reading process is passed out through the oset parameter. If set is a non-null pointer, the signal mask of the process is changed, and the parameter how indicates how to change it. If oset and set are both non-null pointers, back up the original signal mask word to oset first, and then change the signal mask word according to the set and how parameters. Assuming that the current signal mask word is mask, the following table illustrates the optional values ​​​​of the how parameter.

Insert image description here

If calling sigprocmask unblocks several currently pending signals, at least one of the signals will be delivered before sigprocmask returns.

sigpending

#include <signal.h>
sigpending
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1

Next, we write a program that first screens signal No. 2, and then sends signal No. 2. We can see the process of setting the second bit in the block bitmap from 0 to 1.

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

#define BLOCK_SIGNO 2

void show_pending(const sigset_t &pending)
{
    
    
    for (int signo = 31; signo >= 1; signo--)
    {
    
    
        if (sigismember(&pending, signo))
            std::cout << "1";
        else
            std::cout << "0";
    }
    std::cout << "\n";
}

int main()
{
    
    
    // 1.先尝试屏蔽指定信号
    sigset_t block, oblock, pending;

    // 1.1 初始化
    sigemptyset(&block);
    sigemptyset(&oblock);
    sigemptyset(&pending);

    // 1.2 添加要屏蔽的信号
    sigaddset(&block, BLOCK_SIGNO);
    // 1.3 开始屏蔽
    sigprocmask(SIG_SETMASK, &block, &oblock);

    // 2.遍历打印pending信号集
    int cnt = 10;
    while (true)
    {
    
    
        // 2.1初始化
        sigemptyset(&pending);
        // 2.2获取它
        sigpending(&pending);
        // 2.3打印它
        show_pending(pending);
        sleep(1);
    }
}

Insert image description here

Next we change our code to unblock signal No. 2 after 10 seconds.

int main()
{
    
    
    // 1.先尝试屏蔽指定信号
    sigset_t block, oblock, pending;

    // 1.1 初始化
    sigemptyset(&block);
    sigemptyset(&oblock);
    sigemptyset(&pending);

    // 1.2 添加要屏蔽的信号
    sigaddset(&block, BLOCK_SIGNO);
    // 1.3 开始屏蔽
    sigprocmask(SIG_SETMASK, &block, &oblock);

    // 2.遍历打印pending信号集
    int cnt = 10;
    while (true)
    {
    
    
        // 2.1初始化
        sigemptyset(&pending);
        // 2.2获取它
        sigpending(&pending);
        // 2.3打印它
        show_pending(pending);
        sleep(1);

        if (cnt-- == 0)
        {
    
    
            sigprocmask(SIG_SETMASK, &oblock, &block);
            std::cout << "恢复对信号的屏蔽,不屏蔽任何信号" << std::endl;
        }
    }
}

Insert image description here

We found that after unblocking No. 2, our last print statement was not executed. This is because once a specific signal is unblocked, generally the OS must deliver at least one signal immediately. At this time, signal No. 2 is delivered, and the OS Take the default behavior and exit the process directly in the kernel mode without returning to the user mode, so there is no printing.

Next we change the code so that we can see the print statement and the process of setting the bit from 1 to 0.

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

#define BLOCK_SIGNO 2

void show_pending(const sigset_t &pending)
{
    
    
    for (int signo = 31; signo >= 1; signo--)
    {
    
    
        if (sigismember(&pending, signo))
            std::cout << "1";
        else
            std::cout << "0";
    }
    std::cout << "\n";
}

static void handler(int signo)
{
    
    
    std::cout << signo << " 号信号已经被递达" << std::endl;
}

int main()
{
    
    
    signal(BLOCK_SIGNO, handler);
    // 1.先尝试屏蔽指定信号
    sigset_t block, oblock, pending;

    // 1.1 初始化
    sigemptyset(&block);
    sigemptyset(&oblock);
    sigemptyset(&pending);

    // 1.2 添加要屏蔽的信号
    sigaddset(&block, BLOCK_SIGNO);
    // 1.3 开始屏蔽
    sigprocmask(SIG_SETMASK, &block, &oblock);

    // 2.遍历打印pending信号集
    int cnt = 10;
    while (true)
    {
    
    
        // 2.1初始化
        sigemptyset(&pending);
        // 2.2获取它
        sigpending(&pending);
        // 2.3打印它
        show_pending(pending);
        sleep(1);

        if (cnt-- == 0)
        {
    
    
            sigprocmask(SIG_SETMASK, &oblock, &block);
            std::cout << "恢复对信号的屏蔽,不屏蔽任何信号" << std::endl;
        }
    }
}

Insert image description here

Guess you like

Origin blog.csdn.net/qq_67582098/article/details/135006801