【Linux】 —— Overview of zombie process, process replacement and signal

1. Zombie process

1. The concept of the zombie process

In the previous article we mentioned the zombie process. A zombie process is when the process entity ends and is released by the kernel, but the PCB structure remains intact; the child process ends, the parent process does not end and the parent process does not obtain the child process's exit status. With his concept, we first simulate the generation of a zombie process as follows:

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <string.h>

int main()
{
	pid_t n = fork();
	assert(-1 != n);

	if(0 == n)
	{
		printf("child start\n");
		sleep(10);
		printf("child end\n");
	}
	else
	{
		// 保证子进程执行结束,父进程未结束
		printf("father starting\n");
		sleep(20);
		printf("father running\n");
		
	}

	exit(0);
}

Because the parent process sleeps longer than the child process, there will be a zombie process where the child process ends before the parent process. As follows, we use the ps command to view. There is a period of zombie before the child process child end and father over The process appears:
Insert picture description here

2. The solution to the zombie process

Because we already know that the zombie process is generated because the child process ends, the parent process does not end, and the parent process does not obtain the exit status of the child process. After understanding the essence, we will solve how to obtain it, and we can deal with the zombie process.

(1) The role of wait and waitpid functions
When a process terminates normally or abnormally, the kernel sends a SIGCHLD signal to its parent process. Because the termination of the child process is an asynchronous event (which can occur at any time when Fu Jicheng runs), this signal is an asynchronous notification sent by the kernel enjoyment this morning. The parent process has two options for this. The first option is to ignore the signal, and the second option is to provide a function that is called and executed when the signal occurs. And our wait and waitpid function functions are the second way the parent process chooses. Their prototype is as follows:

#include<sys/wait.h>
pid_t wait(int *reval);
pid_t waitpid(pid_t pid,int *reval,int option);

Among them, reval records the exit status of the child process, returns the process ID on success, and -1 on failure. After calling one of these two functions, according to different circumstances, the process will have the following three reactions:

  1. If all your own Chengdu is still running, block
  2. If a child process has terminated and is waiting for the parent process to obtain its termination status, the termination status of the child process is returned immediately
  3. If there are no child processes, an error
    will be returned immediately. If the process calls wait due to receiving the SIGCHLD signal, you can expect wait to return immediately. But if you call wait at any time, the process may block.

(2) The difference between wait and waitpid functions

  1. Before a child process terminates, wait causes its caller to block, and waitpid has an option that allows the caller to not block.
  2. waitpid does not wait for the first termination of the child process after its call, he has several options to control the process it is waiting for.
    If a child process has terminated and is a zombie process, wait immediately returns and obtains the status of the child process, otherwise wait causes its caller to block until a child process terminates. If the caller blocks and he has multiple child processes, then the When a child process terminates, wait returns immediately. Because wait returns the process ID of the terminating process, it can always know which process terminated.

The role of the pid parameter in the waitpid function is explained as follows:
pid == -1: waiting for any child process, in this respect, wait and waitpid are equivalent to
pid> 0: waiting for the child process
pid whose process ID is equal to pid == 0: Wait for any child process whose process ID is equal to the calling process group ID
<-1 Wait for any child process whose group I is equal to the absolute value of pid

With the above understanding of wait, we can have the following solutions to the zombie process mentioned at the beginning of this article. When the parent process just starts running, call wait to process the zombie process

int main()
{
	pid_t n = fork();
	assert(-1 != n);

	if(0 == n)
	{
		printf("child start:%d\n",getpid());
		sleep(10);
		printf("child end\n");
	}
	else
	{
		pid_t id = wait(NULL);
		printf("id = %d\n",id);//子进程的pid
		
		printf("father starting\n");
		sleep(20);
		printf("father over\n");

		
	}

	exit(0);
}

The running results are as follows:
Insert picture description here
from the above running results, we can see that the result returned by Wait is the ID of the child process, and there is no zombie process.
But the above liberation method still has some shortcomings. Calling the wait method directly in the parent process will cause the parent process to block and wait for the end of a child process. So we next give another solution-the end of the child process will send a signal SIGCHLD to the parent process, when the parent process receives the SIGCHLD signal, then call the wait method. But which code of the parent process can guarantee to execute after receiving the signal. In the processing here, we will bind a signal processing function to the SIGCHLD signal (this function will only be called after receiving the SIGCHLD signal)

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>

#include<signal.h>

void fun(int sign)
{
	pid_t pid = wait(NULL);
	printf("fun :: pid = %d\n",pid);
}
int main()
{
	signal(SIGCHLD,fun);
	pid_t pid = fork();
	assert(pid != -1);
	if(pid == 0)
	{
		printf("child begin\n");
		sleep(5);
		printf("child over\n");
	}
	else
	{
		printf("father begin\n");
		sleep(10);
		printf("father over\n");
	}
}

The results are as follows:
Insert picture description here
we will see that the parent process will not block the operation and wait for the end of the child process, and it will end immediately after receiving the signal.

Two, process replacement

1. The concept of process replacement

After we talked about the fork function to create a subprocess, the subprocess often calls the exec function to execute another program. When the process calls the exec function, the program executed by the process is completely replaced with a new program, and the new program starts execution from its main function. Because calling exec does not create a new process, the ID has not changed before and after. Exec just replaced the text, data, heap and stack segment of the current process with a brand new program.
We can use the following code to more clearly implement the process replacement. There are two files, one is test.c and one is main.c
main.c is implemented as follows:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>

int main()
{
	pid_t pid = fork();
	assert(pid != -1);

	if(pid == 0)
	{
		printf("i am child: my pid = %d\n",getpid());
		execl("./test","./test","hello","world",(char *)0);

		int i =0;
		for(;i<5;i++)
		{
			printf("i am child\n");
			sleep(1);
		}
	}

	else
	{
		int i = 0;
		for(;i<10;++i)
		{
			printf("i am father\n");
			sleep(1);
		}
	}
	exit(0);	
}

test.c is implemented as follows

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>

int main(int argc,char *argv[])
{
	printf("i am test:%d\n",getpid());
	int i =0;
	for(;i<argc;++i)
	{
		printf("argv[%d] = %s\n",i,argv[i]);
	}
	exit(0);
}

The results of the program run are as follows:
Insert picture description here
we will find that the child process has executed the .test program, but the ID of the child process has not changed, indicating that no new child process has been generated, and the parent process has continued to execute his instructions.

Process replacement also plays a decisive role in our copy-on-write technology.
The operating system maintains a page table for each process, but after the fork, the child process and the parent process share a page table, the kernel sets the access permissions in the page table entry to read-only, and loads the child process when the child process executes execl The new program generates the page table of the child process to map it into the new physical space.

2. exec function

We have six different exec functions available, which are collectively called exec functions. These functions make process control primitives (create new processes with fork, exec can execute new programs, exit processing terminates, wait function waits for termination) more complete. The following picture shows several of our exec functions.
Insert picture description here
Their return values ​​are -1 for errors, and no values ​​for success. For execl, the first parameter is the file path + name of the program; the middle arg is the program; the last parameter expresses the end of the parameter transfer. For execv, the executable file is called as a new process image by path name. The usage of execle and execl are similar. The execve parameter pathname is the path name of the program to be executed.
The five functions in the above figure are all library functions, and there is an execve which is a kernel system call. The relationship between them is shown in the following figure:
Insert picture description here

Third, the signal

1. Signal overview

Signals are certain specific events that are pre-defined by the operating system. Signals can be generated or received. The main body that generates and receives signals is a process . The role is that one process notifies another process of the occurrence of an event. The signal is a software interrupt, and many more important applications need to process the signal. Signals provide a way to handle asynchronous events. For example, when the end user enters the interrupt key, a program will be stopped by the signal mechanism, or the next program in the pipeline will be terminated early.

(1) Conditions for generating signals

  1. When the user presses certain terminal keys, a signal generated by the terminal is triggered. Pressing the DELETE key in the terminal or pressing ctrl + c in many systems usually generates the interrupt signal SIGINT. This is a method to stop the loss of control program.
  2. Signals are generated due to hardware abnormalities. For example: divisor is 0, invalid memory reference
  3. The process calls the kill (2) function to send the signal to another process or process group
  4. Users can use the kill (1) command to send signals to other processes
  5. It also generates a signal when it detects that a certain software condition has occurred and notifies it about the process.

(2) Types of signals
We can check the types of signals through the path /user/include/bits/signum.h. The following figure shows some signals.
Insert picture description here
(3) Response methods of
signals. There are three corresponding ways of signals. Show:
Insert picture description here

  1. Capturing signals: In order to do this, the kernel must be notified to call a user function when a certain signal occurs. In the user function, you can perform the processing that the user wants to deal with such events. For example: if the process creates a temporary file, you may want to write a signal capture function for the SIGTERM signal to clear the temporary file.
  2. Perform system default actions: The following figure shows the system default actions of some signals. Among them, the system default actions of most signals are to terminate the process.
    Insert picture description here
  3. Ignore this signal: Most signals can be processed in this way, but SIGKILL and SIGSTOP cannot be ignored because they provide superusers with a reliable method to terminate or stop the process.

2. Signal application

(1) Modify the response mode of the signal
We can modify the corresponding mode with the following function

typedef void(*sig_Handler,int)
sig_Handler signal(int sig_type,sig_Handler hander);

Where sig_type represents the signal type, and handler is the signal response function.
Practice one : write a program to enter ctrl + c on the keyboard, the current process outputs hello world

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>

#include<signal.h>

void fun(int sign)
{
	printf("hello world: %d\n",sign);
}

int main()
{
	signal(SIGINT,fun);

	while(1)
	{
		sleep(5);
		printf("i am main,running \n");
	}
}

The results of the code run are as follows:
Insert picture description here
Note that this result will continue to use this response until the next modification

Practice two : the process receives ctrl + c for the first time to send a signal to print hello world, and receives the signal for the second time to end the process.
Because the first time we received the fun method we executed, the second time we received it was actually during the execution of the fun method, so the code implementation is as follows:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>

#include<signal.h>
//fun函数会在第一次收到信号时执行,他的执行肯定是在第二次收到信号之前执行的
void fun(int sign)
{
	printf("hello world\n");

	signal(SIGINT,SIG_DFL);//SIG_DFL是默认的信号处理方式
}
int main()
{
	signal(SIGINT,fun);
	
	while(1)
	{
		printf("i am running\n");
		sleep(2);
	}
	exit(0);
}

The results are as follows:
Insert picture description here
(2) Send a signal to a process in the program
We can use kill to achieve, the prototype of kill is int kill (pid_t pid, int sigtype). The first of the two parameters specifies which process to send to, and the latter parameter specifies which signal. If the process sends a signal to itself, you can use kill (getpid (), SIGINT). This method is equivalent to the rasize (SIGINT) method.
The specific code is implemented as follows:


#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>

int main(int argc,char *argv[])
{
	if(argc < 2)
	{
		printf("please input process's pid. again\n");
		exit(0);
	}

	int sigtype = 15;

	if(strncmp(argv[1],"-9",2) == 0)
	{
		sigtype = 9;
	}
	if(strncmp(argv[1],"-stop",5) == 0)
	{
		sigtype = 19;
	}

	int i = 1;
	for(;i<argc;i++)
	{
		if(i == 1 && strncmp(argv[i],"-",1) == 0)
		{
			continue;
		}

		int pid = 0;
		sscanf(argv[i],"%d",&pid);
		if(kill(pid,sigtype) == -1)
		{
			perror("kill error");
		}
	}
}



Published 98 original articles · won praise 9 · views 3640

Guess you like

Origin blog.csdn.net/qq_43412060/article/details/105519701