【Linux】---进程控制(创建、终止、等待、替换)

文章目录

  • 进程创建
    • fork()
  • 进程退出
    • 进程退出场景
    • 进程退出方法
      • 退出码
      • exit、_exit
  • 进程等待
    • 进程等待的方法
    • wait
    • waitpid
    • 阻塞和非阻塞
  • 进程替换
    • 替换的原理
    • 替换所用到的函数
      • execl
      • execlp
      • execle
  • 简易的shell

进程创建

fork()

fork函数在之前的文章中也已经提到过了。其主要作用是从已存在的进程中创建一个新的进程,也就是新建的进程为子进程,原进程为父进程

当一个进程调用fork函数后,内核会做几件事:

  1. 分配新的内存块和内核数据结构给子进程
  2. 将父进程部分数据结构内容拷贝给子进程
  3. 添加子进程到系统进程列表中
  4. fork返回后,开始调度器调度

下面来看看进程创建的一段简单代码

#include
#include
#include

int main(){
    pid_t id = fork();
    
    if(id == 0){
        printf("I am child process, pid = %d, ppid = %d\n", getpid(), getppid());
    }
    
    printf("I am parent process, pid = %d, ppid = %d\n", getpid(), getppid());
    return 0;
}

image-20221128143753830

当其返回值为0时,说明创建出了子进程。

进程退出

进程退出场景

进程退出总共会有三种情况,也就是我们平常写代码执行的时候也是会遇到这三种情况:

  1. 代码运行完毕,结果正确
  2. 代码运行完毕,结果不正确
  3. 代码遇到异常终止执行

进程退出方法

对于进程退出而言,可以有两种方法退出。一种就是正常的程序运行完毕终止执行,另一种就是程序遇到异常信号终止运行。

那么现在有一个问题,我们平常写代码的时候为什么总是会带上一个 return 0 呢?这里就涉及到一个知识点—退出码

退出码

其实return 0这个0并没有什么特殊的意思,返回的是0就代表着程序执行正常退出,非0就是程序有错误,每一个非0的退出码都代表着不同的错误信息。可以通过程序看看

#include
#include

int main(){
	for(int i = 0; i < 20; i++)
		printf("%d: %s\n", i, strerror(i));
	
	return 0;
}

【Linux】---进程控制(创建、终止、等待、替换)_第1张图片

exit、_exit

那么除了return可以返回退出码退出程序外,exit和**_exit**也是可以的。不过这两者之间还是会有所区别的。

  1. exit是库函数,_exit是系统调用
  2. exit会刷新缓冲区,_exit不会

进程等待

进程等待是非常重要的。之前在进程状态里面谈到了一种状态—僵尸状态。这种状态是非常危险的,会造成内存泄漏。并且一旦进程变成了僵尸状态,那么即使使用kill -9都无法将其杀死。

所以父进程想要获取子进程的任务完成的程度如何,就必须通过进程等待的方式,回收子进程资源,获取子进程退出信息

进程等待的方法

进程等待有两种方法:1、阻塞等待;2、非阻塞等待。可以使用两个函数去实现:wait和waitpid

【Linux】---进程控制(创建、终止、等待、替换)_第2张图片

wait

wait等待成功会返回被等待的进程的pid,失败则返回-1,下面来一段代码感受一下

#include
#include
#include
#include
#include
#include

int main(){
  pid_t id = fork();

  if(id == 0){
    int cnt = 5;
    while(cnt--){
      printf("I am child process, pid = %d\n", getpid());
      sleep(1);
    }
    exit(1);
  }

  pid_t ret = wait(NULL);
  printf("%d\n", ret);

  return 0;
}

【Linux】---进程控制(创建、终止、等待、替换)_第3张图片

可以看到进程最后打印出的是子进程的pid,说明等待成功了。

waitpid

waitpid 相对于 wait 来说能够获取的信息就更多了,可以获取子进程的退出码和子进程返回的状态。

如果子进程是正常终止,那么返回的状态为0,如果收到了异常信号终止则非0

但是这里还要注意的是,waitpid 返回的子进程的数据是有自己的存储方式的。例如 waitpid 返回了一个变量 status 那么这个变量的**高八位为退出状态,低八位为终止信号。

【Linux】---进程控制(创建、终止、等待、替换)_第4张图片

如果进程是被信号所杀,则退出状态就没有用到,终止信号根据实际。如果进程正常终止,则退出状态根据实际,终止信号为0.

所以当waitpid 返回了一个值,我们想要获取终止信号就得用这个值 & 0x7f;获取退出状态就得用这个变量 向右移动8位再 & 0xff

来一段代码感受一下

#include
#include
#include
#include
#include
#include

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

  if(id == 0){
    int cnt = 5;
    while(cnt){
      printf("child running pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
      sleep(1);
    }

    exit(0);
  }

  int status = 0;
  while(1){
    pid_t ret = waitpid(id, &status, 0);
    if(ret == 0){
      //子进程没有退出,waitpid没有等待失败,仅仅是检测到子进程没退出
      //waitpid调用成功 && 子进程没有退出
      printf("wait done, but child is running....., parent running other things\n");
    }
        
    else if(ret > 0){
      //waitpid调用成功 && 子进程已退出
      printf("wait success, exit code: %d, sig: %d\n", (status >> 8) & 0xff, (status & 0x7f));
      break;
    }

    else{
      //waitpid调用失败
      printf("waitpid call failed\n");
      break;
    }

    sleep(1);
  }

  return 0;
}

【Linux】---进程控制(创建、终止、等待、替换)_第5张图片

下面再来看看当子进程收到异常信号时退出 waitpid 返回的结果。现在假设一个野指针的情况

#include
#include
#include
#include
#include
#include

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

  if(id == 0){
    int cnt = 5;
    while(cnt){
      printf("child running pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
      sleep(1);
    }
	int* p;
	*p = 100;
	
    exit(0);
  }

  int status = 0;
  while(1){
    pid_t ret = waitpid(id, &status, 0);
    if(ret == 0){
      //子进程没有退出,waitpid没有等待失败,仅仅是检测到子进程没退出
      //waitpid调用成功 && 子进程没有退出
      printf("wait done, but child is running....., parent running other things\n");
    }
        
    else if(ret > 0){
      //waitpid调用成功 && 子进程已退出
      printf("wait success, exit code: %d, sig: %d\n", (status >> 8) & 0xff, (status & 0x7f));
      break;
    }

    else{
      //waitpid调用失败
      printf("waitpid call failed\n");
      break;
    }

    sleep(1);
  }

  return 0;
}

【Linux】---进程控制(创建、终止、等待、替换)_第6张图片

可以看到,此时waitpid接收到了异常信号退出并返回异常信号的值,可以通过kill -9查看对应的异常信息

阻塞和非阻塞

上面提到了等待可以分为阻塞等待和非阻塞等待,那么这两种有什么区别呢。

通俗点理解

阻塞等待就是父进程在等待子进程退出时并不会再去做其他的事情

非阻塞等待则是父进程在等待时如果他检测到子进程还没有退出,那它就退出检测去做自己的事情。做完自己的事情后又会检测,直至检测到子进程退出

在 waitpid 中可以通过传入 WNOHANG 表示非阻塞等待,传入0则表示阻塞等待。具体来看一段代码感受一下

#include
#include
#include
#include
#include
#include

#define N 10
typedef void (*func_t)();//函数指针
func_t handlerTask[N];

void task1(){
  printf("task1\n");
}

void task2(){
  printf("task2\n");
}

void task3(){
  printf("task3\n");
}

void loadTask(){
  memset(handlerTask, 0, sizeof(handlerTask));
  handlerTask[0] = task1;
  handlerTask[1] = task2;
  handlerTask[2] = task3;
}

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

  if(id == 0){
    int cnt = 5;
    while(cnt){
      printf("child running pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
      sleep(1);
    }

    exit(0);
  }

  loadTask();

  int status = 0;
  while(1){
    pid_t ret = waitpid(id, &status, WNOHANG);//WNOHANG: 非阻塞-> 子进程没有退出,父进程检测时立即退出
    if(ret == 0){
      //子进程没有退出,waitpid没有等待失败,仅仅是检测到子进程没退出
      //waitpid调用成功 && 子进程没有退出
      printf("wait done, but child is running....., parent running other things\n");
      for(int i = 0; handlerTask[i] != NULL; i++)
        handlerTask[i]();

    }

    else if(ret > 0){
      //waitpid调用成功 && 子进程已退出
      printf("wait success, exit code: %d, sig: %d\n", (status >> 8) & 0xff, (status & 0x7f));
      break;
    }

    else{
      //waitpid调用失败
      printf("waitpid call failed\n");
      break;
    }

    sleep(1);
  }

  return 0;
}

【Linux】---进程控制(创建、终止、等待、替换)_第7张图片

可以看到父进程在等待的同时还会去执行指定的任务。等父进程检测到子进程还没有退出时,它就会退出等待去做他的事情。

进程替换

替换的原理

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数 以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动 例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

简单理解:替换只是把代码和数据换掉而已,并没有换进程去执行。我们可以通过替换去用A程序执行B程序。

替换所用到的函数

一般来说 实现替换有六种函数选择

#include `
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 execve(const char *path, char *const argv[], char *const envp[]);

这六个函数各有不同的参数,因此实现的方法不同,但是都是为了替换。

execl

#include
#include
#include
#include
#include
#include


int main(){ 
    printf("process running ...\n");

    //.c -> exe -> load -> process -> 运行 -> 执行所写代码
    printf("process is running ....\n");

    //load -> exe
    //第一个参数是告诉系统要执行谁,第二个是要怎么执行
    execl("/usr/bin/ls", "ls", NULL);// all exec* end of NULL

    printf("process running done...\n");

    return 0;
}

execl需要传入替换程序的地址。

image-20221128162635610

替换完成后,当我们执行程序就会有替换程序的效果了。要注意:当execl调用成功后,之后的语句就不再执行了。还可以在execl里面传入选项,已完成更全面的功能实现

 execl("/usr/bin/ls", "ls", "-a", "-l", NULL);// all exec* end of NULL

execlp

execlp相较于execl是不需要传入地址的,它会自动在环境变量里找。只需要传入需要替换的程序即可

#include
#include
#include
#include
#include
#include


int main(){ 
    printf("process running ...\n");

    //.c -> exe -> load -> process -> 运行 -> 执行所写代码
    printf("process is running ....\n");

    //load -> exe
    execlp("ls", "ls", "-a", "-l", "--color=auto", NULL);

    printf("process running done...\n");

    return 0;
}

image-20221128163246360

execle

exec* 的函数不仅可以替换系统中的程序,也可以替换我们自己的程序。现在我写一个程序,程序的主要功能是打印环境变量。然后我再用另一个程序替换

mybin.c

#include
#include

int main(){
  printf("这是另一个C程序\n");
  printf("PATH: %s\n", getenv("PATH"));
  printf("PWD: %s\n", getenv("PWD"));
  printf("MYENV: %s\n", getenv("MYENV"));

  return 0;
}

myexec.c

#include
#include
#include
#include
#include
#include


int main(){ 
  printf("process running ...\n");
  pid_t id = fork();
  assert(id != -1);

  if(id == 0){
    sleep(1);
    char* const envp_[] = {(char*)"MYENV=1234565", NULL};
    execle("./mybin", "mybin", NULL, envp_);

    exit(1);
  }

  return 0;
}

【Linux】---进程控制(创建、终止、等待、替换)_第8张图片

如果我们自定义了环境变量,那么系统本身的环境变量就不会点出来了。

剩下的几个函数都是一样的道理,这里就不说了,可以自行查文档实现

【Linux】---进程控制(创建、终止、等待、替换)_第9张图片

简易的shell

前面讲完了进程的控制,包括:创建、终止、等待、替换。那么结合这些知识我们就可以自己去写一个简单的shell外壳了。

  1. 获取命令行
  2. 解析命令行
  3. 建立一个子进程(fork)
  4. 替换子进程(execvp)
  5. 父进程等待子进程退出(wait)
#include
#include
#include
#include
#include
#include
#include

#define NUM 1024
#define OPT_NUM 100
char lineCommand[NUM];
char *myargv[OPT_NUM];//指针数组
int  lastCode = 0;
int  lastSig = 0;

int main(){
  while(1){
    //输出提示符
    printf("用户名@主机名 当前路径# ");
    fflush(stdout);
  
    //获取输入,输入结束要有'\n'
    char* s = fgets(lineCommand, sizeof(lineCommand) - 1, stdin);
    assert(s != NULL);
  
    (void)s;
    //清除最后一个'\n'
    lineCommand[strlen(lineCommand) - 1] = 0;
  
    //字符串切割
    myargv[0] = strtok(lineCommand, " ");
    int i = 1;
  
    //将颜色选项放入ls命令中
    if(myargv[0] != NULL && strcmp(myargv[0], "ls") == 0)
      myargv[i++] = (char*)"--color=auto";
  
    //没有子串的话,strtok返回NULL
    while(myargv[i++] = strtok(NULL, " "))
      ;
  
    //cd命令不会创建子进程,就让shell自己执行对应命令,执行系统接口
    if(myargv[0] != NULL && strcmp(myargv[0], "cd") == 0){
              if(myargv[1] != NULL) 
                chdir(myargv[1]);
              continue;
    }
  
    //执行命令
    pid_t id = fork();
    assert(id != -1);
  
    if(id == 0){
      execvp(myargv[0], myargv);
      exit(1);
    }
  
    int status = 0;
    pid_t ret = waitpid(id, &status, 0);
    assert(ret > 0);
    (void) ret;
  
    lastCode = ((status>>8) & 0xFF);
    lastSig = (status & 0x7F);
  
  }

}

【Linux】---进程控制(创建、终止、等待、替换)_第10张图片

运行起来之后,虽然还有很多bug,还是能够执行一些简单的指令的。

你可能感兴趣的:(Linux,linux,运维,服务器)