UNIX 操作系统的设计、用户界面、文化和演变都是建立在它的一套统一的想法和概念上。其中最重要的一点可能是“一切皆文件”,而这个概念被认为是 UNIX 的灵魂之一。
这一关键设计原则提供了一个统一的范式,用于访问各种输入输出资源:文档、目录、磁盘驱动器、CD-ROM、调制解调器、键盘、打印机、显示器、终端,甚至是一些进程间通信和网络通信。所有这些资源拥有一个通用的抽象,UNIX 之父将其称为“文件”。因为每个“文件”都通过相同的 API 暴露出来,所以你可以使用同一组基本命令来读取和写入磁盘、键盘、文档或网络设备。
这个基本概念实际上有双重含义:
-
在 UNIX 中,一切都是字节流
-
在 UNIX 中,文件系统是统一的命名空间
在 UNIX 中,一切都是字节流
那么 UNIX 中的文件是由什么组成的?文件只不过是可以读取和写入的普通字节的集合。如果你持有一个文件引用(也就是文件描述符),就可以使用相同的一组 API 进行 IO 操作,无论设备的类型和底层硬件是什么。
纵观历史,UNIX 是第一个对所有 IO 操作进行统一抽象并提供一小组操作原语的操作系统。当时,大多数操作系统为每种设备或设备家族提供单独的 API。一些早期的微型计算机操作系统甚至要求你使用多个用户命令来复制文件——不同大小的软盘需要使用不同的命令!
从程序员和用户的角度来看,UNIX 通过字节流的形式暴露了如下的资源:
存储在磁盘上的文档、目录、链接、大容量存储设备(如磁盘驱动器、CD-ROM、磁带、USB)、进程间通信(例如管道、共享内存、UNIX 套接字)、网络连接、交互式终端、其他设备(如打印机、图形卡)。用户可以对这些字节流进行读(read)、写(write)、随机访问(lseek)和关闭(close)操作。
统一的 API 对于 UNIX 程序来说非常重要:你可以尽情地开发一个处理文件的程序,却不需要知道文件内容究竟是来自哪里,它们可能保存在本地磁盘或网络某处的远程驱动器上,或者是通过网络流入、由用户输入,甚至是由另一个程序在内存中生成。这大大降低了程序的复杂性,并简化了开发人员的学习曲线。这一基本特性让程序开发变得轻而易举(你只需与两个特殊文件打交道:标准输入和标准输出)。
不过需要注意的是,虽然所有文件都提供了统一的 API,但某些特定类型的设备可能不支持某些操作。例如,你无法对鼠标设备进行 lseek 操作,也无法在 CD-ROM 设备上进行 write 操作(假设你的 CD 是只读的)。
在 UNIX 中,文件系统是统一的命名空间
在 UNIX 中,文件不只是具有统一 API 的字节流,还可以通过统一的方式来引用它们:文件系统就是统一的命名空间。
全局命名空间和挂载机制
UNIX 的文件系统路径提供了统一的模式来标记资源,无论其性质如何。例如,你可以使用 /usr/local 引用本地目录,使用 /home/joe/memo.pdf 引用一个文件,使用 /mnt/cdrom 引用 CD-ROM,使用 /usr 引用网络驱动器上的目录,使用 /dev/sda1 引用一个磁盘分区,使用 /tmp/mysql.sock 引用 UNIX 域套接字,使用 /dev/tty0 引用终端,或使用 /dev/mouse 引用鼠标。这个全局命名空间通常被视为文件和目录的层次结构。文件路径可以引用几乎任何东西:文件系统、设备、网络共享或通信通道。
命名空间是分层的,所有资源可以从根目录(/)开始引用。你可以访问同一命名空间内的多个文件系统:只需要将其他设备或文件系统(如外部磁盘驱动器)“附加”到命名空间的某个位置(如 /backups)。在 UNIX 术语中,这个操作叫作挂载(mount)文件系统,而挂载文件系统的位置称为挂载点(mount point)。你可以将挂载文件系统里的所有资源视为全局命名空间的一部分,只要在访问这些资源时使用挂载点作为前缀(例如 /backups/myproject-Oct07.zip)。
挂载机制对于建立统一而连贯的命名空间来说至关重要,在这种命名空间中,异构资源可以重叠。而在微软的操作系统中,MS-DOS 和 Windows 也将设备视为文件,但没有将文件系统视为统一的命名空间。它们的命名空间是分区的,每个物理存储位置都被视为一个不同的实体:C:\是第一个磁盘驱动器,E:\是 CD-ROM 设备,等等。
伪文件系统
在早期,UNIX 通过提供全局 API 并将设备放入统一的文件系统命名空间,极大促进了输入输出资源的集成。这种方法非常成功,以至于越来越多的资源和系统服务被作为文件系统全局命名空间的一部分。Plan 9 操作系统开创了这一壮举,现在已经出现在了所有的现代 UNIX 系统中。
这种方式导致出现了大量的伪文件系统,它们的行为类似于普通文件系统,但可用于访问与传统文件系统不直接相关的资源。例如,你可以使用伪文件系统来查询和控制进程、访问内核或建立 TCP 连接。这些伪文件系统提供了文件系统语义,用于表示结构化的信息,并为各种对象提供统一的访问方式。伪文件系统(有时也称为虚拟文件系统)通常没有实际的物理存储,它们是基于内存的。
伪文件系统示例:
-
procfs(/proc):proc 文件系统包含了一个特殊文件的层次结构,可用于查询或控制正在运行的进程,或通过标准文件条目(主要是基于文本的)来查看内核。
-
devfs(/dev 或 /devices):devfs 将系统上的所有设备显示为一个动态的文件系统命名空间。devfs 还负责管理这个命名空间,并直接与内核设备驱动程序对接,实现智能设备管理——包括注册设备和取消注册。
-
tmpfs(/tmp):临时文件系统,在重新启动后内容就会消失。tmpfs 旨在提高速度和效率,它的文件系统大小是动态的,并使用了交换空间。
-
portalfs(/p):使用 BSD 的 Portal 文件系统,你可以将服务器进程附加到文件系统全局命名空间中,从而通过文件系统来访问网络服务。例如,只需要打开常规文件 /p/tcp/ph7spot.com/smtp,应用程序就可以与托管在 ph7spot.com 上的 SMTP 服务器发生交互。Portal 文件系统有一些神奇之处,因为它在文件系统中提供了套接字语义,可以在标准的 UNIX 工具(如 cat、grep、awk 等)中使用——甚至可以直接用在 Shell 中!
-
ctfs(/system/contract):合约文件系统充当了与 Solaris 合约子系统的接口。Solaris 合约定义了进程或进程组在发生各种类型的事件和故障时的行为——例如如果进程死了,就重启它。Solaris 合约为软件管理和各种环境的监控提供了高级功能,如集群故障恢复系统、批处理队列系统和网格计算引擎。
结论
在现代 UNIX 操作系统中,所有的设备和大多数进程间通信都被视为文件系统层次结构中的文件或伪文件。这种 UNIX 愿景和设计原则被称为“一切皆文件”,是 UNIX 保持成功和长青的关键因素之一。它提供了一个强大而简单的抽象,作为系统、工具和社区的构建基础。更重要的是,它提供了强大的集成和基本的组合机制,通过连接工具和应用程序来解决眼前的问题。
尽管“一切皆文件”的理念取得了成功,但有些人对它的普遍适用性持怀疑态度。如果说文件被视为字节流,那么就会缺少对元数据的支持:为了能够正确地处理文件,应用程序必须使用自己的方法来确定文件的类型、结构和语义。此外,为了保留元数据,处理文件的工具必须保持元数据不变(例如图片的 XMP 信息)。因此,尽管将 UNIX 文件视为字节流对于基于文本接口的程序交互来说非常有用,但在处理多媒体和二进制文件方面却带来了严重的局限性。
尽管存在局限性,但大多数人仍然承认这个理念所具备的强大威力以及它对操作系统集成产生的巨大影响。自 UNIX 首次发布以来,研究人员一直在努力推广这一核心理念。例如,Plan 9 操作系统率先对系统资源采用了完全整合的方法:不仅是设备和通信渠道,所有的系统接口都使用文件系统来表示。例如,Plan 9 的设计者指出,在 UNIX 系统中,网络设备不能完全被视为常规文件:用于访问它们的套接字具有不同的开放语义,并且位于不同的命名空间中(套接字的主机和端口)。Plan 9 的实现证明了我们可以成功地将所有本地和远程设备统一到全局命名空间中。这个概念最终以 portalfs 的形式回到了 UNIX 上。
Plan 9 的其他创新概念建立在“一切皆文件”的原则之上。例如,Plan 9 在统一命名空间的基础上提供了另一层抽象:可以为每个用户或每个进程定制文件系统命名空间,甚至可以动态调整。最后,Plan 9 证明了“一切皆文件”这一理念可以在更大的范围内实现。事实上,这一基本概念在现代 UNIX 操作系统中得到了进一步的扩展。
英文原文:https://ph7spot.com/musings/in-unix-everything-is-a-file