进程程序替换和shell实现

        先前fork说创建子进程执行代码,如何让子进程执行和父进程完全不一样的代码?程序替换。

一 单进程替换演示

        1 execl函数使用

    最近转到在vs code下写代码,之前也在xhell下用过execl函数,所以才想写篇博客总结总结,没想到在vs code下写了一句简单的execl却总是替换失败。想来想去也不知为何,感觉翻车了。

        后来才知道execl的路径没给对,usr前面要带/,因为usr是根目录下的,路径少了个根目录系统也不知道去哪找usr,因为可能有很多目录都叫usr,可是当我改了后。

        嗯?还是失败了,我记得早上就是加了/没反应,我才奇怪的,临近中午我才意识到vs code的一个坑人的点,你修改了代码,不保存,编译的还是旧代码,只有ctrl + s保存一下,再编译才是新程序。最最重要的是这个ctrl + s不能在输入终端的时候保存,必须得在代码处保存。

哪里是终端呢,下面这就是终端,ctrl+~可进入。

二 替换原理

        替换原理

        单进程下是直接把原程序的所有代码替换,那如何从头开始执行,cpu上的寄存器是有保存下一句指令的地址的,现在代码要更换了,你说这个地址还有用吗那如何拿到新代码的地址呢,先来聊聊替换时做了什么,如上图,execl函数的其中一个参数是/usr/bin/ls,此时系统会把这个ls可执行文件的代码加载到内存。

        由编译原理的内容可得,代码在编译的时候就已经分段了,因为要方便操作系统拿去分段映射,也已经给每条数据代码生成了地址,这个地址是逻辑地址,和虚拟地址几乎没区别,应该可以直接填入页表左侧,而且还会生成一个表头,让系统知道各段的起始位置在哪,而这样cpu拿到表头就可以从代码段开始执行了,此时页表,将新程序的物理地址填入,替换成功后,先前的代码和数据占的内存按理说是要被释放的,会不会没成功,然后也被释放了,os应该不会不会干出还没替换完就断自己后路的事。

三  多进程替换

        当我们了解了单进程的替换工作后,我们就知道此时父子进程一定会发生写时拷贝,因为父子进程代码和数据是独立的,我们还发现pid一直不变,这说明进程替换没有创建新进程。

补充 1

        为什么子进程进程内的下一句代码为啥不执行,因为已经被替换了,替换成功就不执行,失败了呢?继续往后执行。

补充 2 exec函数总结

        第一个参数是为了找到程序,要么写全路径,要么带P,去PATH变量里存的路径去找,这个PATH变量也是怪怪的,有时候我把自己的可执行程序的路径写入了,然后我第一个参数就不传具体路径了,结果还是说没找到。

        第二个参数是和选项有关,我之前好奇明明我第一个参数/usr/bin/ls不是已经传了ls吗(那为什么第二个参数还要传ls呢,不知道啊,大佬就是这么设计的,我们直接用就好了)如果是带l的,给main函数(这个main函数就是替换程序ls的main函数)传的参数就像命令行参数一个一个传,以NULL结尾,带v的,则是把这些命令行参数归成数组传参。所以说exec函数带l和带v是冲突的。带e的则与环境变量有关

        exec函数还可以用自己写的程序,甚至是其它语言的代码来替换。

       先前说了替换本质是修改页表和加载代码文件到内存,这个工作肯定是调用了系统调用完成的,而且任意语言写的代码本质都是01数据,那就可以用统一的方式加载到内存,修改页表不就是填地址吗,这和语言一点关系都没有,所以在同一平台下,execl函数可以调用任意语言写的可执行程序,使其变成进程的代码。所以在execl函数看来,都是一样的文件,都是先加载到内存然后修改页表即可。

补充 3 exec函数导环境变量

        既然exec系列函数是可以调用任意的可执行文件,那如果我要传环境变量如何传呢? 也就是说exec系列函数如何传环境变量给另一个可执行文件。由于程序替换,不改变环境变量这些数据,所以执行另一个可执行文件还是能拿到原先的环境变量。如下代码,打印显示即可。

        注:test.cpp就是被编译成了test2这个可执行文件。

        exec系列函数中只要是带e的函数,就是要导入新的环境变量,经过测试,这个新的环境变量表会覆盖当前进程原有的环境变量。

四 xshell实现

        在xhell下也用了不少的命令,而随着进程的学习,我们也就可以初步实现xhell了。每次在linux下下的vim写代码,都要包含好多头文件,下面代码为了简洁,就不显示头文件了,大家到时候一个个man,熟悉熟悉man的使用以及提高看手册的能力。

        先从主函数入手。第一步是一些变量的初始化,在3 解析指令,分割字符串再解释变量保存的什么的,我们直接看2。

int main()
{
    while(1)
    {
        //1 初始化
        rdir = 0;
        filename = NULL;
        
        2: 人机互动,接收指令  
        int argc = interact();
        if(!argc)//argc为零,无指令
        {
            continue;
        }
        //3: 分割字符串
        splitstring();
        //4 内建命令
       int ret = BuildCommand(argc); 
        //5 执行普通指令
       if(!ret)
         NormalExecute();
    }
    return 0;
}

2 人机互动 接收指令

        当我们在xshell下输入指令,总会显示这一行,然后就阻塞着等待输入指令。

        所以我们的第一步是先打印bash命令行[USR+主机名+路径]$

        这个路径的获取有点绕,我是用getcwd获取放入数组,再打印的,因为如果是直接获取环境变量,这个路径是一直不变的,至于为什么不变呢?如下测试。

  printf("%s\n",getenv("PWD"));  
  chdir("/home");
  printf("%s\n",getenv("PWD"));
  execl("/usr/bin/pwd","pwd",NULL);

        如下,环境变量PWD并不会改变,所以我猜测chdir改变的目录是当前进程的工作目录-cwd,在进程运行时用ls /proc/+pid才可以显示。

       还有就是PWD要显示当前目录,ls显示目录下的文件信息,这两个命令用的cwd变量存的目录,根本就不用环境变量PWD,所以我猜测cd命令改变的也是工作目录,只是顺便更新了pwd环境变量。代码里我就没改这个环境变量了,就直接用个数组来保存当前路径了。

#define LEFT "["
#define RIGHT "]"
#define LABLE "# "
#define LINE_SIZE 1024
char pwd[LINE_SIZE];
const char*getusr()
{
    return getenv("USER");
}
const char*gethost()
{
    return getenv("HOSTNAME");
}
void getpwd()
{
    getcwd(pwd,LINE_SIZE);

    getcwd是获取当前工作目录,并拷贝到pwd数组中,数组长度为1024
}
int interact()
{
    
    getpwd();
    
    封装成三个函数分别获取USR,Host和Pwd
    printf(LEFT"%s@%s %s"RIGHT LABLE,getusr(),gethost(),pwd);

    接收指令,保存到command数组中,用于后面解析指令

    fgets(command,LINE_SIZE,stdin);

    //ls -a\n去除\n字符
    command[strlen(command) - 1] = '\0';

   检查重定向符号 
   
    check_command(command); 这个函数我放在3 解析指令一同说明
   return strlen(command)-1;
}

3 解析指令,分割字符串

        因为我们输入的ls -a -l被当成“ls -a -l”整个字符串了,所以我们需要用空格分离成一个一个字符,后面要用于execl的传参。

#define DELIM " \t" 分隔符的宏定义,包括空格和Tab键
#define OUT_RDIR 1 
#define IN_RDIR  2
#define APPEND_RDIR 3 
#define LINE_SIZE 1024
#define OPTION_SIZE 32

char* filename;
int rdir = 0;
char command[LINE_SIZE];
char* argv[OPTION_SIZE];

void check_command(char * pos) 
{
    while(*pos)  遍历指令数组
    {
        if(*pos == '<')//输入重定向
        {
            rdir = IN_RDIR;
            *pos++ = '\0';
            while(isspace(*pos)) pos++; 跳过空格 cat <    test.txt
            filename = pos;
            break;
        }

        else if((*pos)== '>') 输出重定向  cat >test.txt
        {
            *pos++ = '\0'; 
            if((*pos)== '>')追加重定向 cat >>test.txt
            {  
                rdir = APPEND_RDIR;
                *pos++ = '\0';
                while(isspace(*pos)) pos++;//跳过
                    filename = pos;
                break;
            }
            rdir = OUT_RDIR;
            while(isspace(*pos)) pos++;//跳过
            filename = pos;            
            break;
        }
        else 
        {
            ;
        }
        pos++;
    }

}

void splitstring()
{
    //"ls -a -l"
    int i = 0;

   strtok函数的使用有些奇怪,第一次要传字符数组,之后的解析就传NULL空指针
 第二个参数是DELIM,这个参数是分隔符集合,strtok只要碰到" \t"内的字符
一次切割完成,然后赋值给argv数组。

    argv[i++] = strtok(command,DELIM); 
    while(argv[i++] = strtok(NULL,DELIM));
    
}

        如上rdir和filename记录了重定向的类型以及,重定向的文件名,所以每次循环是一次指令输入到处理,下一次输入新指令,这两个变量的信息就要清空。

4 执行内建命令

        此时我们终于可以解开内建命令的面纱了,还是那句话,真的就只是一个函数而已。

#define LINE_SIZE 1024
#define OPTION_SIZE 32

int lastcode = 0; 保存的是退出码

char command[LINE_SIZE];
char* argv[OPTION_SIZE]; 
char penv[OPTION_SIZE];

int BuildCommand(int argc)
{
    if(strcmp(argv[0],"ls")==0 ) 
   
   在ls -a -l命令中加上颜色选项,我们平时输入的ls -a都是bash默认加上去的
   
    {
        argv[argc++] = "--color";

        argv[argc] = NULL;
    }

    if(strcmp(argv[0],"cd") == 0) chdir改进程的工作目录,当然不能创建子进程执行了。
    {
       chdir(argv[1]);

       改工作目录,并保存到pwd数组中

       getpwd();
       return 1;
    }
    else if(strcmp(argv[0],"echo")== 0) 
    {
        if(strcmp(argv[1],"$?")==0)  输出退出码
        {
            printf("%d\n", lastcode);打印完后,退出码归零,所以多次echo,第二次为0
            lastcode = 0;  
            return 1;
        }
        "echo $PATH"
       else if(argv[1][0] == '$')  输出环境变量
        {
            printf("%s",getenv(argv[1]+1));获取环境变量并打印
            return 1;
        }
    }

       export MYVALUE=10000

    else if(strcmp(argv[0],"export")== 0) 设置环境变量
    {
       memcpy(penv,argv[1],strlen(argv[1]));

       //putenv(argv[1]);不能直接put argv存的是指向command字符数组的指针
       下一次输入命令就被覆盖了,那环境变量就不存在了。

       putenv(penv); //这种保存在一个数组内的方式,只能存一个环境变量,懒得再实现了
      实现主要是为了理解内建命令,不太想完完整整地造一遍 
    }
    return 0;
}

5 执行普通命令

void NormalExecute()
{
    //创建子进程
    int id = fork();
    if(id < 0)
    {
        perror("fork:");
    }
    else if(id == 0)//子进程
    {
        if(rdir == OUT_RDIR) 用dup2函数做重定向
        {
            int fd = open(filename,O_WRONLY|O_CREAT|O_TRUNC,0660);
            dup2(fd,1);
        }
        else if(rdir == IN_RDIR)
        {

            int fd = open(filename,O_RDONLY,0660);
            dup2(fd,0);

        }
        else if(rdir == APPEND_RDIR)
        {

            int fd = open(filename,O_WRONLY|O_CREAT|O_APPEND,0660);
            dup2(fd,1);
        }

        //程序替换
        execvp(argv[0],argv); 这一步才是真正的执行指令,argv[0]是指令名称,
      选项都在argv数组中,直接传入即可,这就是exec函数中带p的传的是选项数组
        exit(2);
    }
    else//父进程
    {
        int status = 0;

        int ret = waitpid(id,&status,0);//阻塞等待

        lastcode = WIFEXITED(status);
       
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小何只露尖尖角

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

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

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

打赏作者

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

抵扣说明:

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

余额充值