【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,ppidprintf("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());// 程序替换成test2execl("/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,需要执行以下功能:
① 获取当前工作目录、用户名、主机名。
② 解析用户输入的命令行并执行命令。
③ 内置支持一些常见命令,如
cd、echo、export等。④ 创建子进程来执行普通命令,并支持基本的命令分割和管道处理。
📚头文件
#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下的程序替换】的全部内容,欢迎指正~
码文不易,还请多多关注支持,这是我持续创作的最大动力!
相关文章:
【Linux】剧幕中的灵魂更迭:探索Shell下的程序替换
🎬 个人主页:谁在夜里看海. 📖 个人专栏:《C系列》《Linux系列》《算法系列》 ⛰️ 一念既出,万山无阻 目录 📖一、进程程序替换 1.替换的演示 ❓替换与执行流 ❓程序替换≠进程替换 2.替换的原理 …...
38 基于单片机的宠物喂食(ESP8266、红外、电机)
目录 一、主要功能 二、硬件资源 三、程序编程 四、实现现象 一、主要功能 基于STC89C52单片机,采用L298N驱动连接P2.3和P2.4口进行电机驱动, 然后串口连接P3.0和P3.1模拟ESP8266, 红外传感器连接ADC0832数模转换器连接单片机的P1.0~P1.…...
Unity中的数学应用 之 角色移动中单位化向量的妙用 (小学难度)
最近准备从简单到困难跟几个教程用以加强自己的业务能力,相信很多小伙伴都做过胡闹厨房这一个案例,其实这个案例比较初级,但是也包含了很多平常可能注意不到小细节,所以我就以它为举例,拓展其中的数学知识 CodeMonkey教…...
设置ip和代理DNS的WindowsBat脚本怎么写?
今天分享一个我们在工作时,常见的在Windows中通过批处理脚本(.bat 文件)来设置IP地址、代理以及DNS 相关配置的示例,大家可以根据实际需求进行修改调整。 一、设置静态IP地址脚本示例 以下脚本用于设置本地连接(你可…...
字符串分割转换(Java Python JS C++ C )
题目描述 给定一个非空字符串S,其被N个‘-’分隔成N+1的子串,给定正整数K,要求除第一个子串外,其余的子串每K个字符组成新的子串,并用‘-’分隔。 对于新组成的每一个子串,如果它含有的小写字母比大写字母多,则将这个子串的所有大写字母转换为小写字母; 反之,如果它…...
【Maven】项目创建
3. Maven的应用 本章主要内容: 使用 Maven 创建 JavaSE 项目使用 Maven 创建 JavaWeb 项目,在本地部署 Tomcat 测试导入 Maven 项目 3.1 基于Maven开发JavaSE的项目 3.1.1 流程 1、File—>new—>Project—>Empty Project Location࿱…...
number的++和--运算 C#
number10 请计算num number --number - number number就是先对number运算,然后再给number赋值--number 先给number赋值,再拿来运算 using System;class Program {static void Main(string[] args){int number 10;int a, b, c, number1, number2;…...
浅谈网络 | 应用层之HTTPS协议
目录 对称加密非对称加密数字证书HTTPS 的工作模式重放与篡改 使用 HTTP 协议浏览新闻虽然问题不大,但在更敏感的场景中,例如支付或其他涉及隐私的数据传输,就会面临巨大的安全风险。如果仍然使用普通的 HTTP 协议,数据在网络传输…...
2、Three.js初步认识场景Scene、相机Camera、渲染器Renderer三要素
三要素之间关系: 有了虚拟场景Scene,相机录像Camera,在相机小屏幕上看到的Renderer Scene当前空间 Mesh人在场景 Camera相机录像 Renderer显示器上 首先先描述下Scene: 这个场景为三要素之一,一切需要展示的东西都需…...
Deepwave 声波正演和弹性波正演
Deepwave Deepwave 调用 scalar 方法实现声波和弹性波正演。 ######## 声波正演 ###################### import torch import numpy as np import deepwave from deepwave import scalardevice torch.device(cuda if torch.cuda.is_available()else cpu)## Set observation…...
【WRF-Urban】多层建筑能源参数化模型概述:原理
【WRF-Urban】多层建筑能源参数化模型概述:原理 1 概述1.1 原理1.2 使用步骤 2参考 多层建筑能源参数化(Multi-layer Building Energy Parameterization, BEP)模型是一种用于模拟城市环境中多层建筑群的能量交换和微气候影响的参数化模型。该…...
基于Qt实现的自定义树结构容器:设计与应用
在Qt框架中,尽管其提供了许多强大的容器类(如 QList, QMap, QTreeWidget 等),但缺少一个通用的、灵活的树结构容器,直接支持多层级数据管理。为了满足这些需求,本文设计并实现了一个可复用的自定义树结构容…...
网络命令Linux
目录 一,Linux 二,CMD 一,Linux ping www.baidu.com 测试联网 -c 2 次数,ping几次 , -i 间隔 -W timeout 超时时间,等待响应的超时时间 ss -lntup |grep -w 22 netstat -lntup |grep -w 22 lsof -i:22 ls…...
简单的Activiti Modoler 流程在线编辑器
简单的Activiti Modoler 流程在线编辑器 1.需求 我们公司使用的流程是activiti5.22.0,版本有些老了,然后使用的编辑器都是eclipse的流程编辑器插件,每次编辑流程需要打开eclipse进行编辑,然后再导入到项目里面,不是特…...
【NodeJS】Express写接口的整体流程
前提条件 开发 Node.js,首先就必须要安装 Node.js。推荐使用 nvm,它可以随意切换 node 版本。下载 nvm,具体可以看本人另一篇文章:nvm的作用、下载、使用、以及Mac使用时遇到commond not found:nvm如何解决。 nvm官方࿱…...
Oracle 锁表的解决方法及避免锁表问题的最佳实践
背景介绍 在 Oracle 数据库中,锁表或锁超时相信大家都不陌生,是一个常见的问题,尤其是在执行 DML(数据操作语言)语句时。当一个会话对表或行进行锁定但未提交事务时,其他会话可能会因为等待锁资源而出现超…...
关于 vue+element 日期时间选择器 限制只能选当天以及30天之前的日期
业务需求,需要实现选择当天以及30天之前的日期,于是我想到的是利用picker-options去限制可选范围 代码如下 <el-date-pickerv-model"searchData.acceptTime"type"datetimerange"value-format"yyyy-MM-dd hh:mm:ss"styl…...
租辆酷车小程序开发(二)—— 接入微服务GRPC
vscode中golang的配置 设置依赖管理 go env -w GO111MODULEon go env -w GOPROXYhttps://goproxy.cn,direct GO111MODULEauto 在$GOPATH/src 外面且根目录有go.mod 文件时,开启模块支持 GO111MODULEoff 无模块支持,go会从GOPATH 和 vendor 文件夹寻找包…...
如何在 Ubuntu 22.04 上安装 Metabase 数据可视化分析工具
简介 Metabase 提供了一个简单易用的界面,让你能够轻松地对数据进行探索和分析。通过本文的指导,你将能够在 Ubuntu 22.04 系统上安装并配置 Metabase,并通过 Nginx 进行反向代理以提高安全性。本教程假设你已经拥有了一个非 root 用户&…...
MySQL 用户与权限管理
MySQL 是一种广泛使用的关系型数据库管理系统,支持多用户访问和权限控制。在多用户环境下,数据库安全至关重要,而用户和权限管理是数据库管理中最基础也是最重要的一部分。通过合理地创建和管理用户、分配和管理权限、使用角色权限,可以有效地保护数据库,确保数据的安全性…...
shell脚本--常见案例
1、自动备份文件或目录 2、批量重命名文件 3、查找并删除指定名称的文件: 4、批量删除文件 5、查找并替换文件内容 6、批量创建文件 7、创建文件夹并移动文件 8、在文件夹中查找文件...
1688商品列表API与其他数据源的对接思路
将1688商品列表API与其他数据源对接时,需结合业务场景设计数据流转链路,重点关注数据格式兼容性、接口调用频率控制及数据一致性维护。以下是具体对接思路及关键技术点: 一、核心对接场景与目标 商品数据同步 场景:将1688商品信息…...
在Ubuntu中设置开机自动运行(sudo)指令的指南
在Ubuntu系统中,有时需要在系统启动时自动执行某些命令,特别是需要 sudo权限的指令。为了实现这一功能,可以使用多种方法,包括编写Systemd服务、配置 rc.local文件或使用 cron任务计划。本文将详细介绍这些方法,并提供…...
Spring AI 入门:Java 开发者的生成式 AI 实践之路
一、Spring AI 简介 在人工智能技术快速迭代的今天,Spring AI 作为 Spring 生态系统的新生力量,正在成为 Java 开发者拥抱生成式 AI 的最佳选择。该框架通过模块化设计实现了与主流 AI 服务(如 OpenAI、Anthropic)的无缝对接&…...
C# SqlSugar:依赖注入与仓储模式实践
C# SqlSugar:依赖注入与仓储模式实践 在 C# 的应用开发中,数据库操作是必不可少的环节。为了让数据访问层更加简洁、高效且易于维护,许多开发者会选择成熟的 ORM(对象关系映射)框架,SqlSugar 就是其中备受…...
IoT/HCIP实验-3/LiteOS操作系统内核实验(任务、内存、信号量、CMSIS..)
文章目录 概述HelloWorld 工程C/C配置编译器主配置Makefile脚本烧录器主配置运行结果程序调用栈 任务管理实验实验结果osal 系统适配层osal_task_create 其他实验实验源码内存管理实验互斥锁实验信号量实验 CMISIS接口实验还是得JlINKCMSIS 简介LiteOS->CMSIS任务间消息交互…...
Swagger和OpenApi的前世今生
Swagger与OpenAPI的关系演进是API标准化进程中的重要篇章,二者共同塑造了现代RESTful API的开发范式。 本期就扒一扒其技术演进的关键节点与核心逻辑: 🔄 一、起源与初创期:Swagger的诞生(2010-2014) 核心…...
省略号和可变参数模板
本文主要介绍如何展开可变参数的参数包 1.C语言的va_list展开可变参数 #include <iostream> #include <cstdarg>void printNumbers(int count, ...) {// 声明va_list类型的变量va_list args;// 使用va_start将可变参数写入变量argsva_start(args, count);for (in…...
Cilium动手实验室: 精通之旅---13.Cilium LoadBalancer IPAM and L2 Service Announcement
Cilium动手实验室: 精通之旅---13.Cilium LoadBalancer IPAM and L2 Service Announcement 1. LAB环境2. L2公告策略2.1 部署Death Star2.2 访问服务2.3 部署L2公告策略2.4 服务宣告 3. 可视化 ARP 流量3.1 部署新服务3.2 准备可视化3.3 再次请求 4. 自动IPAM4.1 IPAM Pool4.2 …...
通过MicroSip配置自己的freeswitch服务器进行调试记录
之前用docker安装的freeswitch的,启动是正常的, 但用下面的Microsip连接不上 主要原因有可能一下几个 1、通过下面命令可以看 [rootlocalhost default]# docker exec -it freeswitch fs_cli -x "sofia status profile internal"Name …...
