Linux—process signal

process signal

bitmap(3)

perceptual understanding signal

  • When you buy a courier online, and your mobile phone prompts that the courier will arrive today, before the courier arrives, you know how to deal with the courier, that is, you can identify the "courier" in advance.

  • At this point you are playing the game, when the courier arrives downstairs, you receive a message saying that the courier has arrived, that is, you have received the signal.

  • At this point you will have three reactions, one is to get up immediately to get the package and open it for use, this is called the default action; the other is to take the package and come back to mom (because it was bought for mom), this is called custom action; Ignore the delivery and continue playing the game. All three are manifestations of the signal being captured.

  • The period from when you receive the notification to when you get the courier is called the time window. That is, the period from receiving the signal to responding to the signal is called the time window.

  • When the message informs you that the courier has arrived, you immediately go to pick up the courier and react according to the default action. This is called synchronization; Take, it's called asynchronous.

Understand signals from the perspective of technical applications

  • In the time window, the process cannot forget the signal, and the process must have the ability to save the signal. Actually the process saves the signal in the PCB. There is an unsigned int structure in the PCB, which can be regarded as a signal bitmap, and the bitmap has 32 bits.

  • There are 62 kinds of signals in the signal table. From number 1 to number 64, there are no numbers 32 and 33. Now we only understand the signals No. 1 to No. 31, the signals [1, 31] are called ordinary signals, and the signals [34, 64] are called real-time signals.

kill -l to view the signal name and number

image-20230602213542449

  • In fact, the operating system sends a signal to the process, and the operating system will enter the pcb of the process (that is, the operating system has permission) to modify the bits on the signal bitmap. If there are all 0, there is no signal, if there is 1, there is a signal, and if there is 1, there is a signal. The position represents the signal number. Then the OS must provide the system call function related to the signal processing signal sent to the user.

image-20230604094125586

  • In fact, there are many ways to send signals to the process, but in essence, the OS sends signals to the target process.

man 7 signal View signal information

image-20230603160954412

signal generation

There are three ways to handle signals:

  • Execute the default processing action for this signal
  • Provide a signal processing function that requires the kernel to switch to the user state to execute the processing function when processing the signal. This method is called capturing a signal, or a custom action
  • ignore this signal

We can customize the execution action of the corresponding signal through the signal function

Function prototype:

       #include <signal.h>

       typedef void (*sighandler_t)(int);

       sighandler_t signal(int signum, sighandler_t handler);
  • signum is the signal number to be processed
  • handler is the processing function to be executed to process the signal, and the parameter type is a function pointer
  • The call returns a pointer to the previous signal handler function successfully, and returns SIG_ERR(-1) if it fails

In fact, when the process receives the signum, it will call back the handler function through the sighandler_t function pointer

The button generates a signal

We know that Ctrl-C can kill the foreground process

image-20230604102845251

  • Actually Ctrl-C produces signal number 2

mytest.cc

#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;

void handler(int signo)
{
    
    
cout<<"进程捕捉到了一个信号,信号编号是: "<<signo<<endl;

}
int main()
{
    
    
    signal(2,handler);
    while(1)
    {
    
    
        cout<<"我是一个进程,mypid: "<<getpid()<<endl;
        sleep(1);
    }
    
    return 0;
}

image-20230604095913501

  • After running, when I press Ctrl-C in the foreground, I can see that Ctrl-C is translated into No. 2 signal by the OS and sent to the process, so that the process can call back the processing function corresponding to No. 2 signal

Query signal No. 2 through man 7 signal

image-20230604101959261

  • It can be seen that the interrupt input from the keyboard can be translated into signal No. 2 - SIGINT

Notice:

  • The signal generated by Ctrl-C can only be sent to the foreground process. Adding an & after a command can be run in the background, so that the Shell can accept new commands and start a new process without waiting for the process to end.
  • . Shell can run a foreground process and any number of background processes at the same time. Only the foreground process can receive the signal generated by the control key like Ctrl-C.
  • In fact, signals are a way of asynchronous notification of events between processes, which belong to soft interrupts.
  • Then the user may press Ctrl-C to generate a signal at any time during the running process of the foreground process, which means that the user space code of the process may receive a SIGINT signal and terminate anywhere, so the signal is relative to the control of the process. The process is asynchronous.

In addition to Ctrl-C can kill the foreground process, Ctrl-\ can also kill the foreground process

image-20230604103049232

  • In fact, Ctrl-\ is the OS sending signal No. 3 to the process

image-20230604103213419

  • Through the man 7 signal query, the exit from the keyboard input can be translated into the signal No. 3 - SIGQUIT

image-20230604172705525

System call to generate signal

Function prototype:

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

       int kill(pid_t pid, int sig);
  • pid is the process that needs to send a signal to the pid
  • sig is the number signal you want to send
  • The call returns 0 on success, and -1 on failure

Send a sig signal to the process with the specified pid

mysign.cc

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

using namespace std;

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


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 error");
    exit(1);
}

    return 0;
}
  • Obtain the process pid and signal signo through the command line parameters. In fact, when the shell parses the command line, it parses the input content into long strings. The strings are separated by spaces, and the number of strings is passed to argc, argv Is an array of pointers to the parsed strings. Since the kill system call only needs two parameters, process pid and signo, the setting here needs to input 3 parameters to the command line, input template: [program process pid signal] such as: ./mysign 23554 9—to ./mysign whose pid is 23554 Process sends signal number 9
  • The atoi function can convert a string into an integer, such as: here you can convert the string "23554" into an integer 23554

mytest.cc

#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;


int main()
{
    
    
    //signal(2,handler);
    while(1)
    {
    
    
        cout<<"我是一个进程,mypid: "<<getpid()<<endl;
        sleep(1);
    }
    return 0;
}

image-20230604105643106

  • Here I send signal 9 to the process ./mysign with pid 22191 to terminate its operation

send yourself a signal

Function prototype:

       #include <signal.h>

       int raise(int sig);
  • sig is the signal sent

  • The call returns 0 successfully

mysign.cc

#include<iostream>
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
using namespace std;


int main()

{
    
    
    int cnt=0;
    while(cnt<10)
    {
    
    
        cout<<"cnt: "<<cnt<<endl;
        cnt++;
        sleep(1);
 if(cnt==5)
 {
    
    
    int tmp=raise(3);
    assert(tmp==0);
 }

    }
    return 0;
}

image-20230604111821048

  • When the parameter cnt increases to 5, it sends itself the No. 3 signal so that the process exits

Send the specified signal to the process

Function prototype:

       #include <stdlib.h>

       void abort(void);
#include<iostream>
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<stdlib.h>
using namespace std;


int main()

{
    
    
    int cnt=0;
    while(cnt<10)
    {
    
    
        cout<<"cnt: "<<cnt<<endl;
        cnt++;
        sleep(1);
 if(cnt==5)
 {
    
    
    abort();
 }

    }
    return 0;
}

image-20230604112422864

  • It can be known by looking up the table that the abort function actually sends the No. 6 signal to the process

image-20230604112529354

image-20230604112733019

The meaning of the signal

  • In fact, most of the signals in the signal table terminate the process. Although the processing actions of these signals are the same, the different signals mean that different events are processed, that is, processes are processed through signals in different scenarios.

hardware generated signal

Divide by 0

#include<iostream>
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<stdlib.h>
using namespace std;
int main()

{
    
    
    int cnt=0;
    while(cnt<10)
    {
    
    
        cout<<"cnt: "<<cnt<<endl;
        cnt++;
        sleep(1);
 if(cnt==5)
 {
    
    
int a=10;
a/=0;
 }

    }
    return 0;
}

image-20230604120333413

  • It can be seen that when the operation of dividing 0 in the process will cause the process to exit directly

  • By looking up the table, we can know that when the process actually divides by 0, the OS will send the 8th signal SIGFPE to the process.

image-20230604120558890

mysign.cc

#include<iostream>
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<stdlib.h>
using namespace std;
void catchsig(int signo)
{
    
    
    cout<<"我是一个进程,此时我接收到了信号: "<<signo<<endl;
    sleep(1);
}
int main(int argc,char *argv[])
{
    
    
 signal(SIGFPE,catchsig);
int a=10;
a/=0;
while(1)
{
    
    
    cout<<"我是一个进程,正在运行中....."<<endl;
    sleep(1);
}
    return 0;
}

image-20230604151302282

When the program runs to a/=0, it will cause the OS to send the No. 8 signal to the process. The reasons are as follows:

image-20230604152412013

  • In fact, when the CPU is doing calculations, it will put the parameters into the registers, and then put the calculated results into the registers. When dividing by 0, the results obtained by the CPU will be so large that the registers cannot be stored. Then The CPU will discard the result and not put it in the register, but set the overflow flag bit from 0 to 1 in the status register. After the OS knows that the CPU is abnormal, it will change the state of the CPU into a signal and send it to the process of the division by 0 operation. so as to terminate the process.

As for the program running to a/=0, the following codes will not run for the following reasons:

image-20230604152609473

  • In fact, when a process receives a signal, it does not necessarily cause the process to exit, and the process will be scheduled again.
  • There is only one copy of registers inside the CPU, but the contents of the registers belong to the scheduled process. When switching to this process, the register needs to restore the context of the process, and then operate again. When the operation reaches the division by 0 operation, the CPU will set the overflow flag of the status register from 0 to 1, and then the OS will send No. 8 signal to the process. . That is, when the process is switched, the state register will be saved and restored countless times, so the OS will send the No. 8 signal countless times to the process.

wild pointer access

image-20230604154710516

  • When a wild pointer is accessed out of bounds in the program, a segment error (Segmentation fault) will be reported

mysign.cc

#include<iostream>
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<stdlib.h>
using namespace std;

void catchsig(int signo)
{
    
    
    cout<<"我是一个进程,此时我接收到了信号: "<<signo<<endl;
    sleep(1);
}
int main(int argc,char *argv[])
{
    
    
 signal(11,catchsig);
while(1)
{
    
    
    cout<<"我是一个进程,正在运行中....."<<endl;
int *ptr;
ptr=nullptr;
*ptr=10;
    sleep(1);
}
    return 0;
}

  • By looking up the table, we can know that when there is an out-of-bounds access by a wild pointer in the program, the OS will send the 11th signal SIGSEGV to the process.

image-20230604155221266

The reasons for sending the No. 11 signal are as follows:

image-20230604160216422

  • The PCB of the process can find the virtual address, and then map it with the physical address through the page table, and then the process can find the data on the physical address.
  • When a process wants to access a physical address out of bounds through a virtual address, the page table will truncate the access. The MMU (memory management unit) on the page table will report an error, but the MMU actually exists in the CPU. When the OS knows that the MMU has reported an error, it will send signal No. 11 to the process to terminate it.

The second meaning of the signal

  • Although the action of most signals is to terminate the process, the signal can feed back certain information to the programmer, allowing the programmer to know where an error occurred and let him correct it.

software generated signal

SIGPIPE is a signal generated by software conditions, which has been introduced in "Pipeline". This section mainly introduces the alarm function and the SIGALRM signal.

alarm function

Function prototype:

       #include <unistd.h>

       unsigned int alarm(unsigned int seconds);
  • seconds is an unsigned integer, which is the number of seconds passed under linux. If the seconds value is 0, it means canceling the previously set alarm clock
  • Calling the alarm function can set an alarm clock, that is, tell the kernel to send a SIGALRM signal to the current process after seconds seconds. The default processing action of this signal is to terminate the current process
  • The return value is 0 or the remaining seconds of the previously set alarm time

According to the table lookup, the signal corresponding to the alarm is SIGALRM No. 14

image-20230604163003733

mysign.cc

#include<iostream>
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<stdlib.h>
using namespace std;

void catchsig(int signo)
{
    
    
    cout<<"我是一个进程,此时我接收到了信号: "<<signo<<endl;
    exit(0);
   
}

int main(int argc,char *argv[])
{
    
    
    int cnt=0;
signal(SIGALRM,catchsig);
alarm(1);
while(1)
{
    
    
    cnt++;
    cout<<"cnt: "<<cnt<<endl;
}
return 0;
}

image-20230604163659441

  • It can be seen that cnt has increased by itself and printed 7w multiple times, and the alarm clock rang after one second during the period, and the process terminated.

  • Because the IO is very slow, the processing speed of the CPU is greatly slowed down. If the cnt is increased automatically, and the cnt is printed after the alarm clock rings, you can know the speed of the cloud server's CPU processing data.

#include<iostream>
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
using namespace std;
    int cnt=0;
void catchsig(int signo)
{
    
    
  cout<<"the alarm is ringing now,cnt: "<<cnt<<endl;
  exit(0);
}
int main(int argc,char *argv[])
{
    
    
signal(SIGALRM,catchsig);
alarm(1);
while(1)
{
    
    
    cnt++;
}
return 0;
}

image-20230604164344502

  • It can be seen that the CPU processing speed of the cloud server is more than 500 million

If in the body of a custom function that handles the SIGALRM signal, I don't let the function exit

image-20230604164643200

  • It can be seen that the information is only printed once, indicating that the catchsig function is called only once, which further indicates that the OS only sends the SIGALRM signal to the process once.
  • In fact, the alarm function is one-time, and it will disappear once it is used, which is equivalent to a one-time alarm clock

But we can still define the alarm function once in the custom function body, and when the alarm clock rings, the new alarm clock is set again. The effect presented is that cnt keeps increasing within 1 second, prints once in 1 second when it expires, then keeps increasing, and prints again, the effect is similar to the sleep function

image-20230604165205906

Software conditions for setting alarms

  • In fact, any process can set an alarm clock in the kernel through the alarm system call, so there will be many alarm clocks in the OS, and the OS needs to manage the data structure of these alarm clocks

Alarm kernel data structure pseudocode

struct alarm 
{
    
    
uint64_t when;//未来的超时时间
int type;//闹钟类型,是一次性的还是周期性的
task_struct *p;//指向设置该闹钟的进程pcb
struct alarm* next;//指向下一个闹钟
}

image-20230604171008221

  • In fact, in the OS, there will be a head pointer pointing to the first alarm clock to go off, followed by the second first alarm clock, and so on
  • The OS will periodically check these alarm clocks, when the first alarm clock rings, remove the first alarm clock, and then check the next one
  • In this way, the management of the alarm clock is transformed into a kernel data structure that manages the alarm clock in the form of a linked list.

core dump

  • When a process is going 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, which is called Core Dump.

man 7 signalAs you can see in the previous passage , there are several types of Actions for each signal, such as: Term, Core, Ign, Cont, Stop; among them, the Action is Core's signal that supports core dump

In the cloud server, the core dump is turned off by default, and we can ulimit -acheck the current resource limit setting by using the command.

image-20230604193105985

  • You can see that the first line shows that the core file size is 0 by default, which means that the core dump is disabled by default. Because the core file may contain sensitive information such as user passwords, it is not safe.
  • The ulimit command changes the Resource Limit of the Shell process, but the PCB of the mysign process is copied from the Shell process, so it also has the same Resource Limit value as the Shell process.

We can ulimit -c sizeset the core file size by command

image-20230604193440146

  • After the core file size is set, it means that the core dump function is enabled.

image-20230604194912069

  • Now take the division by 0 operation as an example. When the process is terminated by the OS sending signal No. 11 due to the division by 0 operation, you will find that an additional sentence (core dumped) is added after the error report, and there are more core files under the directory at this time. The following number is the pid of the process

Since the process needs to be debugged through the core, the mysign file is identified in the makefile as debuggable (with -g)

image-20230604195350472

  • Now enter gdb mode for debugging, enter core-flie core file name

After entering, you can see that the process has received signal No. 8, that is, a floating-point overflow error, and the code is located at line 61

  • Compared with core, the function of term is to directly kill the process

About whether all signals can be captured

We can capture the corresponding signal through the signal function and let it call back our custom function. Can we capture all the signals?

mysign.cc

#include<iostream>
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<stdlib.h>
using namespace std;

void catchsig(int signo)
{
    
    
   cout<<"我是一个进程,此时我接收到了信号: "<<signo<<endl;
   alarm(1);
}

int main(int argc,char* argv[])
{
    
    
 for(int signo=1;signo<=31;signo++)
 {
    
    
    signal(signo,catchsig);
 }
while(1)
{
    
    
    cout<<"我在运行着:"<<getpid()<<endl;
 sleep(1);
}
  • Here I captured signals 1~31 correspondingly

image-20230604201732848

  • Through experiments, it is proved that the No. 9 signal SIGKILL cannot be captured, and in fact the No. 19 signal SIGSTOP cannot be captured either.

Signal related concepts

  • The actual execution of the signal processing action is called signal 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, while ignoring Is an optional processing action after delivery.

Representation of signals in the kernel

image-20230610094312808

  • In the kernel data structure of the process, there is a block bitmap corresponding to the signal, a pending bitmap and an array of function pointers.
  • Each signal has two flags indicating blocking (block) and pending (pending), and a function pointer containing the processing action method. When the signal is generated, the kernel sets the pending flag of the signal in the process control block (set from 0 to 1), and the flag is not cleared until the signal is delivered (set to 0 by 1). In the example above, the SIGHUP signal is neither blocked nor generated, and the default processing action is executed 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. If the signal is generated multiple times before the process unblocks the signal, in POSIX.1, the system is allowed to deliver the signal one or more times. In Linux, regular signals are generated multiple times before they are delivered and only counted once, while real-time signals are generated multiple times before they are delivered and can be placed in a queue in turn. But this article does not discuss real-time signals.

Please note:

  • In the block bitmap and the pending bitmap, the position of the bit represents a certain signal. Among them, 0 on the block bitmap means that the signal is not blocked, 1 means that the signal is blocked; 0 on the pending bitmap means that the signal is not received, and 1 means that the signal is received
  • Handler is essentially an array of pointers, and the address of the method (function) that handles the corresponding signal is stored in the array. There are three methods: default, custom, and ignore.
  • The positions of the three data structures of block, pending and handler are in one-to-one correspondence

Meet sigset_t

  • From the above figure, each signal has only one bit pending flag, which is either 0 or 1, and the number of times the signal has been generated is not recorded, and the blocking flag is also indicated 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 . The meaning of "valid" and "invalid" in the blocking signal set is whether the signal is blocked, while in the pending signal set " Valid" and "invalid" mean whether the signal is pending or not.
  • The blocking signal set is also called the signal mask of the current process (Signal Mask), and the "shielding" here should be understood as blocking rather than ignoring.

capture signal

signal capture

Prerequisites:

  1. When users need to access resources, they need to access them through system calls. The resource can be a hardware resource (keyboard, mouse, monitor, hard disk) or a resource of the OS itself (getpid to obtain the process pid, vector expansion to apply for memory)
  2. In fact, we are in user mode under normal circumstances, and user mode does not have permission to access resources. Therefore, it is necessary to change from the user state to the kernel state, and access the corresponding resources in the kernel state. The way from the user state to the kernel state is the system call. The system call can be understood as a doorway, the user state is outside the doorway, and the kernel state is inside the doorway. Then switching from user state to kernel state will inevitably require consumption, and in order to reduce consumption, state switching should be minimized as much as possible. (such as vector expansion in advance)

image-20230610103712677

  1. There are many registers in the cpu, including visible registers and invisible registers (status registers)
  2. It can be seen that the context of the current process can be restored in the register, and the starting address of the pcb of the current process can be saved, so that the kernel data structure of the current process can be found; there is also the starting address of the user-mode page table, etc.; invisible Among the registers, the CR3 register saves the running level of the current process, 0 represents the kernel state, 3 represents the user state, and the process switching state must pass through the CR3 register.

image-20230610110656827

  1. As mentioned before, in the memory under the 32-bit system, 0-3G is the user space, and 3-4G is the kernel space. Correspondingly, the memory is mapped to the data on the disk through the page table. Since the task_struct of the process finds the user-level page table through mm_struct, and then maps it to the disk, there are many copies of the process in the OS, and each process has a one-to-one correspondence with the user-level page table, so the user-level page table in the OS has Many copies; similarly, 3-4G (kernel space) also needs to be mapped with the data on the disk through the kernel-level page table. Since there is only one copy of the kernel data on the disk, only one copy of the kernel data needs to be loaded into the memory, so only one copy of the kernel-level page table is required.

  2. Since each process has its own address space (mm_struct), the user space is exclusive to each process, and the kernel space is accessible to each process, or the kernel space is shared by each process. Therefore, a process accessing the kernel space only needs to jump to the kernel space in the process address space. This means that 3-4G of kernel space will not be changed when the process is switched.

  3. In fact, when the process is switched, the OS will load the context of the process into the CPU, and then execute the code corresponding to the user space. When the system call needs to be accessed, the OS will go to the CR3 register corresponding to the process, change the user state to the kernel state, and then jump from the user space to the kernel space for resource access, and then change the kernel state to In user mode, return to user space and continue to execute the corresponding context.

How the kernel implements signal capture

  • In fact, when a process falls into the kernel due to an interruption or exception when the process is in the execution context of the user mode, check the block bitmap of the corresponding process and whether the pending bitmap has a signal delivery before returning to the user mode after the exception is processed in the kernel.

When there is a corresponding signal delivered, and the processing method corresponding to the signal is the default action or ignored, it will directly return to the user state after execution and continue the execution context from the place where it was interrupted last time.

image-20230610121846123

When there is a corresponding signal delivery, and the processing method corresponding to the signal is customized, it will switch from the kernel state to the user state through the system call, and then go to the user memory to execute the corresponding processing function, and return through the specific system call sigreturn after execution Kernel state. After clearing the corresponding pending flag, if there is no new signal delivery, finally return to the user mode to continue the execution context from the place where it was interrupted last time.

image-20230610160003401

  • Note that functions sighandlerand mainfunctions use different stack spaces, there is no relationship between them calling each other, and they are two independent control flows!

  • The control process can be roughly abstracted as follows:

image-20230610161735720

  • In fact, the kernel mode has a higher level of authority than the user mode. The kernel mode can enter the user mode for operation (downward compatibility), but it cannot be designed like this! First, the OS does not trust any users, and users may use the OS to access resources in user space out of bounds, causing security problems; second, operating in user mode in user space also makes users responsible for themselves.

Signal Set Manipulation Functions

sigpromask

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

Function prototype:

    #include <signal.h>
    int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  • The optional values ​​of the parameter how are given in the following table
SIG_BLOCK set contains the signal we want to add to the signal mask of the current process, which is equivalent to mask=mask|set
SIG_UNBLOCK set contains the signals we want to remove from the signal mask of the current process, which is equivalent to mask=mask&~set
SIG_SETMASK Set the signal mask word of the current process to the value pointed to by set, which is equivalent to mask=set
  • If the parameter set is a non-null pointer, the signal mask word of the process is changed, and the parameter how indicates how to change it.

  • If oset is a non-null pointer, the current signal mask of the read process is passed through the oset parameter. That is, save the previous signal mask word

  • To sum up, if both oset and set are non-null pointers, first backup the original signal mask word to oset, and then change the signal mask word according to the set and how parameters.

  • Return value: 0 if successful, -1 if error occurs

sigpending

Get the set of pending signals for the current process

Function prototype:

      #include <signal.h>
      int sigpending(sigset_t *set);
  • set is an output parameter, and the pending bitmap of the process is passed out through set

  • Return value: call returns 0 successfully, fails to return -1

sigemptyset

Function prototype:

#include <signal.h>
int sigemptyset(sigset_t *set);
  • 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.
  • Return value: call returns 0 successfully, fails to return -1

the seagull

Function prototype:

#include <signal.h>
int sigaddset (sigset_t *set, int signo);
  • The sigaddset function adds some valid signal signo to the set signal set

  • Return value: call returns 0 successfully, fails to return -1

the charge

#include <signal.h>
int sigdelset(sigset_t *set, int signo);
  • The sigaddset function deletes a valid signal signo in the set signal set
  • Return value: call returns 0 successfully, fails to return -1

sigismember

Function prototype:

#include <signal.h>
int sigismember(const sigset_t *set, int signo); 
  • The sigismember function determines whether the specified signal signo exists in the set signal set
  • Return value: Return 1 if it exists, and return -1 if it does not exist

sigfillset

Function prototype:

#include <signal.h>
int sigfillset(sigset_t *set);
  • The sigfillset function initializes the signal set pointed to by set, and sets the corresponding bits of all signals in it, indicating that the valid signals of this signal set include all signals supported by the system.
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<stdio.h>
#include<vector>
using namespace std;
#define BLOCK_SIGNAL 2
#define MAX_SIGNUM 31

static void show_pending(sigset_t &pending)
{
    
    
    for(int signo=MAX_SIGNUM;signo>=1;signo--)
    {
    
    
        if(sigismember(&pending,signo))
        {
    
    
            cout<<"1";
        }else
        cout<<"0";
    }
    cout<<"\n";
}
int main()
{
    
    
    //先初始化
    sigset_t block,oblock,pending;
    sigemptyset(&block);
    sigemptyset(&oblock);
    sigemptyset(&pending);
    sigaddset(&block,BLOCK_SIGNAL);//把二号信号添加到block位图中
    //将定义的位图设置进进程内核中
    sigprocmask(SIG_SETMASK,&block,&oblock);
    //打印pending位图
  while(true)
  {
    
    
    sigemptyset(&pending);
    sigpending(&pending);//获取进程的pending位图并置进pending位图中
    show_pending(pending);//打印pending位图
    sleep(1);//间隔打印
  }
    return 0;
}
  • Add the No. 2 signal to the mask word of the process through the sigaddset and sigprocmask functions
  • When the process receives the No. 2 signal, it will go to the pending bitmap of the process and set the corresponding position from 0 to 1, but because it is blocked, the signal cannot be delivered, so you can print the pending bitmap to see that the corresponding position of the No. 2 signal is 1

image-20230610174549629

image-20230610175119294

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

using namespace std;

#define BLOCK_SIGNAL 2
#define MAX_SIGNUM 31

static vector<int> sigarr{
    
    2,3};//将2,3号信号添加到进程的信号屏蔽字中

static void show_pending(sigset_t &pending)
{
    
    
    for(int signo=MAX_SIGNUM;signo>=1;signo--)
    {
    
    
        if(sigismember(&pending,signo))
        {
    
    
            cout<<"1";
        }else
        cout<<"0";
    }
    cout<<"\n";
}
void myhandler(int signo)
{
    
    
  cout<<signo<<"信号已经被抵达\n"<<endl;
}
int main()
{
    
    
  signal(2,myhandler);
    //先初始化
    sigset_t block,oblock,pending;
    sigemptyset(&block);
    sigemptyset(&oblock);
    sigemptyset(&pending);
    sigaddset(&block,BLOCK_SIGNAL);//把二号信号添加到block位图中
  //for(const auto&sig:sigarr) sigaddset(&block,sig);
  //把vector内指定的信号添加进block位图中
  
    //将定义的位图设置进进程内核中
    sigprocmask(SIG_SETMASK,&block,&oblock);
    
    //打印pending位图
    int cnt=10;
  while(true)
  {
    
    
    sigemptyset(&pending);
    sigpending(&pending);//获取进程的pending位图并置进pending位图中
    show_pending(pending);//打印pending位图
    sleep(1);//间隔打印
    if(cnt--==0)
    {
    
    
      cout<<"恢复对信号的屏蔽,此时不屏蔽任何信号\n"<<endl;
      sigprocmask(SIG_SETMASK,&oblock,&block);
      
    }
  }

    return 0;
}

image-20230610182843498

  • First, the No. 2 signal is shielded, and the bitmap corresponding to the No. 2 signal is set from 0 to 1 in the blocking signal set of the process, and then the No. 2 signal is received, but it is in a pending state because it is blocked, and the pending (pending) bit The position of the second signal corresponding to the figure is set from 0 to 1
  • After 10 seconds, set the bitmap corresponding to the second signal of the blocked signal set of the process from 1 to 0, that is, cancel the shielding of the second signal. At this time, the second signal is delivered immediately, and the corresponding processing function is executed.
  • Then the signal mask word is all 0 and no longer shields the signal, so the signal is no longer blocked after receiving the second signal, that is, the signal is no longer in a pending state, and the corresponding processing function is directly executed, so the pending bitmap is always all 0.

sigaction

The sigaction function can read and modify the processing action associated with the specified signal

Function prototype:

 #include <signal.h>
 int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  • signum is the number of the specified signal
  • If the act pointer is not empty, modify the processing action of the signal according to act.
  • If the oact pointer is non-null, the original processing action of the signal will be transmitted through oact.
  • Return value: Return 0 if the call is successful, and return -1 if it fails

image-20230611110730571

act and oact point to the sigaction structure

  • Assigning sa_handler to SIG_IGN means ignoring the signal; assigning to SIG_DEL means to execute the default action of the system, or it can be assigned to a function pointer, which points to a custom function, or registers a signal processing function with the kernel, and the return value of this function is void , you can take an int parameter, and you can know the number of the current signal through the parameter, so that you can use the same function to process multiple signals. Note that this is also a callback function, not called by the main function, but by the system.
  • sa_flags and sa_restorer are not introduced here, they are all set to NULL
  • sa_mask is the signal mask word, that is, the blocking bitmap, where multiple signals can be added to the mask word
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<stdio.h>
#include<vector>
using namespace std;
void Count(int cnt)
{
    
    
  while(cnt)
  {
    
    
    printf("cnt: %2d\r",cnt);
    fflush(stdout);
    cnt--;
    sleep(1);
  }
  cout<<"\n";
}
void handler(int signo)
{
    
    
  cout<<"get a signo: "<<signo<<endl;
  Count(10);//在handler方法里待十秒
}
int main()
{
    
    
 struct sigaction act,oact;
 act.sa_handler=handler;
 act.sa_flags=0;
sigaction(SIGINT,&act,&oact);
while(true) sleep(1);
  return 0;
}
  • Put the pointer of the custom method handler into the act structure, and when the subsequent process receives the second signal, it will execute the specified handler method
  • In the handler method, first print the incoming signal number, and then perform a 10-second countdown, that is, stay in the handler method for ten seconds

image-20230611112650765

It can be seen that the No. 2 signal was sent multiple times to the process, but actually only the No. 2 signal was delivered twice.

  • When the process receives the No. 2 signal, the process does not shield any signal at this time, and directly executes the processing method (handler function) corresponding to the No. 2 signal. At this time, the process will set the signal mask word corresponding to the No. 2 signal from 0 to 1. That is, when processing No. 2 signal, shield the subsequent same signal
  • During the execution of the processing function of the No. 2 signal (during 10 seconds), since the pending bitmap corresponding to the No. 2 signal of the process is 0, when the process receives the No. 2 signal again, the OS will set the corresponding position of the pending bitmap from 0 to 1 , that is, the later signal No. 2 is pending. After that, the No. 2 signal is received. Since the corresponding pending bitmap has only one bit, the pending bitmap can make at most one No. 2 signal pending, and the subsequent ones are meaningless (0->1, but not 1- >2, and 1->1 is meaningless)
  • After the processing function of the current No. 2 signal is executed, the signal mask word corresponding to the No. 2 signal is automatically set from 1 to 0, that is, the masking of the No. 2 signal is released. At this time, the pending bitmap corresponding to the No. 2 signal is 1 (there are still 2 If the number signal is pending), immediately set the pending bitmap from 1 to 0, and then execute the corresponding processing function, and the signal mask word is automatically set from 0 to 1 again

It sent the No. 2 signal many times and only delivered the No. 2 signal twice. The fundamental reason is that the pending bitmap can accommodate at most one pending signal, and the subsequent ones are meaningless

image-20230611115145348

  • Here I add signal No. 3 to the signal mask word of the process

image-20230611115306821

  • It can be seen that the No. 2 signal has occurred many times, and it has been delivered twice, and then the No. 3 signal has been sent several times. In fact, the No. 3 signal is received and the default action is executed. The process exits
  • The significance of adding signal No. 3 to the sa_mask bitmap of the structure act is to add signal No. 3 to the signal mask word of the process. When the processing method of No. 2 signal is executed, the process will naturally shield No. 2 signal. At this time, No. 3 signal will be shielded at the same time, that is, the signal mask word corresponding to No. 3 signal is set from 0 to 1. Now that signal No. 3 is received, the corresponding pending bitmap is set from 0 to 1. When the process no longer shields the No. 2 signal after all the No. 2 signals are delivered, the process will unblock the No. 3 signal, and the corresponding pending bitmap will be set from 1 to 0, and then the No. 3 signal will be delivered.

reentrant function

Now there is a scene where there are three nodes, the nodes pointed to by head, node1 and node2. In the main function, let the node1 head be inserted into the linked list first, and then the exception will fall into the kernel, and the node2 node will be inserted into the linked list when the processing function is called. On a theoretical level there is no problem, but in practice:

image-20230611161354514

image-20230611160954143

  • First, the node1 node nods and inserts it before the node pointed to by the head
  • Then it falls into the kernel due to interrupts or exceptions. After handling the exception, it detects that there is a signal that needs to be processed, so it switches to the sighandler function to execute

image-20230611161552333

  • The node2 node is inserted before the node pointed to by head, and then the head pointer points to node2. At this time, after executing the handler function, it returns to the kernel state through a special system call, and then returns to the main main execution flow to continue the execution context

image-20230611161809374

  • Finally head points to node1. In fact, the effective head of the node1 node is inserted into the linked list.
  • The main function execution flow and the handler function execution flow are two execution flows. Like 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, which is called reentrancy.
  • The insert function accesses a global linked list, which may cause confusion due to reentrancy. Functions like this are called non-reentrant functions. Correspondingly, if a function only accesses its own local variables or parameters, it is called a reentrant function.

A function is non-reentrant if it meets one of the following conditions:

  1. Called malloc or free, because malloc also uses a global linked list to manage the heap
  2. A standard I/O library function is called because many implementations of the standard I/O library use global data structures in a non-reentrant manner

keyword volatile

  • Variables modified by volatile maintain memory visibility
#include<stdio.h>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>
int quit=0;

void handler(int signo)
{
    
    
    printf("pid: %d, %d 号信号正在被捕获!\n",getpid(),signo);
    printf("quit: %d",quit);
    quit=1;
    printf("-> %d\n",quit);
}

int main()
{
    
    

signal(2,handler);
while(!quit);
 printf("注意,我是正常退出的!\n");
    return 0;
}
  • The global variable quit is 0, and then the while loop is called in the main function until the No. 2 signal is received. In the processing function handler, the quit changes from 0 to 1, and then returns to the main execution flow main function, and the while is judged to be false and no longer executed. while loop

image-20230611173847945

image-20230611174907007

  • In fact, the quit variable is loaded into the memory, and then the while loop OS will load the variable quit into the CPU for calculation. The first time quit is 0, so the while judgment is true, and the while loop continues; after receiving the No. 2 signal, The processing function handler sets quit to 1, so the while loop is false, execute the following

In gcc compilation, the optimization level can be adjusted, here I adjust it to O3 level

image-20230611174344418

image-20230611174321709

  • It can be seen that after adjusting the optimization level, the process did not exit normally

image-20230611183841049

  • When the OS loads the process into the memory, it will directly load the variable quit into the CPU, and the judgment of the subsequent while loop depends on the variable quit of the CPU instead of the quit variable in the memory. Therefore, when the process receives the No. 2 signal, the handler processing function is executed to set the global variable quit from 0 to 1. However, the variable quit in the memory will not be updated to the CPU, so the while loop is always true and executes an endless loop, causing the process to fail to exit normally.

The variable modified by the keyword volatile keeps the visibility of the memory, and tells the compiler that the variable modified by the keyword is not allowed to be optimized, and any operation on the variable must be performed in real memory.

image-20230611184948068

SIGCHLD signal

When the child process graph exits or is terminated, the child process will send signal 17 SIGCHILD to the parent process

#include<stdio.h>
#include<signal.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
void Count(int cnt)
{
    
    
    while(cnt)
    {
    
    
        //cout<<"cnt:"<<cnt;
        printf("cnt: %d\r",cnt);
        fflush(stdout);
        cnt--;
        sleep(1);
    }
    printf("\n");
}
void handler(int signo)
{
    
    
 printf("我是父进程,pid:%d ,我接收到了子进程的%d 号信号\n",getpid(),signo);
}
int main()
{
    
    
    signal(SIGCHLD,handler);
    printf("我是父进程id: %d, ppid: %d\n",getpid(),getppid());
  pid_t id=fork();
  if(id==0)
  {
    
    
      printf("我是子进程id: %d, ppid: %d\n",getpid(),getppid());
      Count(5);
      exit(1);
  }
  while(1) sleep(1);
    return 0;
}

image-20230611193524717

  • In fact, in the code, the parent process blocks and waits for the child process to send a signal, and then recycles the child process. During the blocked waiting period, the parent process cannot do other things

A method to prevent the parent process from blocking and waiting for the child process to be recycled

  1. In the No. 17 signal function handler, the child process is recycled through waitpid, but the third parameter option of the function passes the parameter HNOHANG, indicating that it does not block and wait for the child process to be recycled

The prototype of the waitpid function

       #include <sys/types.h>
       #include <sys/wait.h>
       pid_t waitpid(pid_t pid, int *status, int options);
  • Waiting for any child process when pid=-1, the waitpid() function at this time degenerates into a normal wait() function
  • status saves the status information of the child process. With this information, the parent process can understand why the child process exits, whether it exits normally or has some error, and it can be set to NULL here.
  • option is set to WHOHANG means not to block and wait for the child process to be recycled
  • Return value: PID of the child process if successful, 0 if options is WNOHANG, -1 if other errors occur

Handling function handler

void handler(int signo)
{
    
    
 pid_t id=0;
 while( (id = waitpid(-1, NULL, WNOHANG)) > 0){
    
    
 printf("wait child success: %d\n", id);
 }
 printf("child is quit! %d\n", getpid());
}
  1. In the signal function, the second parameter is passed to SIG_IGN (this method is available for the linux environment, and the rest are not guaranteed)
    signal(SIGCHLD,SIG_IGN);

Guess you like

Origin blog.csdn.net/m0_71841506/article/details/131172295