统一性和差异性
“一切皆文件”构建了一种以文件描述符(File Descriptor, FD)为标识符;以 read(), write(), fchown(), fstat(), open(), close()
为操作;以文件树为组织形式的,对于操作系统内所有资源的一种统一的访问和管理手段。
虽然基本上所有的资源都可以使用这种“文件接口”进行统一描述,但是实际上不同的资源依然具有不同的“特化文件”表现形式,每种特化的文件都有其更加独特的操作,大约有如下几种:
- 普通文件:正常的文件,无需多言
- 套接字文件:会增加网络编程相关的接口,比如
send(), recv(), connect(), bind(), listen()
等 - 管道文件:用于进程间通信
- 设备文件:引入了灵活的
ioctl()
接口来描述更加复杂的设备功能(或者说驱动功能)
文件描述符
在用户程序中,我们使用文件描述符 FD (一个整型)来表示一个文件,它的本质可以说是真实文件结构的一个 index, pointer 或者 handler ,真实的文件结构在内核中维护。之所以不将整个文件数据都暴露给用户,我觉得是出于封装和隔离的考量,用户没有必要知晓真实的文件结构。
不过之所以用一个整型这么“抽象”的形式,应该是 C 语言的问题了,它的封装能力近乎没有。
Flag
在 Unix/Linux 中文件有“标志 Flag”的属性,他们是文件的“打开选项”,所以以 Open
的 O_
作为前缀,但是其实影响力不止在文件打开的时候,常见的有:
O_RDONLY
: 只读模式O_WRONLY
: 只写模式O_RDWR
: 读写模式O_CREAT
: 创建文件,如果文件已存在则不进行操作O_TRUNC
: 如果文件已存在,打开后将其长度截断为 0O_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 机制非常相像了。