• Linux系统编程基础:进程控制


    在这里插入图片描述

    进程控制主要分为三个方面,分别是:子进程的创建,进程等待,进程替换
    在这里插入图片描述

    一.子进程的创建

    • 父进程调用fork()系统接口创建子进程后,操作系统会为子进程创建独立的PCB结构体虚拟地址空间mm_struct,因此父子进程之间具有互相独立性

    操作系统内核视角下的父子进程存在形式

    • 父进程调用fork()函数之后:在这里插入图片描述

    验证子进程对父进程数据的写时拷贝

    #include 
    #include 
    #include 
    int g_val = 0;
    int main(int argc,const char * argv[])
    {
    	//创建子进程
        pid_t id = fork();
        if(id < 0)
        {
          //创建子进程失败,退出主函数
          perror("fork");
          return 0;
        }
        else if(id == 0)  
        {
          //子进程执行流
          //子进程对变量g_val进行修改,引发写时拷贝
          g_val=100;
          printf("childProcess[%d]: %d : %p\n", getpid(), g_val, &g_val);
        }
        else
        {
           //父进程的执行流
           //父进程先休眠3秒,让子进程先完成g_val变量的写入
           sleep(3);
           printf("parentProcess[%d]: %d : %p\n", getpid(), g_val, &g_val);
        }
        sleep(1);
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 执行结果:在这里插入图片描述
    • 内核视角下,代码段中的写时拷贝图解:在这里插入图片描述
      在这里插入图片描述
    • 写时拷贝保证了父子进程之间互相独立的同时提高了计算机整机的内存使用效率

    二.进程等待

    • 子进程退出后会进入僵尸状态,僵尸状态进程的内核数据结构对象会保留在操作系统内核空间中,在父进程读取子进程的退出信息后,操作系统才会释放掉子进程所占用的所有系统资源
      • 若父进程比子进程先退出,那么操作系统就会接管子进程(成为孤儿进程),子进程退出后,操作系统会自动读取子进程的退出信息.
    • 父进程读取子进程的退出信息这一过程称为进程等待
    • 进程等待常用系统接口:int waitpid(int pid, int *status, int options);
      • 形参int pid:在这里插入图片描述

      • 形参int *status:用于记录进程退出码和退出信号的输出型参数

      • 形参int options表示进程等待的方式:主要分为阻塞等待非阻塞等待两大类:

        • option0时:进程执行阻塞等待,此时进程会进入阻塞状态(PCB退出运行队列,进入阻塞队列),直到其所等待的子进程退出,该进程才会重新进入运行状态读取子进程的退出信息
        • option为非零时(比如使用系统宏WNOHANG):进程执行非阻塞等待, 此时进程会尝试读取其子进程的退出信息,若没能读取到子进程的退出信息,则waitpid系统接口立即返回0
      • 返回值int:若waitpid系统接口读取到子进程的退出信息,返回子进程的pid,若waitpid执行过程中出现错误,则返回-1

    进程非阻塞等待示例:

    #include 
    #include 
    #include 
    #include 
    
    int main()
    {
      	  int pid = 0;
      	  //创建子进程
    	  pid = fork();
    	  if(pid == -1)
    	  {
    		    //子进程创建失败
    		    printf("fork failed\n");
    		    return -1;
    	  }
    	  else if(pid == 0)
    	  {
    		    //子进程的代码执行流
    		    int cnt = 5;
    		    while(cnt--)
    		    {
    		        sleep(1);
    		        printf("child process running\n");
    		    }
    		    exit(11);
    	  }
    	  else
    	  {
    		   //父进程的代码执行流
    		   //非阻塞等待子进程
    		   int status = 0;
    		   bool quit = 0;
    		   while(quit == 0)
    	   	   {
    		      //循环非阻塞等待子进程
    		      int res =  waitpid(pid,&status,WNOHANG);
    		      if(res > 0)
    		      {
    		      	 //子进程已退出
    		         printf("waiting pid success,chilProc exit,退出码:%d\n",WEXITSTATUS(status));
    		         quit = 1;
    		      }
    		      else if(res == 0)
    		      {
    		         //子进程还未退出
    		         printf("waiting pid success,childProc still running\n");
    		      }
    		      else
    		      {
    		         //等待发生错误
    		         printf("waiting pid failed\n");
    		      }
    		      sleep(1);
    		   }
    	  }
    	  return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    在这里插入图片描述

    三.进程替换

    • 进程替换的概念:通过特定的系统调用接口,操作系统可以将进程当前执行的代码段替换成指定系统路径下其他可执行程序的代码段,然后根据进程从头开始执行新的代码段(因此需要通过进程替换系统接口为新代码段传入main函数命令行参数)

    内核视角下的进程替换过程:

    在这里插入图片描述
    在这里插入图片描述

    • 可见进程替换并不是创建新的进程(进程的PCB虚拟内存结构体不变)
    • 注意:进程替换过程中,一些全局性的数据(比如环境变量)不会被替换掉
    • 进程替换系统接口:
      在这里插入图片描述
    • 形参和返回值统一解释:
      • 参数const char*path:表示将要替换现有代码段的目标可执行程序的完整路径
      • 参数const char*file:表示将要替换现有代码段的目标可执行程序的文件名,其系统路径由环境变量PATH决定
      • 参数const char*arg,...:表示传给新代码段main函数命令行参数的字符串,...表示可变参数列表,可以传入多个字符串,最后一个字符串需传入NULL
      • 参数char *const argv[]:表示传给新代码段main函数命令行参数的字符串数组,数组中最后一个字符串需传入NULL
      • 参数cahr*const envp[]:表示传递给新代码段的环境变量字符串数组
      • 当进程替换失败时,exec系列系统接口会返回-1
    • 实质上,exec*系列系统接口在Linux系统中充当着指令集和数据加载器的角色

    综合利用进程控制系统接口实现简单的shell进程

    • shell进程的运行原理:shell进程接收到用户输入的指令后,对指令进行格式化处理,然后创建子进程,子进程通过exec系列系统接口将自身替换成系统命令并执行以响应用户需求,shell进程的这种运行机制保证了自身的进程安全(子进程出现错误不会影响到父进程的运行)
      在这里插入图片描述
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    //用户输入的命令行的最大长度
    #define CStrLen 1024
    //解析命令行得到的格式化字符串数组
    #define SStrLen 50
    //命令行字符串
    char CommStr[CStrLen];
    //格式化命令行字符串数组
    char*  StdStr[SStrLen];
    
    //命令行分隔符
    #define Sep " "
    //操作系统配置的环境变量
    extern char ** environ;
    int main()
    {
      while(1) 
      {
         printf("[我的命令行解释器 myshell]$ ");
         fflush(stdout);
         memset(CommStr,'\0',sizeof StdStr);
         //获取用户输入命令
         if(fgets(CommStr,sizeof CommStr,stdin)== NULL)
         {
           continue;
         }
         CommStr[strlen(CommStr)-1] = '\0';
         StdStr[0] = strtok(CommStr,Sep);
         //根据空格对用户输入的字符串进行分割并存入StdStr字符串数组中
         int i = 1;
         while(StdStr[i++] = strtok(NULL,Sep));
      
         //部分命令(比如cd命令)需要由shell进程自己来执行
         if(strcmp(StdStr[0],"cd")== 0)
         {
            //用chdir函数改变shell进程的工作路径
            if(StdStr[1] != NULL)
            {
                chdir(StdStr[1]);
            }
            continue;
         }
    
    
         //shell创建子进程来执行系统命令
         int pid = fork();
         if(pid == 0)
         {
            //printf("shell的子进程执行系统命令:\n");
            execvp(StdStr[0],StdStr);
            printf("-mybash: %s: command execute failed\n",StdStr[0]);
            exit(-1);
         }
         else
         {
            int status;
            //父进程进行阻塞等待,等待子进程执行完系统命令结束并获取其退出码
            int waitres = waitpid(pid,&status,0);
            if(waitres == -1)
            {
               printf("waitchild process failed\n");
            }
         }
      }
      return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73

    在这里插入图片描述
    在这里插入图片描述

  • 相关阅读:
    pytorch初学笔记(七):神经网络基本骨架 torch.nn.Module
    为何学linux及用处
    SQL拼接动态创建表A关联数据库表B查询(数据分区储存)
    JUC中的原子类
    基于long pull实现简易的消息中心MQ参考
    记录linux清理空间的步骤
    Flutter——自适应设计
    醍醐灌顶,稻盛和夫:人为什么而工作?
    如何让GPT不再胡说八道
    iDEA - Gradle构建多module项目踩坑指南
  • 原文地址:https://blog.csdn.net/weixin_73470348/article/details/133498997