标准输入输出 — 键盘也是文件,以及把字写进卷里¶
这一章讲什么: 「键盘输入」和「读文件」看上去是两种事, POSIX 的回答是:它们是同一个抽象——文件。键盘是 fd=0(标准输入), 应用不用改一行代码,fgets 就能从键盘读。这带来一个新问题: 文件有 EOF(结尾),键盘没有——所以要有 EOT 约定(Ctrl+D)。 之后本章翻转方向:往卷里写文件——沿簇链写、不够就接簇、回改目录项, 最后 cp 命令全程跑通,文件系统的读写闭环完成。
1. 先看现象:grep 从键盘读,cp 往卷里写¶
主线例子:
grep foo @stdin
fopen("@stdin") → OS 把路径翻译成 fd=0 → 键盘伪装成文件
键盘敲的行逐行进 grep;敲到 Ctrl+D(ASCII 4 = EOT)→ read 返回 0 = EOF
→ grep 正常结束
cp memmap abc
open("abc", "w") → 卷里没找到 → 在目录项数组找空位,写 8+3 名,大小清 0
fwrite → 沿簇链写数据;一个簇装不下 → ExtendCluster 从簇 2 起扫 fat[i]==0 的空闲簇,
接到链尾(fat[旧尾]=新簇,新簇=链尾标记)
写完回改目录项的 file_size
cat abc → 内容分毫不差
(写只发生在内存里的卷镜像——MikanOS 没有磁盘驱动,重启即还原)
2. 顶层全景¶
26.1–26.3 标准输入:fd 固定编号(stdin=0/stdout=1/stderr=2);
TerminalFileDescriptor(键盘的文件描述符实现);应用启动前 files_[0] 预置;
@stdin 特殊路径(readfile/grep 直接吃键盘)
26.4 EOF/EOT:文件有终点、键盘没有;Ctrl+D = ASCII 4;返回 0 与 EOF 同行为
26.5 写文件(1):写=改 FAT 的两类数据(簇链数据 + 目录项);
open 创建分支(找空目录项、SetFileName 8+3 大写、大小置 0);ExtendCluster 接簇;
cp 命令先创建空文件
26.6 写文件(2):FileDescriptor::Write(写簇、越簇推进、必要时接簇);回改目录项大小
图说:读半边(25 章)与写半边(本章)合拢,管道只差最后一根管子。
3. 核心原理¶
3.1 标准输入:fd 0 预置 + 键盘的文件化¶
标准输入/输出/错误在程序启动时就开好了,固定编号 stdin=0、stdout=1、stderr=2;
它们就是把键盘、屏幕「显示为文件的机制」1。
实现两个动作:① Task::files_ 数组预置 fd=0 为 TerminalFileDescriptor
(键盘的 Read = 从终端任务队列等按键,和文件 Read 同一签名);
② 特殊路径 @stdin 映射到 fd=0(Linux 用 /dev/stdin)——
于是 readfile/grep 这些「打开路径」的应用零修改就能吃键盘2。
作者的评价:「标准输入系统可以统一处理键盘和文件,这让我们感到很浪漫!」3
3.2 EOF 与 EOT¶
grep @stdin 永不结束的原因:普通文件有 EOF(终点),键盘没有;
数据传输的终点叫 EOT4。
约定:Ctrl+字母 = ASCII 控制字符(按位与 0x1F),Ctrl+D 正好是 4 = EOT;
TerminalFileDescriptor 读到 Ctrl+D 就返回 0——与文件 EOF 行为相同,
「应用无法区分 EOF 和 EOT,只是按 EOF 处理」5。
其余 Ctrl 组合暂忽略(带 ^ 显示)。——至此 grep foo @stdin 成为能善终的命令。
3.3 写入文件:改两类数据¶
MikanOS 的写只写内存里的卷镜像(没有磁盘驱动,重启还原)6。
写 = 重写 FAT 上的两类数据:文件数据(沿簇链写,不够要接簇)和
目录项(文件大小、时间)7。
新文件的创建:open 没找到目标 → 在目录项数组找空位(首字节 0xE5 或 0x00),
SetFileName 把名字转 8+3 大写写进 11 字节,文件大小清零(时间字段暂不维护)8。
ExtendCluster:从簇 2 起扫 fat[i]==0 的空闲簇,收够 n 个就接到链尾
(fat[旧尾]=新簇首、新尾写链尾标记);作者明说不查「空闲簇不够」——
16 MiB 溢出前这检查没价值,留给读者9。
cp 命令(26.5 末)先把空文件创建跑通:26.6 补上 FileDescriptor::Write
(写当前簇、跨簇推进、必要时接簇),写完回改目录项 file_size,cp 全程成功10。
4. 作者的判断与证据¶
判断:一切皆文件,是统一机制而不是花招。 键盘文件化后,readfile/grep 零改动—— 抽象的回报当场兑现。
判断:能省的检查先省,但要说清楚为什么省。 空闲簇不足不查、目录项时间不维护—— 两处都是「16 MiB 教学尺度的理性懒惰」,明写留给读者。
证据: grep @stdin 的交互截图、cp 后 cat 对答案;EOF/EOT 的语义对照。
5. 边界与局限¶
- 写不落盘:重启全还原;没有崩溃一致性( journaling 的反面教材就是这里);
- 目录项的时间字段不维护,大小之外的元数据全是摆设;
- ExtendCluster 线性扫全盘,大卷上慢,且无空闲簇不足的报错;
- 没有真正的新lib write 以外的写接口封装(fwrite 之下就是 write)。
6. 可带走的¶
本章主线例子,一行写完: stdin=0 预置 + @stdin 路径 → grep 吃键盘 → Ctrl+D(EOT=返回0)收尾 → open 建空文件 + ExtendCluster 接簇 + Write 回改目录项 → cp 跑通。
- fd 0/1/2 启动即有:stdin=0、stdout=1、stderr=2;
- 「一切皆文件」的检验:旧应用零修改吃键盘;
- 特殊路径(@stdin / /dev/stdin)是给「非文件的目标」发的文件通行证;
- EOF(文件终点)≠ EOT(传输终点):键盘没有 EOF,靠 Ctrl+D(ASCII 4)约定;
- 写 FAT = 改两类数据:簇链 + 目录项;新文件 = 空目录项 + 8+3 名 + 大小 0;
- 接簇 = 扫
fat[i]==0的空闲簇接到链尾;写完回改目录项大小; - 检查可以省,但省的理由要写清楚(空闲簇不足的 TODO 是教材而非疏漏)。
7. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 标准输入与固定编号 | 26.1 标准输入(osbook_day26a) | text/165-ch26-01-26-1-osbook-day26a.txt:7(搜「标准输入」) · text/165-ch26-01-26-1-osbook-day26a.txt:16(搜「stdin=0」) · text/165-ch26-01-26-1-osbook-day26a.txt:21(搜「显示为文件」) |
| fd 预置与抽象 | 26.3 接收键盘输入 | text/166-ch26-03-26-3.txt:10(搜「抽象为「文件」」) · text/166-ch26-03-26-3.txt:16(搜「fd=0」) |
| @stdin 特殊路径 | 26.3 接收键盘输入 | text/166-ch26-03-26-3.txt:43(搜「@stdin」) · text/166-ch26-03-26-3.txt:45(搜「/dev/stdin」) |
| EOF/EOT 与 Ctrl+D | 26.4 EOF和EOT(osbook_day26b) | text/167-ch26-04-26-4-eof-eot-osbook-day26b.txt:6(搜「永远不会结束」) · text/167-ch26-04-26-4-eof-eot-osbook-day26b.txt:9(搜「End of Transmission」) · text/167-ch26-04-26-4-eof-eot-osbook-day26b.txt:17(搜「ASCII编码4」) · text/167-ch26-04-26-4-eof-eot-osbook-day26b.txt:22(搜「返回0」) |
| 写只进内存 | 26.5 写入文件(1)(osbook_day26c) | text/168-ch26-05-26-5-1-osbook-day26c.txt:22(搜「主内存」) |
| 两类数据与创建分支 | 26.5 写入文件(1)(osbook_day26c) | text/168-ch26-05-26-5-1-osbook-day26c.txt:24(搜「两类」) · text/168-ch26-05-26-5-1-osbook-day26c.txt:40(搜「未找到」) · text/168-ch26-05-26-5-1-osbook-day26c.txt:124(搜「SetFileName」) |
| ExtendCluster 与省略检查 | 26.5 写入文件(1)(osbook_day26c) | text/168-ch26-05-26-5-1-osbook-day26c.txt:88(搜「ExtendCluster」) · text/168-ch26-05-26-5-1-osbook-day26c.txt:104(搜「跳过了这一检查」) |
| cp 与 Write 回改目录项 | 26.5 写入文件(1)(osbook_day26c) | text/168-ch26-05-26-5-1-osbook-day26c.txt:135(搜「cp命令」) · text/169-ch26-06-26-6-2-osbook-day26d.txt:27(搜「Write()」) |
-
出处:「26.1 标准输入」(
text/165-ch26-01-26-1-osbook-day26a.txt:7,搜「标准输入」;:13,搜「stdin、stdout和stderr」;:16,搜「stdin=0」)。 ↩ -
出处:「26.3 接收键盘输入」(
text/166-ch26-03-26-3.txt:16,搜「fd=0」;:42,搜「@stdin」;:45,搜「/dev/stdin」)。 ↩ -
出处:「26.1 标准输入」(
text/165-ch26-01-26-1-osbook-day26a.txt:6,搜「浪漫」)。 ↩ -
出处:「26.4 EOF和EOT」(
text/167-ch26-04-26-4-eof-eot-osbook-day26b.txt:6,搜「永远不会结束」;:9,搜「End of Transmission」)。 ↩ -
出处:「26.4 EOF和EOT」(
text/167-ch26-04-26-4-eof-eot-osbook-day26b.txt:17,搜「ASCII编码4」;:22,搜「返回0」)。 ↩ -
出处:「26.5 写入文件(1)」(
text/168-ch26-05-26-5-1-osbook-day26c.txt:22,搜「主内存」)。 ↩ -
出处:「26.5 写入文件(1)」(
text/168-ch26-05-26-5-1-osbook-day26c.txt:24,搜「两类」;:26,搜「目录条目」)。 ↩ -
出处:「26.5 写入文件(1)」(
text/168-ch26-05-26-5-1-osbook-day26c.txt:40,搜「未找到」;:121,搜「HOGE FOO」;:124,搜「SetFileName」;:128,搜「文件大小被设置为0」)。 ↩ -
出处:「26.5 写入文件(1)」(
text/168-ch26-05-26-5-1-osbook-day26c.txt:88,搜「ExtendCluster」;:97,搜「候选」;:104,搜「跳过了这一检查」)。 ↩ -
出处:「26.5 写入文件(1)」(
text/168-ch26-05-26-5-1-osbook-day26c.txt:135,搜「cp命令」;text/169-ch26-06-26-6-2-osbook-day26d.txt:8,搜「无法复制数据」;:26,搜「Write()方法」)。 ↩