[Linux] process control -- process waiting and replacement

Posted by pp4sale on Tue, 22 Feb 2022 15:12:09 +0100

Process waiting

1. Necessity of process waiting

·We have previously introduced the zombie process, that is, when the child process exits, the parent process does not receive the exit status information of the child process. At this time, the child process becomes a zombie process, resulting in memory leakage. Once the process becomes a zombie process, no means can kill the process, because no one can kill a dead process. Therefore, in order to prevent the harm of memory leakage, it is necessary to wait for the process to recover the zombie process.
·Secondly, process waiting also allows the parent process to obtain the running end state of the child process. Of course, this is not necessary. Preventing memory leakage is the main purpose of process waiting.
·Finally, from the coding level, process waiting can ensure that the parent process exits later than the child process, so as to standardize resource recycling.

2. Method of process waiting

We have understood the necessity of process waiting, that is, why process waiting, and how to achieve process waiting? Next, we will introduce two methods of process waiting, namely wait() and waitpid().

wait() method

Firstly, the function of wait function is to wait for any process to exit and obtain the exit status of the process. Note: wait for any process to exit. Therefore, when multiple processes need to wait, you need to wait for all processes to exit through a loop.

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int*status);

The return value of the wait function will return the pid of the waiting child process when the function runs successfully. If the function fails, it will return - 1. The parameter of the wait function is an output parameter, which is used to obtain the exit status 1 state of the child process. We will introduce it later. When you don't care about this parameter, you can set it to NULL.
Let's understand the wait function through a piece of code:

int main()
{
  int n = 3;
  while(n--)
  {
     pid_t id = fork();
     if(id < 0)
     {
        perror("fork");
        exit(-1);
     }
  
     if(id == 0)//child
     {
      int count = 3;
      while(count)
      {
        printf("child.pid:%d,ppid:%d,count:%d\n",getpid(), getppid(), count--);
        sleep(1);
      }
    exit(0);//Exit child process
  }
  }
  //father
  sleep(3);
  int m = 3;
  while(m--)
  {
     pid_t ret = wait(NULL);
     printf("father wait done, pid:%d\n", ret);
     sleep(1);
  }
  return 0;
}

waitpid() method

pid_t waitpid(pid_t pid, int *status, int options);

Return value:

When it returns normally, waitpid returns the process ID of the collected sub process;
If the option WNOHANG is set and waitpid is found in the call that no child process that has exited can be recycled, 0 is returned;
If there is an error in the call, return - 1. At this time, errno will be set to the corresponding value to indicate the error;

Parameters:

pid parameters

pid = -1, wait for any child process. Equivalent to wait.
pid > 0 waits for a child process whose process ID is equal to pid.

status parameter

First, let's take a closer look at the parameter status, 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. Although status is an integer parameter, the upper 16 bits of status are meaningless.
We know that the int type has 32 bits, while for status, only the lower 16 bits are meaningful. The upper 8 bits of the 16 bits identify the exit code information of the sub process, the lower 7 bits identify the information when the process exits abnormally, and the eighth bit is the core dump flag, which will be introduced later.
The exit of a process is divided into normal termination and abnormal exit:

It can be seen that there is no signal of 0 when the process is killed. Therefore, if the lower 7 bits of status are not 0, it indicates that the process is killed by the signal. At this time, 8-15 bits are meaningless. Only when the lower 7 bits are all 0 indicates that the process terminates normally. At this time, the 8-15 bits in the status are the exit code of the process.

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

int main()
{
  pid_t id = fork();
  if(id < 0)
  {
    perror("fork");
    exit(-1);
  }
  if(id == 0)
  {
    //child
    //int *p = 0x12334;
    //*p = 100;
    int count = 5;
    while(count)
    {
      printf("I am child,pid:%d,count:%d\n",getpid(),count--);
      sleep(1);
    }
    printf("child exit\n");
    exit(10);//The exit code is 10
  }
  //father
  int status = 0;//Get the exit code of the child process
  pid_t ret = waitpid(id, &status, 0);//Wait for the child process with pid id to exit
  int sig = status & 0x7F;//Exit signal
  sleep(8);
  if(sig == 0)//The exit signal is 0, and the child process exits normally
  {
    int code = (status >> 8) & 0xFF;//Subprocess exit code
    printf("wait successfully,exit code:%d,pid:%d\n", code, ret);
  }
  else
  {
    //The exit signal is not 0, and the child process is terminated by the signal
    printf("exit unnormally,sig:%d\n", sig);
  }
  return 0;
}


So far, we have learned about the status parameter in detail, but it is somewhat complicated to obtain the exit code and termination information through the status parameter, so we can replace it with a macro:
Disabled (status): true if it is the status returned by the normally terminated child process. (check whether the process exits normally)
WEXITSTATUS(status): if wired is non-zero, extract the subprocess exit code. (check the exit code of the process)

if(WIFEXITED(status))//The exit signal is 0, and the child process exits normally
int code = WEXITSTATUS(status);//Subprocess exit code

options parameter

In the previous code, the third parameter of waitpid() is 0 by default, which represents the blocking state, and the corresponding non blocking state is WNOHANG.

WNOHANG: if the child process specified by pid does not end, the waitpid() function returns 0 and does not wait. If it ends normally, the ID of the child process is returned.
It should be noted that:
·If the child process has exited, when calling wait/waitpid, wait/waitpid will return immediately and release resources to obtain the child process exit information.
·If wait/waitpid is called at any time and the child process exists and runs normally, the process may be blocked.
·If the child process does not exist, an error is returned immediately.
Non blocking polling scheme Code:

int main()
{
  pid_t id = fork();
  if(id < 0)
  {
    perror("fork");
    exit(-1);
  }
  if(id == 0)//child
  {
    int count = 5;
    while(count)
    {
      printf("I am child, pid:%d, count:%d\n", getpid(), count--);
      sleep(1);
    }
    exit(1);
  }
  //father
  int status = 0;
  int ret = 0;
  do
  {
    ret = waitpid(id, &status, WNOHANG);//Non blocking polling scheme
    if(ret == 0)
    {
      printf("child is running\n");
    }
    sleep(1);
  }while(ret == 0);
  if(WIFEXITED(status))
  {
    //Wait for success
    printf("wait successfully, exit code:%d\n",WEXITSTATUS(status));
  }
  else
  {
    printf("wait failed, sig:%d\n",status & 0x7F);
  }
  return 0;
}

Operation results:

Process replacement

1. Principle of process replacement

We know that after fork creates a child process, the child process executes the same program as the parent process. So can a child process execute another program? This is the process replacement we will introduce next. First of all, after the subprocess is created, it will have its own process address space (virtual memory) and map it to the physical memory through the page table. The principle of process replacement is to map the process address space of the subprocess to the physical memory corresponding to the program to be replaced by changing the mapping relationship of the page table. At this time, the program to be executed by the subprocess is replaced.

The subprocess is not replaced by its own subprocess, although the subprocess is created.

2. Method of process replacement

Substitution function

The system provides us with exec series functions for process replacement. Next, we introduce several common exec series 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 execvpe(const char *file, char *const argv[], char *const envp[]);
int execve(const char *filename, char *const argv[], char *const envp[]);

Refer to the manual through the man instruction:

Function interpretation

·If these functions are executed successfully, the program is replaced and the subsequent code is no longer executed.
·If the call is wrong, - 1 is returned.
·Since the return value of successful exec series function calls is meaningless, the function only has the return value of failed calls, but no successful return value.

Naming comprehension

Let's first understand the meaning of letters in the name of exec series functions

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

Let's familiarize ourselves with the above functions through the code:

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

int main()
{
  pid_t id = fork();
  if(id == 0)
  {
    //child
    printf("I am child. pid: %d,ppid:%d\n",getpid(), getppid());
    execl("/usr/bin/ls", "ls", "-l", "-a", NULL);
    exit(1);//Call failed
  }
  //father
  int count = 3;
  while(count)
  {
    sleep(1);
    printf("I am father. pid: %d, count: %d\n", getpid(), count--);
  }
  int status = 0;
  int ret = waitpid(-1, &status, 0);
  if(WIFEXITED(status))//The subprocess exits normally
  {
    int code = WEXITSTATUS(status);//Get subprocess exit code
    printf("wait successfully, exit code: %d, pid: %d\n", code, ret);
  }
  else
  {
    printf("wait failed,sig: %d\n", (status & 0x7F));
  }
  return 0;
}

The execution result of the above code is:

It can be seen that for the execl function, the first parameter is the path of the program to be replaced, followed by the variable parameter list, that is, the commands and options to be executed on the command line. This parameter transfer method is called list parameter transfer, that is, list method; Finally, it needs to end with NULL to tell the exec series of functions to end the parameter transfer.
Let's take another look at the execv function interface:

char* const my_argv[] = { "ls", "-l", "-a", NULL};
execv("/usr/bin/ls", my_argv);

It can be seen that the execv function interface does not pass in parameters in the form of list, but passes in parameters in the form of array (vector). Of course, there is no difference between the final results of the two.
After knowing the two function interfaces of execl and execv, let's take a look at the execlp function interface:

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

We know that p will automatically search the environment variable PATH (only the system command or the command we import into the PATH can be searched), which means that in the first parameter, we do not need to bring the PATH for the instruction to be replaced, while the latter parameter is the same as the parameter of the execl function interface, and the structure of the modified function is the same as that of the first two functions.
After being familiar with the above three functions, it is not difficult for us to use the execvp function interface, that is, to pass parameters in the form of an array without a path:

execvp("ls", my_argv);

Next, let's take a look at the two function interfaces of execle and execve. First, the meaning of the letter e is environment variable, so we know that the parameters to be passed in are environment variables.
The environment variables passed here can be the environment variables defined by ourselves or the environment variables of the system:

//exec_cmd.c
int main()
{
  char* const my_env[] = {"MYENV=helloworld", NULL};//Self defined environment variables or environment variables of the system
  char* const my_argv[] = {"mycmd", NULL};
  //execl("./mycmd", "mycmd", NULL);
  //execle("./mycmd", "mycmd", NULL, my_env);
  execve("./mycmd", my_argv, my_env);//Switch process
  return 0;
}
//cmd.c
int main()
{
  for(int i = 0; i < 10; i++)
  {
    printf("myenv:%s,i:%d\n", getenv("MYENV"), i);    
  }
  return 0;
}

3. Summary

·From the above contents, we know that program replacement is to let a specific process take and load other programs in the disk through the functions of exec series, so as to achieve the purpose of running, and no new process is created during the process.
·If the subprocess has new program requirements, it needs process replacement.
·As long as the exec series functions return, the function call fails.

Topics: Linux Operation & Maintenance server