【Linux】进程控制 (万字详解)—— 进程创建 | 进程退出 | 进程等待 | 程序替换 | 实现简易shell(下)

本文涉及的产品
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
简介: 【Linux】进程控制 (万字详解)—— 进程创建 | 进程退出 | 进程等待 | 程序替换 | 实现简易shell(下)

🥑细节小问题


1️⃣为什么要用wait/waitpid函数呢??直接用全局变量不行吗??


进程具有独立性,那么数据就要发生写时拷贝,父进程无法拿到,更何况信号呢?

2️⃣既然进程具有独立性,进程退出码不也是子进程的数据吗?,父进程为什么能拿得到呢??wait/waitpid究竟干了什么


这要从僵尸进程:至少要保留该进程的PCB信息!task_struct里面保留了任何进程退出时的退出结果信息!!所以wait本质就是读取了子进程的task_struct结构


🥑理解waitpid


0a2653c851af460fa595bd959398a8f1.png

⚡options


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


waitpid的第三个参数options,用来设置等待方式


0:默认阻塞等待

WNOHANG:非阻塞等待

若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID


小故事:快要期末考了,我这个学期没有上过课,我给学霸张三打电话,问他要C语言的考试重点,他说他在楼上有事情让我等30min。我说:等你完全没有问题,电话别挂,你不下来,我就不挂,我就一直等着,这就是阻塞状态,一个月后,我再次找张三要复习资料,这次不同我每隔5mins 给张三打一次电话,询问他好了没有,这样每一次的打电话过程:非阻塞调用——基于非阻塞调用的轮询检测方案


0a2653c851af460fa595bd959398a8f1.png


🔥阻塞状态


阻塞的本质:意味着进程的PCB被放入等待队列中,并将进程状态由R改为S状态

返回的本质:子进程退出,父进程的PCB从等待队列中拿回,继续执行没执行完的代码,可以被CPU调度了


🔥非阻塞状态


我们看到OS或者某些应用,长时间卡住不动,这种情况我们叫做应用或者程序HANG住了。那么,WNOHANG表示设置等待方式为非阻塞


父进程在等待子进程返回结果,情况有如下:


等待成功,子进程退出

等待成功,子进程还未退出

等待失败

#include<stdio.h>                                                                                       
 #include<stdlib.h>
 #include<unistd.h>
 #include<sys/wait.h>
  int main()
  {
    pid_t id =fork();
    if(id == 0)
    {
      //子进程
      int cnt =5;
      while(cnt)
      {
        printf("我是子进程:%d\n",cnt--);
        sleep(1);
      }
      exit(105);//105 仅仅用来测试
    }
    else{
      int quit =0;
      while(!quit)
      {
        int status =0;
        pid_t result = waitpid(-1, &status, WNOHANG);
        if(result > 0)
        {
          //等待成功 && 子进程退出
          printf("等待子进程退出成功,退出码:%d\n",WEXITSTATUS(status));
          break;
        }
        else if(result == 0)
        {
          //等待成功 && 子进程未退出                                                                                                                                                       
          printf("子进程还在运行,暂时退出不了,你待会再来吧\n");
        }
        else
        {
          //等待失败
          printf("wait失败\n");
          break;
        }
      }
}


这就叫做基于非阻塞等待的轮询方案


0a2653c851af460fa595bd959398a8f1.png


四 . 进程替换


众所周知,fork之后,父子各自执行父进程代码的一部分,父子代码共享,数据写时拷贝各自私有一份,如果子进程就想执行一个全新的程序呢?那就要通过进程替换实现


💢概念和原理

程序替换,是通过特定的接口,加载磁盘上的一个权限的程序(代码和数据),加载到调用进程的地址空间中!仅仅替换当前进程的代码和数据的技术,并没有创建新的进程


0a2653c851af460fa595bd959398a8f1.png


程序替换本质就是把程序的代码+数据,加载到特定进程的上下文中。C/C++程序要运行,必须要先加载内存中,如何加载呢?是通过加载器,加载器的底层原理就是一系列的exec*程序替换函数


2d65d23f6d4748949b924e4057485923.png


上面我们发现,函数替换后,结束语句并没有打印


注:execl是程序替换,调用函数成功之后,会将当前进程的所以代码和数据都进行替换!包括已经执行的和未执行的!(甚至把自己都干掉了,所以没有返回值)


execl一旦调用成功,后续所有代码,全部都不会执行!exec*函数成功是不需要进行返回值检测;只要返回了,就一定是因为调用失败了,直接退出程序即可。


💚小细节


在加载新程序之前,父子的数据和代码的关系?代码共享,数据写时拷贝。

当加载新程序的时候,不就是一种“写入吗”?代码为了保证独立性,必须分离,所以会发生写时拷贝,所以父子进程在代码和数据上就彻底分离了


💢替换函数

#include <unistd.h>`
int execl(const char *path, const char *arg, ...);
int execv(const char *path, char *const argv[]);
int execlp(const char *file, const char *arg, ...);
int execvp(const char *file, char *const argv[]);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execve(const char *path, char *const argv[], char *const envp[]);


这些函数名看起来容易混淆,但只要理解其命名含义就很好记忆

image.png


下面我来一一探究:

🌍execl


int execl(const char *path, const char *arg, ...);


0a2653c851af460fa595bd959398a8f1.png


🌍execv

l即参数用列表传递;v即参数用数组传递


int execv(const char *path, char *const argv[]);


在环境变量中我们提到过,main是可以带有参数的。argv是一个指针数组,指针指向命令行参数字符串。我们可以理解为,通过exec函数,把argv喂给了ls程序的main函数。


2d65d23f6d4748949b924e4057485923.png


🌍execlp && execvp


带p:我会自己在环境变量PATH中查找,告诉我程序名即可


execlp("ls", "ls", "-a", "-l", NULL);                                                                                                             
  char* argv[] = { "ls", "-a", "-l", NULL};   
  execvp("ls", argv);

ps:Makefile默认只生成第一个目标文件,那么如何在一个Makefile文件中一次形成两个可执行文件呢?


0a2653c851af460fa595bd959398a8f1.png


所有的接口,看起来没有很大差别,只是调用参数的不同。这么多的接口,是为了满足不同的调用场景


操作系统只提供了一个系统调用接口execve(2),其他库函数(3)都是对系统调用的简单封装。


0a2653c851af460fa595bd959398a8f1.png


💢程序替换运行其他语言程序

其中bash是解释器,test.sh是我们写的脚本,作为参数的形式给bash读取到,在bash内部执行的,执行对应的功能


2d65d23f6d4748949b924e4057485923.png


五. 实现一个简易的shell


💫 写一个shell 命令行解释器,需要循环以下过程


打印提示行

获取和解析命令

fork创建子进程;替换子进程

父进程等待

各个阶段都有很多细节要注意:


🔥 1. 打印提示行


由于提示行本就是写死的,对于理解Linux意义不大我们就直接打印:[ljj@localhost myshell]#

另外在之前的进度条我们就知道,显示器的刷新策略就是行刷新,所以不想加\n,可以调用fflush(stdout);


🔥 2. 获取命令行


定义一个缓冲区cmd_line[NUM],并初始化。用fgets函数获取,打印的时候我们发现多换了一次行,这是因为我们把回车也读取到了,需要把\n处置0


cmd_line[strlen(cmd_line)-1] = '\0'; //strlen不包括'\0'


0a2653c851af460fa595bd959398a8f1.png


🔥 3.解析命令行

解析字符串,要分割命令行,用strtok。把一个字符串打散成多个子串吗?


#include<string.h>
char *strtok(char *str, const char *delim);


strtok细节:


第一次调用,要传入原始字符串

第二次调用,如果还要解析原始字符串,传入NULL


🔥 4. fork创建子进程;替换子进程


不能用当前进程直接替换,会把前面的解析代码覆盖掉,因此要创建子进程。同时,父进程需要等待子进程退出,并返回结果


那么选择哪个进程替换函数呢?execvp


bash是一个进程;会获取用户输入、对命令行做解析,帮用户和内核打交道;还会创建子进程帮我们执行命令,就算子进程崩了,也不会影响到父进程(王婆和实习生)


🔥5. 内建命令


在运行我们的shell发现,cd.. cd path等代码路径并没有回退,cd 等命令不能移动myshell的位置是因为子进程会退出,并非是父进程bash。

对于cd,我们以内建命令方式运行(即不创建子进程,让父进程shell自己执行),实际上相当于调用了自己的一个函数。更改当前进程路径,有一个系统调用接口chdir ——


0a2653c851af460fa595bd959398a8f1.png


代码实现——迷你shell

#include<stdio.h>                                                                                                                                                                                                                     
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
#define NUM 1024
#define SIZE 32
#define SEP " "
//保存打散之后的字符串
char *g_argv[SIZE];
//保存完整的命令行字符串
char cmd_line[NUM];
// shell 运行原理 :通过让子进程执行命令,父进程等待&&解析命令
int main()
 {
    //0. 命令行解释器,一定是一个常用内存的进程,也即是不退出
     while(1)
      {
        //1. 打印出提示信息
        //[whb@localhost myshell]#
        printf("[ljj@localhost myshell]# ");
        fflush(stdout);
        sleep(10);
        memset(cmd_line,'\0', sizeof cmd_line);
        //2.获取用户的键盘输入{输入的各种指令和选项,"ls -a -l"}
        if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
        {
          continue;
        }
        cmd_line[strlen(cmd_line)-1] = '\0';
        //"ls -a -l\n\0" 这里把最后的\n都输入进去了
        //printf("echo:%s\n", cmd_line);
        //3.解析命令行字符串:"ls -a -l" -> "ls" "-a" "-i"
        g_argv[0] = strtok(cmd_line, SEP); //第一次调用,要传入原始字符串
        int index = 1;
        if(strcmp(g_argv[0], "ls") == 0)
        {
          g_argv[index++] = "--color=auto";
        }
        if(strcmp(g_argv[0], "ll") == 0)
        {
          g_argv[0] = "ls";
          g_argv[index++] = "-l";
          g_argv[index++] = "--color=auto";  
        }
         while(g_argv[index++] = strtok(NULL, SEP)); // 第二次调用,如果还要解析原始字符串,传入NULL
        //for :debug
        //for(index =0; g_argv[index]; index++)
        //    printf("g_argv[%d]:%s\n", index, g_argv[index]);
        //4.todo:内置命令:让父进程(shell)自己执行的命令,叫做内置命令
        //内建命令本质其实就是shell中的一个函数调用
        if(strcmp(g_argv[0], "cd") == 0) //不想让子进程执行
        {
           if(g_argv[1]!= NULL) chdir(g_argv[1]);  //cd path, cd .. 
           continue;
        }
        //5.fork()
        pid_t id = fork();
        if(id == 0) //子进程
        {
           printf("下面功能让子进程执行\n");
           //cd 等命令不能移动myshell的位置,因为子进程会退出
           execvp(g_argv[0],g_argv);// ls -a -l
           exit(1);
        }
        //父进程
        int status =0;
        pid_t ret = waitpid(id, &status, 0);
        if(ret > 0)
        {
          printf("退出码:%d\n", WEXITSTATUS(status));                                                                                                                                     
        }
      }
      return 0;
 }


📢写在最后


能看到这里的都是棒棒哒🙌!

想必进程控制也算是Linux中重要🔥的部分了,如果认真看完以上部分,肯定有所收获。

接下来我还会继续写关于📚《基础IO》等…

💯如有错误可以尽管指出💯

🥇想学吗?我教你啊🥇

🎉🎉觉得博主写的还不错的可以`一键三连撒🎉


相关文章
|
18天前
|
缓存 监控 Linux
linux进程管理万字详解!!!
本文档介绍了Linux系统中进程管理、系统负载监控、内存监控和磁盘监控的基本概念和常用命令。主要内容包括: 1. **进程管理**: - **进程介绍**:程序与进程的关系、进程的生命周期、查看进程号和父进程号的方法。 - **进程监控命令**:`ps`、`pstree`、`pidof`、`top`、`htop`、`lsof`等命令的使用方法和案例。 - **进程管理命令**:控制信号、`kill`、`pkill`、`killall`、前台和后台运行、`screen`、`nohup`等命令的使用方法和案例。
51 4
linux进程管理万字详解!!!
|
8天前
|
存储 运维 监控
深入Linux基础:文件系统与进程管理详解
深入Linux基础:文件系统与进程管理详解
48 8
|
6天前
|
Linux
如何在 Linux 系统中查看进程占用的内存?
如何在 Linux 系统中查看进程占用的内存?
|
17天前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
52 4
|
18天前
|
缓存 负载均衡 算法
Linux内核中的进程调度算法解析####
本文深入探讨了Linux操作系统核心组件之一——进程调度器,着重分析了其采用的CFS(完全公平调度器)算法。不同于传统摘要对研究背景、方法、结果和结论的概述,本文摘要将直接揭示CFS算法的核心优势及其在现代多核处理器环境下如何实现高效、公平的资源分配,同时简要提及该算法如何优化系统响应时间和吞吐量,为读者快速构建对Linux进程调度机制的认知框架。 ####
|
1月前
|
Web App开发 网络协议 Linux
linux命令总结(centos):shell常用命令汇总,平时用不到,用到就懵逼忘了,于是专门写了这篇论文,【便持续更新】
这篇文章是关于Linux命令的总结,涵盖了从基础操作到网络配置等多个方面的命令及其使用方法。
65 1
linux命令总结(centos):shell常用命令汇总,平时用不到,用到就懵逼忘了,于是专门写了这篇论文,【便持续更新】
|
20天前
|
消息中间件 存储 Linux
|
23天前
|
运维 监控 Shell
深入理解Linux系统下的Shell脚本编程
【10月更文挑战第24天】本文将深入浅出地介绍Linux系统中Shell脚本的基础知识和实用技巧,帮助读者从零开始学习编写Shell脚本。通过本文的学习,你将能够掌握Shell脚本的基本语法、变量使用、流程控制以及函数定义等核心概念,并学会如何将这些知识应用于实际问题解决中。文章还将展示几个实用的Shell脚本例子,以加深对知识点的理解和应用。无论你是运维人员还是软件开发者,这篇文章都将为你提供强大的Linux自动化工具。
|
26天前
|
运维 Linux
Linux查找占用的端口,并杀死进程的简单方法
通过上述步骤和命令,您能够迅速识别并根据实际情况管理Linux系统中占用特定端口的进程。为了获得更全面的服务器管理技巧和解决方案,提供了丰富的资源和专业服务,是您提升运维技能的理想选择。
33 1
|
1月前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
【10月更文挑战第9天】本文将深入浅出地介绍Linux系统中的进程管理机制,包括进程的概念、状态、调度以及如何在Linux环境下进行进程控制。我们将通过直观的语言和生动的比喻,让读者轻松掌握这一核心概念。文章不仅适合初学者构建基础,也能帮助有经验的用户加深对进程管理的理解。
25 1
下一篇
无影云桌面