【Linux】剧幕中的灵魂更迭:探索Shell下的程序替换

🎬 个人主页:谁在夜里看海.

📖 个人专栏:《C++系列》《Linux系列》《算法系列》

⛰️ 一念既出,万山无阻


目录

📖一、进程程序替换

1.替换的演示

❓替换与执行流

❓程序替换≠进程替换

2.替换的原理

📚 系统调用exec

📚 进程控制块 (PCB)

📚 内存管理

3. 替换的函数

📚 execl

📚 execv

📚 execp

📚 exece

🚩本质 

📖二、命令行解释器shell

1.shell的本质

2.shell的模拟实现

📚头文件

📚宏定义

📚全局变量

📚获取信息

📚交互式命令行输入

📚字符串分割

📚内置命令

📚普通命令

📚main函数


📖一、进程程序替换

上一篇博客我们讲到了进程的诞生过程:父进程调用fork创建子进程,子进程执行父进程相同的程序。但是很多时候我们希望子进程执行另一个程序,此时就要用到exec函数调用,子进程中调用exec函数之后,该程序就会被调用的程序代替,这就是程序替换

1.替换的演示

#include<stdio.h>
#include<unistd.h>
int main()
{
   int a = 0;
   a++;
   execl("/usr/bin/pwd", "pwd", NULL);
   printf("%d\n", a++);
}

此时程序执行结果:

我们可以看到,原先的程序执行结果应该是打印变量a,但是被替换成了pwd指令(指令本身也是一个可执行程序),这就是程序替换的过程:当进程调用exec函数时,该进程的代码和数据完全被新程序替换,从新程序的启动例程开始执行。

替换与执行流
int main()
{
  int a = 0;
  printf("Before: %d\n",a++);
  execl("/usr/bin/pwd", "pwd", NULL);
  printf("After: %d\n", a++);
}

不对呢,不是说程序替换之后原来的代码和数据都会被替换吗,那为什么这里还会显示原程序的打印信息呢?下面进行分析:

✅虽然进程调用exec函数后会发生程序替换,原程序的代码和数据会被覆盖,但在调用 exec 函数之前,执行流还是要经过原来的步骤的,上述代码中,在调用execl之前,执行流先执行printf函数代码,由于以“\n”结尾,输出缓冲区的数据会被刷新到终端,所以我们能看到“Before: 0”:

修改一下代码,结尾不加“\n”, 此时数据会被保留在输出缓冲区当中,后面又因为发生程序替换,缓冲区的内容被清除了,所以最终终端不会显示"Before: 0"内容:

int main()
{
  int a = 0;
  printf("Before: %d",a++);
  execl("/usr/bin/pwd", "pwd", NULL);
  printf("After: %d\n", a++);
}

❓程序替换≠进程替换

程序替换会改变进程的执行内容,但它不会改变进程的进程ID,也就是说,进程还是原来的进程,程序替换并不是进程替换,且看下面示例:

先写一个可执行程序test2,源代码为:

#include<stdio.h>
#include<unistd.h>

int main()
{
  // 打印当前pid,ppid
  printf("After: pid = %d, ppid = %d\n",getpid(),getppid()); 
}

另一个可执行程序test源代码为:

#include<stdio.h>
#include<unistd.h>

int main()
{
  // \n结尾直接打印当前内容
  printf("Before: pid = %d, ppid = %d\n",getpid(),getppid());
  // 程序替换成test2
  execl("/home/ywh/linux_gitee/test_excel/test2", "test2", NULL);
}

test执行结果:

我们可以看到,程序替换前后都是同一个进程,结论:exec并不创建新进程。

2.替换的原理

📚 系统调用exec

exec 系列函数(如 execl, execv, execve 等)是用来将当前进程的内存空间、程序代码段、数据段等替换成一个新的程序。该系统调用不会创建新进程,而是直接用新程序替换当前进程的内容。

具体来说,exec 调用会:

①:清空当前进程的代码段、数据段、堆栈等。

②:加载并执行新程序的代码段、数据段、堆栈等。

③:保留当前进程的进程 ID (PID)、父进程标识符 (PPID)、文件描述符等。

📚 进程控制块 (PCB)

操作系统通过 进程控制块 (PCB) 来管理进程,每个进程都有一个独立的 PCB,包含了进程的各种状态信息,比如进程的 PID、父进程 ID、程序计数器、堆栈指针等。

当调用 exec 时,进程的 PCB 中的状态信息并没有被改变,操作系统只会根据 exec 调用的参数加载新的程序内容(代码段、数据段等),并且更新程序计数器和堆栈指针等信息。

📚 内存管理

操作系统中的内存管理模块负责为进程分配内存。当进程调用 exec 时,操作系统会:

①:释放原进程的内存(代码段、数据段、堆栈)。

②:加载新程序的内存:从磁盘(例如 ELF 文件或其他可执行文件)中加载新的程序到内存,包括新的代码段、数据段等。

③:更新堆栈和堆的布局,准备新程序的运行环境。

3. 替换的函数

其实有六种以exec开头的函数,统称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[]);

为了便于理解,我们可以把exec后面出现的 l、p、e、v 看作exec的四个选项,下面我们依次介绍这些选项:

📚 execl

l(list) : 参数采用列表 

path:表示要执行的程序路径;

arg:表示程序本身的参数,第一个是程序本身的名称,后续为程序的参数(传递系统指令时,参数就是指令的选项),必须以NULL结尾。

示例:

execl("/bin/ls", "ls", "-l", (char *)NULL);
📚 execv

v(vector) : 参数用数组

path:表示要执行的程序路径;

argv:参数列表,程序的参数以数组的形式传递,数组内部也必须以NULL结尾。

示例:

execv("/bin/ls", (char *[]){"ls", "-l", NULL});
📚 execp

p(path) : 自动搜索环境变量PATH

它可以通过环境变量 PATH 来查找可执行文件,而不需要提供绝对路径。

示例:

execlp("ls", "ls", "-l", (char *)NULL);
📚 exece

e(env) : 表示自己维护环境变量 

execle 允许显式地传递一个 环境变量数组,而不是继承当前进程的环境变量。通过 execle,你可以自定义新进程的环境变量。

示例:

char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
execle("ps", "ps", "-ef", NULL, envp);
🚩本质 

事实上,只有execve才是真正的系统调用,而其他四个函数最后都会调用execve:

📖二、命令行解释器shell

我们在linux学习过程中离不开shell,shell是命令行解释工具,是用户与内核之间的工具,提供了一个接口,通过它,我们可以执行命令、启动程序等与操作系统进行交互。shell解析用户输入的命令,返回执行结果。

❓shell的本质是什么呢?

1.shell的本质

shell本质其实是一个进程

当我们启动一个终端或打开一个命令行窗口的时候,相当于启动了一个shell进程(也叫bash进程),这个进程会等待用户输入的命令,并将命令通过系统调用传递给内核,内核执行相应的操作后,返回给shell。

shell的工作原理就是循环以下操作

1️⃣获取命令行 --> 2️⃣解析命令行 --> 3️⃣fork创建子进程 

--> 4️⃣execve替换子进程 --> 5️⃣wait等待子进程退出 ->1️⃣

根据这些思路,我们可以模拟实现一个shell:

2.shell的模拟实现

实现一个简化版的shell,需要执行以下功能:

① 获取当前工作目录、用户名、主机名。

② 解析用户输入的命令行并执行命令。

③ 内置支持一些常见命令,如cdechoexport等。

④ 创建子进程来执行普通命令,并支持基本的命令分割和管道处理。

📚头文件
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

这些头文件提供了标准输入输出、字符串处理、系统调用等功能。unistd包含与进程相关的函数(如fork,exit)

📚宏定义
#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 44

LEFT、RIGHT、LABLE:用于命令行提示符的格式化;

DELIM:用于命令行字符串的分隔符;

LINE_SIZE、ARGC_SIZE:定义了命令行和参数的缓冲区大小;

EXIT_CODE:用于子进程异常退出的返回值。

📚全局变量
int lastcode = 0;
int quit = 0;
extern char **environ;
char commandline[LINE_SIZE];
char *argv[ARGC_SIZE];
char pwd[LINE_SIZE];
char myenv[LINE_SIZE];

lastcode:保存上一个命令的退出码;

quit:用于控制shell是否退出;

commandline:存储用户输入的命令行字符串;

argv:存储解析后的命令和参数;

pwd:保存当前工作目录;

myenv:存储自定义的环境变量。

📚获取信息
const char *getusername() {
    return getenv("USER");
}

const char *gethostname() {
    return getenv("HOSTNAME");
}

void getpwd() {
    getcwd(pwd, sizeof(pwd));
}

getusername:获取用户名

gethostname:获取主机名

getpwd:获取当前工作目录

📚交互式命令行输入
void interact(char *cline, int size) {
    getpwd();
    printf(LEFT"%s@%s %s"RIGHT""LABLE" ", getusername(), gethostname(), pwd);
    char *s = fgets(cline, size, stdin);
    assert(s);
    (void)s;
    cline[strlen(cline)-1] = '\0';
}

interact函数显示格式化的提示符,并等待用户输入命令。输入命令存储在cline中;输入的命令符末行换行符替换成终止符 '\0'。

📚字符串分割
int splitstring(char cline[], char *_argv[]) {
    int i = 0;
    argv[i++] = strtok(cline, DELIM);
    while(_argv[i++] = strtok(NULL, DELIM));
    return i - 1;
}

splitstring函数使用strtok将输入的命令行字符串按空格和制表符分割成多个命令或参数,存储在指针数组argv中。

📚内置命令
int buildCommand(char *_argv[], int _argc) {
    if(_argc == 2 && strcmp(_argv[0], "cd") == 0) {
        chdir(argv[1]);
        getpwd();
        sprintf(getenv("PWD"), "%s", pwd);
        return 1;
    }
    else if(_argc == 2 && strcmp(_argv[0], "export") == 0) {
        strcpy(myenv, _argv[1]);
        putenv(myenv);
        return 1;
    }
    else if(_argc == 2 && strcmp(_argv[0], "echo") == 0) {
        if(strcmp(_argv[1], "$?") == 0) {
            printf("%d\n", lastcode);
            lastcode=0;
        }
        else if(*_argv[1] == '$') {
            char *val = getenv(_argv[1]+1);
            if(val) printf("%s\n", val);
        }
        else {
            printf("%s\n", _argv[1]);
        }
        return 1;
    }
    if(strcmp(_argv[0], "ls") == 0) {
        _argv[_argc++] = "--color";
        _argv[_argc] = NULL;
    }
    return 0;
}

提供了几个内置命令:

cd:改变当前目录

export:设置一个新的环境变量

enho:打印变量值或退出码

📚普通命令

队友普通命令的执行,需要调用exec程序替换成目标命令的程序:

void NormalExcute(char *_argv[]) {
    pid_t id = fork();
    if(id < 0) {
        perror("fork");
        return;
    }
    else if(id == 0) {
        execvp(_argv[0], _argv);
        exit(EXIT_CODE);
    }
    else {
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if(rid == id) {
            lastcode = WEXITSTATUS(status);
        }
    }
}

NormalExcute使用fork创建子进程,子进程调用execvp,替换当前程序,父进程等待子进程结束。

📚main函数
int main() {
    while(!quit) {
        interact(commandline, sizeof(commandline));
        int argc = splitstring(commandline, argv);
        if(argc == 0) continue;
        int n = buildCommand(argv, argc);
        if(!n) NormalExcute(argv);
    }
    return 0;
}

main函数进入循环,不断接收用户输入的命令并解析执行。

如果命令是内置命令,则在当前进程中执行;如果是普通命令,通过程序替换在子进程中执行。


以上就是【剧幕中的灵魂更迭:探索Shell下的程序替换】的全部内容,欢迎指正~  

码文不易,还请多多关注支持,这是我持续创作的最大动力! 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

DIY-Tomcat项目 part 1 实现和测试Request以及Response

实现Request package Webserver.src.connector;import java.io.IOException; import java.io.InputStream;/* GET /index.html HTTP/1.1Host: localhost:8888Connection: keep-aliveCache-Control: max-age0Upgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 */public cla…

使用IDEA编写测试用例,复杂度校验

最近我们公司要求开发人员必须写测试用例&#xff0c;组织了TDD培训&#xff0c;测试驱动开发&#xff0c;同时衡量代码的圈复杂度&#xff0c;我记录下初次使用的过程。 编写测试用例&#xff0c;查看用例覆盖度 1、要编写测试用例&#xff0c;并看下测试用例的覆盖度&#…

Linux——用户级缓存区及模拟实现fopen、fweite、fclose

linux基础io重定向-CSDN博客 文章目录 目录 文章目录 什么是缓冲区 为什么要有缓冲区 二、编写自己的fopen、fwrite、fclose 1.引入函数 2、引入FILE 3.模拟封装 1、fopen 2、fwrite 3、fclose 4、fflush 总结 前言 用快递站讲述缓冲区 收件区&#xff08;类比输…

python学opencv|读取图像

【1】引言 前序学习了使用matplotlib模块进行画图&#xff0c;今天开始我们逐步尝试探索使用opencv来处理图片。 【2】学习资源 官网的学习链接如下&#xff1a; OpenCV: Getting Started with Images 不过读起来是英文版&#xff0c;可能略有难度&#xff0c;所以另推荐一…

多模态大模型打造沉浸式社交体验,Soul App创始人张璐团队海外首秀GITEX GLOBAL

2024年10月14日至18日,全球科技盛会GITEX GLOBAL在迪拜举办,各大科技企业汇聚一堂,展示前沿技术。在这次大会上,中国社交平台Soul App首次亮相国际大型展会,展示了由Soul App创始人张璐团队研发的多模态AI交互方案,吸引了海外来宾的目光。作为国内较早将AI引入社交关系的社交平…

Android 实现悬浮球的功能

Android 实现悬浮球的功能 在 Android 中&#xff0c;实现悬浮球可以通过以下方式实现&#xff0c;常见的方法是使用 WindowManager 创建一个悬浮窗口。以下是具体的实现步骤&#xff1a; 1. 配置权限 在 AndroidManifest.xml 中添加悬浮窗权限&#xff1a; <uses-permis…

Python与Amazon DynamoDB:构建高效爬虫数据存储解决方案

欢迎访问个人博客地址&#xff1a;https://blog.jiumoz.top/archives/pythonyu-amazon-dynamodb-gou-jian-gao-xiao-pa-chong-shu-ju-cun-chu-jie-jue-fang-an 1. 引言 1.1. 爬虫与NoSQL Python爬虫是一种通过模拟浏览器行为&#xff0c;从互联网上自动抓取数据的工具。它利…

Git 进程占用报错-解决方案

背景 大仓库&#xff0c;由于开发者分支较多&#xff0c;我们在使用 git pull 或 git push 等命令时&#xff08;与远端仓库交互的命令&#xff09;&#xff0c;不知之前配置了什么&#xff0c;我的电脑会必现以下报错&#xff08;有非常长一大串报错-不同分支的git进程占用报…

STM32完全学习——使用标准库完成PWM输出

一、TIM2初始化 我这里使用的是STM32F407ZGT6这个芯片&#xff0c;我这里使用的是定时器TIM2来完成PWM输出&#xff0c;由于这里没有使用中断&#xff0c;因此不需要初始化NVIC&#xff0c;下面先来进行定时器的相关初始化 TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;R…

【爬虫框架:feapder,管理系统 feaplat】

github&#xff1a;https://github.com/Boris-code/feapder 爬虫管理系统 feaplat&#xff1a;http://feapder.com/#/feapder_platform/feaplat 爬虫在线工具库 &#xff1a;http://www.spidertools.cn &#xff1a;https://www.kgtools.cn/1、feapder 简介 对于学习 Python…

路由引入中次优路由和路由环路问题

A公司用的是IS-IS&#xff0c;B公司用的是OSPF&#xff0c;现在这两个公司要合并&#xff0c;网络要相通 项目目标 前期准备 配置IP地址&#xff1a;完成IP地址规划&#xff0c;A公司和B公司内部网络通过路由器R2和R4环回接口模拟。配置路由器接口的IP地址并测试所有直连链路的…

使用Compose Multiplatform开发跨平台的Android调试工具

背景 最近对CMP跨平台很感兴趣&#xff0c;为了练手&#xff0c;在移动端做了一个Android和IOS共享UI和逻辑代码的天气软件&#xff0c;简单适配了一下双端的深浅主题切换&#xff0c;网络状态监测&#xff0c;刷新调用振动器接口。 做了两年多车机Android开发&#xff0c;偶…

通过LabVIEW项目判断开发环境是否正版

在接收或分析他人提供的LabVIEW项目时&#xff0c;判断其开发环境是否为正版软件对于保护知识产权和避免使用非法软件至关重要。本文将详细介绍如何通过项目文件、可执行程序及开发环境信息判断LabVIEW是否为正版。 ​ 1. 从项目文件判断 LabVIEW项目的源码&#xff08;VI 文件…

React的ts文件中通过createElement拼接一段内容出来

比如接口返回一个值 const values [23.00, 40.00/kg];想做到如下效果&#xff0c; 如果单纯的用render渲染会很简单&#xff0c; 但是在ts文件中处理&#xff0c;所以采用了createElement拼接 代码如下&#xff1a; format: (values: string[]) > {if (!values || !val…

计算机网络 实验七 NAT配置实验

一、实验目的 通过本实验理解网络地址转换的原理和技术&#xff0c;掌握扩展NAT/NAPT设计、配置和测试。 二、实验原理 NAT配置实验的原理主要基于网络地址转换&#xff08;NAT&#xff09;技术&#xff0c;该技术用于将内部私有网络地址转换为外部公有网络地址&#xff0c;从…

002 MATLAB语言基础

01 变量命名规则 变量名只能由字母、数字和下划线组成&#xff0c;且必须以字母开头&#xff1b; 变量名区分字母的大小写&#xff1b; 变量名不能超过最大长度限制&#xff1b; 关键字不能作为变量名&#xff0c;如for、end和if等&#xff1b; 注意&#xff1a;存变量命名时…

无线WiFi网络版毫米波雷达人体传感器,智能家居节能减排照明有人无人识别

在这个科技日新月异的时代&#xff0c;智能家居已经不再是遥不可及的未来概念&#xff0c;而是悄然融入了我们的日常生活&#xff0c;为我们的生活带来了未有的便捷与舒适。今天&#xff0c;让我们一起探索一项创新性的智能家居技术——飞睿智能无线WiFi网络版毫米波雷达人体传…

AI前景分析展望——GPTo1 SoraAI

引言 人工智能&#xff08;AI&#xff09;领域的飞速发展已不仅仅局限于学术研究&#xff0c;它已渗透到各个行业&#xff0c;影响着从生产制造到创意产业的方方面面。在这场技术革新的浪潮中&#xff0c;一些领先的AI模型&#xff0c;像Sora和OpenAI的O1&#xff0c;凭借其强大…

Qwen2.5-7B大模型微调记录

Qwen2.5-7B大模型微调记录 研究需要&#xff0c;需要搞一个大模型出来&#xff0c;没有太多的时间自己训练&#xff0c;准备用现成的开源大模型&#xff0c;然后结合研究方向进行微调 前前后后折腾大半个月&#xff0c;总算做完了第一个微调的大模型&#xff0c;模型基于阿里…

Spring Aop 中对JoinPoint的理解

以下是源码中对 JoinPoint 的描述 A runtime joinpoint is an event that occurs on a static joinpoint (i.e. a location in a program). For instance, an invocation is the runtime joinpoint on a method (static joinpoint). The static part of a given joinpoint can…