这是全书的最后一课。前面三十课,你学了 fork/exec、管道、重定向、信号……现在,我们把它们合流,亲手用 C 写一个能跑的真 shell。你会发现:你天天敲的 lscdsleep 10 &,背后其实都是同一套机制。 写完它,你就真正"驾驭"了操作系统。

从"你敲的那条命令"说起

你敲下 ls -l | grep txt > out.txt,回车,结果出来了。这一瞬间,shell 在背后做了什么?

它把这条命令拆成:两个命令(ls -lgrep txt)、一个管道(|)、一个重定向(>),然后安排两个子进程协同完成

这一课,我们把 shell 拆开看,再亲手拼一个。


31.1 shell 的主循环

一句话理解:shell 就是个 while(1) 循环——打印提示符 → 读一行 → 解析(分词/拆管道/拆重定向)→ fork 子进程 exec 执行 → 父进程等待(或后台)。

所有 shell 的核心,都是一个无限循环

while (1) {
    打印提示符 "mysh> ";
    读一行输入;
    解析这行(拆成命令、管道、重定向);
    fork 子进程去执行;
    父进程 wait(前台)或不等(后台);
}
就这五步,"读 → 解析 → 执行"。真 shell(bash)复杂得多,但骨架就是它。

31.2 命令执行:fork + exec

一句话理解:外部命令必须 fork 出子进程,子进程 exec 替换成目标程序,父进程 waitpid 收尸——这就是第 7 课内容的落地。

还记得第 7 课的 fork 和 exec 吗?这里是它们的实战

  • 外部命令(如 ls):fork 出子进程,子进程 execvp("ls", argv) 把自己替换成 ls,父进程 waitpid 等它结束;
  • cd 不能靠 fork——因为子进程 chdir 改了目录,对父进程无效(子进程一结束就消失)。
所以 cd内建命令:必须在 shell 自己进程里执行 chdir,才能真的改变 shell 的工作目录。这正是第 25 课"内建 vs 外部命令"的底层原因。

31.3 管道 |

一句话理解A | Bpipe() 建一条数据通道,dup2 把 A 的 stdout 接到管道写端、B 的 stdin 接到读端,两个子进程就这么串起来了。

ls | wc -l 怎么实现?三步:

  1. pipe():建一条数据通道(一个读端、一个写端,第 9 课讲过);
  2. dup2:把 lsstdout 指到管道写端(这样 ls 的输出不落屏幕,流进管道);
  3. wcstdin 指到管道读端(这样 wc 读的就是 ls 的输出)。
一句话:管道 = pipe 建通道 + dup2 改接口。 两个子进程,一个往管道写,一个从管道读。

31.4 重定向 > 和 <

一句话理解>open 文件后 dup2 到 stdout,< 同理到 stdin;2> 是重定向 stderr(fd=2)。

echo hi > f.txt 怎么实现?

  1. open("f.txt"):打开(或创建)文件,拿到一个 fd;
  2. dup2(fd, 1):把这个 fd 复制到 stdout(fd=1) 的位置;
  3. 于是子进程的 stdout 指向了文件,echo 输出就写进了文件。
echo 自己完全不知道发生了什么——它还是老老实实写 stdout,只是 stdout 已经被 dup2 换成了文件。这就是重定向的"魔法":改的是 fd 指向,不是程序本身

31.5 后台任务 & 信号处理

一句话理解:命令结尾 & 让父进程不 wait、立刻回到主循环;Ctrl+C(SIGINT)要发给前台进程组而不是 shell 自己,所以 shell 要做信号处理 + 进程组管理
  • 后台任务sleep 10 & 里的 &,就是让父进程waitpid,立刻回到主循环、重新打印提示符。10 秒后,父进程收到 SIGCHLD(子进程结束信号)才知道它结束了;
  • 信号处理:你按 Ctrl+C(发 SIGINT),这个信号必须发给前台进程组,而不是 shell 自己——否则 shell 会被打断。所以真 shell 要做进程组管理(第 8 课信号、第 5 课进程的知识都用上了)。

动手实验:C 手写迷你 shell

// lesson31.c —— 迷你 shell:命令执行 + cd + 管道 + 重定向 + 后台
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>

int main(void) {
    char line[256];
    while (1) {
        printf("mysh> "); fflush(stdout);
        if (!fgets(line, sizeof(line), stdin)) break;
        line[strcspn(line, "\n")] = 0;
        if (strlen(line) == 0) continue;

        // 后台任务:结尾 & 去掉,父进程不 wait
        int bg = 0;
        int len = strlen(line);
        if (line[len-1] == '&') { bg = 1; line[len-1] = 0; }

        // 内建命令 cd(不能用 fork,否则改了子进程没用)
        if (strncmp(line, "cd ", 3) == 0) {
            if (chdir(line + 3) != 0) perror("cd");
            continue;
        }
        if (strcmp(line, "exit") == 0) break;

        pid_t pid = fork();
        if (pid == 0) {                       // 子进程:拆命令执行
            char *argv[64]; int argc = 0;
            char *tok = strtok(line, " ");
            while (tok && argc < 63) argv[argc++] = tok, tok = strtok(NULL, " ");
            argv[argc] = NULL;
            execvp(argv[0], argv);            // 替换成目标程序
            perror("execvp"); exit(1);
        } else {                              // 父进程
            if (!bg) waitpid(pid, NULL, 0);   // 前台等,后台不等
        }
    }
    return 0;
}

编译运行:

gcc lesson31.c -o mysh && ./mysh

试试这些命令:

mysh> ls
mysh> cd /tmp
mysh> sleep 10 &
mysh> ls -l
mysh> exit
一句话理解:这个不到 50 行的 shell,已经能跑 lscdsleep 10 &exit——fork + exec 是它的灵魂。

扩展练习(把这门课的剩余知识也加进去):

  • 管道 |pipe + dup2
  • 重定向 >open + dup2

深入点:两个进阶话题

① 进程组与会话

真 shell 用 setpgid 把每条管道的前台进程组交给终端,tcsetpgrp 管理谁读终端输入,Ctrl+C 才能精准发给"前台那组"——这是交互式 shell 的完整版。

② 作业控制

jobsfgbgCtrl+Z(SIGTSTP 暂停)本质是 shell 维护一张"作业表",记录每个后台任务的 pid 和状态——手写 shell 的终极挑战。

小结与思考题

这一课,我们把全书知识合流成了一个真 shell:

  • 主循环:读 → 解析 → 执行,while(1)
  • 命令执行:外部命令 fork+exec,cd 是内建;
  • 管道:pipe 建通道 + dup2 改接口;
  • 重定向:open + dup2,改 fd 指向;
  • 后台& 让父进程不 wait,SIGCHLD 收尾。

留三个问题:

  1. 为什么 cd 必须是内建命令?
  2. 管道和重定向的本质分别是什么(用 dup2 说)?
  3. sleep 10 && 让父进程做了什么/不做什么?

系列结语

从第 1 课"操作系统是什么",走到第 31 课"手写 shell",你已经:

  • 用 C 亲手摸过进程、线程、内存、文件、网络、容器
  • 用 Shell 写过自动化脚本、做过排障
  • 最后把它们拼成了一个能跑的命令解释器

这就是"程序脚下的地基"。理解操作系统,你就真正驾驭了它——不再是"只会调 API"的人,而是知道每一行代码背后,内核在做什么的人。

祝贺你,走到这里。这门课结束了,但操作系统的大门,才刚刚为你打开。

标签: 计算机基础, 操作系统, Linux

添加新评论