跳转至

标准输入输出 — 键盘也是文件,以及把字写进卷里

这一章讲什么: 「键盘输入」和「读文件」看上去是两种事, 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 跑通。

  1. fd 0/1/2 启动即有:stdin=0、stdout=1、stderr=2;
  2. 「一切皆文件」的检验:旧应用零修改吃键盘;
  3. 特殊路径(@stdin / /dev/stdin)是给「非文件的目标」发的文件通行证;
  4. EOF(文件终点)≠ EOT(传输终点):键盘没有 EOF,靠 Ctrl+D(ASCII 4)约定;
  5. 写 FAT = 改两类数据:簇链 + 目录项;新文件 = 空目录项 + 8+3 名 + 大小 0;
  6. 接簇 = 扫 fat[i]==0 的空闲簇接到链尾;写完回改目录项大小;
  7. 检查可以省,但省的理由要写清楚(空闲簇不足的 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()」)

  1. 出处:「26.1 标准输入」(text/165-ch26-01-26-1-osbook-day26a.txt:7,搜「标准输入」;:13,搜「stdin、stdout和stderr」;:16,搜「stdin=0」)。 ↩

  2. 出处:「26.3 接收键盘输入」(text/166-ch26-03-26-3.txt:16,搜「fd=0」;:42,搜「@stdin」;:45,搜「/dev/stdin」)。 ↩

  3. 出处:「26.1 标准输入」(text/165-ch26-01-26-1-osbook-day26a.txt:6,搜「浪漫」)。 ↩

  4. 出处:「26.4 EOF和EOT」(text/167-ch26-04-26-4-eof-eot-osbook-day26b.txt:6,搜「永远不会结束」;:9,搜「End of Transmission」)。 ↩

  5. 出处:「26.4 EOF和EOT」(text/167-ch26-04-26-4-eof-eot-osbook-day26b.txt:17,搜「ASCII编码4」;:22,搜「返回0」)。 ↩

  6. 出处:「26.5 写入文件(1)」(text/168-ch26-05-26-5-1-osbook-day26c.txt:22,搜「主内存」)。 ↩

  7. 出处:「26.5 写入文件(1)」(text/168-ch26-05-26-5-1-osbook-day26c.txt:24,搜「两类」;:26,搜「目录条目」)。 ↩

  8. 出处:「26.5 写入文件(1)」(text/168-ch26-05-26-5-1-osbook-day26c.txt:40,搜「未找到」;:121,搜「HOGE FOO」;:124,搜「SetFileName」;:128,搜「文件大小被设置为0」)。 ↩

  9. 出处:「26.5 写入文件(1)」(text/168-ch26-05-26-5-1-osbook-day26c.txt:88,搜「ExtendCluster」;:97,搜「候选」;:104,搜「跳过了这一检查」)。 ↩

  10. 出处:「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()方法」)。 ↩