Linux 进程控制实战:微型 Shell 命令行解释器实现
前面四篇文章,我们已经掌握了进程控制的'全链路技能':用 fork 创建子进程、exec 替换程序、 回收资源、 终止进程。今天,我们将这些知识'组装'成一个能实际运行的工具——(简称'迷你 Shell')。
通过 C 语言实现一个支持内建命令与外部命令执行的微型 Shell 命令行解释器。核心功能包括命令行提示符展示、输入获取与解析、工作目录切换、环境变量管理及进程控制。利用 fork 创建子进程、exec 替换程序、waitpid 回收资源,完整演示 Linux 进程控制全链路逻辑,帮助开发者深入理解 Shell 工作原理及系统编程基础。

前面四篇文章,我们已经掌握了进程控制的'全链路技能':用 fork 创建子进程、exec 替换程序、 回收资源、 终止进程。今天,我们将这些知识'组装'成一个能实际运行的工具——(简称'迷你 Shell')。
waitpidexit这个迷你 Shell 将支持:命令行提示符(如 [user@host dir]#)、内建命令(cd/export/env/echo)、外部命令(ls/ps等)、环境变量管理(继承与导出),完全遵循 Linux Shell 的核心工作逻辑。通过亲手实现,你会彻底明白'输入一条命令后,Shell 到底在做什么'。
在写代码前,我们先回归本质:Shell 是一个'命令管家'—— 它的核心工作是'接收用户命令→解析命令→调度资源执行命令→反馈结果',具体流程可拆解为一个无限循环:
[用户名@主机名 工作目录]$,告诉用户'可以输入命令了';ls -l 或 cd /home);ls -l 拆成 ["ls", "-l", NULL]);cd):Shell 自己执行(需修改 Shell 进程自身状态,不能用子进程);ls):Shell 创建子进程,子进程用 exec 替换为目标程序,Shell 等待子进程退出;举个通俗的例子:Shell 就像餐厅服务员——提示符是'请问需要点什么?',用户输入是'一份牛排'(命令),解析是'牛排 + 七分熟'(命令 + 参数),执行是:
waitpid),做好后给你端过来(反馈结果)。我们将迷你 Shell 拆解为 5 个核心模块,逐个实现并讲解,最后整合为完整代码。每个模块都对应 Shell 的一个关键功能,且能复用前面学的进程控制知识。
命令行提示符(如 [ubuntu@localhost myshell]$)的作用是'提示用户输入命令',它需要包含三个关键信息:用户名、主机名、当前工作目录。我们通过 Linux 提供的函数获取这些信息:
getenv("USER")(从环境变量中获取当前登录用户);getenv("HOSTNAME")(从环境变量中获取主机名);getcwd()(获取当前进程的工作目录,比 getenv("PWD") 更准确,因为 cd 后 PWD 可能未更新)。/home/ubuntu/myshell),我们可以简化为'最后一级目录'(如 myshell),更符合常用 Shell 的习惯;printf 默认是'行缓冲',若提示符不含 \n,需用 fflush(stdout) 强制刷新,否则提示符会'卡着不显示'。#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
// 全局变量:存储当前工作目录(避免频繁分配内存)
#define BUF_SIZE 1024
char g_pwd[BUF_SIZE] = {0};
char g_last_pwd[BUF_SIZE] = {0};// 存储上次工作目录,用于 cd -
// 1. 获取用户名
static char* get_username() {
char* username = getenv("USER");
return (username == NULL) ? "unknown" : username;
}
// 2. 获取主机名
static char* get_hostname() {
char* hostname = getenv("HOSTNAME");
return (hostname == NULL) ? "unknown-host" : hostname;
}
// 3. 获取当前工作目录(并更新 PWD 环境变量)
static char* get_current_dir() {
if (getcwd(g_pwd, BUF_SIZE) == NULL) {
perror("getcwd failed");
return "unknown-dir";
}
// 更新环境变量 PWD(确保 echo $PWD 能显示正确路径)
static char pwd_env[BUF_SIZE] = {0};
snprintf(pwd_env, BUF_SIZE, "PWD=%s", g_pwd);
putenv(pwd_env); // putenv 修改当前进程的环境变量
return g_pwd;
}
// 4. 简化工作目录(只显示最后一级)
static char* simplify_dir(char* full_dir) {
if (full_dir == NULL || strcmp(full_dir, "/") == 0) {
return "/"; // 根目录直接返回 /
}
// 逆序查找最后一个/(如/home/ubuntu/myshell → 找到最后一个/,返回 myshell)
char* last_slash = strrchr(full_dir, '/');
return (last_slash == NULL) ? full_dir : (last_slash + 1);
}
// 5. 打印命令行提示符
void print_prompt() {
char* username = get_username();
char* hostname = get_hostname();
char* full_dir = get_current_dir();
char* simple_dir = simplify_dir(full_dir);
// 格式:[用户名@主机名 简化目录]$
printf("[%s@%s %s]$ ", username, hostname, simple_dir);
fflush(stdout); // 强制刷新缓冲区,确保提示符立即显示
}
调用 print_prompt() 后,终端会显示类似:
[ubuntu@localhost myshell]$
用户输入的命令是'字符串'(如 ls -l /home),我们需要将其拆分为'命令名 + 参数数组'(如 ["ls", "-l", "/home", NULL]),才能传给 execvp 执行。这一步分两个子任务:
scanf 的问题:遇到空格就停止读取,无法获取带空格的命令(如 echo "hello world");fgets 的优势:读取一整行输入,包含空格,完美适配命令输入场景;fgets 会把用户输入的'回车符 \n'也读入,需将其替换为字符串结束符 \0;若输入为空行(只按回车),直接跳过。strtok 是 C 标准库的字符串拆分函数,能按指定分隔符(这里是空格)拆分字符串:
strtok(command, " "),传入要拆分的命令字符串,返回第一个'非空格'的子串(命令名);strtok(NULL, " "),传入 NULL 表示'继续拆分上次的字符串',直到返回 NULL(拆分结束);NULL,因为 execvp 要求参数数组以 NULL 结尾。#include <ctype.h> // 包含 isspace 函数
// 全局变量:存储命令参数(命令名 + 参数,最后以 NULL 结尾)
#define ARGV_MAX 64
char* g_argv[ARGV_MAX] = {0};
int g_argc = 0; // 命令参数个数(包含命令名)
// 1. 去除字符串前后的空格(避免空参数,如" ls -l " → "ls -l")
static void trim_space(char* str) {
if (str == NULL) return;
// 去除前面的空格
char* start = str;
while (isspace(*start)) start++;
// 去除后面的空格
char* end = str + strlen(str) - 1;
while (end >= start && isspace(*end)) end--;
// 给字符串加结束符 *(end + 1) = '\0';
// 移动字符串(覆盖前面的空格)
memmove(str, start, end - start + 2); // +2:包含 end+1 的 \0
}
// 2. 获取用户输入的命令
int get_command(char* command_buf, int buf_size) {
// 读取一行命令(fgets 会读入 \n)
if (fgets(command_buf, buf_size, stdin) == NULL) {
// 若用户按 Ctrl+D(EOF),返回 -1 表示退出
printf("\n");
return -1;
}
// 去除换行符(将 \n 替换为 \0)
command_buf[strcspn(command_buf, "\n")] = '\0';
// 去除前后空格
trim_space(command_buf);
// 处理空行(用户只按了回车)
if (strlen(command_buf) == 0) {
return 0;
}
return 1; // 成功获取有效命令
}
// 3. 解析命令(拆分为 g_argv 数组)
void parse_command(char* command_buf) {
// 重置全局变量(避免上次命令的残留)
memset(g_argv, 0, sizeof(g_argv));
g_argc = 0;
// 第一次拆分:获取命令名
char* token = strtok(command_buf, " ");
while (token != NULL && g_argc < ARGV_MAX - 1) {
// 留一个位置放 NULL
g_argv[g_argc++] = token;
// 后续拆分:获取参数
token = strtok(NULL, " ");
}
g_argv[g_argc] = NULL; // 参数数组必须以 NULL 结尾
}
// 调试用:打印解析后的参数(可选)
void debug_print_argv() {
printf("解析结果:argc=%d\n", g_argc);
for (int i = 0; i < g_argc; i++) {
printf("argv[%d] = %s\n", i, g_argv[i]);
}
}
用户输入 ls -l /home,解析后:
g_argc = 3;g_argv = ["ls", "-l", "/home", NULL]。内建命令(Built-in Command)是必须由 Shell 进程自身执行的命令,因为它们需要修改 Shell 的'自身状态'(如 cd 修改工作目录、export 修改环境变量)—— 若用子进程执行,修改的是子进程的状态,父进程(Shell)的状态不会变(进程具有独立性)。
迷你 Shell 将实现 4 个核心内建命令:cd、export、env、echo,我们逐个实现。
cd 的核心是调用 chdir 系统函数修改当前进程的工作目录,但需要处理特殊场景:
cd(无参数):切换到用户家目录(getenv("HOME"));cd ~:同无参数,切换到家目录;cd -:切换到'上次的工作目录'(需用 g_last_pwd 存储上次目录);cd 目录路径:切换到指定目录(如 cd /home)。export 的作用是'将变量添加到当前进程的环境变量表中',供后续执行的命令继承(如 export MY_VAR=123):
putenv 函数(C 标准库),将'KEY=VALUE'格式的字符串添加到环境变量表;export 无参数时,可打印所有已导出的环境变量(可选功能)。env 的作用是'打印当前进程的所有环境变量',实现方式是遍历全局环境变量数组 environ(extern char **environ),逐个打印每个环境变量(格式为'KEY=VALUE')。
echo 支持三种场景:
echo 文本:直接打印文本(如 echo hello → 输出 hello);echo $环境变量:打印指定环境变量的值(如 echo $PATH → 输出 /bin:/usr/bin);echo $?:打印'上次命令的退出码'(需用全局变量 g_last_code 存储)。#include <sys/wait.h>
// 全局变量:存储上次命令的退出码(供 echo $? 使用)
int g_last_code = 0;
// 全局变量:存储环境变量表(从父进程继承)
extern char** environ;
// 1. 判断是否为内建命令(返回 1=是,0=否)
int is_builtin_command() {
if (g_argc == 0 || g_argv[0] == NULL) return 0;
// 支持的内建命令列表
const char* builtin_list[] = {"cd", "export", "env", "echo", NULL};
for (int i = 0; builtin_list[i] != NULL; i++) {
if (strcmp(g_argv[0], builtin_list[i]) == 0) {
return 1;
}
}
return 0;
}
// 2. 执行内建命令 cd
static void exec_cd() {
char* target_dir = NULL;
// 保存当前目录(用于 cd -)
strncpy(g_last_pwd, g_pwd, BUF_SIZE);
// 处理不同参数场景
if (g_argc == 1) {
// cd 无参数 → 切换到家目录
target_dir = getenv("HOME");
} else if (strcmp(g_argv[1], "~") == 0) {
// cd ~ → 切换到家目录
target_dir = getenv("HOME");
} else if (strcmp(g_argv[1], "-") == 0) {
// cd - → 切换到上次目录
target_dir = g_last_pwd;
printf("%s\n", target_dir); // 模仿 bash,打印切换后的目录
} else {
// cd 目录路径 → 切换到指定目录
target_dir = g_argv[1];
}
// 调用 chdir 切换目录
if (chdir(target_dir) == -1) {
perror("cd failed");
g_last_code = 1; // 退出码设为 1(表示失败)
} else {
g_last_code = 0; // 退出码设为 0(表示成功)
}
}
// 3. 执行内建命令 export
static void exec_export() {
if (g_argc != 2) {
// 参数错误(如 export 无参数或多参数)
fprintf(stderr, "用法:export KEY=VALUE\n");
g_last_code = 2;
return;
}
// 检查参数格式(必须包含=,如 MY_VAR=123)
if (strchr(g_argv[1], '=') == NULL) {
fprintf(stderr, "错误:export 参数必须包含'='\n");
g_last_code = 2;
return;
}
// 调用 putenv 添加环境变量
if (putenv(g_argv[1]) != 0) {
perror("export failed");
g_last_code = 1;
} else {
g_last_code = 0;
}
}
// 4. 执行内建命令 env
static void exec_env() {
// 遍历 environ 数组,打印所有环境变量
for (int i = 0; environ[i] != NULL; i++) {
printf("%s\n", environ[i]);
}
g_last_code = 0;
}
// 5. 执行内建命令 echo
static void exec_echo() {
if (g_argc < 2) {
// echo 无参数 → 打印空行
printf("\n");
g_last_code = 0;
return;
}
char* content = g_argv[1];
if (content[0] == '$') {
// 场景 1:echo $变量(如$PATH、$?)
if (strcmp(content, "?$") == 0) {
// echo $? → 打印上次命令的退出码
printf("%d\n", g_last_code);
} else {
// echo $环境变量 → 打印环境变量的值
char* var_name = content + 1; // 跳过$,取变量名(如$PATH → PATH)
char* var_value = getenv(var_name);
if (var_value != NULL) {
printf("%s\n", var_value);
}
// 变量不存在时,不打印(模仿 bash)
}
} else {
// 场景 2:echo 文本 → 打印文本(支持多参数,如 echo hello world)
for (int i = 1; i < g_argc; i++) {
printf("%s ", g_argv[i]);
}
printf("\n");
}
g_last_code = 0;
}
// 6. 统一执行内建命令
void exec_builtin_command() {
if (strcmp(g_argv[0], "cd") == 0) {
exec_cd();
} else if (strcmp(g_argv[0], "export") == 0) {
exec_export();
} else if (strcmp(g_argv[0], "env") == 0) {
exec_env();
} else if (strcmp(g_argv[0], "echo") == 0) {
exec_echo();
}
}
外部命令(如 ls/ps/gcc)是'独立的可执行程序',需要 Shell 创建子进程执行(避免覆盖 Shell 自身代码),核心流程是:
fork 创建子进程;execvp 替换为目标程序(自动从 PATH 查找命令路径);waitpid 等待子进程退出,获取退出码(更新 g_last_code);execvp 失败(如命令不存在),子进程退出并设置错误退出码。void exec_external_command() {
pid_t pid = fork();
if (pid == -1) {
// fork 失败(如系统进程过多)
perror("fork failed");
g_last_code = 1;
return;
}
if (pid == 0) {
// 子进程:执行外部命令
execvp(g_argv[0], g_argv);
// 只有 execvp 失败时,才会执行到这里(成功则子进程代码被覆盖)
perror("command not found");
exit(127); // 退出码 127:标准的'命令未找到'错误码
} else {
// 父进程:等待子进程退出,获取退出码
int status;
waitpid(pid, &status, 0); // 阻塞等待子进程
// 解析子进程的退出状态,更新 g_last_code
if (WIFEXITED(status)) {
// 正常退出:获取退出码
g_last_code = WEXITSTATUS(status);
} else if (WIFSIGNALED(status)) {
// 被信号终止(如 Ctrl+C → SIGINT,kill -9 → SIGKILL)
g_last_code = 128 + WTERMSIG(status); // 符合 Linux 标准(如 128+2=130)
}
}
}
用户输入 ls -l,执行流程:
ls 是外部命令,fork 子进程;execvp("ls", ["ls", "-l", NULL]),从 PATH 找到 /bin/ls,替换为 ls 程序;ls 执行完毕后退出,父进程 waitpid 获取退出码(0 表示成功);g_last_code 更新为 0,下次执行 echo $? 会输出 0。Shell 启动时,需要继承父进程的环境变量(如 PATH/HOME/USER),这些环境变量存储在全局数组 environ 中(无需手动定义,只需用 extern 声明)。我们可以在 Shell 启动时,打印关键环境变量(可选),确保继承正常。
// 初始化环境变量(打印关键变量,验证继承)
void init_env() {
printf("=== 迷你 Shell 启动 ===");
// 打印关键环境变量(可选,用于调试)
char* path = getenv("PATH");
char* home = getenv("HOME");
char* user = getenv("USER");
if (path != NULL) printf("\nPATH: %s", path);
if (home != NULL) printf("\nHOME: %s", home);
if (user != NULL) printf("\nUSER: %s", user);
printf("\n====================\n\n");
// 初始化上次工作目录(启动时的当前目录)
get_current_dir();
strncpy(g_last_pwd, g_pwd, BUF_SIZE);
}
将上述模块整合为完整代码(mini_shell.c),添加 main 函数实现'无限循环'的核心逻辑,再编译运行验证功能。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/wait.h>
// 全局常量定义
#define BUF_SIZE 1024 // 命令缓冲区大小
#define ARGV_MAX 64 // 命令参数最大个数
// 全局变量定义
char g_pwd[BUF_SIZE] = {0}; // 当前工作目录
char g_last_pwd[BUF_SIZE] = {0}; // 上次工作目录(用于 cd -)
char* g_argv[ARGV_MAX] = {0}; // 命令参数数组
int g_argc = 0; // 命令参数个数
int g_last_code = 0; // 上次命令的退出码
extern char** environ; // 全局环境变量数组
// ------------------------------ 模块 1:命令行提示符 ------------------------------
static char* get_username() {
char* username = getenv("USER");
return (username == NULL) ? "unknown" : username;
}
static char* get_hostname() {
char* hostname = getenv("HOSTNAME");
return (hostname == NULL) ? "unknown-host" : hostname;
}
static char* get_current_dir() {
if (getcwd(g_pwd, BUF_SIZE) == NULL) {
perror("getcwd failed");
return "unknown-dir";
}
static char pwd_env[BUF_SIZE] = {0};
snprintf(pwd_env, BUF_SIZE, "PWD=%s", g_pwd);
putenv(pwd_env);
return g_pwd;
}
static char* simplify_dir(char* full_dir) {
if (full_dir == NULL || strcmp(full_dir, "/") == 0) {
return "/";
}
char* last_slash = strrchr(full_dir, '/');
return (last_slash == NULL) ? full_dir : (last_slash + 1);
}
void print_prompt() {
char* username = get_username();
char* hostname = get_hostname();
char* full_dir = get_current_dir();
char* simple_dir = simplify_dir(full_dir);
printf("[%s@%s %s]$ ", username, hostname, simple_dir);
fflush(stdout);
}
// ------------------------------ 模块 2:命令获取与解析 ------------------------------
static void trim_space(char* str) {
if (str == NULL) return;
char* start = str;
while (isspace(*start)) start++;
char* end = str + strlen(str) - 1;
while (end >= start && isspace(*end)) end--;
*(end + 1) = '\0';
memmove(str, start, end - start + 2);
}
int get_command(char* command_buf, int buf_size) {
if (fgets(command_buf, buf_size, stdin) == NULL) {
printf("\n");
return -1;
}
command_buf[strcspn(command_buf, "\n")] = '\0';
trim_space(command_buf);
if (strlen(command_buf) == 0) {
return 0;
}
return 1;
}
void parse_command(char* command_buf) {
memset(g_argv, 0, sizeof(g_argv));
g_argc = 0;
char* token = strtok(command_buf, " ");
while (token != NULL && g_argc < ARGV_MAX - 1) {
g_argv[g_argc++] = token;
token = strtok(NULL, " ");
}
g_argv[g_argc] = NULL;
}
// ------------------------------ 模块 3:内建命令处理 ------------------------------
int is_builtin_command() {
if (g_argc == 0 || g_argv[0] == NULL) return 0;
const char* builtin_list[] = {"cd", "export", "env", "echo", NULL};
for (int i = 0; builtin_list[i] != NULL; i++) {
if (strcmp(g_argv[0], builtin_list[i]) == 0) {
return 1;
}
}
return 0;
}
static void exec_cd() {
char* target_dir = NULL;
strncpy(g_last_pwd, g_pwd, BUF_SIZE);
if (g_argc == 1) {
target_dir = getenv("HOME");
} else if (strcmp(g_argv[1], "~") == 0) {
target_dir = getenv("HOME");
} else if (strcmp(g_argv[1], "-") == 0) {
target_dir = g_last_pwd;
printf("%s\n", target_dir);
} else {
target_dir = g_argv[1];
}
if (chdir(target_dir) == -1) {
perror("cd failed");
g_last_code = 1;
} else {
g_last_code = 0;
}
}
static void exec_export() {
if (g_argc != 2) {
fprintf(stderr, "用法:export KEY=VALUE\n");
g_last_code = 2;
return;
}
if (strchr(g_argv[1], '=') == NULL) {
fprintf(stderr, "错误:export 参数必须包含'='\n");
g_last_code = 2;
return;
}
if (putenv(g_argv[1]) != 0) {
perror("export failed");
g_last_code = 1;
} else {
g_last_code = 0;
}
}
static void exec_env() {
for (int i = 0; environ[i] != NULL; i++) {
printf("%s\n", environ[i]);
}
g_last_code = 0;
}
static void exec_echo() {
if (g_argc < 2) {
printf("\n");
g_last_code = 0;
return;
}
char* content = g_argv[1];
if (content[0] == '$') {
if (strcmp(content, "?$") == 0) {
printf("%d\n", g_last_code);
} else {
char* var_name = content + 1;
char* var_value = getenv(var_name);
if (var_value != NULL) {
printf("%s\n", var_value);
}
}
} else {
for (int i = 1; i < g_argc; i++) {
printf("%s ", g_argv[i]);
}
printf("\n");
}
g_last_code = 0;
}
void exec_builtin_command() {
if (strcmp(g_argv[0], "cd") == 0) {
exec_cd();
} else if (strcmp(g_argv[0], "export") == 0) {
exec_export();
} else if (strcmp(g_argv[0], "env") == 0) {
exec_env();
} else if (strcmp(g_argv[0], "echo") == 0) {
exec_echo();
}
}
// ------------------------------ 模块 4:外部命令执行 ------------------------------
void exec_external_command() {
pid_t pid = fork();
if (pid == -1) {
perror("fork failed");
g_last_code = 1;
return;
}
if (pid == 0) {
execvp(g_argv[0], g_argv);
perror("command not found");
exit(127);
} else {
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
g_last_code = WEXITSTATUS(status);
} else if (WIFSIGNALED(status)) {
g_last_code = 128 + WTERMSIG(status);
}
}
}
// ------------------------------ 模块 5:环境变量初始化 ------------------------------
void init_env() {
printf("=== 迷你 Shell 启动 ===");
char* path = getenv("PATH");
char* home = getenv("HOME");
char* user = getenv("USER");
if (path != NULL) printf("\nPATH: %s", path);
if (home != NULL) printf("\nHOME: %s", home);
if (user != NULL) printf("\nUSER: %s", user);
printf("\n====================\n\n");
get_current_dir();
strncpy(g_last_pwd, g_pwd, BUF_SIZE);
}
// ------------------------------ 主函数(核心循环) ------------------------------
int main() {
char command_buf[BUF_SIZE] = {0};
init_env(); // 初始化环境变量
// Shell 核心循环:获取命令→解析→执行→循环
while (1) {
print_prompt(); // 1. 打印提示符
int ret = get_command(command_buf, BUF_SIZE); // 2. 获取命令
if (ret == -1) {
// 用户按 Ctrl+D,退出 Shell
printf("=== 迷你 Shell 退出 ===\n");
break;
} else if (ret == 0) {
// 空行,跳过
continue;
}
parse_command(command_buf); // 3. 解析命令
// debug_print_argv(); // 调试用:打印解析结果
if (is_builtin_command()) {
// 4. 执行内建命令
exec_builtin_command();
} else {
// 5. 执行外部命令
exec_external_command();
}
}
return 0;
}
在 Linux 终端中,执行编译命令:
gcc mini_shell.c -o mini_shell -Wall
-o mini_shell:指定输出文件名为 mini_shell;-Wall:显示所有警告(避免潜在错误)。./mini_shell
启动后会显示初始化信息和提示符:
=== 迷你 Shell 启动 ===
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOME: /home/ubuntu
USER: ubuntu
====================
[ubuntu@localhost myshell]$
退出迷你 Shell:按 Ctrl+D 或输入 exit(可扩展 exit 内建命令),Shell 会退出:
[ubuntu@localhost ubuntu]$ === 迷你 Shell 退出 ===
测试 echo $?(查看退出码):
[ubuntu@localhost ubuntu]$ ls -l # 成功执行,退出码 0
[ubuntu@localhost ubuntu]$ echo$?
0
[ubuntu@localhost ubuntu]$ lss # 命令不存在,退出码 127
command not found: lss
[ubuntu@localhost ubuntu]$ echo$?
127
测试外部命令 ls 与 ps:
[ubuntu@localhost ubuntu]$ ls -l # 执行外部命令 ls
total 4
drwxrwxr-x 2 ubuntu ubuntu 4096 Oct 116:00 myshell
[ubuntu@localhost ubuntu]$ ps # 执行外部命令 ps
PID TTY TIME CMD
1234 pts/0 00:00:00 bash
5678 pts/0 00:00:00 mini_shell
5679 pts/0 00:00:00 ps
测试内建命令 env:
[ubuntu@localhost ubuntu]$ env|grep MY_VAR # 查看导出的环境变量
MY_VAR=hello_mini_shell
测试内建命令 export 与 echo:
[ubuntu@localhost ubuntu]$ export MY_VAR=hello_mini_shell
[ubuntu@localhost ubuntu]$ echo$MY_VAR # 打印环境变量 hello_mini_shell
[ubuntu@localhost ubuntu]$ echo$PATH # 打印系统环境变量
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
[ubuntu@localhost ubuntu]$ echo hello world # 打印文本
hello world
测试内建命令 cd:
[ubuntu@localhost myshell]$ cd /home
[ubuntu@localhost home]$ cd ubuntu
[ubuntu@localhost ubuntu]$ cd - # 切换到上次目录(/home)
/home
[ubuntu@localhost home]$ cd ~ # 切换到家目录(/home/ubuntu)
[ubuntu@localhost ubuntu]$
当前的迷你 Shell 已实现核心功能,但还可以扩展以下进阶功能,使其更接近真实 Shell(如 bash):
ll=ls -l)struct alias_map)存储'别名→原命令'的映射;alias:alias ll='ls -l',将别名添加到哈希表;ll→ls -l)。Tab 键(需关闭终端的'行缓冲',用 tcsetattr 修改终端属性);l),遍历 PATH 目录,找出以 l 开头的命令(如 ls/less);若输入的是路径前缀(如 /hom),调用 readdir 遍历目录,补全为 /home。ls > file.txt)>/< 等重定向符号,拆分'命令'与'重定向目标';open 打开目标文件,用 dup2 重定向标准输出(stdout)或标准输入(stdin),再执行 execvp。ls | grep txt)pipe 创建管道,fork 两个子进程:
ls,将标准输出重定向到管道写入端;grep txt,将标准输入重定向到管道读取端;这个迷你 Shell 虽然简单,但完全基于前面四篇文章的核心知识,是进程控制的'集大成者'。我们可以用一张图总结其核心逻辑:
用户输入 → 提示符(print_prompt) ↓
获取命令(get_command)→ 空行/EOF 处理 ↓
解析命令(parse_command)→ 生成 g_argv 数组 ↓
判断命令类型:
├─ 内建命令(is_builtin_command)→ Shell 自身执行(如 cd 修改工作目录)
└─ 外部命令 → fork 子进程 → 子进程 execvp 替换 → 父进程 waitpid 回收 ↓
更新退出码(g_last_code)→ 回到提示符,循环
通过这个实战,你应该能深刻理解:
fork(创建子进程)→ exec(替换程序)→ waitpid(回收资源)→ exit(终止进程);至此,Linux 进程控制系统系列文章已全部完成。从进程的创建、终止、等待、替换,到最终实现迷你 Shell,我们走过了'理论→实践'的完整路径。希望你能亲手运行代码,修改扩展,真正将这些知识内化为自己的技能。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online