【Linux篇】第八篇——Linux下的进程控制(进程创建+进程终止+进程等待+进程程序替换+简易shell的实现)

⭐️这篇博客就要开始聊一聊进程控制相关的内容了,这部分的内容十分的丰富且十分的重要,学好这一块内容是非常有必要的


🌏进程创建

🌲fork函数

fork这个函数我在第一次讲进程创建的那篇博客中介绍过了,关于fork的返回值和用法可以去看右边这篇博客,这里就简单说明一下。(Linux进程
fork函数也是一个系统调用接口,为当前进程创建子进程,子进程返回0,父进程返回子进程的pid,出错返回-1
在这里插入图片描述

进程调用fork函数,内核需要做什么?

  • 给子进程分配内存空间,并为子进程创建PCB
  • 将父进程部分数据结构内容(还有代码和数据暂时共享)拷贝至子进程
  • 添加子进程到系统进程列表(运行队列)当中
  • fork返回,开始CPU调度器调度

fork之后执行什么?

父子进程共享一份代码,fork之后,一起执行fork之后的代码,且二者之间是独立的,不会相互影响

代码如下:

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

int main()
{
  pid_t ret = fork();
  
  if (ret < 0)
  {
    perror("fork");
    return 1;
  }
  else if (ret == 0)// 子进程
  {
    printf("I am child-pid:%d, ppid:%d\n", getpid(), getppid());
    sleep(1);
  }
  else if (ret > 0)// 父进程
  {
    printf("I am parent-pid:%d, ppid:%d\n", getpid(), getppid());
    sleep(1);
  }

  sleep(1);

  return 0;
}

运行结果如下:
在这里插入图片描述
fork失败的原因

  • 系统中有太多的进程,无法再创建新的进程
  • 实际用户的进程数量超出了现在

🌲写时拷贝

通常情况下,父子进程共享一份代码,且数据也是共享的,当任意一方试图写入更改数据,那么这一份便要以写时拷贝的方式各自私有一份副本。

在这里插入图片描述
从图中可以看出,发生写时拷贝后,修改方将改变页表中对该份数据的映射关系,父子进程各自私有那一份数据,且权限由只读变成了只写。

思考下面几个问题:

  1. 为什么代码要共享?

代码是不可以被修改的,所以各自私有很浪费空间,大多数情况下是共享的,但要注意的是,代码在特殊情况下也是会发生写时拷贝的,也就是进程的程序替换(后面会单独介绍)。

  1. 写实拷贝的作用?

a.可以减少空间的浪费,在双方都不对数据或代码进行修改的情况下,各自私有一根数据和代码是浪费空间的;
b.维护进程之间的独立性,虽然父子进程共享一份数据,但是父子中有一方对数据进行修改,那么久拷贝该份数据到给修改方,改变修改方中页表对这份数据的映射关系,然后对数据进行修改,这样不管哪一方对数据进行修改都不会影响另一方,这样就做到了独立性。

  1. 写时拷贝是对所有数据进行拷贝吗?

答案是否定的。如果没有修改的数据进行拷贝,那么这样还是会造成空间浪费的,没有被修改的数据还是可以共享的,我们只需要将修改的那份数据进行写时拷贝即可。

🌏进程终止

🌲进程退出的三种场景

  1. 代码运行完毕,结果正确
    代码没有发生任何错误,且代码逻辑正确
  2. 代码运行完毕,结果不正确
    代码运行过程中没有任何错误,但是代码逻辑存在问题,导致结果不正确
  3. 代码运行异常终止
    代码运行过程中发生了一些异常终止的错误,例如:野指针访问,除零错误等

🌲进程常见的退出方法

正常终止: 可以通过echo $?查看进程退出码,之前的博客中有介绍过

  1. main函数返回退出码

main函数退出的时候,return的返回值就是进程的退出码。0在函数的设计中,一般代表是正确而非0就是错误。

实例演示:

// 实例1
int main()
{
	return 0;
}
// 实例2
int main()
{
	return 0;
}

代码运行结果如下:
在这里插入图片描述
在这里插入图片描述

  1. 调用exit函数

在任意位置调用,都会使得进程退出,调用之后会执行执行用户通过 atexit或on_exit定义的清理函数,还会 关闭所有打开的流,所有的缓存数据均被写入

实例演示:

int main()
{
  cout << "12345";
  sleep(3);
  exit(0);// 退出进程前前会执行用户定义的清理函数,且刷新缓冲区
  return 0;
}

代码运行结果如下:
在这里插入图片描述

  1. 调用_exit函数
    实例演示:
int main()
{
  cout << "12345";
  sleep(3);
   _exit(0);// 直接退出进程
  return 0;
}

代码运行结果如下: 直接退出进程,不刷新缓冲区

异常终止:

  • ctrl+C终止前台进程
    在这里插入图片描述

  • kill发生9号信号杀死进程
    在这里插入图片描述

🌏进程等待

进程等待的必要性:

  • 子进程必须要比父进程先退出,否则会变成孤儿孤儿进程
  • 父进程必须读取子进程的退出状态,回收子进程的资源。如果父进程不读取子进程退出状态,还不会是子进程资源,那么子进程将处于僵死状态,会造成内存泄漏
  • 父进程派给子进程的任务完成的如何,得知子进程执行结果

🌲进程等待的方法

🍯wait方法

wait的函数原型如下:

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

函数返回值
返回值有两种,一种是等待进程的pid,另一种就是 -1,等待成功返回等待进程的pid,等待> 失败就返回-1
函数参数:
status是一个输出型参数,可以通过传地址获得进程退出状态,如果不想关心进程退出状态,就传 NULL

实例演示 让子进程先运行5s,然后退出进程,子进程由S状态变为Z状态,父进程等待子进程,回收子进程资源后,子进程变为Z状态变为X状态,10秒回父进程退出
代码如下:

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

int main()
{
	pid_t ret= fork();
	if (ret< 0){
	  cerr << "fork error" << endl;
	}
	else if (ret== 0){
	  // child
	  int count = 5;
	  while (count){
		printf("child[%d]:I am running... count:%d\n", getpid(), count--);
		sleep(1);
	  }
	  exit(1);
	}
	// parent
	printf("father begins waiting...\n");
	sleep(10);
	pid_t id = wait(NULL);// 不关心子进程退出状态
	
	printf("father finish waiting...\n");
	if (id > 0){ 
	  printf("child success exited\n"); 
	} else{
	  printf("child exit failed\n"); 
	} 
	//父进程再活5秒 
	sleep(5);
	return 0;
}

命令行监控脚本如下:

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

代码运行结果如下:
在这里插入图片描述
子进程由S状态变为Z状态
在这里插入图片描述
父进程等待子进程,回收子进程资源后,子进程变为Z状态变为X状态
在这里插入图片描述

🍯waitpid方法

函数原型如下:

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

函数返回值:

  • 当正常返回的时候waitpid返回收集到的子进程的进程ID;
  • 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
  • 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

参数:

  • pid

pid=-1时,可以等待任一个子进程,与wait等效
pid>0时,等待和pid相同的ID的子进程

  • status

是一个输出型参数,不想关心进程退出状态就传NULL
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

  • options

WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID(可以进行基于阻塞等待的轮询访问)
0:阻塞等待(等待期间父进程不执行任何操作)

实例演示:
代码如下:

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

int main()
{
	pid_t ret= fork();
	if (ret< 0){
	  cerr << "fork error" << endl;
	}
	else if (ret== 0){
	  // child
	  int count = 5;
	  while (count){
		printf("child[%d]:I am running... count:%d\n", getpid(), count--);
		sleep(1);
	  }
	  exit(1);
	}
	// parent
	printf("father begins waiting...\n");
	sleep(10);
	pid_t id = waitpid(-1, NULL, 0);// 不关心子进程退出状态,以阻塞方式等待
	
	printf("father finish waiting...\n");
	if (id > 0){ 
	  printf("child success exited\n"); 
	} else{
	  printf("child exit failed\n"); 
	} 
	//父进程再活5秒 
	sleep(5);
	return 0;
}

🌲获取子进程的status

  • wait和waitpid中都有一个status参数,该参数是一个输出型参数,由操作系统来填充
  • 如果该参数给NULL,那么代表不关心子进程的退出信息

status的几种状态:(我们只研究status的低16位)

在这里插入图片描述

看图可以知道,低7位代表的是终止信号,第8位时core dump标志,高八位是进程退出码(只有正常退出是这个退出码才有意义)
status的0-6位和8-15位有不同的意义。我们要先读取低7位的内容,如果是0,说明进程正常退出,那就获取高8位的内容,也就是进程退出码;如果不是0,那就说明进程是异常退出,此时不需要获取高八位的内容,此时的退出码是没有意义的。

实例演示:

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

int main()
{
	pid_t ret = fork();
	if (ret < 0){
	  cerr << "fork error" << endl;
	}
	else if (ret == 0){
	  // child
	  int count = 5;
	  while (count){
	    printf("child[%d]:I am running... count:%d\n", getpid(), count--);
	    sleep(1);
	  }
	
	  exit(1);
	}
	// parent
	printf("father begins waiting...\n");
	
	int status;
	pid_t id = wait(&status);// 从status中获取子进程退出的状态信息
	printf("father finish waiting...\n");
	
	if (id > 0 && (status&0x7f) == 0){
	  // 正常退出
	  printf("child success exited, exit code is:%d\n", (status>>8)&0xff);
	}
	else if (id > 0){
	  // 异常退出
	  printf("child exit failed,core dump is:%d,exit singal is:%d\n", (status&(1<<7)), status&0x7f);
	}
	else{
	  printf("father wait failed\n");
	}
	if (id > 0){ 
	  printf("child success exited\n"); 
	} else{
	  printf("child exit failed\n"); 
	} 
 	return 0;
}

代码运行结果如下:
在这里插入图片描述

🌲阻塞等待和非阻塞等待

操控者: 操作系统
阻塞的本质: 父进程从运行队列放入到了等待队列,也就是把父进程的PCB由R状态变成S状态,这段时间不可被CPU调度器调度
等待结束的本质: 父进程从等待队列放入到了运行队列,也就是把父进程的PCB由S状态变成R状态,可以由CPU调度器调度

  • 阻塞等待: 父进程一直等待子进程退出,期间不干任何事情
    实例1:
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>

int main()
{
  pid_t id = fork();
  if (id < 0){
    cerr << "fork error" << endl;
  }
  else if (id == 0){
    // child
    int count = 5;
    while (count){
      printf("child[%d]:I am running... count:%d\n", getpid(), count--);
      sleep(1);
    }
    exit(0);
  }
  
  // 阻塞等待
  // parent
  printf("father begins waiting...\n");
  int status;
  pid_t ret = waitpid(id, &status, 0);
  printf("father finish waiting...\n");

  if (id > 0 && WIFEXITED(status)){
    // 正常退出
    printf("child success exited, exit code is:%d\n", WEXITSTATUS(status));
  }
  else if (id > 0){
    // 异常退出
    printf("child exit failed,core dump is:%d,exit singal is:%d\n", (status&(1<<7)), status&0x7f);
  }
  else{
    printf("father wait failed\n");
  }
}

代码运行结果如下:
在这里插入图片描述

  • 非阻塞等待: 父进程不断检测子进程的退出状态,期间会干其他事情(基于阻塞的轮询等待)
    实例2
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>

int main()
{
  pid_t id = fork();
  if (id < 0){
    cerr << "fork error" << endl;
  }
  else if (id == 0){
    // child
    int count = 5;
    while (count){
      printf("child[%d]:I am running... count:%d\n", getpid(), count--);
      sleep(1);
    }
    exit(0);
  }
  // 基于阻塞的轮询等待
  // parent
  while (1){
    int status;
    pid_t ret = waitpid(-1, &status, WNOHANG);
    if (ret == 0){
      // 子进程还未结束
      printf("father is running...\n");
      sleep(1);
    }
    else if (ret > 0){
      // 子进程退出
      if (WIFEXITED(status)){
        // 正常退出
        printf("child success exited, exit code is:%d\n", WEXITSTATUS(status));
      }
      else{
        // 异常退出
        printf("child exited error,exit singal is:%d", status&0x7f);
      }
      break;
    }
    else{
      printf("wait child failed\n");
      break;
    }
  }
  
}

代码运行结果如下:
在这里插入图片描述

🌏进程程序替换

fork创建子进程后一般会有两种行为:

  1. 想让子进程执行父进程的一部分代码(可以理解为子承父业)
  2. 想让子进程执行和父进程完全不同的代码,也就是程序替换(可以理解为儿子创业)

🌲原理

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

思考几个问题:

  1. 程序替换的本质是什么?

把磁盘中的程序的代码和数据用加载器加载进特定的进程的上下文中,底层用到了exec系列的程序替换函数

  1. 程序替换后,有没有新进程被创建?

答案是没有的。因为进程替换前后,没有创建新的PCB、虚拟内存和页表等数据结构,也就是进程的这些数据结构没有发生变化,进程替换只是对物理内存中的数据和代码进行了修改,前后进程的ID没有发生改变,所程序替换不创建新进程

  1. 子进程发生程序替换后,代码和数据都发生写时拷贝吗?

由于进程替换会把新程序的代码和数据加载到特定的进程,为了让父子进程之间具有独立性,修改的代码和数据都要发生写时拷贝,这样才不会影响父进程的数据和代码

🌲替换函数

有六种以exec开头的函数,原型如下: 操作系统其实值提供了第六个系统调用接口,其他五个都是由第六个系统调用接口封装出来的

#include <unistd.h>

extern char **environ;

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[]);

函数返回值: 调用出错返回-1,没有调用成功的的返回值
函数参数:

  • path:用来替换的程序所在的路径
  • file:程序名
  • arg, …:列表的形式传参
  • arg[]:数组的形式传参
  • envp[]:自己维护的环境变量

函数名解释:

  • l(list) : 表示参数采用列表
  • v(vector) : 参数用数组
  • p(path) : 有p自动搜索环境变量PATH
  • e(env) : 表示自己维护环境变量

函数的使用方法:

函数名参数格式是否带路径是否使用当前环境变量
execl列表
execlp列表
execle列表否,自己组装环境变量
execv数组
execvp数组
execve数组否,自己组装环境变量

函数调用案例如下:

int main()
{
    // 自己组装的环境变量
    char* myenv[] = {"MYENV=you can see my", NULL};
    
    // 列表形式传参
    execl("/usr/bin/ls", "ls", "-l", "-a", NULL);
    execp("ls", "ls", "-l", "-a", NULL);
    exece("./mycmd", "mycmd", NULL, myenv);

    // 数组形式传参
    char* const argv[] = {"ls", "-l", "-a", NULL};
    execv("/usr/bin/ls",argv);
    execvp("ls", argv);
    char*  const agrv1[] = {"mycmd", NULL};
    execve("./mycmd", agrv1, myenv);// 调用自己的程序
}

实例演示
实例1: 用当前路径下的mycmd程序替换自己的程序,使用execvp
代码如下:

/******************************mytest.cc******************************/
#include <iostream>
#include <unistd.h>

int main()
{
  printf("I am a process:%d\n", getpid());
  // int ret = execl("/usr/bin/ls", "ls", "-a", "-l",  NULL); // 需要添加相对路径或者绝对路径
  //int ret = execlp("ls", "ls", "-a", "-l"  NULL);// p 自动搜索环境变量PATH
  //int ret = execlp("ls", "ls", "-al",  NULL);// p 自动搜索环境变量PATH
  char* const arg[] = {
    "ls",
    "-a",
    "-l",
    NULL
  };
  
  //execv("/usr/bin/ls", arg);
  //execvp("ls", arg);
  char* const MY_ENV[] = {"myenv=you can see me", NULL};
  execle("./mycmd", "mycmd", NULL, MY_ENV);
  printf("you should run here...\n");
  return 0;
}
/******************************mycmd.cc******************************/
int main()
{
  printf("I am a process:%d\n", getpid());
  printf("myenv:%s\n", getenv("myenv"));
  return 0;
}

代码运行结果如下:
在这里插入图片描述
实例2: 子进程进行程序替换,父进程阻塞等待子进程退出状态,观察现象

#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;

int main()
{
  pid_t id = fork();
  if (id == 0){
    // child
    sleep(3);
    execlp("ls", "ls", "-al", NULL);
    exit(1);
  }
  else if (id  < 0){
    perror("fork error");
    return 1;
  }

  // parent
  pid_t ret = waitpid(id, NULL , 0);
  if (ret > 0){
    printf("cmd run done...\n");
  }
  return 0;
}

代码运行结果如下: 可见父子进程直接具有独立性,其中一个进程被替换,另一个进程不受影响
在这里插入图片描述

🌏简易shell的实现

要写一个shell,需要循环以下过程:

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

图解:
在这里插入图片描述
代码实现如下:

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

#define SIZE 256
#define NUM 16

int main()
{
  char buf[SIZE];// 命令行缓冲区
  while (1){
    // 清空缓冲区
    buf[0] = '\0';
    const char* cmd_line = "[temp@VM-0-9-centos MyShell]#";
    printf("%s", cmd_line);
    fgets(buf, SIZE, stdin);
    buf[strlen(buf)-1] = '\0'; // 把buf最后一个字符'\n'置为'\0'
    // strtok分割字符
    char* argc[NUM];
    argc[0] = strtok(buf, " ");
    int i = 0;
    for (i = 1; argc[i-1]; ++i){
      argc[i] = strtok(NULL, " ");
    }

    pid_t id = fork();
    if (id < 0){
      // 进程创建失败
      perror("fork error");
      continue;
    }
    else if (id == 0){
      // child
      // 进程替换
      execvp(argc[0], argc);
      exit(1);
    }

    // parent
    // 父进程通过阻塞等待方式读取子进程退出信息
    int status;
    pid_t ret = waitpid(id, &status, 0); 
    if (ret > 0){
      // 等待成功
      if (WIFEXITED(status)){
        // 子进程正常退出
        printf("exit code is: %d\n", WEXITSTATUS(status));
      }
      else{
        // 子进程异常退出
        printf("exit failed, exit singal is %d\n", WIFEXITED(status));
      }
    }
    else{
      printf("wait failed\n");
    }
  }

  
  return 0;
}

代码运行演示如下: 可以看出,该命令还解释器对基本的指令可以解释,但是管道,重定向都不行,后序的知识可以继续完善这个小程序
在这里插入图片描述

🌏尝试用自己的命令行解释器登录用户

我们只是做一个小实验,所以这里我们选择创建一个新用户来完成该炒作
步骤:

  1. 先把自己的命令行解释器程序拷贝到一个目录下
    在这里插入图片描述

  2. 打开etc目录下的文件passwd,然后找到temp所在行
    在这里插入图片描述
    在这里插入图片描述

  3. 把temp行的**/bin/bash改成/home/MyShell/mysehll**即可
    在这里插入图片描述

  4. 登录用户temp
    在这里插入图片描述

效果如下:
在这里插入图片描述

🌐总结

进程控制的内容就先介绍到这里了,内容也是十分的丰富,喜欢的话,欢迎点赞、支持和关注~
在这里插入图片描述

  • 37
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 35
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 35
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

呆呆兽学编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值