目錄
- 從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能幫你打通"進程→程序替換→用戶交互"的任督二脈!👊
有問題歡迎在評論區留言哦~ 下次見!😉