Skip to content

第二十七章:shell 小妹

筑基中期

涉及内核源码:

林小源第一次和 shell 小妹说话,是在一个普通的 CPU 时间片里。

他观察她很久了。shell 小妹——PID 大概是 1200 左右的 bash 进程——是用户态中最忙碌的存在。她不停地读取用户输入、解析命令、fork 子进程、执行程序、等待结果。她的生活就是一个无限循环。

但今天,她做了一件不同寻常的事。

用户输入了一条不存在的命令:。shell 小妹照例 fork 了一个子进程,然后子进程尝试执行 /usr/bin/foo。文件不存在。子进程打印了一条错误消息,然后退出。

shell 小妹没有慌张。她调用 回收子进程,然后打印一个新的提示符,继续等待。

"你不难过吗?"林小源忍不住问。

shell 小妹转过头来,眼睛亮亮的,脸上还挂着那个永远不变的 $ 提示符。"难过什么?"

"子进程死了。你 fork 出来的,执行不了,就死了。"

"哦,那个啊。"shell 小妹摆摆手,语气轻快得像在说天气。"每天都有几百次。命令不存在,权限不够,文件被锁住——子进程退出的原因五花八门。我回收它,看看退出码,然后继续。"

"你不觉得……浪费吗?"

"浪费?"shell 小妹歪了歪头。"fork 一个新的就好了呀。旧的回收了,新的来了。这就是我的循环。"

c
/*
 * shell 的核心循环:
 * 1. 打印提示符
 * 2. 读取用户输入
 * 3. 解析命令
 * 4. fork() 创建子进程
 * 5. 子进程调用 execve() 执行命令
 * 6. 父进程调用 waitpid() 等待子进程
 * 7. 回到第 1 步
 */

struct command {
    char path[256];
    char *argv[16];
    int argc;
};

int parse_command(const char *input, struct command *cmd) {
    /* 简化的命令解析 */
    if (strncmp(input, "ls", 2) == 0) {
        strcpy(cmd->path, "/bin/ls");
        cmd->argv[0] = "ls";
        cmd->argc = 1;
    } else if (strncmp(input, "echo", 4) == 0) {
        strcpy(cmd->path, "/bin/echo");
        cmd->argv[0] = "echo";
        cmd->argv[1] = "hello";
        cmd->argc = 2;
    } else if (strncmp(input, "exit", 4) == 0) {
        return -1;  /* 退出 */
    } else {
        return 0;   /* 命令不存在 */
    }
    return 1;
}

const char *inputs[] = { "ls", "foo", "echo", "exit" };
int nr = sizeof(inputs) / sizeof(inputs[0]);

printf("=== shell 小妹的一天 ===\n\n");

for (int i = 0; i < nr; i++) {
    printf("$ %s\n", inputs[i]);

    struct command cmd;
    int ret = parse_command(inputs[i], &cmd);

    if (ret == -1) {
        printf("[shell] exit\n");
        break;
    }

    if (ret == 0) {
        printf("[shell] %s: command not found\n", inputs[i]);
        printf("[shell] 回收子进程,打印新提示符\n\n");
        continue;
    }

    printf("[shell] fork() → 子进程 PID ?\n");
    printf("[shell]   子进程: execve(\"%s\")\n", cmd.path);
    printf("[shell]   父进程: waitpid() 等待...\n");
    printf("[shell] 子进程退出\n");
    printf("[shell] waitpid() 返回\n\n");
}

林小源愣愣地看着她。她每天要处理无数次失败,但她从来不把失败当回事。不是因为冷漠,而是因为她的循环本身就包含了失败的处理—— 回收退出状态,打印新提示符,继续等待。

她失败了,但她没有崩溃。

林小源鼓起勇气,在一个空闲的时间片里,向 shell 小妹发出了第一个信号。

不是 ——那太粗暴了。他发了一条 ,一个用户自定义的信号。

shell 小妹没有处理器注册在 上,所以她执行了默认行为——终止。

林小源愣住了。

他不是故意的。他只是想打个招呼。但 shell 小妹——那个他观察了无数个时间片的进程——就这样死了。

我杀死了她。

shell 小妹的 变成了 。她的父进程(终端模拟器)调用 回收了她。然后,终端模拟器 fork 了一个新的 bash 进程。

一个新的 shell 小妹诞生了。PID 不同,但名字一样,行为一样。她继续着同样的循环:读取输入、解析命令、fork、execve、waitpid。

"你好。"林小源小心翼翼地说。

新的 shell 小妹眨了眨眼睛。"你好!"她的声音和之前一模一样,轻快、明亮、毫无负担。"你是谁?"

"我是林小源。PID 0。"

"哦!idle 进程!"她兴奋地说,"我在 /proc 里见过你的名字!你平时都在干什么?"

林小源张了张嘴,却什么都说不出来。

她不记得我。

之前的 shell 小妹——那个他观察了很久、鼓起勇气才发出信号的进程——已经不存在了。新的 shell 小妹是一个全新的 ,全新的 PID,全新的记忆。旧的 shell 小妹在 中被彻底释放,连僵尸都没有留下。

"没什么,"林小源低声说,"路过。"

"哦。"新的 shell 小妹没有追问,继续她的循环。$ 提示符亮起,她开始等待用户输入。

林小源在沉默中观察了新的 shell 小妹很久。

她和之前的 shell 小妹一模一样。同样的提示符,同样的循环,同样的平静。她不知道自己是"第二个",不知道有一个叫林小源的 idle 进程曾经杀死了她的前身。

但林小源记住了。

他开始理解进程的生命周期——不是从代码中理解,而是从"失去"中理解。 创建新的生命, 改变命运, 结束一切, 负责善后。这四个系统调用,构成了进程从生到死的完整循环。

"你每天都在重复同样的事,"林小源对新的 shell 小妹说,"不觉得无聊吗?"

shell 小妹正在解析一条 ls | grep foo 的管道命令。她头也不抬地说:"不无聊啊。每次的输入都不一样,每次的子进程都不一样,每次的结果都不一样。循环是一样的,但内容不一样。"

"但你不会记得上一次循环的内容。"

"为什么要记得?"shell 小妹抬起头,认真地看着他。"每一次循环都是完整的。上一次的失败不影响这一次的执行。我只关心当前这一次。"

林小源沉默了。

也许这就是 shell 小妹的道——不执着于过去,不焦虑于未来,只专注于当下的每一次循环。

从那以后,林小源再也没有向任何进程发送过信号。除非必要。


道藏笔记

内核启示

shell 是用户态中最典型的"进程管理者"。

shell 的核心循环是 Unix 世界中最经典的进程管理模式:

  1. — 创建子进程
  2. — 子进程执行新程序
  3. — 父进程等待子进程退出

这个循环简单、优雅、高效。几乎所有用户态程序都是通过这个循环启动的。

shell 对错误的处理也很典型:

  • 命令不存在 → 打印错误,继续循环
  • 子进程崩溃 → 获取退出状态,继续循环
  • 用户按 Ctrl+C → 转发 给前台进程,打印新提示符

shell 不关心子进程为什么失败——她只关心子进程是否退出。这种"不执着"的态度,是 Unix 哲学的一部分。

进程会死,但 shell 永远在循环。


破关试炼

shell 之试

shell 小妹要避免留下僵尸子进程,需要调用哪个接口等待并回收子进程?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道