Linux Process Control

1, Process creation

Fork function is a very important function in Linux. It creates a new process from an existing process. The new process is a child process and the original process is a parent process. The initial use of fork function has been given in the concept of Linux process. Now let's have a deeper understanding

#include <unistd.h>
pid_t fork(void);
// Return value: 0 is returned from the process, the parent process returns the child process id, and - 1 is returned in case of error

(1) . why return 0 to the child process and return the pid of the child process to the parent process

The parent process can have multiple child processes, and the child process has only one parent process, which also means that the parent process needs to get the identification of the child process to find the child process, while the child process does not need the identification to find the parent process (because the child process has only one parent process); The parent process assigns tasks to multiple child processes for execution. The parent process needs to know the pid of each process to know which task each process is executing (return the pid of the child process to the parent process), while the child process does not care about the parent process (return 0 to the child process)

(2) How to understand that fork has two return values?

The process calls fork. When the control is transferred to the code in fork in the kernel, the kernel does the following things:
1) . create task_struct,mm_struct, page table and other kernel data structures
2) . copy the code and data of the parent process to the child process
3) . add the sub process to the process list of the system and the run queue of the cpu

We need to understand that the subprocess is not created after fork. In fact, when executing the fork code in the kernel, the subprocess has been created after completing the above things, rather than waiting for the fork code to execute before creating the subprocess, because there are two execution streams before the fork code is executed, so there will be two return values in the end

Copy on write

Usually, the parent-child code is shared. When the parent-child does not write again, the data is also shared. When either party tries to write, they will copy a copy on write. See the figure below for details

Sharing: the page table corresponding to the parent-child process points to the same physical memory

(1) . why copy on write?

Because the process is independent

(2) Why not separate the parent-child process data when creating the child process?

a. Because the child process does not necessarily use all the data of the parent process, and the child process does not necessarily write to the data (it may only read)

b. Delay allocation: when a child process is just created, it is not necessarily scheduled immediately. When the process is scheduled, it can allocate physical memory space to maximize the utilization of physical memory space

(3) Will the code be copied on write? 90% won't, but that doesn't mean you can't

fork general usage

(1) A parent process wants to copy itself so that the parent and child processes execute different code segments at the same time. For example, a parent process waits for a client request and generates a child process to process the request.

(2) A process performs a different procedure. For example, when the child process returns from fork, the exec function is called. bash process creates subprocesses, which execute programs

Why the fork call failed

(1) There are too many processes in the system

(2) . the number of processes for the actual user exceeded the limit

2, Process termination

Process exit scenario

We mentioned echo $? In the Linux Process concept? Command, which prints the exit code of the last process

(1) . the code runs and the result is correct

(2) . the code has finished running and the result is incorrect

(3) . code terminated abnormally

#include<stdio.h>
int main()
{
	int a = 1 / 0;
	printf("%d\n",a);
	
	// int* p;
	// *p = 100;
}

There is only one case when the exit code is 0: the code is run and the result is correct
A non-zero exit code indicates that there is a problem with the program. There are many possible causes of the problem

We can use the strerror function to view the information corresponding to the exit code. Each exit code has corresponding information to help users confirm the cause of task failure

//test.c content

#include<stdio.h>
#include<string.h>
int main()
{
        for(int i = 0;i <= 133;i++) // A total of 134 exit codes
        {
                printf("%d : %s\n",i,strerror(i));
        }
        return 0;
}

The relevant information corresponding to the exit code is shown in the figure below:

Common exit methods of process

Normal termination (you can view the process exit code through echo $):
(1) . return from main (only return of main function can end the process)
(2) . call exit
(3). _exit

Exit and_ The difference between exit: exit will release the resources once occupied by the process (for example, buffer)_ Exit directly terminates the process and does not do any closing work

#include<stdio.h>
#include<string.h>
#include<unistd.h>
int main()
{
        printf("hello world!");
        sleep(3);
        exit(10);
        printf("\n.......................................\n");
        sleep(3);
}

Phenomenon: sleep for 3 seconds before printing Hello world, The reason is hello world! It is cached in the output buffer. Because the line buffer is used, it is not encountered. At first, it is not refreshed. After sleeping for 3 seconds, execute exit(10), and the process exits to refresh the contents of the output buffer

The same code, if you replace exit(10) with_ exit(10). The phenomenon is that after sleeping for 3 seconds, the process exits and nothing is output. The reason is execution_ exit(10), the process exits and the contents of the buffer will not be refreshed

Abnormal exit: essentially, an error occurs when the process is running, resulting in the process receiving a signal
(1). ctrl + c, signal
(2).
//test.c content

#include<stdio.h>
int main()
{
	int a = 1 / 0;
	printf("%d\n",a);
	
	// int* p;
	// *p = 100;
}

The exit code is 136, and the maximum exit code is 133. Therefore, we can see that when the process exits abnormally, its exit code is meaningless

What does the operating system do when the process terminates?
Release the previously applied data structure, release the previously applied memory, and remove it from various queues and other data structures

3, Process wait

Process wait necessity

As mentioned earlier, if the child process exits and the parent process is ignored, it may cause the problem of "zombie process" and then cause memory leakage. In addition, once the process becomes a zombie state, it will be invulnerable, and the kill-9 of "killing without blinking an eye" can do nothing, because no one can do anything
Kill a dead process. Finally, we need to know how the tasks assigned by the parent process to the child process are completed. For example, when the subprocess runs, whether the result is right or wrong, or whether it exits normally.

The parent process reclaims the child process resources and obtains the child process exit information by waiting for the process

Method of process waiting

wait method

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
Return value:
 Successfully returned to the waiting process pid,Failure Return-1. 
Parameters:
 Output type parameter to obtain the exit status of the child process,If you don't care, you can set it as NULL

//test.c content

#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
        pid_t id = fork();
        if(id == 0)
        {
                int count = 0;
                while(count < 10)
                {
                        printf("I am a child,pid : %d,ppid : %d\n",getpid(),getppid());
                        count++;
                        sleep(1);
                }
                exit(0);
        }
        else
        {
                printf("I am a father,pid : %d,ppid : %d\n",getpid(),getppid());
                pid_t ret = wait(NULL);
                if(ret >= 0)
                {
                        printf("wait child success!,%d\n",ret);
                }
                printf("father is run.............\n");
                sleep(10);
        }
}

(1) When the parent process waits while the child process is running, it is waiting for the child process to exit. This state is called blocking and waiting
(2) . it is uncertain who runs the parent-child process first, but after the wait, in most cases, the child process exits first. The parent process reads the child process exit information before the parent process exits
(3) . the success of the parent process waiting (only means that the child process exits), does not mean that the child process runs successfully

waitpid method

pid_ t waitpid(pid_t pid, int *status, int options);
Return value:
 When returning normally waitpid Returns the process of the collected child process ID;
 If options are set WNOHANG,While calling waitpid No exited child processes were found to collect,Returns 0;
 If an error occurs in the call,Then return-1,At this time errno Will be set to the appropriate value to indicate the error;
Parameters:
 pid: 
 Pid=-1,Wait for any child process. And wait Equivalent.
 Pid>0.Wait for its process ID And pid Equal child processes.
 status:
 WIFEXITED(status): True if it is the status returned by the normally terminated child process. (check whether the process exits normally)
 WEXITSTATUS(status): if WIFEXITED Non zero, extract the subprocess exit code. (view the exit code of the process)
 options:
 WNOHANG: if pid If the specified child process does not end, then waitpid()The function returns 0 without waiting. If it ends normally, it will return to this child
 Cheng ID. 

(1) . if the child process has exited, when calling wait/waitpid, wait/waitpid will return immediately, release resources and obtain the child process exit information.

(2) . if wait/waitpid is called at any time, and the child process exists and runs normally, the parent process may block.

(3) . if the child process does not exist, an error is returned immediately.

Get child process status

Both wait and waitpid have a status parameter, which is an output parameter filled by the operating system. If NULL is passed, it indicates that it does not care about the exit status information of the child process. Otherwise, the operating system will feed back the exit information of the child process to the parent process according to this parameter.

Status can not be simply treated as a shape, but as a bitmap. The specific details are shown in the figure below (only the lower 16 bits of status are studied)
Bits):

The lower 7 bits represent the exit signal when the process exits, and the lower 8 bits represent the exit code when the process exits

//test.c content

#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
        pid_t id = fork();
        if(id == 0)
        {
                int count = 0;
                while(count < 10)
                {
                        printf("I am a child,pid : %d,ppid : %d\n",getpid(),getppid());
                        //if(count == 10)
                        //{
                        //	int a = 1 / 0; //  Floating point number error (SIGFPE) upon receiving signal No. 8
                        //	int *p;
                        //	*p = 100;     //  Received signal 11, segment error
                        //}
                        count++;
                        sleep(1);
                }
                exit(10);
        }
        else
        {
                printf("I am a father,pid : %d,ppid : %d\n",getpid(),getppid());
                int status = 0;
                pid_t ret = waitpid(id,&status,0);
                if(ret >= 0)
                {
                        printf("wait child success!,%d\n",ret);
                        printf("status : %d\n",status);
                        printf("child get signal : %d\n",status & 0x7F);
                        printf("child exit code : %d\n",(status >> 8) & 0xFF);
                }
                printf("father is run.............\n");
                sleep(10);
        }
}

Normal exit

Abnormal exit

We can also get the signal and exit code without bit operation, and we can use macros

 WIFEXITED(status): True if it is the status returned by the normally terminated child process. (check whether the process exits normally)
 // The essence is to check the signal. If the signal is 0, it indicates normal termination, and if the signal is not 0, it indicates abnormal termination
 WEXITSTATUS(status): if WIFEXITED Non zero, extract the subprocess exit code. (view the exit code of the process)
 // If it is terminated normally, the exit code can be obtained
if(WIFEXITED(status)
{
	printf("child exit code : %d\n",WEXITSTATUS(status));
}
else
{
	printf("child not exit normal!\n");
}

Create multiple child processes, and the parent process waits

#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
        pid_t ids[10];
        for(int i = 0;i < 10;i++)
        {
                pid_t id = fork();
                if(id == 0)
                {
                        int count = 0;
                        while(count < 10)
                        {
                                printf("I am a child, pid : %d,ppid : %d\n",getpid(),getppid());
                                sleep(1);
                                count++;
                        }
                        exit(i);
                }
                ids[i] = id;
        }

        int count = 0;
        while(count < 10)
        {
                int status = 0;
                pid_t ret = waitpid(ids[count],&status,0);
                if(ret >= 0)
                {
                		printf("wait child success!,%d\n",ret);
                		printf("status : %d\n",status);
                	    if(WIFEXITED(status))
                	    {
                	    	printf("child exit normal!\n");
                        	printf("child exit code : %d\n",WEXITSTATUS(status));
                	    }
                	    else
                	    {
                	    	printf("child not exit normal!\n");
                	    	printf("child get signal : %d \n",status & 0x7F);
                	    }
                }
                count++;
        }
}

Non blocking wait

To realize non blocking waiting, the third parameter of waitpid needs to be set to WNOHANG. The difference between non blocking waiting and blocking waiting is that non blocking waiting will not die and other child processes exit. When it is detected that the child process has not exited, it returns 0, and then the parent process can do other things first, and then detect whether the child process exits after a period of time

//test.c content

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
        pid_t id = fork();
        if(id == 0)
        {
                int count = 0;
                while(count < 10)
                {
                        printf("child,pid : %d,ppid : %d\n",getpid(),getppid());
                        sleep(3);
                        count++;
                }
                exit(1);
        }
        while(1)
        {
                int status = 0;
                pid_t ret = waitpid(id,&status,WNOHANG);
                if(ret > 0)
                {
                        if(WIFEXITED(status))
                        {
                                printf("child exit normal!\n");
                                printf("child exit code : %d\n",WEXITSTATUS(status));
                        }
                        else
                        {
                                printf("child not exit normal!\n");
                                printf("child get signal : %d\n",status & 0x7F);
                        }
                        break;
                }
                else if(ret == 0)
                {
                        printf("father do other things!\n");
                        sleep(1);
                }
                else
                {
                        printf("waitpid error\n");
                        break;
                }
        }
        exit(0);
}

Process program replacement

Replacement principle

After creating a child process with fork, it executes the same program as the parent process (but it may execute different code branches). The child process often calls an exec function to execute another program. When a process calls an exec function, the user space code and data of the process are completely replaced by the new program and executed from the start routine of the new program. Calling exec does not create a new process, so the pid of the process does not change before and after calling exec (only the previous code and data are replaced with the new code and data in the disk)

(1) In our program, we can use the execl function to stop our program from executing and execute other programs (load the programs in the disk into memory)
(2) . process program replacement. Once replaced, it will not return, and subsequent codes will not be executed
(3) . if the program replacement fails, it will return and the subsequent programs will not be affected

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
        pid_t id = fork();
        if(id == 0)
        {
                printf("I am a child process!\n");
                sleep(3);
                // The underlying technology adopted by the loader under Linux makes our program no longer execute, but execute other programs
                execl("/usr/bin/ls","ls","-l","-a",NULL);
                exit(10);
        }

        int status = 0;
        pid_t ret = waitpid(id,&status,0);
        if(ret > 0)
        {
                if(WIFEXITED(status))
                {
                        printf("child exit normal!\n");
                        printf("child exit code : %d\n",WEXITSTATUS(status));
                }
                else
                {
                        printf("child not exit normal!\n");
                        printf("child get signal : %d\n",status & 0x7F);
                }
        }
        else
        {
                printf("waitpid error!\n");
        }
}

Substitution function

In fact, there are six functions starting with exec, collectively referred to as exec functions:

#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);

l(list) : Indicates the parameter adoption list
v(vector) : Array for parameters
p(path) : yes p Auto search environment variables PATH
e(env) : Indicates that you maintain environment variables

The first five functions actually call execve system calls at the bottom. It can be understood that the first five functions encapsulate execve system calls

int execve(const char *path, char *const argv[], char *const envp[]);

// use
execl("/usr/bin/ls","ls","-a","-l","-i",NULL);
execlp("ls","ls","-a","-l","-i",NULL);
char* myargv[] = {"ls","-a","-l","-i",NULL};
execv("/usr/bin/ls",myargv);
execvp("ls",myargv);
char* myenvp[] = {"MYVAL=hello world!",NULL};
execle("./test2","./test2",NULL,myenvp);
execve("/usr/bin/ls",myargv,myenvp);

// example
#include <unistd.h>
int main()
{
 	char *const argv[] = {"ps", "-ef", NULL};
 	char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
 	execl("/bin/ps", "ps", "-ef", NULL);
 	// With p, you can use the environment variable PATH without writing the full PATH
 	execlp("ps", "ps", "-ef", NULL);
 	// With e, you need to assemble your own environment variables
 	execle("ps", "ps", "-ef", NULL, envp);
 	execv("/bin/ps", argv);
 
 	// With p, you can use the environment variable PATH without writing the full PATH
 	execvp("ps", argv);
 	// With e, you need to assemble your own environment variables
	execve("/bin/ps", argv, envp);
 	exit(0);
}

(1) . if these functions are called successfully, a new program will be loaded and executed from the startup code without returning.
(2) - 1 if the call fails
(3) . therefore, the exec function only has an error return value and no successful return value.
(4) The. Exec function can also call our own program (refer to the following code)
(5) The environment variables derived from the. Execle function are overwritten. The environment variables imported by yourself will overwrite the default environment variables

// Makefile
.PHONY:all
all:test2 test

test2:test2.c
	gcc -o $@ $^
test : test.c
	gcc -o $@ $^

.PHONY:clean
clean:
	rm -f test2 test

// test2.c
#include<stdio.h>
int main()
{
	printf("I am a process!\n");
	// Using the execle function, you can compare the changes before and after importing environment variables using the execle function
	//printf("my env : %s\n",getenv("MYVAL"));
	//printf("OS env : %s\n",getenv("PATH"));
}

// test.c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
        pid_t id = fork();
        if(id == 0)
        {
                printf("I am a child process!\n");
                sleep(3);
                execl("./test2","./test2",NULL);
                // Use of execle
                //char* myenvp[] = {"MYVAL=hello world!",NULL};
                //execle("./test2","./test2",NULL,myenvp);
                exit(10);
        }

        int status = 0;
        pid_t ret = waitpid(id,&status,0);
        if(ret > 0)
        {
                if(WIFEXITED(status))
                {
                        printf("child exit normal!\n");
                        printf("child exit code : %d\n",WEXITSTATUS(status));
                }
                else
                {
                        printf("child not exit normal!\n");
                        printf("child get signal : %d\n",status & 0x7F);
                }
        }
        else
        {
                printf("waitpid error!\n");
        }
}               

Implementation of simple shell

When the system starts and the user logs in, the bash program will be automatically called to run and become a process. When executing ls, PWD and other instructions, a sub process will be created to complete the task

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>
#define LEN 1024
#define NUM 32
int main()
{
        char cmd[LEN];
        char* argv[NUM];
        while(1)
        {
                printf("[luanyiping@bogon shell]$");
                // Read instructions from standard input into cmd
                fgets(cmd,LEN,stdin);
                // Change the last '\ n' to '\ 0'
                cmd[strlen(cmd) - 1] = '\0';
                // Split instruction with space as separator
                argv[0] = strtok(cmd," ");
                int i = 1;
                while(argv[i] != strtok(NULL," "))
                {
                      i++;
                }
				// Create sub processes to complete the task
                pid_t id = fork();
                if(id == 0)
                {
                		// Process program replacement
                        execvp(argv[0],argv);
                        exit(10);
                }
				// Blocking wait for child process to exit
                int status = 0;
                pid_t ret = waitpid(id,&status,0);
                if(ret > 0)
                {
                        printf("child exit code : %d\n",WEXITSTATUS(status));
                }
        }
}                                            

Tags: Linux

Posted on Fri, 01 Oct 2021 21:16:32 -0400 by mysql_query