Day 8 实现简易Shell

一、什么是 Shell

在 Linux 中,Shell 本质上是一个用户态程序,它的核心职责只有一件事:

读取用户输入的命令,将其解析为程序执行请求,并通过内核机制创建和管理进程。

从内核视角看,Shell 并不特殊:

  • 它不是系统调用
  • 它不运行在内核态
  • 它和 lscat 一样,都是普通进程

Shell 的“特殊性”仅在于:

  • 长期运行
  • 频繁调用 fork / exec / wait
  • 充当用户与内核之间的交互中介

因此可以说:

Shell 是一个以“进程管理”为核心逻辑的普通程序。

二、一个「简易 Shell」需要实现哪些功能

为了避免目标失控,我们先定义一个教学级、可控范围内的简易 Shell
这个 Shell 不追求功能全面,而追求概念完整、语义正确

1️⃣ 命令读取(Command Reading)

Shell 首先要做的,是从标准输入中读取一行用户命令,例如:

ls -l /tmp

这一步的技术本质是:

  • stdin 读取一行文本
  • 正确处理换行符
  • 正确处理 EOF(Ctrl+D)

常见手段包括:

  • getline()(推荐,自动扩容)
  • read()(更底层,需自行管理缓冲区)

这是 Shell 与用户交互的起点。


2️⃣ 参数解析(Argument Parsing)

Shell 不能把整行字符串直接交给内核,它必须将命令拆解为:

argv[0] = "ls"
argv[1] = "-l"
argv[2] = "/tmp"
argv[3] = NULL

这一步的本质是:

把“文本命令”转换为 exec 系列函数所需的参数数组。

在简易 Shell 中,解析规则可以非常简单:

  • 以空格分割
  • 不考虑引号、不考虑转义、不考虑变量替换

但即便如此,这一步也清晰地区分了:

  • Shell 层面的语法处理
  • 内核层面的进程执行

3️⃣ 创建进程并执行命令(fork + exec)

这是 Shell 的核心执行路径:

  1. Shell 调用 fork()
  2. 子进程调用 execvp()
  3. 父进程调用 wait()waitpid()

这一步体现了 Unix 设计中最重要的原则之一:

进程创建(fork)与程序执行(exec)是分离的。


4️⃣ 内建命令(Built-in Commands)

并非所有命令都能用 exec 实现。

例如:

cd /home
exit

这些命令的特点是:

  • 必须影响 Shell 自身的状态
  • 如果放到子进程中执行,将毫无意义

因此,一个简易 Shell 至少需要实现:

  • cd
  • exit

这一步让你真正理解:

为什么 Shell 不能只是 fork + exec 的包装器。


5️⃣ 基础重定向与管道(核心进阶)

为了体现 Shell 的“操作系统味道”,简易 Shell 通常还会支持:

  • 单个管道:ls | wc -l
  • 基本重定向:><

这背后的技术核心是:

  • pipe
  • dup2
  • 文件描述符的继承与关闭

这一步把你前面学过的 fd / pipe / dup2 串成一个完整系统。


三、循序渐进的技术路线图(强烈推荐)

下面是一条完全可执行、不会跳跃、每一步都有“完成感”的路线图


🚀 Stage 1:最小 Shell 骨架(基础必做)

目标:
让 Shell 能执行最简单的外部命令。

你需要完成:

  1. 循环读取一行命令(getline)
  2. 将命令拆分为 argv(空格分割)
  3. fork
  4. execvp
  5. wait

完成标志:

myshell$ ls
myshell$ ps

代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int split_by_space(char *line, char *argv[], int max_argv);
int main(int, char **)
{

    char *line = NULL;
    size_t size = 0;
    ssize_t nread;
    while (1)
    {
        printf("mysh>");
        fflush(stdout);
        nread = getline(&line, &size, stdin);

        if (nread == -1)
        {
            printf("\n");
            break;
        }
        if (line[nread - 1] == '\n')
        {
            line[nread - 1] = '\0';
        }

        if (line[0] == '\0')
        {
            continue;
        }

        char *argv[64];
        split_by_space(line, argv, 64);

        pid_t pid = fork();
        if (pid == 0)
        {
            execvp(argv[0], argv);
            perror("exec");
            exit(1);
        }
        wait(NULL);

    }
    free(line);
    return 0;
}

int split_by_space(char *line, char *argv[], int max_argv)
{

    int argc = 0;
    char *token;
    char *saveprt = NULL;
    token = strtok_r(line, " ", &saveprt);
    while (token != NULL && argc < max_argv)
    {
        // printf("token is:%s\n", token);
        argv[argc++] = token;
        token = strtok_r(NULL, " ", &saveprt);
    }
    argv[argc] = NULL;
    return argc;
}


🚀 Stage 2:内建命令与健壮性

目标:
让 Shell 行为“像一个真正的 Shell”。

你需要完成:

  • cd(父进程中执行)
  • exit
  • exec 失败处理
  • Ctrl+D(EOF)退出

完成标志:

myshell$ cd /tmp
myshell$ exit

🚀 Stage 3:管道(pipe + dup2)

目标:
实现最经典的 Unix 管道模型。

你需要完成:

  • 单管道解析
  • pipe 创建
  • dup2 重定向 stdin/stdout
  • 正确关闭 fd

完成标志:

myshell$ ls | wc -l

🚀 Stage 4:重定向(输入 / 输出)

目标:
理解 fd 与文件的统一抽象。

你需要完成:

  • > 输出重定向
  • < 输入重定向
  • 与 exec 配合

完成标志:

myshell$ ls > out.txt
myshell$ wc -l < out.txt

🚀 Stage 5(可选):信号与后台执行

进阶方向,可自由选择:

  • Ctrl+C 不杀 shell
  • & 后台执行
  • SIGCHLD 回收子进程

posted @ 2025-12-25 17:11  seekwhale13  阅读(8)  评论(0)    收藏  举报