从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 | 数组形式+自动搜PATH | execvp("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需要做三件事:
- 读取命令:从终端读取用户输入(如
ls -l
) - 解析命令:把命令拆分成可执行程序和参数(如程序"ls"参数"-l")
- 执行命令: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
),提示符更清爽! - 模块化设计:拆分为
MakeCommandLine
、GetCommandLine
等函数,代码可读性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能帮你打通"进程→程序替换→用户交互"的任督二脉!👊
有问题欢迎在评论区留言哦~ 下次见!😉