【Linux】进程程序替换及shell的模拟实现

作者:@阿亮joy.
专栏:《学会Linux》
座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
在这里插入图片描述

目录

    • 进程程序替换
      • 替换原理
      • 替换函数
        • 1. execl
        • 2. execlp
        • 3. execv
        • 4. exevp
        • 5. execle
        • 6. execve
    • shell 的模拟实现
    • 补充知识
    • 总结

进程程序替换

替换原理

用 fork 函数创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec 函数以执行另一个程序。当进程调用一种 exec 函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用 exec 并不创建新进程,所以调用 exec 前后该进程的 ID 并未改变。

【Linux】进程程序替换及shell的模拟实现_第1张图片

替换函数

exec 函数族提供了一个在进程中启动另一个程序执行的方法,其可以根据指定的文件名或目录名找到可执行程序,并用它来取代原调用进程的数据段、代码段和堆栈段。在执行完之后,原调用进程的内容除了进程 ID 外,其他全部被新的进程替换了。

exec 函数族

#include `

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 char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);

系统调用

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

【Linux】进程程序替换及shell的模拟实现_第2张图片

【Linux】进程程序替换及shell的模拟实现_第3张图片

exec 函数说明

  • exec 函数如果调用成功则加载新的程序从启动代码开始执行,不再返回;如果调用出错则返回 -1。
  • exec 函数调用成功为什么没有返回值呢?因为 exec 函数调用成功,exec 函数之后的代码就不会被执行了,所以 exec 函数调用成功的返回值没有任何意义。故只需要调用失败时的返回值。
  • 参数 pathname:可执行程序的路径名。
  • 参数 filename:可执行程序的名字。
  • l的 exec 函数:可变参数列表,要以NULL结尾。
  • p的 exec 函数:不需要给该函数传可执行程序的路径,只需要告诉该函数要执行程序的名字。该函数会在环境变量 PATH 里的路径中进行程序的查找。
  • v的 exec 函数:v表示 vector,即将所有的执行参数放入数组中,统一传递,而不使用可变参数列表的方式进行传递。
  • e的 exec 函数:自定义环境变量

1. execl

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

int main()
{
    // .c -> exe -> load -> process -> 运行 -> 执行我们现在所写的代码
    printf("process is running.....\n");
    execl("/usr/bin/ls", "ls", NULL);
    // execl的第一个参数是要执行的程序, 第二个参数是如何执行, exec函数都必须以NULL结尾
    printf("process running done...\n");

    return 0;
}

【Linux】进程程序替换及shell的模拟实现_第4张图片

【Linux】进程程序替换及shell的模拟实现_第5张图片
【Linux】进程程序替换及shell的模拟实现_第6张图片

注:printf 函数没有执行的原因是 printf 函数在 execl 之后。execl 函数执行完毕的时候,代码已经被全部覆盖了,开始执行新程序的代码了,所以 printf 函数就无法执行了。如果 exec 函数调用失败,就是程序替换失败,返回 -1,并执行后序的代码。

【Linux】进程程序替换及shell的模拟实现_第7张图片
【Linux】进程程序替换及shell的模拟实现_第8张图片

当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用 exec 函数族中的任意一个函数让自己重生。上述的场景就是如此,当这种情况并不常见。

如果一个进程想执行拎一个程序,那么它就可以调用 fork 函数新建一个子进程,然后调用 exec 函数族中的任意一个函数将子进程替换掉,这种情况非常普遍。

#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    printf("process is running.....\n");
    pid_t id = fork();
    assert(id != -1);

    // child
    if(id == 0)
    {
        sleep(1);
        execl("/usr/bin/ls", "ls", "-a", "-l", "--color=auto", NULL);
        exit(1);    // 进程替换失败
    }
    
    int status = 0;
    pid_t ret = waitpid(id, &status, 0);
    if(ret > 0)
    {
        printf("wait success, exit code:%d, signal number:%d\n", (status >> 8) & 0xFF, status & 0x7F);
    }
    return 0;
}

【Linux】进程程序替换及shell的模拟实现_第9张图片

子进程进行进程替换并不会影响父进程,因为进程的独立性!虚拟地址空间加上页表来保证进程的独立性,一旦有执行流想替换代码或者数据,就会发生写时拷贝!

【Linux】进程程序替换及shell的模拟实现_第10张图片

2. execlp

int execlp(const char *file, const char *arg, ...);

【Linux】进程程序替换及shell的模拟实现_第11张图片

【Linux】进程程序替换及shell的模拟实现_第12张图片
上面的代码有两个 ls, 这两个 ls 并不重复。第一个 ls 是告诉系统要执行哪个程序,第二个 ls 是如何执行!

3. execv

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

【Linux】进程程序替换及shell的模拟实现_第13张图片
【Linux】进程程序替换及shell的模拟实现_第14张图片

4. exevp

int execvp(const char *file, char *const argv[]);

【Linux】进程程序替换及shell的模拟实现_第15张图片
【Linux】进程程序替换及shell的模拟实现_第16张图片

以上的进程替换替换的全是系统写好的程序,其实我们也可以替换我们自己写的程序。

【Linux】进程程序替换及shell的模拟实现_第17张图片
可生成多个可执行程序的 Makefile 文件

【Linux】进程程序替换及shell的模拟实现_第18张图片
【Linux】进程程序替换及shell的模拟实现_第19张图片
【Linux】进程程序替换及shell的模拟实现_第20张图片

用 C语言程序调用 C++ 的程序

【Linux】进程程序替换及shell的模拟实现_第21张图片

【Linux】进程程序替换及shell的模拟实现_第22张图片

【Linux】进程程序替换及shell的模拟实现_第23张图片

用 C语言程序调用 python 的程序

【Linux】进程程序替换及shell的模拟实现_第24张图片

【Linux】进程程序替换及shell的模拟实现_第25张图片

【Linux】进程程序替换及shell的模拟实现_第26张图片
【Linux】进程程序替换及shell的模拟实现_第27张图片
可以使用程序调换,调用任何后端语言对应的可执行程序!

5. execle

int execle(const char *path, const char *arg, ...,char *const envp[]);

【Linux】进程程序替换及shell的模拟实现_第28张图片

传入自定义环境变量
【Linux】进程程序替换及shell的模拟实现_第29张图片

【Linux】进程程序替换及shell的模拟实现_第30张图片

传入系统的环境变量

【Linux】进程程序替换及shell的模拟实现_第31张图片
【Linux】进程程序替换及shell的模拟实现_第32张图片

execle 函数能够传入环境变量,但是我们发现传入系统环境变量,就不能传入自定义环境变量了。如果我们先要两个都有的话,就可以借助 putenv 函数了。

【Linux】进程程序替换及shell的模拟实现_第33张图片

【Linux】进程程序替换及shell的模拟实现_第34张图片

当使用 exec 函数将程序加载到内存的时候,其在调用 main 函数之前首先调用一个特殊的例程,并且将此启动例程指定为程序的起始位置。这个启动例程将从内核取得该可执行程序的命令行参数和环境变量,然后传递给 main 函数。

尽管前 4 个 exec 函数没有传环境变量,但是子进程照样能够通过 environ 拿到默认的环境变量,其是通过进程地址空间的方式让子进程拿到的!

6. execve

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

在程序替换中,execve 是系统调用。其余 6 个函数都是对 execve 系统调用做的封装,以满足开发者的需求。

和 mian 函数的命令行参数结合

【Linux】进程程序替换及shell的模拟实现_第35张图片
【Linux】进程程序替换及shell的模拟实现_第36张图片

以上就实现了用我们的程序去执行系统的程序。如果再把前面的./myexec去掉,就相当于我们自己写了个shell。那么接下来,我们就模拟实现一个简易的shell

shell 的模拟实现

现在我们已经学习到了进程创建、进程退出、进程登台、进程程序替换等知识,那么我们理解这些知识模拟实现简易版的命令行解释器 shell。

注:本次模拟实现的 shell 并不是十全十美的,多少会有一些 BUG。对于一些常用的命令,还是能实现的。

实现思路:

  • 通常来说,shell 读取一行新的输入,对输入进行命令解析。然后创建子进程并进行程序替换执行输入的命令。但是对于一些内建(内置)命令,shell 会自己执行,而不是通过创建子进程再进行程序替换的方式。
  • 什么是内建(内置命令)?不需要创建子进程来执行,而是让 shell 自己执行的命令称为内建命令或者内置命令,其本质是调用相应的系统接口。
  • echo 和 cd 就是常见的内建命令。因为 echo 是个内建命令,命令行解释器 bash 不会创建子进程来执行 echo 命令而是自己去执行该目录,所以 echo 能够输出不具有全局属性的本地变量。
  • shell 的循环过程
    • 获取命令行
    • 解析命令行
    • 建立一个子进程(fork)
    • 替换子进程(execvp)
    • 父进程等待子进程退出(wait)

【Linux】进程程序替换及shell的模拟实现_第37张图片

关于 cd 为什么是内建命令,我们需要了解什么是当前路径!当前路径就是当前进程的工作路径。默认情况下,当前路径就是可执行程序所处的路径。进程的工作路径可以通过系统调用chdir来修改。如果我们创建子进程来执行 cd 命令的话,子进程执行 cd 命令改变子进程的工作路径。而子进程执行 cd 命令后就退出了,并不会改变父进程 bash 的工作路径。所以说,要想改变父进程 bash 的工作路径,就只能父进程 bash 来执行 cd 命令了。所以,cd 也是内建命令。

让子进程执行 cd 命令的情况

【Linux】进程程序替换及shell的模拟实现_第38张图片

【Linux】进程程序替换及shell的模拟实现_第39张图片

【Linux】进程程序替换及shell的模拟实现_第40张图片

【Linux】进程程序替换及shell的模拟实现_第41张图片

使用 chdir 修改进程的工作路径

【Linux】进程程序替换及shell的模拟实现_第42张图片

【Linux】进程程序替换及shell的模拟实现_第43张图片

myshell 源码

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define NUM 1024	
#define OPT_NUM 64  // 命令行参数的最多个数

char lineCommand[NUM];
char* myargv[OPT_NUM];
// 上一个进程的退出信息
int lastCode = 0;
int lastSignal = 0;

int main()
{
    while(1)
    {
        char* user = getenv("USER");	// 当前登录用户
        // 根据用户输出对应的提示信息, get_current_dir_name函数可以获得当前的工作路径
        if(strcmp(user, "root") == 0)
        {
            printf("[%s@%s %s]# ", user, getenv("HOSTNAME"), get_current_dir_name());
        }
        else
        {
            printf("[%s@%s %s]$ ", user, getenv("HOSTNAME"), get_current_dir_name());
        }
        fflush(stdout); // 刷新缓冲区
        
        // 获取用户输入
        char* s = fgets(lineCommand, sizeof(lineCommand) - 1, stdin);
        assert(s != NULL);
        // 清除最后一个\n, abcd\n
        lineCommand[strlen(lineCommand) - 1] = 0;

        // 字符串切割:"ls -a -l" -> "ls" "-a" "-l"
        myargv[0] = strtok(lineCommand, " ");
        int i = 1;
        // 因为无法执行"ll"指令, 所以这里做一下处理
        if(myargv[0] != NULL && strcmp(myargv[0], "ll") == 0)
        {
            myargv[0] = "ls";
            myargv[i++] = "-l";
        }
        if(myargv[0] != NULL && strcmp(myargv[0], "ls") == 0)
        {
            myargv[i++] = "--color=auto";
        }
        // 如果切割完毕, strtok返回NULL, myargv[end] = NULL
        while(myargv[i++] = strtok(NULL, " "));

        // 如果是cd命令, 不需要创建子进程来执行, 让当前进程的父进程shell执行对应的命令, 本质就是调用系统接口
        // 像这种不需要创建子进程来执行, 而是让shell自己执行的命令, 称为内建命令或者内置命令
        // echo和cd就是一个内建命令
        if(myargv[0] != NULL && strcmp(myargv[0], "cd") == 0)
        {
            // 如果cd命令没有第二个参数, 则切换到家目录
            if(myargv[1] == NULL)
            {
                chdir(getenv("HOME"));  // 更改到家目录
            }
            else
            {
                if(strcmp(myargv[1], "-") == 0) // 该功能还有BUG, 因为环境变量的问题
                {
                    chdir(getenv("OLDPWD"));    // 回到上一次所处的路径
                }
                else if(strcmp(myargv[1], "~") == 0)
                {
                    chdir(getenv("HOME"));  // 去到家目录
                }
                else
                {
                    chdir(myargv[1]);   // 更改到指定目录
                }
            }
            continue;   // 不创建子进程, continue回到while循环处
        }

        // 实现echo命令, 当前的echo命令功能也不是很全
        if(myargv[0] != NULL && myargv[1] != NULL && strcmp(myargv[0], "echo") == 0)
        {
            if(strcmp(myargv[1], "$?") == 0)
            {
                printf("%d, %d\n", lastSignal, lastCode);
            }
            else
            {
                printf("%s\n", myargv[1]);
            }
            continue;
        }

        // 创建子进程来执行命令
        pid_t id = fork();
        assert(id != -1);

        // child process
        if(id == 0)
        {
            execvp(myargv[0], myargv);
            exit(1);    // 进程替换失败
        }
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);   // 阻塞等待
        assert(ret > 0);
        lastCode = ((status >> 8) & 0xFF);
        lastSignal = (status & 0x7F);
    }
    return 0;
}

myshell 使用演示

【Linux】进程程序替换及shell的模拟实现_第44张图片

myshell 的源码里已经有了相应的注释,所以就不详细讲解了。我们无法做到使用 cd 命令时,使得 bash 和 myshell 的工作路径一起跟着改变。因为当你登录上 Xshell 时,操作系统已经将 bash 进程给创建好了,myshell 是 bash 的一个子进程,所以 myshell 执行 cd 命令并不会修改 bash 的工作路径。

补充知识

exec 和 exit 就像 call 和 return 一样。一个 C 语言程序有很多函数组成,一个函数可以调用另外一个函数,同时传递给它一些参数。被调用的函数执行一定的操作,然后返回一个值。每个函数都有他的局部变量,不同的函数通过 call 和 return 进行通信。这种通过参数和返回值在拥有私有数据的函数间通信的模式是结构化程序设计的基础。Linux 鼓励将这种应用于程序之内的模式扩展到程序之间。如下图:

【Linux】进程程序替换及shell的模拟实现_第45张图片
一个 C 语言程序可以 fork 和 exec 另一个程序,并传给它一些参数。这个被调用的程序执行一定的操作,然后通过 exit 来返回值。调用它的进程可以通过 wait 来获取 exit 的返回值。

总结

本篇博客主要讲解了进程的程序替换并且综合前面学到的进程创建、进程退出和进程等待的知识模拟实现了一个简易版的命令行解释器 myshell。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!❣️

你可能感兴趣的:(学会Linux,linux,进程程序替换,shell的模拟实现)