跳到主要内容
Linux 进程控制实战:手写微型 Shell 命令行解释器 | 极客日志
C
Linux 进程控制实战:手写微型 Shell 命令行解释器 基于 Linux 系统调用 fork、exec、waitpid,使用 C 语言从零构建微型 Shell 命令行解释器。实现包括命令行提示符生成、用户输入解析、内建命令(cd/export/env/echo)处理及外部命令执行流程。通过完整源码展示进程生命周期管理,帮助开发者深入理解 Shell 工作原理与进程控制核心逻辑。
雾岛听风 发布于 2026/2/10 更新于 2026/4/25 1 浏览Linux 进程控制实战:手写微型 Shell 命令行解释器
前面几篇文章,我们已经掌握了进程控制的'全链路技能':用 fork 创建子进程、exec 替换程序、waitpid 回收资源、exit 终止进程。今天,我们将这些知识'组装'成一个能实际运行的工具——微型 Shell 命令行解释器 (简称'迷你 Shell')。
这个迷你 Shell 将支持:命令行提示符、内建命令(cd/export/env/echo)、外部命令(ls/ps等)、环境变量管理,完全遵循 Linux Shell 的核心工作逻辑。通过亲手实现,你会彻底明白'输入一条命令后,Shell 到底在做什么'。
一、先搞懂:Shell 的本质是'命令管家'
在写代码前,我们先回归本质:Shell 是一个'命令管家' ——它的核心工作是'接收用户命令→解析命令→调度资源执行命令→反馈结果',具体流程可拆解为一个无限循环:
展示提示符 :打印 [用户名@主机名 工作目录]$,告诉用户'可以输入命令了';
获取命令 :读取用户输入的一行命令(如 ls -l 或 cd /home);
解析命令 :将命令拆分为'命令名 + 参数'(如 ls -l 拆成 "ls", "-l", NULL);
执行命令 :
若为内建命令 (如 cd):Shell 自己执行(需修改 Shell 进程自身状态,不能用子进程);
若为外部命令 (如 ls):Shell 创建子进程,子进程用 exec 替换为目标程序,Shell 等待子进程退出;
循环往复 :回到第一步,等待用户输入下一条命令。
举个通俗的例子:Shell 就像餐厅服务员——提示符是'请问需要点什么?',用户输入是'一份牛排'(命令),解析是'牛排 + 七分熟'(命令 + 参数),执行是:
内建命令:服务员自己给你倒杯水(不用叫后厨);
外部命令:服务员叫后厨(子进程)做牛排,自己在旁边等(waitpid),做好后给你端过来(反馈结果)。
二、迷你 Shell 的核心模块设计
我们将迷你 Shell 拆解为 5 个核心模块,逐个实现并讲解,最后整合为完整代码。每个模块都对应 Shell 的一个关键功能,且能复用前面学的进程控制知识。
2.1 模块 1:命令行提示符 —— 给用户'交互的入口'
命令行提示符(如 [ubuntu@localhost myshell]$)的作用是'提示用户输入命令',它需要包含三个关键信息:用户名 、主机名 、当前工作目录 。我们通过 Linux 提供的函数获取这些信息:
用户名:getenv("USER")(从环境变量中获取当前登录用户);
主机名:getenv("HOSTNAME")(从环境变量中获取主机名);
当前工作目录:getcwd()(获取当前进程的工作目录,比 getenv("PWD") 更准确,因为 cd 后 PWD 可能未更新)。
实现细节
工作目录简化:默认显示完整路径(如 /home/ubuntu/myshell),我们可以简化为'最后一级目录'(如 ),更符合常用 Shell 的习惯;
myshell
缓冲区刷新: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 };
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 );
}
调用 print_prompt() 后,终端会显示类似:
[ubuntu@localhost myshell]$
2.2 模块 2:命令获取与解析 —— 把'字符串'变成'可执行指令' 用户输入的命令是'字符串'(如 ls -l /home),我们需要将其拆分为'命令名 + 参数数组'(如 ["ls", "-l", "/home", NULL]),才能传给 execvp 执行。这一步分两个子任务:
子任务 1:获取用户输入(用 fgets 而非 scanf)
scanf 的问题:遇到空格就停止读取,无法获取带空格的命令(如 echo "hello world");
fgets 的优势:读取一整行输入,包含空格,完美适配命令输入场景;
处理细节:fgets 会把用户输入的'回车符 \n'也读入,需将其替换为字符串结束符 \0;若输入为空行(只按回车),直接跳过。
子任务 2:解析命令(用 strtok 按空格拆分) strtok 是 C 标准库的字符串拆分函数,能按指定分隔符(这里是空格)拆分字符串:
第一次调用:strtok(command, " "),传入要拆分的命令字符串,返回第一个'非空格'的子串(命令名);
后续调用:strtok(NULL, " "),传入 NULL 表示'继续拆分上次的字符串',直到返回 NULL(拆分结束);
注意:拆分后的参数数组最后必须加 NULL,因为 execvp 要求参数数组以 NULL 结尾。
代码实现(命令获取与解析模块) #include <ctype.h>
#define ARGV_MAX 64
char * g_argv[ARGV_MAX] = {0 };
int g_argc = 0 ;
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--;
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 ;
}
g_argc = 3;
g_argv = ["ls", "-l", "/home", NULL]。
2.3 模块 3:内建命令处理 —— Shell'自己动手'的命令 内建命令(Built-in Command)是必须由 Shell 进程自身执行 的命令,因为它们需要修改 Shell 的'自身状态'(如 cd 修改工作目录、export 修改环境变量)——若用子进程执行,修改的是子进程的状态,父进程(Shell)的状态不会变(进程具有独立性)。
迷你 Shell 将实现 4 个核心内建命令:cd、export、env、echo,我们逐个实现。
3.3.1 内建命令 1:cd —— 切换工作目录 cd 的核心是调用 chdir 系统函数修改当前进程的工作目录,但需要处理特殊场景:
cd(无参数):切换到用户家目录(getenv("HOME"));
cd ~:同无参数,切换到家目录;
cd -:切换到'上次的工作目录'(需用 g_last_pwd 存储上次目录);
cd 目录路径:切换到指定目录(如 cd /home)。
3.3.2 内建命令 2:export —— 导出环境变量 export 的作用是'将变量添加到当前进程的环境变量表中',供后续执行的命令继承(如 export MY_VAR=123):
实现方式:用 putenv 函数(C 标准库),将'KEY=VALUE'格式的字符串添加到环境变量表;
注意:export 无参数时,可打印所有已导出的环境变量(可选功能)。
3.3.3 内建命令 3:env —— 打印所有环境变量 env 的作用是'打印当前进程的所有环境变量',实现方式是遍历全局环境变量数组 environ(extern char **environ),逐个打印每个环境变量(格式为'KEY=VALUE')。
3.3.4 内建命令 4:echo —— 打印内容或环境变量
echo 文本:直接打印文本(如 echo hello → 输出 hello);
echo $环境变量:打印指定环境变量的值(如 echo $PATH → 输出 /bin:/usr/bin);
echo $?:打印'上次命令的退出码'(需用全局变量 g_last_code 存储)。
代码实现(内建命令模块) #include <sys/wait.h>
int g_last_code = 0 ;
extern char ** environ;
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();
}
}
2.4 模块 4:外部命令执行 —— Shell'找帮手'的命令 外部命令(如 ls/ps/gcc)是'独立的可执行程序',需要 Shell 创建子进程执行(避免覆盖 Shell 自身代码),核心流程是:
fork 创建子进程;
子进程用 execvp 替换为目标程序(自动从 PATH 查找命令路径);
父进程用 waitpid 等待子进程退出,获取退出码(更新 g_last_code);
若 execvp 失败(如命令不存在),子进程退出并设置错误退出码。
代码实现(外部命令模块) 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);
}
}
}
Shell 判断 ls 是外部命令,fork 子进程;
子进程调用 execvp("ls", ["ls", "-l", NULL]),从 PATH 找到 /bin/ls,替换为 ls 程序;
ls 执行完毕后退出,父进程 waitpid 获取退出码(0 表示成功);
g_last_code 更新为 0,下次执行 echo $? 会输出 0。
2.5 模块 5:环境变量初始化 —— 继承父进程的'配置' 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);
}
三、迷你 Shell 完整源码与运行演示 将上述模块整合为完整代码(mini_shell.c),添加 main 函数实现'无限循环'的核心逻辑,再编译运行验证功能。
3.1 完整源码 #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 };
char * g_argv[ARGV_MAX] = {0 };
int g_argc = 0 ;
int g_last_code = 0 ;
extern char ** environ;
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 );
}
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 ;
}
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();
}
}
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);
}
}
}
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();
while (1 ) {
print_prompt();
int ret = get_command(command_buf, BUF_SIZE);
if (ret == -1 ) {
printf ("=== 迷你 Shell 退出 ===\n" );
break ;
} else if (ret == 0 ) {
continue ;
}
parse_command(command_buf);
if (is_builtin_command()) {
exec_builtin_command();
} else {
exec_external_command();
}
}
return 0 ;
}
3.2 编译与运行演示
步骤 1:编译代码 gcc mini_shell.c -o mini_shell -Wall
-o mini_shell:指定输出文件名为 mini_shell;
-Wall:显示所有警告(避免潜在错误)。
步骤 2:运行迷你 Shell === 迷你 Shell 启动 ===
PATH : /usr/local /sbin:/usr /local/bin :/usr/sbin :/usr/bin :/sbin :/bin
HOME : /home/ubuntu
USER : ubuntu
====================
[ubuntu@localhost myshell]$
步骤 3:测试核心功能 退出迷你 Shell :按 Ctrl+D 或输入 exit(可扩展 exit 内建命令),Shell 会退出:
[ubuntu@localhost ubuntu]$ === 迷你 Shell 退出 ===
[ubuntu@localhost ubuntu]$ ls -l
[ubuntu@localhost ubuntu]$ echo $?
0
[ubuntu@localhost ubuntu]$ lss
command not found: lss
[ubuntu@localhost ubuntu]$ echo $?
127
[ubuntu@localhost ubuntu]$ ls -l
total 4
drwxrwxr-x 2 ubuntu ubuntu 4096 Oct 116 : 00 myshell
[ubuntu@localhost ubuntu]$ 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
[ubuntu@localhost ubuntu] $ env | grep MY_VAR
MY_VAR =hello_mini_shell
[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
[ubuntu@localhost myshell]$ cd /home
[ubuntu@localhost home]$ cd ubuntu
[ubuntu@localhost ubuntu]$ cd -
/home
[ubuntu@localhost home]$ cd ~
[ubuntu@localhost ubuntu]$
四、进阶扩展:让迷你 Shell 更实用 当前的迷你 Shell 已实现核心功能,但还可以扩展以下进阶功能,使其更接近真实 Shell(如 bash):
4.1 扩展 1:支持命令别名(如 ll=ls -l)
实现思路:用哈希表(如 struct alias_map)存储'别名→原命令'的映射;
新增内建命令 alias:alias ll='ls -l',将别名添加到哈希表;
命令解析时:若命令是别名,替换为原命令(如 ll → ls -l)。
4.2 扩展 2:支持命令补全(按 Tab 补全命令 / 路径)
实现思路:监听 Tab 键(需关闭终端的'行缓冲',用 tcsetattr 修改终端属性);
补全逻辑:若输入的是命令前缀(如 l),遍历 PATH 目录,找出以 l 开头的命令(如 ls/less);若输入的是路径前缀(如 /hom),调用 readdir 遍历目录,补全为 /home。
4.3 扩展 3:支持重定向(如 ls > file.txt)
实现思路:解析命令时识别 >/< 等重定向符号,拆分'命令'与'重定向目标';
子进程执行前:调用 open 打开目标文件,用 dup2 重定向标准输出(stdout)或标准输入(stdin),再执行 execvp。
4.4 扩展 4:支持管道(如 ls | grep txt)
实现思路:用 pipe 创建管道,fork 两个子进程:
子进程 1:执行 ls,将标准输出重定向到管道写入端;
子进程 2:执行 grep txt,将标准输入重定向到管道读取端;
父进程:等待两个子进程退出,关闭管道两端。
五、总结:迷你 Shell 背后的'进程控制逻辑' 这个迷你 Shell 虽然简单,但完全基于前面文章的核心知识,是进程控制的'集大成者'。我们可以用一张图总结其核心逻辑:
用户输入 → 提示符(print_prompt) ↓
获取命令(get_command)→ 空行/EOF 处理 ↓
解析命令(parse_command)→ 生成 g_argv 数组 ↓
判断命令类型:
├─ 内建命令(is_builtin_command)→ Shell 自身执行(如 cd 修改工作目录)
└─ 外部命令 → fork 子进程 → 子进程 execvp 替换 → 父进程 waitpid 回收 ↓
更新退出码(g_last_code)→ 回到提示符,循环
内建命令与外部命令的本质区别:是否需要修改 Shell 自身状态;
进程控制的'全链路'应用:fork(创建子进程)→ exec(替换程序)→ waitpid(回收资源)→ exit(终止进程);
Linux Shell 的工作原理:不是'自己执行命令',而是'管理子进程执行命令'的'管家'。
至此,Linux 进程控制系统系列文章已全部完成。从进程的创建、终止、等待、替换,到最终实现迷你 Shell,我们走过了'理论→实践'的完整路径。希望你能亲手运行代码,修改扩展,真正将这些知识内化为自己的技能。
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online