Linux learning record - 이십삼 process signal (2)


1. Reentrant functions

A function is reentrant when both execution streams execute it. For example, when the same function insert is executed in main, the time slice of this process is up, the kernel is embedded, and then another function is executed. Insert is also called in this function, and when it finishes executing the code, it returns to main and continues At the position just now, this is equivalent to the insert being repeatedly entered, which will cause some problems. This also shows that insert is a non-reentrant function. If there is no problem after reentry, it is a reentrant function. Reentrancy is a property of functions.

If a global data structure is used inside a function, then this function is usually non-reentrant; such as malloc, most functions of the standard IO library are non-reentrant functions. If there are only some local variables in a function, it can be reentrant.

2. The volatile keyword

#include <stdio.h>
#include <signal.h>

int quit = 0;

void handler(int signo)
{
    
    
    printf("change quit from 0 to 1\n");
    quit = 1;
}

int main()
{
    
    
    signal(2, handler);
    while(!quit);//这里故意没有携带while的代码块,故意让编译器认为在main中,quit只会被检测
    printf("man quit 正常\n");
    return 0;
}

For code like this, it can be found that if the handler is not executed, the while will have an infinite loop, and after the handler is executed, the while will end and the normal statement will be printed.

But in fact, there are some compilation levels when gcc compiles, and it does optimization.

insert image description here

mysignal:mysignal.c
    gcc -o mysignal mysignal.cc -O2
.PHONY:clean
clean:
    rm -f mysignal

The optimization done by each compiler is different. Try these options such as O1 O2 O3 O0 respectively. After running, when we send the No. 2 signal, that is, Ctrl + C, if you can exit and print man quit normally, it means you The compiler optimization level for is the level currently set.

How is this optimized? Why should such a simple code be optimized? If you press Ctrl + C without exiting, keep pressing it, and it will always print change quit from 0 to 1, but quit seems to have not changed to 1, or it will exit. So there are a lot of questions now, take your time.

There are two types of CPU operations, arithmetic operations and logical operations, as the name implies. All calculations must be done in the CPU, so when it comes to the while loop, the quit global variable that exists in the physical memory must be loaded into the register of the CPU, and handed over to the CPU to make a logical judgment, and the result is notified to while, and the PC pointer in the CPU points to this The code and data of the program, if the while condition is true, then continue to execute the while code, if not, the PC pointer will point to the next line of code, which is where printf is.

After changing the signal processing action, when the signal is received, quit becomes 1, and then loaded into the memory again, and then judged, it will exit.

For this code, the compiler optimizes it because the main function block only judges quit, and every time the while judges, it returns to the code after the judgment, and then puts quit in the register again, and then judges, so the compiler just After putting quit into the register when compiling the code, it will only be judged in the register, and will not return to the memory to load again. This is the compiler's optimization of it. In the future, you only need to look at the data in the register. So even if quit is changed in memory, the register cannot see it.

Therefore, the compiler optimization is not necessarily good. The CPU only sees the data of the register but not the memory. This means that the memory location is not visible. Then in order for the CPU to read data from the memory every time without using the register data, the volatile keyword must be used.

volatile int quit = 0;

The role of this keyword is to ensure memory visibility.

3. How to understand compiler optimization

The essence of the compiler is to manipulate the code.

The CPU is actually very stupid, and executes whatever code the user gives. On the contrary, the operating system is very smart.

In the code just now, if it is not optimized, before the code is converted into assembly code, first put a certain data in the memory into a register, then put it into another register, and then reverse the logic in this register, and then check the following The data in the register is enough. If the condition is satisfied, jump to the loop and continue to detect. If it is not satisfied, run the following code; if it is optimized, then the loop starts from the logic reverse instead of loading memory data, so Repeat the cycle, it can only see the first data, even if the data is changed later, it will not be seen, because you will find that there is no need to change the data in the cycle, the changed data is changed in the memory, so just like this improved.

4. SIGCHLD signal

Before, we knew that the parent process would block waiting or non-blocking polling to know that the child process exited. Both of these methods required the parent process to actively detect. This is because the child process exited and the parent process did not know. But this is not because the child process just left itself and exited without saying anything.

The child process will send a SIGCHLD signal to the parent process after the process ends, but the parent process ignores this signal by default.

The first parameter of waitpid can also be written like this.

insert image description here

Test code:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <uhistd.h>
#include <signal.h>
pid_t id;

void handler(int signo)
{
    
    
    sleep(5);
    printf("捕捉到一个信号: %d, who: %d\n", signo. getpid());
    pid_t res = waitpid(-1, NULL, 0);//-1的意思就是等待任意一个子进程,直到没有可回收的就结束了。
    if(res > 0)
    {
    
    
        printf("wait success, res: %d, id: %d\n", res, id);
    }
}

int main()
{
    
    
    signal(SIGCHLD, handler);
    id = fork();
    if(id == 0)
    {
    
    
        int cnt = 5;
        while(cnt)
        {
    
    
            printf("我是子进程, 我的pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
            --cnt;
        }
        exit(1);
    }
    while(1)
    {
    
    
        sleep(1);
    }
    return 0;
}

Open another window and use this command while:; do ps axj | head -1 && ps axj | grep mysignal; echo “################”; sleep 1; done , check every other line, the content of the double quotes after ehco can be customized.

But there is a problem with this code. If 10 child processes are created in a loop, then the 10 child processes exit in turn. When exiting, the first one changes the corresponding position of the pending bit of the parent process to 1, but the parent process ignores it, and then the first one The two are also repeatedly set to 1, which is equivalent to the loss of the first signal. After the code runs, you will find that only a few have been recycled. Therefore, the recycling in the handler must also have a cycle.

void handler(int signo)
{
    
    
    sleep(5);
    printf("捕捉到一个信号: %d, who: %d\n", signo, getpid());
    pid_t res = waitpid(-1, NULL, 0);
    while(1)
    {
    
    
        pid_t res = waitpid(-1, NULL, 0);
        if(res > 0)
        {
    
    
            printf("wait success, res: %d, id: %d\n", res, id);
        }
        else break;
    }
}

But what if not all processes exit? For example, the running time of each process is different, 5 have been returned, 5 have not been returned, after 5 are recycled, will the 6th continue to be recycled? It will continue to recycle, so it turns back to blocking waiting. Replace the 0 of waitpid with WNOHANG, which is non-blocking, so that the parent process can do other things when no process exits.

There is another way to avoid creating a zombie process. The parent process calls sigaction to set the processing action of SIGCHLD to SIG_IGN, so that the forked child process will be automatically cleaned up when it terminates, so that the parent process does not need to manage the child process. Child processes are also not notified. When the parent process is set to SIG_IGN, there is a status bit in the parent process pcb, which represents the use of this processing action. When the child process is forked, this status bit will also be passed on. When the child process exits, the system will follow this status bit. Go to automatic recycling. But this is only valid in Linux and is not guaranteed to be available on other Unix systems.

Overall code:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <uhistd.h>
#include <signal.h>
pid_t id;

void handler(int signo)
{
    
    
    printf("捕捉到一个信号: %d, who: %d\n", signo. getpid());
    sleep(5);
    while(1)
    {
    
    
        pid_t res = waitpid(-1, NULL, WNOHANG);
        if(res > 0)
        {
    
    
            printf("wait success, res: %d, id: %d\n", res, id);
        }
        else break;
    }
    printf("handler done\n");
}

int main()
{
    
    
    //signal(SIGCHLD, handler);
    signal(SIGCHLD, SIG_IGN);
    int i = 1;
    for(; i <= 10; ++i)
    {
    
    
        id = fork();
        if(id == 0)
        {
    
    
            int cnt = 5;
            while(cnt)
            {
    
    
                printf("我是子进程,我的pid: %d, ppid: %d\n", getpid(), getppid());
                sleep(1);
                --cnt;
            }
            exit(1);
        }
    }
    while(1)
    {
    
    
        sleep(1);
    }
    return 0;
}

Finish.

Guess you like

Origin blog.csdn.net/kongqizyd146/article/details/130529571