编程语言研究书库

文件读取 — 目录、路径、文件描述符,和第一个「管道上游」grep

文件读取 — 目录、路径、文件描述符,和第一个「管道上游」grep

这一章讲什么: 上一章应用还不会读文件。本章补齐三段: 目录与路径——目录就是「目录项数组」的文件,路径按 / 切段逐级下树; 文件描述符——应用用一个整数操作文件,OS 替你记住读到哪了; grep——第一个「读文件、处理、输出」的纯工具应用,管道时代的前哨。

1. 先看现象:一条路径的解析之旅

主线例子:

  $ ls efi/boot
    FindFile("efi/boot"):以 / 切段 → 根目录找 "efi"(是目录)→ 进它的簇递归找 "boot"
    → 返回 {目录项, post_slash=false};是目录 → 列出其内容
  $ cat apps/rpn    —— 同一套 FindFile,cat 与启动应用自动获得目录能力
  应用读文件:
    fd = open("memmap")     ← 系统调用;OS 建 FileDescriptor{目录项, 偏移=0},登记进当前任务的表
    read(fd, buf, 100)      ← 按偏移读簇链;偏移推进,下次接着读
  $ grep 3FE.D000 memmap    ← <regex> 搜索,匹配行原样输出

2. 顶层全景

  25.1 目录树与路径;FindFile 递归(NextPathElement 切段;post_slash 语义);ls 带路径
  25.2 应用读文件:fd 抽象的理由(不暴露 FAT 目录项);
       FileDescriptor{fat_entry_, rd_off_, rd_cluster_, rd_cluster_off_};
       Task::files_ 每任务一张;open/read 系统调用与 Newlib 桥接;
       OS 管偏移的理由(簇链只能从头追)
  25.3 grep 应用:std::regex / regex_search;posix_memalign 空壳

  图说:目录 → 读 → 搜,文件系统的「读」半边就此闭环。

3. 核心原理

3.1 目录与路径:递归就是下树

FAT 的目录是特殊文件(内容 = 目录项数组),属性 ATTR_DIRECTORY 标记1。 路径 = 用 / 分隔的目录名串;以 / 开头是绝对路径(从根出发),否则是相对路径2。 FindFile 升级为递归:NextPathElement 切出第一段,在本目录项数组里找, 找到且是目录、路径还有剩 → 带着剩余路径递归进那个簇3。 返回值多了一个 post_slash:告诉调用方「路径是否以 / 结尾」(efi/boot 与 efi/boot/ 在语义上有别,后者明确要求目标是目录)4。 ls、cat、启动应用全都走这一个 FindFile——改一处,全体受益5。

3.2 文件描述符:整数背后的设计账

给应用设计读文件,最低限度三步:路径 → 目录项 → 簇号 → 数据6。 但「直接给应用目录项和簇」被否了:那是 FAT 的私事——换个文件系统结构全变, 应用不该被绑死在 FAT 上(「操作系统负责抽象文件系统」)7。 于是用 fd(文件描述符)= 整数;Newlib 本来就按 POSIX 用整数处理文件,顺势。 关键结构 FileDescriptor:目录项引用 + 读取偏移;偏移还拆成两层—— 逻辑偏移(从文件头)与簇内偏移(从当前簇头),因为 FAT 的数据是一段段簇, 读的时候要用「簇号 + 簇内偏移」这对坐标8。 为什么偏移归 OS 管:应用自己管也可以(每次系统调用都从头指定偏移), 但 FAT 簇链只能从头追,每次全量重追极慢;OS 记住读到哪,顺序读就不用回头9。 fd 属于任务:Task 里加 files_ 数组,两个应用拿到相同编号也互不相干10。

3.3 grep:第一个工具应用

grep = 逐行读文件 + 正则匹配 + 输出命中行。难的是正则,但 C++ 标配 <regex>: std::regex 装模式,regex_search 判匹配——十来行写完11。 唯一的坑:libc++ 的 <regex> 依赖 posix_memalign(对齐分配),newlib_support.c 补个实现12。 「. 匹配任意单字符」的演示:grep 3FE.D000 memmap 命中含 3FE0D000 的行13。 ——它就是下一章管道的完美上游。

3.4 路径解析与读文件补完(25.1/25.2 节细节)

FindFile 的递归长什么样。 升级后的 FindFile(path, directory_cluster=0) 第一步先处理 入口:path 以 / 开头(或没给起始簇)就把起点定成根目录14;然后 NextPathElement 切出 第一段名字,在当前目录的目录项数组里找它15。找到了三种去向:

  • 名字后面还有 /(路径没走完)且目标是目录 → 以它的簇号为新起点,带着剩余路径递归;
  • 路径正好走完 → 返回这条目录项,第二个返回值 post_slash 记「结尾是不是 /」;
  • 任何一段找不到 → 返回空,终端显示 "No such file or directory"16。

ls efi/boot 的两次递归,书里列了一张表:第一次 "efi/boot" 切出 "efi" 在根目录找到, 第二次 "boot" 在 efi 的簇里找到17。

FileDescriptor 的读路径。 read(fd) 时 OS 拿「逻辑偏移 rd_off_」去定位:先算出它落在 哪个簇、簇内偏移多少(rd_cluster_/rd_cluster_off_),再从卷镜像里那个簇的对应位置拷数据 给应用;拷完把两个偏移一起前进,下次 read 接着读18。FAT 的簇链这时只走到「当前簇」 为止——正因为 OS 记住了位置,顺序读文件才不用每次从头追链19。这套接口就是 Newlib fread 的底座:应用调 fread,库调 read,系统调用做簇链翻译20。

3.5 顺手的彩蛋:ls 的权限标志

25.2 节末尾有个容易被滑过的细节:书里演示用 ls 显示出了 rwxr-xr-x 这样的权限标志, 并顺手教了读法——三段各三个字母,分别是文件所有者、所有者组、其他人的权限; r 可读、w 可写、x 可执行21。这套记法是 Linux 多用户设计的遗产(防止别人随意读 你的文件)。MikanOS 自己是单用户、不做权限 Enforcement,但 ls 照抄了这个格式—— 命令的对外格式向业界看齐,内部实现可以极简,这是作者一贯的姿势。 再补一句工程观察:读文件的系统调用只做「按 fd 拷字节」,文件名到 fd 的翻译发生在 open 那一次——之后 read 全程只见整数。这个「名字早翻译、全程用整数」的分工,正是 第 26 章标准输入能无缝顶替文件的前提:stdin 也只是一个预先开好的 fd22。

4. 作者的判断与证据

判断:抽象先于功能。 fd 层挡在 FAT 细节前面,是「换文件系统不Rebuild应用」的 最低保险;偏移管理同理——先想清楚「谁记住状态」,再设计系统调用。

判断:工具应用是操作系统的成绩单。 grep 全部素材来自既成机制(fd、read、printf), 本章几乎没写新内核代码——内核读到这个程度,工具会自己长出来。

证据: ls efi/boot 与 cat apps/rpn 直接可用;grep 的命中截图。

5. 边界与局限

  • 没有当前目录概念(cd 是留给读者的作业),相对路径从根解析;
  • 长名仍不支持,8+3 的短名世界;
  • 读没有缓存层,每次 read 直接翻译簇链(偏移记录让它不至于重走全程);
  • <regex> 在新lib上属「碰巧能链」,posix_memalign 也只是空壳级支持。

6. 可带走的

本章主线例子,一行写完: 路径切段递归下树(FindFile+post_slash)→ ls/cat/启动全体受益 → fd=open、read(偏移归 OS 管)→ grep 用 <regex> 做第一个工具应用。

  1. 目录 = 目录项数组的文件;路径按 / 切段递归下树;
  2. post_slash 这类「语义的边角」要进返回值,不能靠调用方再解析一遍路径;
  3. fd 是抽象层:应用只见整数,不见 FAT;换文件系统应用不动;
  4. 偏移归 OS 管:FAT 簇链只能从头追,状态在 OS 手里顺序读才便宜;
  5. fd 挂在任务上:同号不同义,互不干扰;
  6. 工具应用 = 现成机制的组合拳;grep 是管道的上游形态。

7. 原文地图

主题原书章原文位置
目录树与路径25.1 目录支持(osbook_day25a)text/162-ch25-01-25-1-osbook-day25a.txt:9(搜「目录树」) · text/162-ch25-01-25-1-osbook-day25a.txt:16(搜「路径(path)」) · text/162-ch25-01-25-1-osbook-day25a.txt:20(搜「绝对路径」)
FindFile 递归与 post_slash25.1 目录支持(osbook_day25a)text/162-ch25-01-25-1-osbook-day25a.txt:43(搜「深入目录树」) · text/162-ch25-01-25-1-osbook-day25a.txt:53(搜「true」) · text/162-ch25-01-25-1-osbook-day25a.txt:68(搜「递归函数」)
cat/启动全体受益25.2 读取文件(osbook_day25b)text/163-ch25-02-25-2-osbook-day25b.txt:15(搜「cat命令」)
不暴露目录项的理由25.2 读取文件(osbook_day25b)text/163-ch25-02-25-2-osbook-day25b.txt:41(搜「抽象文件系统」)
fd 与每任务表25.2 读取文件(osbook_day25b)text/163-ch25-02-25-2-osbook-day25b.txt:109(搜「文件描述符编号」) · text/163-ch25-02-25-2-osbook-day25b.txt:125(搜「files_」)
偏移归 OS 管的理由25.2 读取文件(osbook_day25b)text/163-ch25-02-25-2-osbook-day25b.txt:48(搜「偏移量」) · text/163-ch25-02-25-2-osbook-day25b.txt:56(搜「降低读取文件的效率」)
FileDescriptor 双层偏移25.2 读取文件(osbook_day25b)text/163-ch25-02-25-2-osbook-day25b.txt:140(搜「FileDescriptor类」) · text/163-ch25-02-25-2-osbook-day25b.txt:148(搜「rd_cluster_off_」)
grep 与 regex25.3 正则表达式搜索(osbook_day25c)text/164-ch25-03-25-3-osbook-day25c.txt:7(搜「正则表达式搜索」) · text/164-ch25-03-25-3-osbook-day25c.txt:17(搜「std::regex」)
posix_memalign25.3 正则表达式搜索(osbook_day25c)text/164-ch25-03-25-3-osbook-day25c.txt:24(搜「posix_memalign」)

Footnotes

  1. 出处:「25.1 目录支持」(text/162-ch25-01-25-1-osbook-day25a.txt:9,搜「目录树」;:16,搜「路径(path)」;:20,搜「绝对路径」)。 ↩

  2. 出处:「25.1 目录支持」(text/162-ch25-01-25-1-osbook-day25a.txt:25,搜「特殊文件」;:27,搜「ATTR_DIRECTORY」)。 ↩

  3. 出处:「25.1 目录支持」(text/162-ch25-01-25-1-osbook-day25a.txt:68,搜「递归函数」;:82,搜「NextPathElement」)。 ↩

  4. 出处:「25.1 目录支持」(text/162-ch25-01-25-1-osbook-day25a.txt:62,搜「post_slash」;表25.1 给出示例)。 ↩

  5. 出处:「25.2 读取文件」(text/163-ch25-02-25-2-osbook-day25b.txt:15,搜「cat命令」;:16,搜「apps中启动应用」)。 ↩

  6. 出处:「25.2 读取文件」(text/163-ch25-02-25-2-osbook-day25b.txt:21,搜「最低要求」;:32,搜「块编号」)。 ↩

  7. 出处:「25.2 读取文件」(text/163-ch25-02-25-2-osbook-day25b.txt:41,搜「抽象文件系统」;:44,搜「文件描述符(fd)」)。 ↩

  8. 出处:「25.2 读取文件」(text/163-ch25-02-25-2-osbook-day25b.txt:140,搜「FileDescriptor类」;:146,搜「rd_off_」;:150,搜「rd_cluster_off_」)。 ↩

  9. 出处:「25.2 读取文件」(text/163-ch25-02-25-2-osbook-day25b.txt:48,搜「偏移量」;:56,搜「降低读取文件的效率」;:59,搜「从头开始追踪」)。 ↩

  10. 出处:「25.2 读取文件」(text/163-ch25-02-25-2-osbook-day25b.txt:109,搜「文件描述符编号」;:111,搜「不会有任何关」;:125,搜「files_」)。 ↩

  11. 出处:「25.3 正则表达式搜索」(text/164-ch25-03-25-3-osbook-day25c.txt:17,搜「std::regex」;:19,搜「regex_search」;:31,搜「3FE.D000」)。 ↩

  12. 出处:「25.3 正则表达式搜索」(text/164-ch25-03-25-3-osbook-day25c.txt:24,搜「posix_memalign」;:27,搜「newlib_support.c」)。 ↩

  13. 出处:「25.3 正则表达式搜索」(text/164-ch25-03-25-3-osbook-day25c.txt:31,搜「3FE.D000」;:32,搜「任意单字符」)。 ↩

  14. 出处:「25.1 目录支持」(text/162-ch25-01-25-1-osbook-day25a.txt:80,搜「绝对路径」;:81,搜「代表根目录的值」)。 ↩

  15. 出处:「25.1 目录支持」(text/162-ch25-01-25-1-osbook-day25a.txt:82,搜「NextPathElement」)。 ↩

  16. 出处:「25.1 目录支持」(text/162-ch25-01-25-1-osbook-day25a.txt:57,搜「nullptr」;:57,搜「No such file or directory」)。 ↩

  17. 出处:「25.1 目录支持」(text/162-ch25-01-25-1-osbook-day25a.txt:74,搜「表25.2」;:76,搜「第1回」)。 ↩

  18. 出处:「25.2 读取文件」(text/163-ch25-02-25-2-osbook-day25b.txt:140,搜「FileDescriptor类」;:150,搜「rd_cluster_off_」;:152,搜「簇编号和簇内偏移」)。 ↩

  19. 出处:「25.2 读取文件」(text/163-ch25-02-25-2-osbook-day25b.txt:59,搜「从头开始追踪」;:60,搜「不必遍历簇链」)。 ↩

  20. 出处:「25.2 读取文件」(text/163-ch25-02-25-2-osbook-day25b.txt:43,搜「fopen()和fread()」;:46,搜「效仿这种做法」)。 ↩

  21. 出处:「25.2 读取文件」(text/164-ch25-03-25-3-osbook-day25c.txt:1,搜「rwxr-xr-x」;:3,搜「可执行」)。 ↩

  22. 出处:「25.2 读取文件」(text/166-ch26-03-26-3.txt:16,搜「fd=0」——标准输入预置为 fd 0 的做法,见下一章的展开)。 ↩

On this page