【Linux】Process control, process replacement

1. Process creation

First introduction to fork function

The fork function is a very important function in Linux. It creates a new process from an existing process. The new process is the child process, and the original process is the parent process.

#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1  

The process calls fork. After control is transferred to the fork code in the kernel, the kernel does:

  • Allocate new memory blocks and kernel data structures to the child process

  • Copy part of the data structure content of the parent process to the child process

  • Add child process to system process list

  • fork returns and starts scheduler scheduling

image-20230907220741961

When a process calls fork, there are two processes with the same binary code. And they all run to the same place. But each process will be able to start their own journey.

Before fork, the parent process executes independently. After fork, the parent and son execution streams execute separately.

Note that after fork, who executes first is completely determined by the scheduler.

fork function return value

The child process returns 0, and
the parent process returns the pid of the child process.

Why does the fork function return 0 to the child process and the PID of the child process to the parent process?

The reason is very simple. A father can have many children, but a child can only have one father. Therefore, a parent process can create many child processes, and a child process can only have one parent process. Therefore, the parent process does not need to be marked, and for the parent process, it may have to manage multiple child processes, so the parent process needs to know the PID of the child process to manage the next operations.

Why does the fork function have two return values?

When the parent process calls the fork function, the fork function performs a series of operations internally, including but not limited to creating the process control block task_struct for the child process, process address space mm_struct, page table and other information processing, that is to say, during fork The child process has been created before the return inside the function, so the return statement of the fork function is executed by the parent process and the child process at the same time. This is why fork has two return values.

image-20230908113131115

copy on write

Usually, the code of the father and the son is shared. When the father and the son are not writing, the data is also shared. When either party tries to write, they each have a copy in the form of copy-on-write. See the figure below for details:

image-20230907221614923

Why does data need to be copied on write?

One of the biggest characteristics of processes is their independence. If data is modified in one process without copy-on-write, it will affect the data of other processes. In order to ensure the independence of each process, copy-on-write is necessary. of

Why not copy the data when creating the child process?

In fact, many times, the child process does not necessarily use all the data of the parent process, and if in some cases the child process simply reads the data of the parent process, there is no need to copy the data. All correct approaches should be to allocate on demand and then allocate when data needs to be modified, so that memory space can be used efficiently.

Will the code be copied on write?

In many cases, there is no need to copy the code. However, if there is no guarantee, for example, when replacing a process, copy-on-write code copying is required.

Common usage of fork

  • A parent process wants to duplicate 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 spawns a child process to handle the request.

  • A process wants to execute a different program. For example, after the child process returns from fork, it calls the exec function.

Reasons why the fork call failed

Usually nothing goes wrong

Unless there are too many processes in the system and
the number of actual user processes exceeds the limit

2. Process termination

Process exit scenario

  • The code is run and the result is correct

  • The code runs and the result is incorrect

  • Code terminates abnormally

Common exit methods for processes

Normal termination (you can check the process exit code through echo $?):

  1. Return from main

  2. call exit

  3. _exit (system call)

quit unexpectedly:

ctrl + c, signal terminated

_exit function

#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值  

Note: Although status is int, only the lower 8 bits can be used by the parent process. So when _exit(-1) is executed in the terminal, echo $?the return value is 255

Note: Exit codes have corresponding string meanings to help users confirm the cause of execution failure. The specific meanings of these exit codes are artificially specified. The string meanings of the same exit code may be different in different environments.

The strerror function in C language can obtain the error information corresponding to the error code in C language through the error code.

We don't often use the method of using the _exit function to exit the process. The _exit function can also exit the process anywhere in the code, but the _exit function will directly terminate the process and will not do any finishing work before exiting the process.

For example, if _exit is used in the following code to terminate the process, the data in the buffer will not be output.

image-20230908125233187

image-20230908125215530

exit function

#include <unistd.h>
void exit(int status);  

In fact, the underlying implementation of exit will eventually call exit, but before calling exit, it also does other work:

  1. Execute user-defined cleanup function.

  2. All open streams are closed and all cached data is written

  3. call_exit

image-20230907224303845

For example, in the following code, exit will output the data in the buffer before terminating the process.

image-20230908125122546

image-20230908125142529

return exit

return is a more common way to exit a process. Executing return n is equivalent to executing exit(n), because the runtime function calling main will use the return value of main as the parameter of exit.

The differences and connections between return, exit and _exit

Only return in the main function can exit the process. Return in the sub-function cannot exit the process. The exit function and _exit function can exit the process when used anywhere in the code.

Executing return num in the main function is equivalent to executing exit(num), because after calling the main function, the return value of the main function will be used as the parameter of exit to call the exit function.

img

3. Process waiting

The necessity of process waiting

The child process exits. If the parent process is left alone, it may cause 僵尸进程problems and cause memory leaks. Moreover, once the process enters 僵尸状态, it will be invulnerable. Even the "kill without blinking" kill -9 can't do anything, because no one can kill it. Kill a dead process.

Moreover, child processes are generally created by the parent process to dispatch tasks, so the parent process needs to know the running results and result status of the child process.

The parent process needs to wait for the process to obtain the running results of the child process and recycle the resources of the child process.

Process waiting method

wait method

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:
    成功返回被等待进程pid,失败返回-1
参数:
    输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

waitpid method

pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
    当正常返回的时候waitpid返回收集到的子进程的进程ID;
    如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0
    如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
    pid:
        Pid=-1,等待任一个子进程。与wait等效。
        Pid>0.等待其进程ID与pid相等的子进程。
status:
    WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
    WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
    WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进
    程的ID。
  • If the child process has exited, when call wait/waitpid, wait/waitpid will return immediately, release resources, and obtain the child process exit information.

  • If wait/waitpid is called at any time and the child process exists and is running normally, the process may be blocked.

  • If the child process does not exist, an error is returned immediately.

image-20230908132937580

Get child process status

The two functions wait and waitpid used by the process to wait have a status parameter, which is an output parameter and is filled in by the operating system.

If you pass NULL for the status parameter, it means that you do 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 through this parameter.

status is an integer variable, but status cannot simply be treated as an integer. Different bits of status represent different information. The specific details are as follows (only the lower 16 bits of status are studied):

image-20230908133104800

//在代码中我们可以这样获取对应的状态码
exitCode = (status >> 8) & 0xFF; //退出码
exitSignal = status & 0x7F;      //退出信号

//对于此,系统当中提供了两个宏来获取退出码和退出信号。
//WIFEXITED(status):用于查看进程是否是正常退出,本质是检查是否收到信号。
///WEXITSTATUS(status):用于获取进程的退出码。
exitNormal = WIFEXITED(status);  //是否正常退出
exitCode = WEXITSTATUS(status);  //获取退出码

However, it should be noted that if a process exits abnormally, it means that the process was killed by a signal, and the corresponding exit code has no meaning. Just look at its exit signal.

//wait函数测试代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
    
    
  pid_t pid=fork();
  if(pid==-1){
    
    
    perror("fork");
    exit(1);
  }else if(pid==0){
    
    
      //子进程
      int cnt=5;
      while(cnt--){
    
    
        printf("我是子进程,我的pid是:%d,我的ppid是%d\n",getpid(),getppid());
        sleep(1);
      }
      exit(2);
  }else{
    
    
      //父进程
      int status;
      int ret=wait(&status);
      if(ret==-1){
    
    
        perror("wait");
        exit(3);
      }else{
    
    
        int exitSignal=status & 0x7F;//退出信号
        int exitCode=(status >>8) & 0xFF; //退出码
        printf("exitSignal:%d exitCode:%d \n",exitSignal,exitCode);
      }
  }
  return 0;
}

image-20230908135532424

We can use the following monitoring script to monitor the process in real time:

while :;do ps -axj | head -1 && ps -axj | grep test | grep -v grep ; echo "#############"; sleep 1;done

image-20230908135602896

Blocking and non-blocking waiting methods

Blocking wait mode of process

After creating a child process, the parent process can use the waitpid function to wait for the child process (in this case, set the third parameter of waitpid to 0) until the child process exits and reads the exit information of the child process.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
     
     
  pid_t pid=fork();
  if(pid==-1){
     
     
    perror("fork");
    exit(-1);
  }
  if(pid==0){
     
     
    //子进程
    int cnt=5;
    while(cnt--){
     
     
      printf("我是子进程,我的pid:%d,我的ppid:%d\n",getpid(),getppid());
      sleep(1);
    }
    exit(1);
  }
  //父进程
  int status;
  int ret=waitpid(pid,&status,0);//阻塞等待
  if(ret==-1){
     
     
      perror("wait");
      return 1;
  }else{
     
     
      printf("wait success,exitSignal:%d exitCode:%d\n",status & 0x7F,(status >>8)& 0xFF);
  }

  return 0;
}
//运行结果
//我是子进程,我的pid:5776,我的ppid:5775
//我是子进程,我的pid:5776,我的ppid:5775
//我是子进程,我的pid:5776,我的ppid:5775
//我是子进程,我的pid:5776,我的ppid:5775
//我是子进程,我的pid:5776,我的ppid:5775
//wait success,exitSignal:0 exitCode:1

Similarly, we can test that while the parent process is running, we can try to use the kill -9 command to kill the child process. At this time, the parent process can also wait for the child process to succeed. (The exit code of a process killed by a signal is meaningless)

image-20230908141336705

Non-blocking waiting method of process :

In the above example, when the child process does not exit, the parent process is waiting for the child process to exit. During the waiting period, the parent process cannot do anything. This kind of waiting is called blocking waiting.

In fact, we can ask the parent process not to wait for the child process to exit. Instead, the parent process can do some things of its own when the child process does not exit. When the child process exits, the child process's exit information is read, that is, non-blocking waiting.

The method is very simple. Pass WNOHANG to the third parameter potions of the waitpid function. In this way, if the waiting child process does not end, the waitpid function will directly return 0 and will not wait. If the waiting child process ends normally, the pid of the child process is returned.

The parent process can call the waitpid function every once in a while. If the waiting child process has not exited, the parent process can do some other things first, and then call the waitpid function after a while to read the exit information of the child process.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(){
     
     
  pid_t pid=fork();
  if(pid==-1){
     
     
    perror("fork");
    exit(-1);
  }
  if(pid==0){
     
     
    //子进程
    int cnt=5;
    while(cnt--){
     
     
      printf("我是子进程,我的pid:%d,我的ppid:%d\n",getpid(),getppid());
      sleep(1);
    }
    exit(1);
  }
  //父进程
  int status;
  while(1){
     
     
       int ret=waitpid(pid,&status,WNOHANG);//非阻塞等待
      if(ret==-1){
     
     
          perror("wait");
          return 1;
      }else if(ret==0){
     
     
          printf("非阻塞等待中.....正在执行其他任务.......\n");
          sleep(1);
      }else {
     
     
          printf("wait success,exitSignal:%d exitCode:%d\n",status & 0x7F,(status >>8)& 0xFF);
          break;
      }
  }
  return 0;
}

image-20230908142407772

4. Process program replacement

replacement principle

The child process created with fork often executes the same program as the parent process. Although it is possible to execute different codes, if you want the child process to execute another program, you often need to call the exec function.

When a process calls an exec function, the user space code and data of the process are completely replaced by the new program, and execution starts from the new program's startup routine. Calling exec does not create a new process, so the ID of the process does not change before and after calling exec.

After the child process replaces the process program, will it affect the code and data of the parent process?

Obviously not, because we have said that one of the biggest characteristics of the process is independence. If one of the processes modifies data, copy-on-write will be performed.

replacement function

In fact, there are six functions starting with exec, collectively called 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[]);
  • 一、int execl(const char *path, const char *arg, …);

The first parameter is the path to the program to be executed, and the second parameter is a variable parameter list, indicating how you want to execute the program, and ends with NULL.

For example, the ls program is to be executed.

execl("usr/bin/ls","ls","-a","-l",NULL);

image-20230908153527688

image-20230908153551745

It turns out that after the exec function is successfully executed, all the code under the exec function will not be executed concurrently, because all code data has been replaced after the exec function is executed.

  • 二、int execlp(const char *file, const char *arg, …);

The first parameter is the name of the program to be executed, and the second parameter is a variable parameter list, indicating how you want to execute the program, and ends with NULL.

For example, the ls program is to be executed.

execlp("ls","ls","-a","-l",NULL)

image-20230908153839250

image-20230908153825240

  • 三、int execle(const char *path, const char *arg, …, char *const envp[]);

The first parameter is the path to the program to be executed, the second parameter is a variable parameter list, indicating how you want to execute the program, and ends with NULL, and the third parameter is the environment variable you set yourself.

For example, if you set the MYVAL environment variable, you can use the environment variable inside the mycmd program.

char* myenvp[] = {
    
     "MYVAL=2023", NULL };
execle("./mycmd", "mycmd", NULL, myenvp);
  • 四、int execv(const char *path, char *const argv[]);

The first parameter is the path to execute the program, and the second parameter is an array of pointers. The contents of the array indicate how you want to execute the program. The array is terminated by NULL.

For example, the ls program is to be executed.

char* myargv[] = {
    
     "ls", "-a", "-l", NULL };
execv("/usr/bin/ls", myargv);
  • 五、int execvp(const char *file, char *const argv[]);

The first parameter is the name of the program to be executed, and the second parameter is an array of pointers. The contents of the array indicate how you want to execute the program. The array is terminated by NULL.

For example, the ls program is to be executed.

char* myargv[] = {
    
     "ls", "-a", "-l", NULL };
execvp("ls", myargv)
  • 六、int execve(const char *path, char *const argv[], char *const envp[]);

The first parameter is the path to execute the program, the second parameter is an array of pointers, the contents of the array indicate how you want to execute the program, the array ends with NULL, and the third parameter is the environment variable you set yourself.

For example, if you set the MYVAL environment variable, you can use the environment variable inside the mycmd program.

char* myargv[] = {
    
     "mycmd", NULL };
char* myenvp[] = {
    
     "MYVAL=2023", NULL };
execve("./mycmd", myargv, myenvp);

In fact, there is another function called execve system call

#include <unistd.h>
int execve(const char *filename, char *const argv[],
                  char *const envp[]);

In fact, exec is a family of functions, including execle, execlp, execvp, execv, and execl, but their specific implementation is based on calling execve(). The difference lies in the specification of path names, parameters and environment variables. The following distinguishes these functions from these three aspects:

image-20230908152737084

image-20230908155430586

Naming comprehension

These function prototypes seem easy to confuse, but they are easy to remember as long as you master the rules.

  • l(list): indicates that the parameters are in a list

  • v(vector): Array for parameters

  • p(path): Automatically search the environment variable PATH with p

  • e(env): Indicates that you maintain environment variables yourself

Function explanation

  • If these functions are called successfully, the specified program will be loaded and executed from the startup code without returning.
  • If the call fails, -1 is returned.

In other words, as long as the exec series function returns, it means that the call failed.

Make a simple version of the command line interpreter shell

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>

#define LEN 1024 //命令的最大的长度
#define NUM 32 //命令拆分后的最大个数
int main()
{
    
    
  char cmd[LEN];
  char* myargv[NUM];
  const char* user=getenv("USER");//获取用户名
  const char* path=getenv("PWD");//获取当前路径
  while(1)
  {
    
    
    printf("[%s@ %s]# ",user,path);
    fgets(cmd,LEN,stdin);
    cmd[strlen(cmd)-1]='\0';
    //分割解析命令
    int i=0;
    myargv[i++]=strtok(cmd," ");
    while(myargv[i]=strtok(NULL," ")) i++;
    if(fork()==0)
    {
    
    

      //子进程执行替换任务
      execvp(myargv[0],myargv);
      exit(1);
    }
    int status;
    int ret=waitpid(-1,&status,0);//回收资源
    if(ret==-1){
    
    
      perror("wait");
    }else{
    
    
      printf("exit code:%d\n", (status>>8)& 0xFF);
    }
  }
  return 0;
}

image-20230908163925964

Guess you like

Origin blog.csdn.net/dongming8886/article/details/132878102