现代操作系统最核心抽象之一 —— 文件
本文来自我的大规模数据系统专栏《系统日知录》,专注存储、数据库、分布式系统、AI Infra 和计算机基础知识。点击👉这里订阅,解锁更多文章。你的支持,是我前行的最大动力。
我们对文件(File)如此熟悉,以至于很少去思考其本质和关联的一些概念。本文参考 XV6 小册[1]将会简单梳理下文件抽象的本质、妙处和一些细节。
本质说到文件,用惯了图形化操作系统的我们,第一反应是:文件夹中的一个个图标。但现代操作系统鼻祖 —— Unix 最初设计“文件”时,对其定义远不止于此。即使在今天的 Linux、MacOS 、Windows 的应用开发者眼里,文件的范围也要更大的多。
这也是软件世界最常见的迷思之一——普通用户眼里的形形色色的图标,在系统程序员眼里都是流动的数据。普通用户眼中的操作系统其实是程序员常说的 GUI[2](Graphic User Interface,用户图形界面) ,也即一个表面层。依靠进程(Process)、文件(File)、管道(Pipe)等核心抽象构建的各种系统调用和周边工具,才是真正的向下管理硬件、向上封装接口的操作系统。它是由一段段数据和一个个控制有机组合而成的。
这里借用超棒科幻片 The Matrix[3] (黑客帝国)一幕名场景,来助大家体会下这种感觉:
黑客帝国名场面
值得一提的是,在时下大火的 AI 背后的深度学习,也是基于矩阵的( Matrix-Based)。此外,笔者从事过的领域——图数据库的老大 Neo4j,以及其开源的最流行的图查询语言——OpenCypher,得名灵感都来自于本系列电影(Neo 就是主角,Cypher 是第一集中的叛徒)。之前在面试 Neo4j 的时候,还和他们的 talent acquisition 求证过这一点。
不得不说,这个拍于 1999 年的电影系列,用一幕幕精彩的场景堆砌出的各种隐喻和明喻,都十分精准和超前。这是唯一一个我看了超过三遍的系列。
回到正题,在 Unix 设计中,文件的本质是什么?答曰——字节流。
因此,操作系统中的“文件”远不止存在于外存(磁盘或 SSD)那些各种格式的文件,任何 IO 设备(网络通信、外设设备)、管道,甚至内存本身,都可以作为文件被进程打开——因为他们都可以被当做字节流。正是这种抓住本质的抽象,使得操作系统长我们今天看到的这样——其他过于复杂的抽象和实现都淹没在了历史长河中。
妙处那么,使用字节流来作为文件的抽象有何妙处呢?答案是软件工程中提到的最多的一个词——解耦。有了这种抽象,进程(对 CPU 运算能力的抽象)就无需关心各种设备和组件底层细节。这会极大简化各种系统程序和应用程序对硬件的使用方式。
在这种“万法归一”的抽象基础上,还有一个很重要的伴生抽象——“管道”(Pipe)。如果说文件是对单个对象的抽象,那么管道就是连接不同对象的关键。没有管道,文件顶多是“字节集”;有了管道,文件才可以流动,成为真正的“字节流”。
数据作为软件中的基本元素,在流动的过程中,被不同的程序锻打、组合,最后套一个表面层,成为一个个可以被普通用户使用的网页、APP 和桌面软件。这背后,都离不开文件和管道。
细节说完了感性部分,让我们来看下文件接口细节。通常来说,文件是可以被多个进程共享的,因此在软件的基础上,我们进一步抽象出:文件描述符(file descriptor,缩写 fd)。它其实是对文件的一个视图,包含一个文件指向和一个偏移量。其中偏移量是隐式维护的。
Kafka 在对 Topic 进行抽象的时候,也是用的类似的方式—— Consumer 会维护一个关联到 Topic 的偏移量。从这个侧面也可以看出文件的抽象是多么成功。实际上,后来很多数据系统的接口都有它的影子。
文件描述符是一个进程范围的整数。且我们固定 0 是标准输入,1 是标准输出。这样就可以通过管道来将数据流进行重定向,从而对不同的“处理过程”进行自由组合:
- 每个处理过程(cmd 工具)都只管专注一小件事。
- 需要输入就从标准输入读取;需要输出,就输出到标准输出。
- 管道负责将一个个小工具串成一个复杂的处理过程。
这就是大名鼎鼎的 KISS[4] (keep it simple,stupid)原则,也是我们日常生活中拆解复杂任务[5]的一种惯常手段,也是现在常见的数据流工具 (Spark,Flink)的背后原理。
围绕文件描述符,我们有几大文件操作语义:
- open:以某种模式打开文件,会返回一个文件描述符,并隐式的初始化该 fd 的 offset = 0 。
- read(fd, buf, n) :从文件描述符 fd 关联的文件和偏移量处读取最多 n 个字节到 buf 中;读取成功后,会前移 fd 关联的偏移量(因此调用者就可以用一个循环来连续读取,而不用自己维护偏移量),且返回实际读到的字节数;如果读取失败则返回值小于 0。
- write(fd, buf, n) :从 buf 中写入 n 个字节到文件描述符 fd 关联的文件和偏移量处;并且会自动前移 fd 相应字节的偏移量。写入成功,返回 n;写入失败,则返回值会小于 n。
- close(fd) :释放 fd 和所占资源(比如文件指针和偏移量等信息),且进程之后就可以复用该 fd 标号。
有了这几个基本的接口,进程就可以对文件的字节流进行管理和读写。
最后,实现和承载文件这一抽象的系统,我们通常称为文件系统。当然,这又是另外一个经典的话题。用关系数据库来类比,文件的抽象类似关系模型,open-read—close 类似 SQL,而文件系统就是类似 DBMS 的那个实现系统。
如果大家对这个话题感兴趣可以留言,我可以之后再专门写文章来聊文件系统。
参考资料
[1]6.S081 参考书 XV6: https://pdos.csail.mit.edu/6.828/2020/xv6/book-riscv-rev1.pdf
[2]GUI: https://en.wikipedia.org/wiki/Graphical_user_interface
[3]黑客帝国名场面: https://movie.douban.com/subject/1291843/
[4]KISS: https://en.wikipedia.org/wiki/KISS_principle
[5]拆解复杂任务: https://www.qtmuniao.com/2023/08/21/life-engineering-many-passes/
最后欢迎关注我的公众号: 木鸟杂记 ,专注分布式系统、数据库和存储等大规模数据系统,关注后可回复“资料”领取一份我总结的分布式系统和数据库的入门资料大全。