设计者的取舍:路径、fork 的诞生与全局文件队列
这一篇不太"教科书"------6 月末到 7 月初的几篇笔记(
路径、Hash表链表、能够这么设计)本质上都在追问同一个问题:这些机制为什么被设计成这样?
路径为什么长这样?fork/exec 为什么是这个顺序?调度为什么不用优先级当 hash 键?
把三个"为什么"放在一起看,会看到 Unix 设计者一以贯之的取舍逻辑。
一、路径:一个"优点明显大于缺点"的继承
路径机制继承自 UNIX。它解决的三个问题:
| 优点 | 说明 |
|---|---|
| 全局唯一定位 | 树形目录 + 字符串路径,全局唯一标识一个文件,顺带解决重名冲突(不同目录下可以有同名文件) |
| 逻辑分层管理 | 按用途分类:/bin 程序、/etc 配置、/home 用户文件------人和程序都好管理 |
| 系统级收益 | 文件不再杂乱无章,权限、配额、备份都能按目录树落地 |
代价当然有:如果没有路径,定位一个文件要 O(n) 遍历数百万个文件 ;而有了路径,人类要多敲几个字符------对人麻烦了一点,对管理舒服了一大截。
笔记里那句大实话值得记下:"我们是来学习它的设计方法的,不是来学习设计思路来源的。"
很多机制(包括路径)当年诞生得相当"无厘头"------设计者觉得这么做很好,做了,效果确实好,误打误撞成了标准。学系统,学的是"它是什么、怎么运作",而不是替古人辩护。
适量追问可以,大量钻牛角尖不行------"能够这么设计,就是优点大于缺点了。"
路径机制落地到进程侧,就是那个老朋友:PCB 必须带 CWD------文件系统需要路径找文件,进程需要基准解析相对路径,两者一拍即合。
二、fork/exec 的诞生:一段真实的进化史
这是本篇最有"考古感"的部分。在 fork 出现之前,根本没有"创建子进程"这回事。 shell 执行一条命令的完整流程是这样的:
【fork 之前的原始 shell】
① 清理临时文件,重置标准输入输出
② 把 ls/cat 等命令程序从磁盘载入内存,
【整块覆盖掉 shell 自身的代码和数据】← shell 自己"变身"为目标程序
③ 运行命令
④ 命令退出 → 重新从磁盘载入 shell,覆盖回内存 → 回到命令行等待输入
问题一目了然:
- 每执行一条命令,shell 要"死"一次;
- 命令结束后还要再读一次磁盘把 shell 加载回来------又是整整一遍 IO;
- 要连续执行多条命令?CPU 都要"饿死"了,时间浪费极其严重。
于是进化分三步走:
第一步:fork() ------ 创建子进程,shell 不再为下一条命令"让路"
第二步:exec() ------ 子进程直接覆盖自己的代码段变成目标程序
第三步:写时拷贝 ------ 让"复制"这件事本身变得便宜
中间还有个过渡形态:早期方案是在 if (id == 0) 里手写代码 去执行目标程序------繁琐且不通用。exec 的"直接覆盖源代码"把这个别扭彻底解决。fork + exec + COW,三者是一套配合演进出来的组合拳,缺一不可。
2.1 为什么子进程默认继承父进程的绝大部分属性?
fork 之后,父子 PCB 的字段分布非常有趣:
完全相同、直接复用的(刚诞生时一模一样):
- 文件系统上下文:cwd、根目录、umask
- 权限身份:uid/gid、会话、进程组、nice、调度策略
- 信号:处理函数、屏蔽集、未决信号
- 资源限制 rlimit、打开的文件描述符表
- 环境变量、内存映射(页表共享 + COW)
必须不同的:
- PID、PPID
- 退出状态、退出码、僵尸标记
- 内核栈、运行时计时、等待队列私有数据
为什么默认"抄"而不是"从零建"?
系统无法预判你的需求。
90% 的场景(shell、脚本、工具调用)都需要复用父进程的目录、终端、权限、文件句柄;
少数不需要的场景,系统提供了接口让你手动清理。
如果默认是"空环境",绝大多数程序都要重复写一大堆初始化代码------得不偿失。
默认继承 + 按需修改,又是一笔"优点大于缺点"的账。
三、为什么进程用 hash 调度,文件却要全局链表?
这一节是两套管理结构的对照实验,非常有意思。
3.1 前提:两套结构在哪
| 存放位置 | 结构 | |
|---|---|---|
| 进程调度 | CPU 内核空间的 runqueue | hash 组织的队列(活跃/过期队列) |
| 进程打开的文件 | 每个进程自己的 PCB 里 (struct file* 数组) |
进程私有 |
| 系统全部文件 | 没有天然的集中结构 | 需要设计 |
3.2 为什么调度不用"优先级"当 hash 键?
直觉上"按优先级 hash 查进程"很合理,实际上完全行不通:
- 内核调度器(CFS)会动态调整优先级和权重 ------如果优先级是 hash 键,每次优先级变化,内核就得删旧条目、重算 hash、插新桶;
- 进程还在活跃队列和过期队列之间反复横跳;
- 等你按 hash 查到这个节点,它的优先级早变了、位置早挪了------查询的收益赶不上维护的成本。
结论:对于"属性频繁变化"的对象,别拿属性当 hash 键。 全局队列至少位置稳定。
3.3 为什么文件要"全局链表 + 每进程数组"双层设计?
反过来,文件这边的问题正好相反------文件是"散落"在各个进程里的:
- 每个进程只能管控自己打开的文件,拿不到别人打开的(不然你自己一关,别人还怎么用?);
- 想知道"系统里有没有这个文件被打开",难道要遍历每个进程的文件数组?不同进程还可能重复打开同一个文件,查找重复、效率极低。
所以设计是:
每进程:struct file* 数组(进程私有的"我打开了谁")
系统级:全局文件链表("系统里正开着谁",支持快速全局查询与去重)
四、一点方法论:计算机世界是"单纯"的
最后把笔记里那段"暴论"整理成可用的心法:
现实世界不能完全掌控,所以才有无数种情况和对应的解法;
计算机世界是可控的------进程、文件、权限都能被精确操纵,所以规则可以简单、单纯、却严谨。
落到学习上的三条推论:
- 别急着质疑设计------先搞清它解决什么问题、付出了什么代价,多数时候"优点大于缺点"成立;
- 别神话设计------路径、fork、exec 都是历史演进 + 工程取舍的产物,不是神谕;
- 对照着学 ------"进程用 hash、文件用链表"这种差异对比,比孤立记忆十倍有效:结构跟着"对象的变化频率与查询需求"走。
小结
| 问题 | 答案 |
|---|---|
| 路径为什么存在 | 全局唯一定位 + 分层管理,代价小收益大,UNIX 遗产 |
| fork 之前怎么执行命令 | shell 自己被覆盖成目标程序,结束再从磁盘载回,IO 浪费巨大 |
| fork/exec/COW 的关系 | fork 保住 shell → exec 免去手写执行代码 → COW 让复制变便宜 |
| 子进程为何默认继承父属性 | 90% 场景要复用;默认继承 + 按需修改,系统无法预判需求 |
| 调度为何不用优先级当 hash 键 | 优先级被 CFS 动态调整,条目迁移成本 > 查询收益 |
| 文件为何要全局链表 | 文件散在各进程里,全局查询/去重必须有集中结构 |
本文基于 2026 年 6 月末至 7 月初的《路径》《Hash表链表》《能够这么设计》等笔记整理。