从0到1实现Shell!Linux进程程序替换详解

目录

  • 从0到1实现Shell!Linux进程程序替换详解 🚀
    • 引言:为什么进程需要"变身术"?
    • 一、程序替换:进程的"换衣服"魔法 🔄
      • 1.1 什么是程序替换?
      • 1.2 程序替换的原理:内存中的"乾坤大挪移"
      • 1.3 exec函数族:六种"换装"姿势 💃
      • 1.4 动手试试:让进程"变身"执行ls命令
    • 二、fork+exec:Shell的"分身+换装"秘籍 🧙‍♂️
      • 2.1 为什么需要fork?
      • 2.2 fork+exec的经典组合
    • 三、手把手实现迷你Shell:命令行解释器 🛠️
      • 3.1 Shell的工作流程
      • 3.2 实现步骤详解
        • 步骤1:打印个性化提示符(带简化路径)
        • 步骤2:读取命令行输入(带空命令处理)
        • 步骤3:解析命令行参数(更简洁的循环方式)
        • 步骤4:执行命令(标准fork+execvp流程)
      • 3.3 完整代码:终极版迷你Shell!
      • 3.4 测试我们的终极版Shell!
    • 四、常见问题与进阶方向 🚀
      • 4.1 为什么`DirName`函数要特殊处理根目录?
      • 4.2 可以添加哪些高级功能?
    • 总结:从"玩具"到"工具"的进化

在这里插入图片描述
🌟个人主页 :L_autinue_Star

🌟当前专栏:c++进阶


从0到1实现Shell!Linux进程程序替换详解 🚀

引言:为什么进程需要"变身术"?

小伙伴们好呀!👋 在之前的博客里,我们聊了进程的概念和控制,知道了进程就像一个个独立的"工作单元",在操作系统中忙碌地跑来跑去。但你有没有想过:如果一个进程想"跳槽"去执行另一个程序,该怎么办呢? 🤔

比如我们在终端输入ls命令时,bash进程是怎么突然变成ls进程的?今天咱们就来揭开这个神秘面纱——聊聊进程程序替换,最后再手把手教你实现一个迷你版Shell!是不是超期待?😎

一、程序替换:进程的"换衣服"魔法 🔄

1.1 什么是程序替换?

想象一下:你正在扮演奥特曼打小怪兽(当前进程执行代码),突然接到导演通知:“下一场演迪迦!”(需要执行新程序)。你不需要换个人(创建新进程),只需要当场换衣服、换剧本(替换代码和数据)——这就是程序替换!

专业点说:用磁盘上的新程序,完全替换当前进程的代码段和数据段,从新程序的main函数开始执行。进程ID不变,但"灵魂"已经焕然一新~

1.2 程序替换的原理:内存中的"乾坤大挪移"

在这里插入图片描述

进程的地址空间就像一个"舞台":

  • 原来的程序(如bash)在舞台上表演(代码段、数据段)
  • 调用exec函数后,新程序(如ls)会把原来的"道具"(代码/数据)全部清走,换上自己的"行头"
  • 但舞台本身(进程PCB、PID)没变,只是表演者换了

1.3 exec函数族:六种"换装"姿势 💃

Linux给我们提供了6个exec开头的函数,统称exec函数族。它们就像不同款式的"换装魔法棒",用法略有不同但效果一致~

函数名特点栗子
execl参数是列表形式execl("/bin/ls", "ls", "-l", NULL)
execlp自动搜索PATH,不用写全路径execlp("ls", "ls", "-l", NULL)
execle自己传环境变量execle("./myprog", "myprog", NULL, myenv)
execv参数是数组形式char* argv[] = {"ls", "-l", NULL}; execv("/bin/ls", argv)
execvp数组形式+自动搜PATHexecvp("ls", argv)
execve系统调用接口,最底层(其他函数最终调用它)

敲黑板:这些函数如果成功,就不会返回(因为代码段已经被替换了!);只有失败才返回-1。

1.4 动手试试:让进程"变身"执行ls命令

💻 代码示例

#include <unistd.h>
#include <stdio.h>int main() {printf("变身前:我是进程%d\n", getpid());// 用execlp执行ls -l(p表示自动搜PATH)execlp("ls", "ls", "-l", NULL);  // 注意最后一个参数必须是NULL!// 如果执行到这里,说明execlp失败了perror("变身失败");  // 打印错误原因return 1;
}

运行结果:
在这里插入图片描述

🎉 看到了吗?进程从打印"变身前"变成了执行ls -l!如果把execlp换成execl("/bin/ls", "ls", "-l", NULL)效果一样~

二、fork+exec:Shell的"分身+换装"秘籍 🧙‍♂️

2.1 为什么需要fork?

细心的小伙伴会问:"如果程序替换会覆盖当前进程,那bash自己岂不是就消失了?"🤔

没错!所以Shell执行命令时,会先fork一个子进程,然后在子进程中执行程序替换。这样父进程(bash本身)就能安然无恙,继续等待下一个命令~

这就像:餐厅服务员(bash)接到订单(命令)后,不会自己去厨房做菜(执行程序),而是叫一个厨师(子进程)去做,自己继续接待客人~

2.2 fork+exec的经典组合

💻 代码示例

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>int main() {pid_t pid = fork();  // 创建子进程if (pid == 0) {  // 子进程printf("子进程%d:我要变身ls啦!\n", getpid());execlp("ls", "ls", "-l", NULL);exit(1);  // 如果execlp失败,退出子进程} else if (pid > 0) {  // 父进程printf("父进程%d:等待子进程完成...\n", getpid());wait(NULL);  // 等待子进程退出(避免僵尸进程)printf("父进程%d:子进程干完活啦!\n", getpid());}return 0;
}

运行结果:
在这里插入图片描述

三、手把手实现迷你Shell:命令行解释器 🛠️

3.1 Shell的工作流程

一个简易的Shell需要做三件事:

  1. 读取命令:从终端读取用户输入(如ls -l
  2. 解析命令:把命令拆分成可执行程序和参数(如程序"ls"参数"-l")
  3. 执行命令:fork子进程,在子进程中执行程序替换

就像餐厅点餐流程:记录订单(读命令)→ 分析菜品 (解析)→ 厨师做菜(执行)

3.2 实现步骤详解

步骤1:打印个性化提示符(带简化路径)

专业的Shell会显示用户名@主机名 简化路径(如[user@localhost ~]# )。我们新增DirName函数提取路径最后一部分:

#include <string>
using namespace std;#define FORMAT "[%s@%s %s]# "  // 提示符格式宏// 提取路径最后一部分(如"/home/user" → "user")
string DirName(const char *pwd) {string dir = pwd;if (dir == "/") return "/";  // 根目录特殊处理auto pos = dir.rfind("/");   // 查找最后一个斜杠return dir.substr(pos + 1);  // 返回斜杠后的部分
}// 生成并打印提示符
void PrintCommandPrompt() {char prompt[COMMAND_SIZE];snprintf(prompt, sizeof(prompt), FORMAT, GetUserName(), GetHostName(), DirName(GetPwd()).c_str());printf("%s", prompt);fflush(stdout);  // 确保提示符立即显示
}
步骤2:读取命令行输入(带空命令处理)
bool GetCommandLine(char *out, int size) {char *c = fgets(out, size, stdin);  // 读取一行输入if (c == NULL) return false;        // 处理Ctrl+D退出out[strlen(out)-1] = '\0';          // 去掉换行符return strlen(out) > 0;             // 过滤空命令
}
步骤3:解析命令行参数(更简洁的循环方式)
#define MAXARGC 128
char* g_argv[MAXARGC];  // 参数数组
int g_argc = 0;         // 参数个数bool CommandParse(char *commandline) {g_argc = 0;g_argv[g_argc++] = strtok(commandline, " ");  // 第一个参数while ((g_argv[g_argc++] = strtok(nullptr, " ")));  // 循环提取后续参数g_argc--;  // 修正最后一个NULL的计数return true;
}
步骤4:执行命令(标准fork+execvp流程)
int Execute() {pid_t id = fork();if (id == 0) {  // 子进程execvp(g_argv[0], g_argv);  // 执行程序替换exit(1);                    // 替换失败才会执行}// 父进程等待子进程waitpid(id, nullptr, 0);return 0;
}

3.3 完整代码:终极版迷你Shell!

💻 myshell.cpp(支持简化路径显示+模块化设计):

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string>#define COMMAND_SIZE 1024  // 命令最大长度
#define MAXARGC 128        // 参数最大个数
#define FORMAT "[%s@%s %s]# "  // 提示符格式:[用户名@主机名 路径]#// 全局参数数组
char* g_argv[MAXARGC];
int g_argc = 0;// 获取用户名(从环境变量)
const char* GetUserName() {const char* name = getenv("USER");return name ? name : "None";
}// 获取主机名(从环境变量)
const char* GetHostName() {const char* hostname = getenv("HOSTNAME");return hostname ? hostname : "None";
}// 获取当前工作目录(从环境变量)
const char* GetPwd() {const char* pwd = getenv("PWD");return pwd ? pwd : "None";
}// 提取路径最后一部分(简化显示)
std::string DirName(const char* pwd) {std::string dir = pwd;if (dir == "/") return "/";  // 根目录特殊处理size_t pos = dir.rfind("/");return (pos != std::string::npos) ? dir.substr(pos + 1) : dir;
}// 生成命令提示符
void MakeCommandLine(char cmd_prompt[], int size) {snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), DirName(GetPwd()).c_str());
}// 打印命令提示符
void PrintCommandPrompt() {char prompt[COMMAND_SIZE];MakeCommandLine(prompt, sizeof(prompt));printf("%s", prompt);fflush(stdout);  // 刷新缓冲区,确保提示符显示
}// 获取用户输入的命令
bool GetCommandLine(char* out, int size) {char* c = fgets(out, size, stdin);if (!c) return false;  // 处理Ctrl+D退出out[strlen(out) - 1] = '\0';  // 移除换行符return strlen(out) > 0;       // 忽略空命令
}// 解析命令行参数
bool CommandParse(char* commandline) {g_argc = 0;g_argv[g_argc++] = strtok(commandline, " ");  // 第一个参数while ((g_argv[g_argc++] = strtok(nullptr, " ")));  // 循环提取剩余参数g_argc--;  // 修正最后一个NULL的计数return true;
}// 执行命令
int Execute() {pid_t id = fork();if (id == 0) {  // 子进程execvp(g_argv[0], g_argv);  // 执行程序替换perror("command not found");  // 替换失败时提示exit(1);} else if (id > 0) {  // 父进程waitpid(id, nullptr, 0);  // 等待子进程结束}return 0;
}int main() {while (true) {// 1. 打印命令提示符PrintCommandPrompt();// 2. 获取命令行输入char commandline[COMMAND_SIZE];if (!GetCommandLine(commandline, sizeof(commandline)))continue;// 3. 解析命令行参数CommandParse(commandline);// 4. 执行命令Execute();}return 0;
}

3.4 测试我们的终极版Shell!

编译运行:
在这里插入图片描述
我这里并未将dirname函数接入方便和原生的shell更好区别

终极版特性亮点

  • 智能路径显示:自动提取路径最后一部分(如/home/user/Desktop显示为Desktop),提示符更清爽!
  • 模块化设计:拆分为MakeCommandLineGetCommandLine等函数,代码可读性UP!
  • 健壮性提升:过滤空命令输入,处理Ctrl+D优雅退出
  • 错误提示:命令不存在时显示command not found

四、常见问题与进阶方向 🚀

4.1 为什么DirName函数要特殊处理根目录?

如果当前路径是/(根目录),rfind("/")会返回0,substr(1)会得到空字符串。所以需要单独判断,确保根目录显示为/而不是空白~

4.2 可以添加哪些高级功能?

这些高级功能我们将在后续文章中逐步实现,包括内置命令(如cd/exit)、输入输出重定向、管道等核心特性,敬请期待哦!🚀

总结:从"玩具"到"工具"的进化

今天我们不仅学习了:

  • 程序替换的核心原理(exec函数族的使用)
  • fork+exec的经典组合(Shell的实现基石)
    还亲手实现了一个带智能路径显示的模块化Shell

这个Shell虽然简单,但已经包含了真实Shell的核心骨架。进程管理是Linux系统编程的灵魂,而亲手实现Shell能帮你打通"进程→程序替换→用户交互"的任督二脉!👊

有问题欢迎在评论区留言哦~ 下次见!😉

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

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

相关文章

暑期算法训练.2

目录 6.力扣 11.盛水最多的容器 6.1 题目解析&#xff1a; 6.2 算法思路&#xff1a; 6.2.1 暴力解法&#xff1a; 6.2.2 优化算法&#xff1a; 6.3 代码演示&#xff1a; ​编辑 6.4 总结反思&#xff1a; 7.力扣 611.有效的三角形个数 7.1 题目解析&#xff1a; 7.2…

华为OD 消消乐游戏

1. 题意 游戏规则&#xff1a;输入一个只包含英文字母的字符串&#xff0c;字符串中的两个字母如果相邻且相同&#xff0c;就可以消除。 在字符串上反复执行消除的动作&#xff0c;直到无法继续消除为止&#xff0c;此时游戏结束。 输出最终得到的字符串长度。 输入 输入原始…

小白学HTML,操作HTML文件篇(2)

目录 一、添加多媒体 1.添加网页图片 2.添加网页音频 3.添加网页视频 二、创建容器 1. 标签 2.布局 三、创建表格 1.表格标签 2.添加表格表头 3.添加表格标题 一、添加多媒体 在 HTML 网页中可以轻松地使用标签来添加图片、音频、视频等多媒体&#xff0c;而这些多媒体并…

微服务架构中实现跨服务的字段级权限统一控制

结合集中式权限管理、分布式上下文传递、动态策略执行等技术 ​​一、核心架构设计​​ ​​1. 分层控制模型​​ ​​网关层​​:统一校验用户身份与基础权限,拦截非法请求。 ​​服务层​​:基于用户权限动态过滤数据字段,实现业务级控制。 ​​策略中心​​:集中管理权…

【实现100个unity特效之27】使用unity的ShaderGraph实现一个带裁剪边缘光的裁剪效果(2d3d通用)

文章目录普通裁剪效果1、创建一个Lit Shader Graph2、ShaderGraph前置配置3、添加节点4、效果5、修改裁剪方向带边缘色的裁剪1、在裁剪的基础上添加裁剪边缘光2、边缘的亮度3、修改裁剪方向4、效果5、我们可以代码控制它的变化&#xff0c;如下2D3D游戏通用专栏推荐完结普通裁剪…

Android Scoped Storage适配完全指南

Android Scoped Storage适配完全指南关键词&#xff1a;Android、Scoped Storage、适配、存储权限、文件访问摘要&#xff1a;本文将全面介绍Android Scoped Storage的相关知识&#xff0c;从背景出发&#xff0c;详细解释核心概念&#xff0c;阐述其原理和架构&#xff0c;给出…

Typecho集成PHPMailer实现邮件订阅功能完整指南

文章目录 Typecho使用PHPMailer实现文章推送订阅功能详解 1. 背景与需求分析 1.1 为什么选择PHPMailer 1.2 功能需求 2. 环境准备与配置 2.1 安装PHPMailer 2.2 数据库设计 3. 核心功能实现 3.1 邮件服务封装类 3.2 订阅功能实现 3.2.1 订阅表单处理 3.2.2 确认订阅处理 3.3 文…

无线-二层组网-直接转发

文章目录无线二层组网直接转发&#x1f3e1;作者主页&#xff1a;点击&#xff01; &#x1f916;Datacom专栏&#xff1a;点击&#xff01; ⏰️创作时间&#xff1a;2025年07月16日08点00分 无线二层组网 直接转发 本地转发中所有的沿途都需要配置对应VLAN的通过&#xff…

gin go-kratos go-zero框架对比

Gin、Go-Kratos 和 Go-Zero 是 Go 语言中三种常见的服务框架&#xff0c;它们在定位、设计理念、复杂度和适用场景上差异较大。下面我们从功能定位、设计理念、优劣对比、使用建议等维度进行深入对比。&#x1f9ed; 一句话总结框架定位Gin轻量级、高性能的 HTTP 路由框架Go-Kr…

4G模块 A7670发送英文短信到手机

命令说明ATi显示产品的标志信息 ATCIMI查询IMSI ATCICCID从SIM卡读取ICCID ATCGSN查询产品序列号 ATCPIN查询卡状态 ATCSQ查询信号强度 ATCGATT查询当前PS域状态 ATCREG查询GPRS注册状态 ATCEREG查询4G注册状态 ATCGPADDR查询PDP地址 ATCMGF选择短信格式 ATCMGS发送短信流程第一…

归并排序递归法和非递归法的简单简单介绍

基本思想&#xff1a; 归并排序&#xff08;MERGE-SORT&#xff09;是建立在归并操作上的一种有效的排序算法,该算法是采用分治法&#xff08;Divide and Conquer&#xff09;的一个非常典型的应用。将已有序的子序列合并&#xff0c;得到完全有序的序列&#xff1b;即先使每个…

webrtc之子带分割下——SplittingFilter源码分析

文章目录前言一、频带分割过程1.SplittingFilter的创建2.频带分割整体流程1&#xff09;分割时机2&#xff09;分割规则3&#xff09;分割核心代码3.频带合并二、算法实现1.实现原理介绍2.All pass QMF系统源码1&#xff09;提高精度2&#xff09;经过串联全通滤波器3&#xff…

Java运维之Tomcat升级

Tomcat升级准备工作 下述所有过程中,包含了两种升级方式,一种是备份旧版本的 bin 和 lib,将新版本的 bin 和 lib 对旧版本进行覆盖;另一种是直接备份旧版本的Tomcat包,运行新版本,将旧版本的配置文件(conf/ * )和应用(webapps/ * )等同步到新版本。 1. 到官网下载指…

MySQL的可重复读隔离级别实现原理分析

MySQL 的 可重复读&#xff08;Repeatable Read, RR&#xff09; 隔离级别主要通过 多版本并发控制&#xff08;Multi-Version Concurrency Control, MVCC&#xff09; 和 锁机制&#xff08;特别是间隙锁&#xff09; 来实现的。其核心目标是&#xff1a;在一个事务内&#xf…

利用Java自定义格式,循环导出数据、图片到excel

利用Java自定义格式&#xff0c;循环导出数据、图片到excel1、自定义格式循环导出数据1.1.设置格式1.1.1、居中样式1.1.2、应用样式到合并区域1.1.3、合并单元格1.1.4、设置列宽1.2、写入数据1.2.1、创建标签头部1.2.2、写入标签内容2、自定义格式循环导出图片2.1、设置格式并插…

SAP学习笔记 - 开发45 - RAP开发 Managed App New Service Definition,Metadata Extension

上一章讲了在 Data Model View ( CDS View for BO Structure )基础上创建 Projection View ( CDS View for BO Projection )。 SAP学习笔记 - 开发44 - RAP开发 Managed App 建 Projection View&#xff0c;Provider Contract&#xff0c;用 redirected to 设定父子关系-CSDN博…

React强大且灵活hooks库——ahooks入门实践之高级类hook(advanced)详解

什么是 ahooks&#xff1f; ahooks 是一个 React Hooks 库&#xff0c;提供了大量实用的自定义 hooks&#xff0c;帮助开发者更高效地构建 React 应用。其中高级类 hooks 是 ahooks 的一个重要分类&#xff0c;专门用于处理一些高级场景&#xff0c;如受控值、事件发射器、性能…

计算机网络——数据链路层(25王道最新版)

数据链路层前言数据链路层的功能封装成帧&#xff08;组帧&#xff09;字符计数法字节填充法零比特填充法违规编码法小节差错控制检错编码奇偶校验码CRC校验码&#xff08;循环冗余校验码&#xff09;基本思想如何构造如何检错纠错纠错编码海明校验码设计思路求解步骤&#xff…

【PTA数据结构 | C语言版】字符串替换算法

本专栏持续输出数据结构题目集&#xff0c;欢迎订阅。 文章目录题目代码题目 请编写程序&#xff0c;将给定主串 s 中的子串 sub_s 替换成另一个给定字符串 t&#xff0c;再输出替换后的主串 s。 输入格式&#xff1a; 输入给出 3 个非空字符串&#xff0c;依次为&#xff1a…

事物生效,订单类内部更新订单

代码如下以下代码1不生效&#xff0c;2生效解决方案1&#xff0c;外层方法加注解&#xff0c;内层不加2&#xff0c;不要拆分方法&#xff0c;把更新订单操作放在带事物的大方法中3&#xff0c;拆方法&#xff08;内部&#xff09;&#xff0c;注入自己&#xff0c;用代理对象调…