0


【Linux】进程控制 — 进程程序替换 + 实现简易shell

文章目录

📖 前言

上一节我们讲了进程终止和进程等待等一系列问题,并做了相应的验证,本章将继续对进程控制进行学习,我们将学习进程程序替换,进行相关验证,运用系统进程程序替换接口,自己模拟写一个shell,该shell能够实现执行指令,等一系列命令行操作……


1. 进程程序替换

1.1 程序替换的概念:

概念引入:

将可执行程序加载到内存,并且重新调整子进程的页表映射,使之指向新的进程的代码和数据段,这种过程就叫做程序替换。

子进程执行的是父进程的代码片段,那么如果我们想让创建出来的子进程,执行全新的程序呢?

此时就要用到:进程的程序替换。

1.2 为什么要程序替换:

原因:

  • 原因是我们想让我们的子进程执行一个全新的程序。
  • 不同语言写的功能互相调用,这就是为什么要有程序替换的原因。

我们一般在服务器设计(Linux编程)的时候,往往需要子进程干两件种类事情:

    • 1.让子进程执行父进程的代码片段(服务器代码)
    • 2.让子进程执行磁盘中一个全新的程序(shell, 想让客户端执行对应的程序,通过我们的进程,执行其他人写的进程代码等等)C/C++ -> C/C++/Python/Shell/Php/Java…

1.3 程序替换的原理:

程序替换的原理:

  • 将磁盘中的程序,加载入内存结构。
  • 重新建立页表映射,谁执行程序替换,就重新建立子进程的映射关系。

效果:让我们的父进程和子进程彻底分离,并让子进程执行一个全新的程序!

父进程的映射关系:

在这里插入图片描述
程序替换之后,子进程的映射关系:

在这里插入图片描述
调整子进程的页表,让其不再与父进程代码和数据有任何关系,而是指向自己的代码和自己的数据区。

注意:

  • 请问:这个过程有没有创建新的讲程呢?
  • —— 没有!!!

小结:

  • 说白了就是让fork创建子进程,不想让子进程执行父进程代码片段。
  • 我们想让子进程执行磁盘当中全新的程序,而且我们没有创建新的进程。
  • 因为子进程的内核数据结构基本没变,只是重新建立了虚拟到物理的映射关系罢了。
  • 包括子进程的PID都不变,压根就没有创建新的进程,只不过让新的进程执行了不同的程序罢了。

2. 六个exec替换函数

上述我们讲了什么是程序替换,下面就要来见见猪跑了。
程序替换是由操作系统完成的,调用系统调用接口来完成操作。

  • 我们如果想执行一个全新的程序,我们需要做几件事情:

  • 先找到这个程序在哪里? —— 程序在那里

  • 程序可能携带选项进行执行(也可以不携带) —— 怎么执行

明确告诉OS,我想怎么执行这个程序是什么,要不要带选项。
我们平时在命令行中敲的指令都是一个一个可执行程序。

  • 程序替换的是子进程:(重点)

  • 进程替换永远影响的是进程的本身,子进程的替换永远不会影响父进程,因为进程具有独立性。

  • 独立性体现在内核层面,不同进程有不同的地址空间,有不同的页表替换只是加入新的代码和数据。

  • 重新建立的是页表映射但并不影响内核数据结构的具体情况。

  • 子进程虽然和父进程代码共享数据写实拷贝,但是一旦发生进程替换了,就认为代码和数据发生了双写实拷贝,就彻底将两个进程分开了。

  • 所以引入子进程的原因就是,一方面把需求做到位,另一方面不影响父进程,因为父进程可能还要接收新的命令,再去执行新的程序。

六个exec替换函数:

在这里插入图片描述

2.1 execl函数:

intexecl(constchar*path,constchar*arg,...);

path:

  • 这个是路径,可执行程序的路径。

arg:

  • 命令行怎么写(1s -1 -a), 这个参数就怎么填"ls",“-l”,“-a”,最后必须是NULL结尾
  • 标识 “如何执行程序的” 参数传递完毕

… :

  • 可变参数,可以传多个参数

第一个参数是解决了,程序在哪里的问题,第二个参数往后所有的参数,解决的都是程序如何执行的问题。

代码演示:

#include<stdio.h>#include<unistd.h>intmain(){//让我的程序执行系统上的: ls -a -i这样的一个命令printf("我是一个进程,我的pid是 : %d\n",getpid());//int ret = execl("/usr/bin/ls", "ls", "-l", "-a", NULL); //带选项//execl("/usr/bin/top", "top", NULL); //不带选项//execl("/usr/bin/which", "which", "pwd", NULL); //不带选项//下面这行代码没有打印出来//一旦代码执行到这里,必然是进程替换失败了//替换失败的情况int ret =execl("/usr/bin/lsssss","ls","-l","-a",NULL);//带选项printf("我执行完毕了,我的pid : %d, ret = %d\n",getpid(), ret);return0;}

一旦进程替换成功了,就不会再执行程序替换函数以后的代码了,因为直接去是该进程被替换掉了。

在这里插入图片描述
显而易见,代码中程序替换以后的打印内容并没有显示出来,说明进程替换以后的代码压根就没执行,而是去执行ls进程了。

总结:

  • 一旦替换成功,是将当前进程的代码和数据全部替换了!!
  • 前一个printf被执行是因为程序替换并没有执行。
  • 所以替换上面的代码依旧是当前进程执行执行,execl之后代码就不复存在了。

所以程序替换不用判断返回值:

不需要返回值,一旦有值返回那么必然是返回失败了!!!因为只要成功了,就不会有返回值,而失败的时候,必然会继续向后执行!!最多通过返回值得到什么原因导致的替换失败!

引入进程创建:

  • 子进程执行程序替换,会不会影响父进程呢??
    • 不会(因为进程具有独立性)
  • 为什么,如何做到的??
    • 数据层面发生写时拷贝!
    • 当程序替换的时候,我们可以理解成为,代码和数据都发生了写时拷贝完成父子的分离!

在这里插入图片描述)

2.2 execv函数:

intexecv(constchar*path,char*const argv[]);

实现的功能和

execl

一模一样。

path:

  • 这个是路径,可执行程序的路径。

argv[]:

  • 如何执行,和execl的唯一区别就是传参方式的不一样

在这里插入图片描述
代码演示:

#include<stdio.h>#include<unistd.h>#include<stdlib.h>#include<sys/wait.h>intmain(){printf("我是父进程,我的pid是:%d\n",getpid());
    pid_t id =fork();if(id ==0){//子进程//我们要子进程执行全新的程序,以前我们是子进程执行父进程的代码片段printf("我是子进程,我的pid是:%d\n",getpid());//char* const argv_[] = {//    (char*)"ls",//    (char*)"-l",//    (char*)"-a",//    (char*)"-i",//    NULL//};char*const argv_[]={(char*)"top",NULL};//execv("/usr/bin/ls", argv_);execv("/usr/bin/top", argv_);}//一定是父进程int status =0;int ret =waitpid(id,&status,0);if(ret == id){sleep(2);printf("进程等待成功!\n");}return0;}

程序替换不仅可以替换成指令,还可以替换成我们自己写的可执行程序。

  • 用exec系列,这种系统级的函数,可以把任何语言耦合到一起。
  • 任何程序都可以用系统级接口调用其他语言的。
  • 所以说操作系统是所有技术的基座。

2.3 execlp函数:

intexeclp(constchar*file,constchar*arg,...);

file:

  • 你想执行什么程序。 —— 找到它
  • 执行指令的时候,默认的搜索路径,在哪里搜索呢?—— 环境变量PATH
  • 命名带p的,可以不带路径,只说出你要执行哪一个程序即可!

arg:

  • 想如何执行它

代码演示:

#include<stdio.h>#include<unistd.h>#include<stdlib.h>#include<sys/wait.h>//有时候不想让父进程做一件事,只想让子进程做一件事//将进程创建引入进来intmain(){printf("我是父进程,我的pid是:%d\n",getpid());
    pid_t id =fork();if(id ==0){//子进程//我们要子进程执行全新的程序,以前我们是子进程执行父进程的代码片段printf("我是子进程,我的pid是:%d\n",getpid());execlp("ls","ls","-a","-l","-i",NULL);//这里出现了两个ls,含义一样吗?-- 不一样!//第一个参数是供系统去找要执行谁的指令,后面一坨是表示如何执行该指令exit(100);//只要执行了exit,就意味着,execl系列的函数失败了 -- 进程替换失败了}//一定是父进程int status =0;int ret =waitpid(id,&status,0);if(ret == id){sleep(2);printf("wait success, ret : %d, 我所等待子进程的退出码: %d, 退出信号是: %d\n", ret,(status >>8)&0xFF, status &0x7F);}return0;}

作用和execI和execv是一样的,也是执行一个新的程序。

2.4 execvp函数:

intexecvp(constchar*file,char*const argv[]);

file:

  • PATH找,只要程序名即可。

argv[]:

  • 如何执行,将命令行参数字符串,统一放入数组中即可完成调用!

在这里插入图片描述

2.5 execle函数:

intexecle(constchar*path,constchar*arg,...,char*const envp[]);

envp[]:

  • 环境变量

execle:test.c程序替换代码演示:

#include<stdio.h>#include<unistd.h>#include<stdlib.h>#include<sys/wait.h>intmain(){//环境变量的指针声明externchar** environ;printf("我是父进程,我的pid是:%d\n",getpid());
    pid_t id =fork();if(id ==0){//子进程//我们要子进程执行全新的程序,以前我们是子进程执行父进程的代码片段printf("我是子进程,我的pid是:%d\n",getpid());//绝对路径//execl("/home/Zh_Ser/linux/lesson16/mycmd", "mycmd", NULL);//相对路径//execl("./mycmd", "mycmd", NULL);//我们来手动导入一个环境变量char*const env_[]={(char*)"MYPATH=You Can See Me!!",NULL};//e: 添加环境变量给目标进程,是覆盖式的!//execle("./mycmd", "mycmd", NULL, env_);//execle("/usr/bin/ls", "ls", NULL, env_);execle("./mycmd","mycmd",NULL, environ);exit(100);//只要执行了exit,就意味着,execl系列的函数失败了 -- 进程替换失败了}//一定是父进程int status =0;int ret =waitpid(id,&status,0);if(ret == id){sleep(2);printf("进程等待成功!\n");}return0;}

上述代码代码在程序替换的时候,执行了./mycmd,目的是手动导入环境变量的时候,执行./mycmd获取导入的环境变量。

mycmd.cpp代码演示:

#include<iostream>#include<stdlib.h>#include<stdio.h>#include<unistd.h>usingnamespace std;intmain(){externchar** environ;

    cout <<"打印环境变量"<< endl;for(int i =0; environ[i]; i++){printf("%d: %s\n", i, environ[i]);}

    cout <<"+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"<< endl;//根据环境变量名,获取环境变量的内容
    cout <<"PATH:"<<getenv("PATH")<< endl;
    cout <<"----------------------------------------------"<< endl;
    cout <<"MYPATH:"<<getenv("MYPATH")<< endl;
    cout <<"----------------------------------------------"<< endl;//程序崩溃了 -- 因为环境变量里根本就没有MYPATH  
    
    cout <<"Hello World!"<< endl;
    cout <<"Hello World!"<< endl;
    cout <<"Hello World!"<< endl;
    cout <<"Hello World!"<< endl;return0;}

mycmd是为了获取环境变量。

如果我们用这种方式导入环境变量:

在这里插入图片描述
出现的情况:

在这里插入图片描述
将mycmd.cpp中的getenv(“PATH”)给屏蔽掉,再执行test结果就可以将MYPATH打印出来:

在这里插入图片描述
e: 添加环境变量给目标进程,是覆盖式的!所以环境变量只剩下MYPATHT。

子进程会继承父进程的环境变量的!!(重点)

  • 子进程会继承父进程的环境变量,当父进程调用fork()创建子进程时,子进程会继承父进程的所有环境变量
  • 当子进程调用execlp()等函数执行其他程序时,子进程也会继承父进程的环境变量。
  • 如果需要在子进程中更改环境变量,可以使用setenv()或putenv()等函数进行更改。
  • 但是,更改的环境变量只会影响当前进程和它的子进程,并不会影响父进程或其他进程的环境变量。

如何理解覆盖?(重点)

  • 当子进程调用execle()函数替换自己的程序时,可以传递一个新的环境变量数组,以覆盖子进程继承的父进程的环境变量。如果不传递新的环境变量数组,子进程会继承父进程的环境变量。因此,如果在调用execle()函数时没有传递新的环境变量数组,子进程的环境变量不会被覆盖。
  • 如果传递了新的环境变量数组,则子进程的环境变量将被替换为新的环境变量数组中的值。这可能会导致子进程无法访问父进程中的一些环境变量,除非在新的环境变量数组中显式地包含它们。

验证execle覆盖了子进程会继承父进程的环境变量:

  • 我们执行test程序的时候
  • 调用execle接口,程序替换去执行mytest
  • 既然mytest是替换了子进程,它就会继承父进程的全部环境变量
  • execle函数我们传了一个env_将子进程的环境变量覆盖了

在这里插入图片描述
我们在mycmd程序开始的地方,加了查看全部环境变量的代码:

externchar** environ;for(int i =0; environ[i]; i++){printf("%d: %s\n", i, environ[i]);}

目的是通过该代码查看子进程(mycmd)的环境变量,被execle传的env_覆盖之后的样子:

在这里插入图片描述
**显而易见,子进程的环境变量只有env_[]的内容了!!!所以

getenv("PATH")

才获取不到!!!**

正确做法:

我们将全部环境变量传过去,将environ传过去。

在这里插入图片描述


补充:(重点)

  • ls 是一个常见的系统命令,它通常位于系统的某个标准路径(如 /bin 或 /usr/bin)。即使 PATH 为空,execlp() 会检查这些标准路径,找到 ls 的可执行文件并执行它。
  • 可能是直接在execlp中定义好的路径了,所以 PATH 环境变量没了也可以找到。

详细说明:

  • 如果PATH环境变量为空,execlp()函数会无法在环境变量中查找可执行文件的路径。但是,execlp()函数会检查一些默认路径,例如/bin、/usr/bin等,来查找可执行文件。因此,即使PATH为空execlp()函数也可能会在这些默认路径中找到可执行文件并执行它
  • 但是,如果在默认路径中也找不到可执行文件,则execlp()函数会执行失败,并将errno设置为ENOENT,表示无法找到可执行文件。因此,如果需要执行特定路径下的可执行文件,最好使用execv()或execve()等函数,并指定可执行文件的完整路径。这样可以避免依赖PATH环境变量来查找可执行文件的路径。

验证:

  • 我们在在mycmd程序中再进行程序替换
  • 用execlp函数第一个参数是在PATH路径下找的可执行文件
  • mycmd进程的环境变量中只有一句话(只有MYPATH了)
  • 但是我们照样可以在mycmd中进行程序替换执行出ls的结果!!

在这里插入图片描述
即使我们将父进程中的PATH给改了,命令行中都用不了ls,execlp照样可以找到ls并执行它。

在这里插入图片描述

2.6 execvpe函数:

intexecvpe(constchar*file,char*const argv[],char*const envp[]);

有了上面的基础这个想必就不用再啰嗦了,只是第二个参数传的不同,这里传的是一个指针数组。

在这里插入图片描述
这些函数原型看起来很容易混,但只要掌握了规律:

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

为什么有那么多的接口?

  • 目的是:适配应用场景
  • 其实上述函数都是对系统接口的封装

严格意义来说不是系统接口,是基于系统接口之上的封装。

真正意义上的系统接口:

intexecve(constchar*filename,char*const argv[],char*const envp[]);

上述6个函数在执行时都会调用execve()函数,将参数列表和环境变量数组转换为execve()函数所需的格式,并调用execve()函数来执行可执行文件。因此,execve()函数是这些函数的底层实现。


3. 实现简易版shell

只要我们懂得了程序替换的原理,会用程序替换的接口,就很好理解:

  • shell本身执行起来就是个死循环
  • 我们命令行就是去执行其他程序
  • shell创建子进程,将子进程给替换掉就ok了
  • 过程中要获取输入指令等操作…

myshell代码实现:

#include<stdio.h>#include<string.h>#include<string.h>#include<stdlib.h>#include<unistd.h>#include<sys/wait.h>#defineSEP" "#defineNUM1024#defineSIZE128char command_line[NUM];char* command_args[SIZE];char env_buffer[NUM];externchar** environ;//对应上层的内建命令intChangeDir(constchar* new_path){chdir(new_path);return0;//调用成功}voidPutEnvInMyShell(char* new_env){putenv(new_env);}intmain(){//shell本质就是一个死循环while(1){//不关心获取这些属性的接口,搜索一下都有//1.显示提示符printf("[用户名@我的主机名 当前目录]# ");fflush(stdout);//2.获取用户输入memset(command_line,'\0',sizeof(command_line));//从键盘获取,标准输入,stdin,获取到的是C风格的字符串(stdio.h结尾的),'\0'结尾fgets(command_line, NUM,stdin);
        command_line[strlen(command_line)-1]='\0';//清空\n回车//printf("%s\n", command_line);//3. "ls -a -l -i" -> "ls" "-a" "-l" "-i" 字符串切分 -- 因为这些参数一定得以列表或者数组方式传递给程序替换接口//shell必须切分,因为必须调用execl函数//将第一个字符串地址用0号下标指向,第二个字符串地址用1号下标指向 
        command_args[0]=strtok(command_line, SEP);int index =1;//给ls命令添加颜色: 如果提取出来的程序名是ls -- 1下标设置成改颜色的if(strcmp(command_args[0],"ls")==0) command_args[index++]=(char*)"--color=auto";//strtok截取成功返回字符串起始地址//截取失败,返回NULLwhile(command_args[index++]=strtok(NULL, SEP));//for debug//int i = 0;//for(i = 0; i < index; i++)//{//    printf("%d : %s\n", i, command_args[i]);//}//4.TODO -- 编写后面的逻辑,内建命令(由父Shell自己实现的自己调用的一个函数)if(strcmp(command_args[0],"cd")==0&& command_args[1]!=NULL){//让调用方进行路径切换,父进程ChangeDir(command_args[1]);continue;}//走到这里一定是将命令行参数解析完了,包括命令 + 选项//将环境变量的信息导入在了父进程的上下文当中if(strcmp(command_args[0],"export")==0&& command_args[1]!=NULL){//环境变量列表(是个指针数组,每个元素是个指针指向一个环境变量)//我们传的是一个字符串首地址,但是环境变量的内容还是我们自己维护的//目前,环境变量信息在comman_line,会被清空,那么环境变量当然就没有了//所以此处我们需要自己保存一下环境变量的内容strcpy(env_buffer, command_args[1]);PutEnvInMyShell(env_buffer);//PutEnvInMyShell(command_args[1]);//MYENV=112233continue;}//5.创建进程,执行//如果自己直接程序替换的话,就把自己写的shell给替换了
        
        pid_t id =fork();if(id ==0){//子进程//6.程序替换//execvpe(command_args[0], command_args, environ);execvp(command_args[0], command_args);exit(1);//执行到这里,子进程一定替换失败了}int status =0;
        
        pid_t ret =waitpid(id,&status,0);if(ret >0){printf("等待子进程成功: sig: %d, code: %d\n", status &0x007F,(status &0xFF00)>>8);}}//end whilereturn0;}

ls设置颜色的办法:
在这里插入图片描述

3.1 内建命令等问题的解决:

3.1 - 1 cd命令的处理:

在命令行中操作cd时,会跳转路径,但是用绝对命令时,就不行了,还是原来的路径:

  • 一个进程也存在对应路径, 进程对应的路径可以理解成
  • 当进程启动的时候时,在哪个路径启动时,这个进程所在路径就是当前进程所启动的路径

一般一个进程的路径是会被于进程继承的,路径的变化我们希望的是父进程路径的变化。

在这里插入图片描述
原因就是,我们平时用的cd时做过处理的cd:

  • 我们知道指令都是一些可执行程序
  • 执行可执行程序就是去执行其他程序,程序替换了
  • 如果我们创建的子进程跳转路径
  • 子进程退出之后,只是子进程的路径跳转了
  • 并不影响父进程的路径,会发现命令行路径还是没变
  • 显然这这种做法是不可取的

就不能用程序替换的方式来执行一些特殊的命令了:

  • 我们可以在父进程将一些命令单独处理
  • 让其不进行程序替换

重点:

  • 程序替换影响的是子进程和父进程没关系,子进程一 跑就完了,曾经所有的操作就没有意义,路径切换就没意义了,所以我们要让父进程的路径发生变化。
  • 如果有些行为,是必须让父进程shell执行的,不想让子进程执行,绝对不能创建子进程!只能是父进程自己实现对应的代码!

内建命令:

  • 我们把由父进程自己提供的代码或者提供的逻辑(在命令行上体现的也是一个命令),但是这部分命令不是子进程执行的,而是父进程自己执行的,我们叫做内建命令。
  • 由shell自己执行的命令,我们称之为内建(内置bind- in)命令。

更改工作目录的函数:

在这里插入图片描述
验证一下:
在这里插入图片描述

3.2 - 2 export的处理:

导入环境变量:

在这里插入图片描述
export不是一个可执行程序和cd,ls,cat等指令不同:

export是一个shell内置命令,用于设置环境变量。它并不是一个可执行程序,而是由shell解释器直接执行的命令。当我们在shell中使用export命令时,它会将指定的环境变量设置为当前shell进程的环境变量,以便后续的命令或程序可以使用该环境变量。

所以用execvp进行程序替换的时候,是不能替换成功的!

在这里插入图片描述
注意:

  • 环境变量列表(是个指针数组,每个元素是个指针指向一个环境变量)
  • 我们传的是一个字符串首地址,但是环境变量的内容还是我们自己维护的
  • 目前,环境变量信息在comman_line,会被清空,那么环境变量当然就没有了
  • 所以此处我们需要自己保存一下环境变量的内容

环境变量是数据,进程替换不是替换进程的代码和数据吗?

  • 但是环境变量是属于系统的数据
  • 子进程在执行程序替换时
  • 当前进程的环境变量数据,不会被替换掉
  • 而且是以父进程为模版继承下来的
  • 所以才会让父进程以内建命令的方式putenv,子进程就能直接获取了

环境变量的数据,在进程的上下文中:

  1. 环境变量会被子进程继承下去,所以他会有全局属性。
  2. 当我们进行程序替换的时候,当前进程的环境变量非但不会被替换,而且是继承父进程的!!
标签: linux

本文转载自: https://blog.csdn.net/m0_63059866/article/details/130839292
版权归原作者 yy_上上谦 所有, 如有侵权,请联系我们删除。

“【Linux】进程控制 — 进程程序替换 + 实现简易shell”的评论:

还没有评论