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)); } } }