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

Posted 呆呆兽学编程

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 以上是关于Linux篇第八篇——Linux下的进程控制(进程创建+进程终止+进程等待+进程程序替换+简易shell的实现)的主要内容,如果未能解决你的问题,请参考以下文章

Python学习笔记——进阶篇第八周———CPU运行原理与多线程

python基础篇第八篇面向对象(下)

Linux学习第八篇之文件搜索命令find

Lua从青铜到王者基础篇第八篇:Lua表和模块与包

Python学习笔记——进阶篇第八周———Socket编程进阶&多线程多进程

Linux篇第七篇——进程地址空间(程序地址空间+虚拟地址空间)