目录
前言
一、前置认知:什么是 Shell?它到底在做什么?
1.1 Shell 的本质:用户与内核之间的 “翻译官”
1.2 Shell 的核心工作流程:一个无限循环的 “命令处理机器”
完整工作流程拆解
可视化流程示意图
编辑1.3 实现自主 Shell 的核心技术栈
二、从零开始:实现一个最基础的 Shell(支持外部命令执行)
2.1 步骤 1:搭建 Shell 的主循环框架
实战代码(my_shell_v1.c)
代码编译与执行
执行效果
核心知识点解析
2.2 步骤 2:实现命令行解析功能(拆分命令与参数)
实战代码(补充解析功能,my_shell_v2.c)
代码编译与执行
执行效果
核心知识点解析
2.3 步骤 3:实现外部命令执行(fork+exec+wait)
实战代码(补充执行功能,my_shell_v3.c)
代码编译与执行
执行效果
核心知识点解析
三、功能升级:添加内置命令支持(cd、exit、pwd)
3.1 内置命令的特点与实现思路
内置命令的核心特点
常见内置命令与对应的系统调用
实现思路
3.2 实战代码:实现 cd、exit、pwd 内置命令(my_shell_v4.c)
代码编译与执行
执行效果
核心知识点解析
四、进阶功能:支持后台执行(& 符号)
4.1 实战代码:添加后台执行支持(my_shell_v5.c)
代码编译与执行
执行效果
核心知识点解析
总结
前言
在 Linux 的日常使用中,我们每天都在和 bash、zsh 这些 Shell 命令行解释器打交道。当你在终端输入ls -l、pwd、git clone这些命令时,是否曾好奇过:背后的 Shell 是如何 “读懂” 你的输入,又是如何将命令转化为实际的系统操作的?
其实,Shell 命令行解释器并非什么神秘的 “黑盒”,它的核心工作流程清晰易懂,我们完全可以凭借对 Linux 进程控制(fork/exec/wait)、字符串处理、终端交互的理解,亲手打造一个属于自己的简易 Shell。
本文将从 Shell 的核心工作原理入手,一步步拆解命令行解释器的实现细节,从最基础的命令读取与解析,到支持内置命令、后台执行、重定向等高级功能,最终完成一个可实际运行的自主 Shell 命令行解释器。全程结合 C 语言实战代码,语言生动易懂,逻辑层层递进,即使你是 Linux 编程新手,也能跟着本文一步步实现属于自己的终端神器。下面就让我们正式开始吧!
一、前置认知:什么是 Shell?它到底在做什么?
1.1 Shell 的本质:用户与内核之间的 “翻译官”
Linux 内核是操作系统的核心,负责管理硬件资源、进程调度、内存管理等底层工作,但内核并不直接与用户交互 —— 用户无法直接向内核发送命令,而 Shell 就是连接用户与内核的 “桥梁” 和 “翻译官”。
简单来说,Shell 是一个命令行解释器,它接收用户输入的命令行,对命令进行解析和处理,最终通过系统调用或启动子进程的方式,让内核执行对应的操作,并将执行结果反馈给用户。
我们可以用一个生动的类比来理解:
- 内核就像一家公司的 CEO,掌握着公司的所有核心资源和决策权,不直接对接普通员工(用户);
- Shell 就像 CEO 的秘书,负责接收普通员工的请求(用户输入的命令),将请求整理、翻译为 CEO 能理解的语言(系统调用、子进程执行),再将 CEO 的处理结果(命令执行结果)反馈给员工。
1.2 Shell 的核心工作流程:一个无限循环的 “命令处理机器”
无论是 bash 还是我们将要实现的简易 Shell,其核心工作流程都是一个无限循环,这个循环可以概括为 “读取→解析→执行→反馈” 四个步骤,直到用户输入退出命令(如exit)才会终止循环。
完整工作流程拆解
- 内置命令(如cd、exit、pwd):直接在 Shell 进程自身中执行,无需创建子进程;
- 外部命令(如ls、cat、gcc):通过fork创建子进程,再通过exec函数族替换子进程程序,执行对应的外部命令,最后通过wait/waitpid等待子进程执行完成(后台执行除外);
可视化流程示意图
1.3 实现自主 Shell 的核心技术栈
要完成一个可运行的 Shell 命令行解释器,我们需要用到以下几方面的技术知识,这些都是 Linux C 编程的核心内容:
二、从零开始:实现一个最基础的 Shell(支持外部命令执行)
我们先从最简单的版本入手,实现一个具备 “读取→解析→执行外部命令” 核心功能的基础 Shell,暂不考虑内置命令、后台执行等高级功能,先搭建起 Shell 的整体框架。
2.1 步骤 1:搭建 Shell 的主循环框架
Shell 的核心是一个无限循环,我们先实现这个循环,包括打印提示符、读取用户输入、处理空命令等基础功能。
实战代码(my_shell_v1.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define MAX_CMD_LEN 1024 // 定义命令行的最大长度,防止缓冲区溢出
// 函数声明:修剪字符串前后的空格和换行符
void trim_cmd(char *cmd);
int main(void)
{
char cmd_buf[MAX_CMD_LEN]; // 存储用户输入的命令行缓冲区
// Shell主循环:无限循环,直到用户输入exit退出
while (1)
{
// 1. 打印命令行提示符,格式类似 [my_shell] $
printf("[my_shell] $ ");
fflush(stdout); // 强制刷新标准输出缓冲区,确保提示符立即显示
// 2. 读取用户输入的命令行
if (fgets(cmd_buf, MAX_CMD_LEN, stdin) == NULL)
{
// 读取失败(如Ctrl+D),直接退出循环
perror("fgets read cmd failed");
break;
}
// 3. 处理读取到的命令行:修剪前后空格和换行符
trim_cmd(cmd_buf);
// 4. 处理空命令(用户仅输入空格或回车)
if (strlen(cmd_buf) == 0)
{
continue;
}
// 5. 后续步骤:解析命令、执行命令(后续补充)
printf("你输入的命令是:%s\\n", cmd_buf);
}
printf("my_shell exit successfully\\n");
return 0;
}
// 辅助函数:修剪字符串前后的空格、制表符、换行符
void trim_cmd(char *cmd)
{
if (cmd == NULL || strlen(cmd) == 0)
{
return;
}
// 步骤1:修剪字符串开头的空白字符(空格、制表符)
int start = 0;
while (cmd[start] == ' ' || cmd[start] == '\\t' || cmd[start] == '\\n')
{
start++;
}
// 步骤2:修剪字符串结尾的空白字符(空格、制表符、换行符)
int end = strlen(cmd) – 1;
while (end >= start && (cmd[end] == ' ' || cmd[end] == '\\t' || cmd[end] == '\\n'))
{
end–;
}
// 步骤3:将修剪后的字符串重新拷贝到原缓冲区,添加字符串结束符
if (start > end)
{
// 全是空白字符,置为空字符串
cmd[0] = '\\0';
}
else
{
memmove(cmd, cmd + start, end – start + 1);
cmd[end – start + 1] = '\\0';
}
}
代码编译与执行
# 编译C语言代码
gcc my_shell_v1.c -o my_shell_v1
# 执行编译后的Shell程序
./my_shell_v1
执行效果
[my_shell] $
你输入的命令是:
[my_shell] $ ls -l
你输入的命令是:ls -l
[my_shell] $ pwd
你输入的命令是:pwd
[my_shell] $ ^C
核心知识点解析
2.2 步骤 2:实现命令行解析功能(拆分命令与参数)
用户输入的命令行是一个完整的字符串(如ls -l /home),我们需要将其拆分为命令名和参数列表(如{"ls", "-l", "/home", NULL}),以便后续传递给exec函数族执行。
这里我们使用strtok函数实现字符串拆分,strtok可以按照指定的分隔符(这里是空格、制表符)拆分字符串,返回拆分后的子字符串指针。
实战代码(补充解析功能,my_shell_v2.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define MAX_CMD_LEN 1024 // 命令行最大长度
#define MAX_ARG_NUM 64 // 命令参数的最大个数
// 函数声明
void trim_cmd(char *cmd);
int parse_cmd(char *cmd, char *argv[]);
int main(void)
{
char cmd_buf[MAX_CMD_LEN];
char *argv[MAX_ARG_NUM]; // 存储命令参数的数组,argv[0]是命令名
while (1)
{
// 1. 打印提示符
printf("[my_shell] $ ");
fflush(stdout);
// 2. 读取用户输入
if (fgets(cmd_buf, MAX_CMD_LEN, stdin) == NULL)
{
perror("fgets read cmd failed");
break;
}
// 3. 修剪命令行
trim_cmd(cmd_buf);
if (strlen(cmd_buf) == 0)
{
continue;
}
// 4. 解析命令行,拆分命令与参数
int arg_count = parse_cmd(cmd_buf, argv);
if (arg_count == 0)
{
continue;
}
// 5. 打印解析结果(测试用,后续替换为执行逻辑)
printf("解析结果:命令名=%s,参数个数=%d\\n", argv[0], arg_count);
printf("参数列表:");
for (int i = 0; i < arg_count; i++)
{
printf("%s ", argv[i]);
}
printf("\\n");
}
printf("my_shell exit successfully\\n");
return 0;
}
// 辅助函数:修剪字符串前后的空白字符
void trim_cmd(char *cmd)
{
if (cmd == NULL || strlen(cmd) == 0)
{
return;
}
int start = 0;
while (cmd[start] == ' ' || cmd[start] == '\\t' || cmd[start] == '\\n')
{
start++;
}
int end = strlen(cmd) – 1;
while (end >= start && (cmd[end] == ' ' || cmd[end] == '\\t' || cmd[end] == '\\n'))
{
end–;
}
if (start > end)
{
cmd[0] = '\\0';
}
else
{
memmove(cmd, cmd + start, end – start + 1);
cmd[end – start + 1] = '\\0';
}
}
// 核心函数:解析命令行,拆分命令与参数
// 参数:cmd-修剪后的命令行字符串,argv-存储参数的数组
// 返回值:参数的个数
int parse_cmd(char *cmd, char *argv[])
{
if (cmd == NULL || argv == NULL || strlen(cmd) == 0)
{
return 0;
}
int arg_count = 0; // 记录参数个数
// 第一次调用strtok,传入命令行字符串和分隔符
char *token = strtok(cmd, " \\t"); // 以空格和制表符作为分隔符
while (token != NULL && arg_count < MAX_ARG_NUM – 1)
{
argv[arg_count++] = token; // 将拆分后的子字符串存入argv数组
token = strtok(NULL, " \\t"); // 后续调用strtok,第一个参数传NULL
}
argv[arg_count] = NULL; // 关键:argv数组必须以NULL结尾,供exec函数使用
return arg_count;
}
代码编译与执行
gcc my_shell_v2.c -o my_shell_v2
./my_shell_v2
执行效果
[my_shell] $ ls -l /home
解析结果:命令名=ls,参数个数=3
参数列表:ls -l /home
[my_shell] $ pwd
解析结果:命令名=pwd,参数个数=1
参数列表:pwd
核心知识点解析
2.3 步骤 3:实现外部命令执行(fork+exec+wait)
解析完命令与参数后,我们需要实现外部命令的执行逻辑。核心思路是:通过fork创建子进程,在子进程中调用execvp函数执行外部命令,在父进程中调用waitpid等待子进程执行完成,回收子进程资源,避免僵尸进程。

实战代码(补充执行功能,my_shell_v3.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#define MAX_CMD_LEN 1024 // 命令行最大长度
#define MAX_ARG_NUM 64 // 命令参数的最大个数
// 函数声明
void trim_cmd(char *cmd);
int parse_cmd(char *cmd, char *argv[]);
int main(void)
{
char cmd_buf[MAX_CMD_LEN];
char *argv[MAX_ARG_NUM];
while (1)
{
// 1. 打印提示符
printf("[my_shell] $ ");
fflush(stdout);
// 2. 读取用户输入
if (fgets(cmd_buf, MAX_CMD_LEN, stdin) == NULL)
{
perror("fgets read cmd failed");
break;
}
// 3. 修剪命令行
trim_cmd(cmd_buf);
if (strlen(cmd_buf) == 0)
{
continue;
}
// 4. 解析命令行
int arg_count = parse_cmd(cmd_buf, argv);
if (arg_count == 0)
{
continue;
}
// 5. 执行外部命令(fork+exec+wait)
pid_t pid = fork(); // 创建子进程
if (pid == -1)
{
// fork创建子进程失败
perror("fork create child process failed");
continue;
}
else if (pid == 0)
{
// 子进程:执行外部命令
execvp(argv[0], argv);
// 若execvp返回,说明执行失败(命令不存在、权限不足等)
perror("execvp execute command failed");
exit(EXIT_FAILURE); // 子进程执行失败,退出并返回非0状态码
}
else
{
// 父进程(Shell进程):等待子进程执行完成
int status;
pid_t ret_pid = waitpid(pid, &status, 0); // 阻塞等待子进程
if (ret_pid == -1)
{
perror("waitpid wait child process failed");
continue;
}
// 可选:打印子进程的退出状态(调试用)
printf("子进程(PID:%d)执行完成,退出状态码:%d\\n", pid, WEXITSTATUS(status));
}
}
printf("my_shell exit successfully\\n");
return 0;
}
// 辅助函数:修剪字符串前后的空白字符
void trim_cmd(char *cmd)
{
if (cmd == NULL || strlen(cmd) == 0)
{
return;
}
int start = 0;
while (cmd[start] == ' ' || cmd[start] == '\\t' || cmd[start] == '\\n')
{
start++;
}
int end = strlen(cmd) – 1;
while (end >= start && (cmd[end] == ' ' || cmd[end] == '\\t' || cmd[end] == '\\n'))
{
end–;
}
if (start > end)
{
cmd[0] = '\\0';
}
else
{
memmove(cmd, cmd + start, end – start + 1);
cmd[end – start + 1] = '\\0';
}
}
// 核心函数:解析命令行,拆分命令与参数
int parse_cmd(char *cmd, char *argv[])
{
if (cmd == NULL || argv == NULL || strlen(cmd) == 0)
{
return 0;
}
int arg_count = 0;
char *token = strtok(cmd, " \\t");
while (token != NULL && arg_count < MAX_ARG_NUM – 1)
{
argv[arg_count++] = token;
token = strtok(NULL, " \\t");
}
argv[arg_count] = NULL;
return arg_count;
}
代码编译与执行
gcc my_shell_v3.c -o my_shell_v3
./my_shell_v3
执行效果
[my_shell] $ ls -l
总用量 24
-rwxr-xr-x 1 root root 8944 1月 17 15:30 my_shell_v3
-rw-r–r– 1 root root 2568 1月 17 15:29 my_shell_v3.c
-rwxr-xr-x 1 root root 8832 1月 17 14:50 my_shell_v2
-rw-r–r– 1 root root 1876 1月 17 14:49 my_shell_v2.c
-rwxr-xr-x 1 root root 8704 1月 17 14:20 my_shell_v1
-rw-r–r– 1 root root 1024 1月 17 14:19 my_shell_v1.c
子进程(PID:12345)执行完成,退出状态码:0
[my_shell] $ pwd
/root/my_shell
子进程(PID:12346)执行完成,退出状态码:0
[my_shell] $ whoami
root
子进程(PID:12347)执行完成,退出状态码:0
核心知识点解析
三、功能升级:添加内置命令支持(cd、exit、pwd)
在基础 Shell 中,我们可以执行ls、pwd等外部命令,但当我们尝试执行cd命令时,会发现无法正常工作:
[my_shell] $ cd /home
子进程(PID:12348)执行完成,退出状态码:0
[my_shell] $ pwd
/root/my_shell
这是因为cd命令是内置命令,它需要修改 Shell 进程自身的当前工作目录,而如果通过fork创建子进程执行cd,只会修改子进程的工作目录,子进程退出后,父进程(Shell)的工作目录不会发生任何变化。
因此,对于内置命令,我们需要在 Shell 进程自身中直接执行,无需创建子进程。
3.1 内置命令的特点与实现思路
内置命令的核心特点
常见内置命令与对应的系统调用
| exit | 退出 Shell | 直接终止主循环,调用exit函数 |
| cd | 切换工作目录 | 调用chdir系统调用 |
| pwd | 打印当前工作目录 | 调用getcwd系统调用 |
| echo | 打印指定内容 | 直接使用printf函数 |
实现思路
3.2 实战代码:实现 cd、exit、pwd 内置命令(my_shell_v4.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <limits.h>
#define MAX_CMD_LEN 1024 // 命令行最大长度
#define MAX_ARG_NUM 64 // 命令参数的最大个数
// 函数声明
void trim_cmd(char *cmd);
int parse_cmd(char *cmd, char *argv[]);
int execute_builtin_cmd(char *argv[]); // 执行内置命令
int main(void)
{
char cmd_buf[MAX_CMD_LEN];
char *argv[MAX_ARG_NUM];
while (1)
{
// 1. 打印提示符
printf("[my_shell] $ ");
fflush(stdout);
// 2. 读取用户输入
if (fgets(cmd_buf, MAX_CMD_LEN, stdin) == NULL)
{
perror("fgets read cmd failed");
break;
}
// 3. 修剪命令行
trim_cmd(cmd_buf);
if (strlen(cmd_buf) == 0)
{
continue;
}
// 4. 解析命令行
int arg_count = parse_cmd(cmd_buf, argv);
if (arg_count == 0)
{
continue;
}
// 5. 先判断是否为内置命令,若是则直接执行
if (execute_builtin_cmd(argv))
{
continue;
}
// 6. 非内置命令,执行外部命令(fork+exec+wait)
pid_t pid = fork();
if (pid == -1)
{
perror("fork create child process failed");
continue;
}
else if (pid == 0)
{
execvp(argv[0], argv);
perror("execvp execute command failed");
exit(EXIT_FAILURE);
}
else
{
int status;
pid_t ret_pid = waitpid(pid, &status, 0);
if (ret_pid == -1)
{
perror("waitpid wait child process failed");
continue;
}
}
}
printf("my_shell exit successfully\\n");
return 0;
}
// 辅助函数:修剪字符串前后的空白字符
void trim_cmd(char *cmd)
{
if (cmd == NULL || strlen(cmd) == 0)
{
return;
}
int start = 0;
while (cmd[start] == ' ' || cmd[start] == '\\t' || cmd[start] == '\\n')
{
start++;
}
int end = strlen(cmd) – 1;
while (end >= start && (cmd[end] == ' ' || cmd[end] == '\\t' || cmd[end] == '\\n'))
{
end–;
}
if (start > end)
{
cmd[0] = '\\0';
}
else
{
memmove(cmd, cmd + start, end – start + 1);
cmd[end – start + 1] = '\\0';
}
}
// 核心函数:解析命令行,拆分命令与参数
int parse_cmd(char *cmd, char *argv[])
{
if (cmd == NULL || argv == NULL || strlen(cmd) == 0)
{
return 0;
}
int arg_count = 0;
char *token = strtok(cmd, " \\t");
while (token != NULL && arg_count < MAX_ARG_NUM – 1)
{
argv[arg_count++] = token;
token = strtok(NULL, " \\t");
}
argv[arg_count] = NULL;
return arg_count;
}
// 核心函数:执行内置命令
// 返回值:1-是内置命令并执行完成,0-不是内置命令
int execute_builtin_cmd(char *argv[])
{
if (argv == NULL || argv[0] == NULL)
{
return 0;
}
// 1. 实现exit命令:退出Shell
if (strcmp(argv[0], "exit") == 0)
{
printf("Goodbye! my_shell exit now…\\n");
exit(EXIT_SUCCESS); // 终止Shell进程
}
// 2. 实现pwd命令:打印当前工作目录
if (strcmp(argv[0], "pwd") == 0)
{
char cwd[PATH_MAX]; // PATH_MAX定义在limits.h中,是系统最大路径长度
if (getcwd(cwd, sizeof(cwd)) != NULL)
{
printf("%s\\n", cwd);
}
else
{
perror("getcwd get current working directory failed");
}
return 1;
}
// 3. 实现cd命令:切换工作目录
if (strcmp(argv[0], "cd") == 0)
{
// 获取目标目录:cd后无参数,默认切换到用户主目录(HOME环境变量)
char *target_dir = argv[1];
if (target_dir == NULL)
{
target_dir = getenv("HOME"); // 获取HOME环境变量
if (target_dir == NULL)
{
fprintf(stderr, "cd: no HOME environment variable found\\n");
return 1;
}
}
// 调用chdir系统调用切换工作目录
if (chdir(target_dir) == -1)
{
perror("cd: change directory failed");
}
return 1;
}
// 4. 后续可扩展其他内置命令:echo、alias等
// if (strcmp(argv[0], "echo") == 0) {…}
// 不是内置命令,返回0
return 0;
}
代码编译与执行
gcc my_shell_v4.c -o my_shell_v4
./my_shell_v4
执行效果
[my_shell] $ pwd
/root/my_shell
[my_shell] $ cd /home
[my_shell] $ pwd
/home
[my_shell] $ cd
[my_shell] $ pwd
/root
[my_shell] $ exit
Goodbye! my_shell exit now…
my_shell exit successfully
核心知识点解析
- 调用chdir系统调用切换工作目录,chdir的参数是目标目录的路径,成功返回 0,失败返回 – 1;
- 处理cd无参数的情况:默认切换到用户的主目录,通过getenv("HOME")获取HOME环境变量的值,这与 bash 的行为一致。
四、进阶功能:支持后台执行(& 符号)
在 bash 中,我们可以在命令末尾添加&符号,让命令在后台执行,此时 Shell 不会阻塞等待命令执行完成,而是立即返回提示符,允许用户输入下一条命令。例如:
[bash] $ sleep 10 &
[1] 12349
我们的简易 Shell 也可以实现这个功能,核心思路是:
4.1 实战代码:添加后台执行支持(my_shell_v5.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <limits.h>
#include <signal.h>
#define MAX_CMD_LEN 1024 // 命令行最大长度
#define MAX_ARG_NUM 64 // 命令参数的最大个数
// 函数声明
void trim_cmd(char *cmd);
int parse_cmd(char *cmd, char *argv[], int *is_background);
int execute_builtin_cmd(char *argv[]);
void sigchld_handler(int signo); // 处理SIGCHLD信号,回收后台子进程
int main(void)
{
char cmd_buf[MAX_CMD_LEN];
char *argv[MAX_ARG_NUM];
int is_background; // 标记是否为后台执行:1-后台执行,0-前台执行
// 注册SIGCHLD信号处理函数,回收后台子进程,避免僵尸进程
if (signal(SIGCHLD, sigchld_handler) == SIG_ERR)
{
perror("signal register SIGCHLD handler failed");
exit(EXIT_FAILURE);
}
while (1)
{
// 1. 打印提示符
printf("[my_shell] $ ");
fflush(stdout);
// 2. 读取用户输入
if (fgets(cmd_buf, MAX_CMD_LEN, stdin) == NULL)
{
perror("fgets read cmd failed");
break;
}
// 3. 修剪命令行
trim_cmd(cmd_buf);
if (strlen(cmd_buf) == 0)
{
continue;
}
// 4. 解析命令行(新增:判断是否为后台执行)
is_background = 0;
int arg_count = parse_cmd(cmd_buf, argv, &is_background);
if (arg_count == 0)
{
continue;
}
// 5. 执行内置命令
if (execute_builtin_cmd(argv))
{
continue;
}
// 6. 执行外部命令(支持前台/后台执行)
pid_t pid = fork();
if (pid == -1)
{
perror("fork create child process failed");
continue;
}
else if (pid == 0)
{
// 子进程:执行外部命令
execvp(argv[0], argv);
perror("execvp execute command failed");
exit(EXIT_FAILURE);
}
else
{
// 父进程(Shell进程):根据是否后台执行,决定是否等待
if (is_background)
{
// 后台执行:打印后台进程信息,不阻塞等待
printf("后台进程启动,PID:%d\\n", pid);
}
else
{
// 前台执行:阻塞等待子进程执行完成
int status;
pid_t ret_pid = waitpid(pid, &status, 0);
if (ret_pid == -1)
{
perror("waitpid wait child process failed");
continue;
}
}
}
}
printf("my_shell exit successfully\\n");
return 0;
}
// 辅助函数:修剪字符串前后的空白字符
void trim_cmd(char *cmd)
{
if (cmd == NULL || strlen(cmd) == 0)
{
return;
}
int start = 0;
while (cmd[start] == ' ' || cmd[start] == '\\t' || cmd[start] == '\\n')
{
start++;
}
int end = strlen(cmd) – 1;
while (end >= start && (cmd[end] == ' ' || cmd[end] == '\\t' || cmd[end] == '\\n'))
{
end–;
}
if (start > end)
{
cmd[0] = '\\0';
}
else
{
memmove(cmd, cmd + start, end – start + 1);
cmd[end – start + 1] = '\\0';
}
}
// 核心函数:解析命令行,拆分命令与参数(新增:判断后台执行)
// 参数:is_background-输出型参数,1-后台执行,0-前台执行
int parse_cmd(char *cmd, char *argv[], int *is_background)
{
if (cmd == NULL || argv == NULL || is_background == NULL || strlen(cmd) == 0)
{
return 0;
}
int arg_count = 0;
char *token = strtok(cmd, " \\t");
while (token != NULL && arg_count < MAX_ARG_NUM – 1)
{
argv[arg_count++] = token;
token = strtok(NULL, " \\t");
}
argv[arg_count] = NULL;
// 判断是否为后台执行:最后一个参数是否为&
if (arg_count > 0 && strcmp(argv[arg_count – 1], "&") == 0)
{
*is_background = 1;
argv[arg_count – 1] = NULL; // 移除&符号,避免传递给exec函数
arg_count–; // 参数个数减1
}
else
{
*is_background = 0;
}
return arg_count;
}
// 核心函数:执行内置命令
int execute_builtin_cmd(char *argv[])
{
if (argv == NULL || argv[0] == NULL)
{
return 0;
}
// 1. 实现exit命令
if (strcmp(argv[0], "exit") == 0)
{
printf("Goodbye! my_shell exit now…\\n");
exit(EXIT_SUCCESS);
}
// 2. 实现pwd命令
if (strcmp(argv[0], "pwd") == 0)
{
char cwd[PATH_MAX];
if (getcwd(cwd, sizeof(cwd)) != NULL)
{
printf("%s\\n", cwd);
}
else
{
perror("getcwd get current working directory failed");
}
return 1;
}
// 3. 实现cd命令
if (strcmp(argv[0], "cd") == 0)
{
char *target_dir = argv[1];
if (target_dir == NULL)
{
target_dir = getenv("HOME");
if (target_dir == NULL)
{
fprintf(stderr, "cd: no HOME environment variable found\\n");
return 1;
}
}
if (chdir(target_dir) == -1)
{
perror("cd: change directory failed");
}
return 1;
}
// 不是内置命令,返回0
return 0;
}
// 信号处理函数:处理SIGCHLD信号,回收后台子进程(避免僵尸进程)
void sigchld_handler(int signo)
{
(void)signo; // 消除未使用参数的警告
// 循环调用waitpid,回收所有已退出的子进程(非阻塞模式)
pid_t pid;
int status;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0)
{
printf("后台进程(PID:%d)执行完成,已自动回收\\n", pid);
}
}
代码编译与执行
gcc my_shell_v5.c -o my_shell_v5
./my_shell_v5
执行效果
[my_shell] $ sleep 10 &
后台进程启动,PID:12350
[my_shell] $ pwd
/root/my_shell
[my_shell] $ ls -l
总用量 32
-rwxr-xr-x 1 root root 9152 1月 17 16:30 my_shell_v5
-rw-r–r– 1 root root 3876 1月 17 16:29 my_shell_v5.c
…(其他文件列表)
后台进程(PID:12350)执行完成,已自动回收
[my_shell] $ exit
Goodbye! my_shell exit now…
核心知识点解析
总结
这个 Shell 虽然简单,但已经具备了 bash 的核心工作流程,能够满足基本的终端使用需求,让我们深刻理解了 Shell 命令行解释器的底层工作原理。
打造自主 Shell 的过程,是一个将 Linux C 编程知识融会贯通的过程。通过这个过程,我们不仅掌握了 Shell 的工作原理,还加深了对进程控制、字符串处理、信号处理等核心知识点的理解,为后续开发更复杂的 Linux 应用程序打下了坚实的基础。
如果你在实现过程中遇到了问题,或者有更好的功能扩展思路,欢迎在评论区留言讨论!




