统一性和差异性

“一切皆文件”构建了一种以文件描述符(File Descriptor, FD)为标识符;以 read(), write(), fchown(), fstat(), open(), close() 为操作;以文件树为组织形式的,对于操作系统内所有资源的一种统一的访问和管理手段。

虽然基本上所有的资源都可以使用这种“文件接口”进行统一描述,但是实际上不同的资源依然具有不同的“特化文件”表现形式,每种特化的文件都有其更加独特的操作,大约有如下几种:

  • 普通文件:正常的文件,无需多言
  • 套接字文件:会增加网络编程相关的接口,比如 send(), recv(), connect(), bind(), listen()
  • 管道文件:用于进程间通信
  • 设备文件:引入了灵活的 ioctl() 接口来描述更加复杂的设备功能(或者说驱动功能)

文件描述符

在用户程序中,我们使用文件描述符 FD (一个整型)来表示一个文件,它的本质可以说是真实文件结构的一个 index, pointer 或者 handler ,真实的文件结构在内核中维护。之所以不将整个文件数据都暴露给用户,我觉得是出于封装和隔离的考量,用户没有必要知晓真实的文件结构。

不过之所以用一个整型这么“抽象”的形式,应该是 C 语言的问题了,它的封装能力近乎没有。

Flag

在 Unix/Linux 中文件有“标志 Flag”的属性,他们是文件的“打开选项”,所以以 OpenO_ 作为前缀,但是其实影响力不止在文件打开的时候,常见的有:

  • O_RDONLY: 只读模式
  • O_WRONLY: 只写模式
  • O_RDWR: 读写模式
  • O_CREAT: 创建文件,如果文件已存在则不进行操作
  • O_TRUNC: 如果文件已存在,打开后将其长度截断为 0
  • O_APPEND: 追加写入模式

多路 I/O 复用

传统的文件读写操作都是都是同步阻塞操作,也就是 read() 之类的操作必须要等到读出完整的内容后才能执行后续的操作。这种模式显然是不适合于 I/O 设备的。 I/O 设备制造数据的时间并不确定,比如网卡需要等待服务器的信息,加速器也存在排队问题。所以更好的方式异步编程。

针对这种需求, Unix/Linux 提出了一套配套的接口。在打开文件的时候指定 O_NONBLOCK 标志,表示该文件描述符应该以非阻塞模式工作。在使用该标志时,诸如读、写等操作不会使进程阻塞,进而能够立即返回。当没有所需结果时,会返回 -1 让程序知道操作失败。这样程序就可以去干别的事情来节省时间了。

此外,Unix/Linux 还提供 select(), poll() 接口,用于从一个 fd 集合( FD_SET )中选择出一个准备就绪的 fd 。从其签名中就可以看出其用法:

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

其中的 timeout 表示阻塞一定的时间。

直接 I/O 操作

我们可以通过 mmap() 可以将文件映射到内存,允许应用程序像访问内存一样访问文件。 mmap() 的签名如下:

void* mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

其中 prot 是对于内存保护(protect)性质(可读可写)的描述,而 flags 是对于映射性质(共享,私有,匿名)的描述。

mmap() 有很多神奇应用,这都来自于“一切皆文件”这个强大抽象。比如说设备是一个文件,当我们将设备文件 mmap 后,就可以避免通过内核来读写数据了。这对于性能要求较高的应用程序非常有用。而普通文件则更常用常规读取和写入操作。

再比如说,我们可以将内存视为一种文件(类似于 I/O 设备文件),然后我们就可以通过 mmap() 在用户和内核之间共享内存了。更进一步,其实内核中的所有资源我们都可以视为一种“文件”,然后在内核中将其操作定义好,那么就可以在用户态使用 FD 来间接控制了,其形式大概如下:

static struct file_operations fops = {
    .open = custom_open,
    .release = custom_release,
    // ...
    .mmap = custom_mmap,
};

在这种情况下,其实整个 Unix/Linux 的 File 设计,就与 Capability 机制非常相像了。