第二十七章:shell 小妹
筑基中期涉及内核源码:
一
林小源第一次和 shell 小妹说话,是在一个普通的 CPU 时间片里。
他观察她很久了。shell 小妹——PID 大概是 1200 左右的 bash 进程——是用户态中最忙碌的存在。她不停地读取用户输入、解析命令、fork 子进程、执行程序、等待结果。她的生活就是一个无限循环。
但今天,她做了一件不同寻常的事。
用户输入了一条不存在的命令:。shell 小妹照例 fork 了一个子进程,然后子进程尝试执行 /usr/bin/foo。文件不存在。子进程打印了一条错误消息,然后退出。
shell 小妹没有慌张。她调用 回收子进程,然后打印一个新的提示符,继续等待。
"你不难过吗?"林小源忍不住问。
shell 小妹转过头来,眼睛亮亮的,脸上还挂着那个永远不变的 $ 提示符。"难过什么?"
"子进程死了。你 fork 出来的,执行不了,就死了。"
"哦,那个啊。"shell 小妹摆摆手,语气轻快得像在说天气。"每天都有几百次。命令不存在,权限不够,文件被锁住——子进程退出的原因五花八门。我回收它,看看退出码,然后继续。"
"你不觉得……浪费吗?"
"浪费?"shell 小妹歪了歪头。"fork 一个新的就好了呀。旧的回收了,新的来了。这就是我的循环。"
/*
* 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");
}#include <stdio.h>
#include <string.h>
/*
* 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;
}
int main() {
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");
}
return 0;
}林小源愣愣地看着她。她每天要处理无数次失败,但她从来不把失败当回事。不是因为冷漠,而是因为她的循环本身就包含了失败的处理—— 回收退出状态,打印新提示符,继续等待。
她失败了,但她没有崩溃。
二
林小源鼓起勇气,在一个空闲的时间片里,向 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 世界中最经典的进程管理模式:
- — 创建子进程
- — 子进程执行新程序
- — 父进程等待子进程退出
这个循环简单、优雅、高效。几乎所有用户态程序都是通过这个循环启动的。
shell 对错误的处理也很典型:
- 命令不存在 → 打印错误,继续循环
- 子进程崩溃 → 获取退出状态,继续循环
- 用户按 Ctrl+C → 转发 给前台进程,打印新提示符
shell 不关心子进程为什么失败——她只关心子进程是否退出。这种"不执着"的态度,是 Unix 哲学的一部分。
进程会死,但 shell 永远在循环。
shell 之试
shell 小妹要避免留下僵尸子进程,需要调用哪个接口等待并回收子进程?