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

简介: 【Linux】进程程序替换及shell的模拟实现(下)

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

be80d581b20a4b7596c50ee337c75077.png

f4ddd8707927481e90b6f60ec3f9700f.png


f64524fa55b34436928a1251b1ebf15a.png


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


9830c40b3c4d4b28ae101ebc02948abb.png

7a8e029572874e5c90e9fdb9c42fc899.png56f73a4f7b7b42eb999296c70a6b06ed.png

a4652448e66f4c3eb8716025efae1887.png

可以使用程序调换,调用任何后端语言对应的可执行程序!


5. execle


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

7c319307660143bf885853264f05fcbf.png


传入自定义环境变量

0a8f16ad1bbe4dc9bab9235776840132.png


cd165986fd304514aa8811bc353b54ba.png


传入系统的环境变量

3af141676f2a434da85f40f2bd018cfc.png

30968fe7c0d74ae7acf1982b84cd8672.png

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

af65498114f246a2a9360eedd2036a44.png

c28a288332f7481f92a85027dd985437.png


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


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


6. execve


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


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


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

fd7486d9e81e4cf19a48ed5286fccd3d.png


a5b9ac1074a541c5a14e88b8929420f5.png


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


👉shell 的模拟实现👈


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


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


实现思路:


  • 通常来说,shell 读取一行新的输入,对输入进行命令解析。然后创建子进程并进行程序替换执行输入的命令。但是对于一些内建(内置)命令,shell 会自己执行,而不是通过创建子进程再进行程序替换的方式。
  • 什么是内建(内置命令)?不需要创建子进程来执行,而是让 shell 自己执行的命令称为内建命令或者内置命令,其本质是调用相应的系统接口。

echo 和 cd 就是常见的内建命令。因为 echo 是个内建命令,命令行解释器 bash 不会创建子进程来执行 echo 命令而是自己去执行该目录,所以 echo 能够输出不具有全局属性的本地变量。

shell 的循环过程

获取命令行

解析命令行

建立一个子进程(fork)

替换子进程(execvp)

父进程等待子进程退出(wait)

dca487cad8824413979cea68fd67c796.png




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


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

cd67e71594424ef4859c4d882dfeecb7.png


8b7991fb8bb94e6a8aa1b5f1e4322031.png18f75e42505340198947640f43494541.png


d28d16f77abe4bc7a63da6105f98e61d.png


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


b8b55b62f1b34940a56db450f6e6f9d4.png

11ff999d2cdd4b08b10d48c0d67914f9.png


myshell 源码


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <string.h>
#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 使用演示

99d8d3a3c938487d8d8aa6256f11859d.png


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



👉补充知识👈


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


155a5f104e5443dcb46378e940207bf5.png

一个 C 语言程序可以 fork 和 exec 另一个程序,并传给它一些参数。这个被调用的程序执行一定的操作,然后通过 exit 来返回值。调用它的进程可以通过 wait 来获取 exit 的返回值。



👉总结👈


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














相关文章
|
4月前
|
安全 Linux iOS开发
Nessus Professional 10.10 Auto Installer for RHEL 10, AlmaLinux 10, Rocky Linux 10 - Nessus 自动化安装程序
Nessus Professional 10.10 Auto Installer for RHEL 10, AlmaLinux 10, Rocky Linux 10 - Nessus 自动化安装程序
283 6
Nessus Professional 10.10 Auto Installer for RHEL 10, AlmaLinux 10, Rocky Linux 10 - Nessus 自动化安装程序
|
5月前
|
存储 安全 Unix
七、Linux Shell 与脚本基础
别再一遍遍地敲重复的命令了,把它们写进Shell脚本,就能一键搞定。脚本本质上就是个存着一堆命令的文本文件,但要让它“活”起来,有几个关键点:文件开头最好用#!/usr/bin/env bash来指定解释器,并用chmod +x给它执行权限。执行时也有讲究:./script.sh是在一个新“房间”(子Shell)里跑,不影响你;而source script.sh是在当前“房间”里跑,适合用来加载环境变量和配置文件。
525 9
|
5月前
|
算法 Linux Shell
Linux实用技能:打包压缩、热键、Shell与权限管理
本文详解Linux打包压缩技巧、常用命令与原理,涵盖.zip与.tgz格式操作、跨系统传文件方法、Shell运行机制及权限管理,助你高效使用Linux系统。
Linux实用技能:打包压缩、热键、Shell与权限管理
|
5月前
|
存储 Shell Linux
八、Linux Shell 脚本:变量与字符串
Shell脚本里的变量就像一个个贴着标签的“箱子”。装东西(赋值)时,=两边千万不能有空格。用单引号''装进去的东西会原封不动,用双引号""则会让里面的$变量先“变身”再装箱。默认箱子只能在当前“房间”(Shell进程)用,想让隔壁房间(子进程)也能看到,就得给箱子盖个export的“出口”戳。此外,Shell还自带了$?(上条命令的成绩单)和$1(别人递进来的第一个包裹)等许多特殊箱子,非常有用。
510 2
|
7月前
|
Web App开发 缓存 安全
Linux一键清理系统垃圾:释放30GB空间的Shell脚本实战​
这篇博客介绍了一个实用的Linux系统盘清理脚本,主要功能包括: 安全权限检查和旧内核清理,保留当前使用内核 7天以上日志文件清理和系统日志压缩 浏览器缓存(Chrome/Firefox)、APT缓存、临时文件清理 智能清理Snap旧版本和Docker无用数据 提供磁盘空间使用前后对比和大文件查找功能 脚本采用交互式设计确保安全性,适合定期维护开发环境、服务器和个人电脑。文章详细解析了脚本的关键功能代码,并给出了使用建议。完整脚本已开源,用户可根据需求自定义调整清理策略。
877 0
|
9月前
|
Linux Shell
Centos或Linux编写一键式Shell脚本删除用户、组指导手册
Centos或Linux编写一键式Shell脚本删除用户、组指导手册
266 4
|
9月前
|
Linux Shell 数据安全/隐私保护
Centos或Linux编写一键式Shell脚本创建用户、组、目录分配权限指导手册
Centos或Linux编写一键式Shell脚本创建用户、组、目录分配权限指导手册
514 3
|
5月前
|
Linux 应用服务中间件 Shell
二、Linux文本处理与文件操作核心命令
熟悉了Linux的基本“行走”后,就该拿起真正的“工具”干活了。用grep这个“放大镜”在文件里搜索内容,用find这个“探测器”在系统中寻找文件,再用tar把东西打包带走。最关键的是要学会使用管道符|,它像一条流水线,能把这些命令串联起来,让简单工具组合出强大的功能,比如 ps -ef | grep 'nginx' 就能快速找出nginx进程。
605 1
二、Linux文本处理与文件操作核心命令