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

news2025/4/5 2:19:08

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

目录

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

👉进程程序替换👈

替换原理

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

在这里插入图片描述

替换函数

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

exec 函数族

#include <unistd.h>`

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[]);

在这里插入图片描述

在这里插入图片描述

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 <stdio.h>
#include <unistd.h>

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;
}

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

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

在这里插入图片描述
在这里插入图片描述

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

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

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>

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;
}

在这里插入图片描述

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

在这里插入图片描述

2. execlp

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

在这里插入图片描述

在这里插入图片描述
上面的代码有两个 ls, 这两个 ls 并不重复。第一个 ls 是告诉系统要执行哪个程序,第二个 ls 是如何执行!

3. execv

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

在这里插入图片描述
在这里插入图片描述

4. exevp

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

在这里插入图片描述
在这里插入图片描述

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

在这里插入图片描述
可生成多个可执行程序的 Makefile 文件

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

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

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
可以使用程序调换,调用任何后端语言对应的可执行程序!

5. execle

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

在这里插入图片描述

传入自定义环境变量
在这里插入图片描述

在这里插入图片描述

传入系统的环境变量

在这里插入图片描述
在这里插入图片描述

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

在这里插入图片描述

在这里插入图片描述

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

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

6. execve

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

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

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

在这里插入图片描述
在这里插入图片描述

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

👉shell 的模拟实现👈

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

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

实现思路:

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

在这里插入图片描述

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

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

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

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

在这里插入图片描述

在这里插入图片描述

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 使用演示

在这里插入图片描述

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

👉补充知识👈

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

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

👉总结👈

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/133941.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Mybatis-Plus 映射匹配兼容性

目录 问题一&#xff1a;表字段与编码属性设计不同步 问题二&#xff1a;编码中添加了数据库中未定义的属性 问题三&#xff1a;采用默认查询开放了更多的字段查看权限 TableField 问题四&#xff1a;表名与编码开发设计不同步 TableName 从表中查询出数据&#xff0c;并…

31.项目部署

目录 1 一些概念 1.1 项目部署 1.2 WSGI 1.3 uWSGI 1.4 Nginx 2 安装环境与迁移项目 2.1 项目内容 2.2 项目配置 2.2.1 DEBUG 2.2.2 STATIC_ROOT 2.2.3 ALLOWED_HOST 3 uWSGI 3.1 安装uWSGI 3.2 配置uWSGI 3.3 启动 uWSGI 3.4 停止 uWSGI 4 …

官方更新:基于VRA Tokenomics 社区常见问题解答

您好 Verasity 社区&#xff0c; 我们最近通过 Medium 发布了最新的代币经济学&#xff0c;您可以在此处阅读。 我们引入了一些新概念来促进我们作为产品的增长&#xff0c;例如我们的企业收购基金&#xff0c;我们还澄清了我们的流通和总供应量。 在我们的代币经济学文章发布…

SpringBoot概念、创建、运行、Spring Boot 配置文件

Spring Boot 就是 Spring 框架的脚⼿架&#xff0c;它就是为了快速开发 Spring 框架⽽诞⽣的。 1.Spring Boot 优点 快速集成框架&#xff0c;Spring Boot 提供了启动添加依赖的功能&#xff0c;⽤于秒级集成各种框架。内置运⾏容器&#xff0c;⽆需配置 Tomcat 等 Web 容器…

c++11 标准模板(STL)(std::deque)(五)

定义于头文件 <deque> std::deque 迭代器 返回指向容器第一个元素的迭代器 std::deque<T,Allocator>::begin, std::deque<T,Allocator>::cbegin iterator begin(); (C11 前) iterator begin() noexcept; (C11 起) const_iterator begin() const; (C11 前)…

Spring简介及IOC使用

Spring介绍 官网&#xff1a;https://spring.io/ Spring是一个对象的容器&#xff0c;负责管理对象的创建、销毁&#xff0c;以及对象的属性注入&#xff0c;对象的之间的依赖关系。 Spring可以整合其它框架&#xff0c;他基于IOC和AOP来构架多层JavaEE系统&#xff0c;以帮助分…

linux常用命令(六)- 文件属性查看

查看文件类型 - file file命令用于辨识文件类型。 语法 file [-bcvz] [文件或目录...]b&#xff1a;列出辨识结果时&#xff0c;不显示文件名称。c&#xff1a;详细显示指令执行过程&#xff0c;便于排错或分析程序执行的情形。v&#xff1a;显示版本信息。z&#xff1a;尝试…

基于麻雀搜索算法(SSA)优化长短期记忆神经网络参数SSA-LSTM冷、热、电负荷预测(Python代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

第三十六讲:无线AP胖AP模式配置与管理

胖AP(Fat AP)配置一个开放式WLAN非常方便&#xff0c;需要完成的操作包括有线和无线两部分的配置。有线部分即ethernet接口的配置&#xff0c;保证AP能够接入Internet,无线部分的配置包括关联WLAN与VLAN&#xff0c;广播SSID,启用VAP&#xff0c;若无其他DHCP服务器的话&#x…

万物皆有裂痕,那是光进来的地方|2022年个人总结

前两天朋友说要做个视频&#xff0c;记录下2022年的不平凡。突然想到今年是否要写年终总结呢&#xff1f;反复挣扎后&#xff0c;感觉还是应该写个总结&#xff0c;一则记录这一年的不平凡&#xff0c;二则检视这一年的生活与工作状态。- 1 - 工作的危与机2022年对所有的人来说…

Cocos Creator 常见错误排查方法

Cocos Creator 新手开发的时候经常会遇到一些错误不知道如何解决&#xff0c;今天把这些错误总结一下&#xff0c;下次遇到的时候&#xff0c;自己知道如何分析。 对啦&#xff01;这里有个游戏开发交流小组里面聚集了一帮热爱学习游戏的零基础小白&#xff0c;也有一些正在从…

【bp靶场portswigger-服务端2】身份认证漏洞-16个实验(全)

目录 一、身份验证定义 1、三个身份验证因素 2、身份验证和授权 3、身份验证漏洞的产生 4、实验的字典 二、基于密码的登录中的漏洞 1、强制策略 2、用户枚举 3、有缺陷的强力保护 实验1&#xff1a;通过不同响应的用户名枚举 实验4&#xff1a;通过细微不同的响应进…

【Web前端】一文带你吃透CSS(完结篇)

前端学习路线小总结&#xff1a; 基础入门&#xff1a;HTML CSS JavaScript 三大主流框架&#xff1a;VUE REACT Angular 深入学习&#xff1a;小程序 Node jQuery TypeScript 前端工程化 文章目录一.CSS布局-对齐1.水平对齐1.1元素居中对齐1.2文本居中对齐1.3图片居中对齐1.4使…

九章 - 2022年度文章大合集

今天&#xff0c;是九章成立的第639天&#xff0c;元旦节。九章全体员工在这里祝大家&#xff0c;2023年元旦快乐&#xff01;2022年&#xff0c;《九章智驾》这个账号里共发布了87篇原创深度内容&#xff0c;内容覆盖了自动驾驶的各个领域&#xff0c;有一直关注我们的读者的深…

怎么录屏?如何录制足球比赛直播视频?

等了多时&#xff0c;热血的足球赛事终于要来了。 不知道今年你看好哪支球队。皇马&#xff1f;曼联&#xff1f;拜仁&#xff1f; 虽然今年不用熬夜&#xff0c;但总是怕加班错过笔试。 今天给大家送一个安利神器&#xff0c;让你完美记录足球赛事精彩瞬间&#xff01; 如果您…

Eclipse JNI Demo --【Linux】

目录 1、建工程 2、建java类 3、生成头文件 4、CPP实现 5、配置及编译 6、验证 1、建工程 先创建一个android标准工程&#xff1a; 2、建java类 在com.phuket.tour.studio包下&#xff0c;建立一个Java文件Mp3Encoder.java&#xff0c;并且在文件中新增一个方法encode&…

MySQL数据库服务器的优化步骤

在数据库调优中&#xff0c;我们的目标就是响应时间更快&#xff0c; 吞吐量更大。利用宏观的监控工具和微观的日志分析可以帮我们快速找到调优的思路和方式。 当我们遇到数据库调优问题的时候&#xff0c;该如何思考呢&#xff1f; 这里把思考的流程整理成下面这张图。整个流…

Docker环境下配置Es自定义分词器(ik)

分词 一个 tokenizer&#xff08;分词器&#xff09;接收一个字符流&#xff0c;将之分割为独立的 tokens&#xff08;词元&#xff0c;通常是独立 的单词&#xff09;&#xff0c;然后输出 tokens 流。 例如&#xff0c;whitespace tokenizer 遇到空白字符时分割文本。它会将…

路由 OSPF简介、OSPF与其它路由的区别、OSPF区域概念、OSPF路由代名词IR、BR、ABR、ASBR概念简介。

4.2.0 路由 OSPF&#xff08;简介、OSPF与静态、OSPF区域概念、OSPF路由代名词&#xff09; 目录OSPF简介OSPF与其它路由的区别OSPF区域概念OSPF路由代名词概念OSPF简介 OSPF 开放式最短路径优先&#xff08;Open Shortest Path First&#xff09; OSPF动态路由协议属于内部网…

【C语言 数据结构】二叉树

文章目录二叉树一、二叉树的概念二、二叉树的基本形态三、二叉树的性质四、特殊的二叉树五、二叉树的存储结构5.1 顺序5.2 链表5.2.1 二叉链表5.2.1 三叉链表六、二叉树的遍历先序遍历&#xff08;T L R&#xff09;中序遍历&#xff08;L T R&#xff09;后序遍历&#xff08;…