Disk-file-io 读写磁盘数据是程序设计中最基本也是最重要的操作之一。实际系统 为磁盘 I/O 提供了多种接口,不同的接口不但语
Disk-file-io
读写磁盘数据是程序设计中最基本也是最重要的操作之一。实际系统 为磁盘 I/O 提供了多种接口,不同的接口不但语义有所不同,而且性能也有差异。 理解这些接口对实现正确和高效的程序是很重要的,本文将结合内核的实现原理来比较 各种磁盘 I/O 系统调用,希望能从最底层来透彻理解这些接口。
目录[隐藏] |
内核访问磁盘文件的原理
Linux 内核访问磁盘文件的大体原理如右图所示。
内核会为每个进程关联一个到 file 结构的指针数组,我们熟悉的文件描述符是这个数组的 下标值,数组中每个元素指向一个 file 结构,一个 file 结构代表这个进程打开的一个文件。 file 结构中有一个到 inode 结构的指针(注意,这里的 inode 是一个在内存中结构,而 不是保存在磁盘上的 inode),在内核中,每一个 inode 唯一标识一个文件,因此, 如果同时有多个进程打开同一个文件,他们的 file 结构指向同一个 inode。 inode 结构中有一个到 page cache 结构的指针,内核为每个文件单独维护一个 page cache, 用户进程对文件的大多数读写操作实际上都只是直接作用在 page cache 上面,内核会在适当的 时机将 page cache 中的内容写回到磁盘上,这样能够大大减少访问磁盘的次数,从而提高 系统性能。
page cache 是 Linux 内核文件访问过程中至关重要的数据结构,我们需要再了解一些 关于它的细节。page cache 中保存了用户进程访问过的该文件的内容,这些内容以页 为单位保存在内存中,当用户需要访问文件的某个偏移量上的数据时,内核会以偏移量为索引, 找到相应的内存页,如果该页还没有读入内存,则需要访问磁盘读取数据。 为了提高搜索页的速度同时节省 page cache 数据结构占用的空间,Linux 内核使用 基树(radix tree)来保存 page cache 中的页。
访问磁盘数据的系统调用比较
在了解了内核访问磁盘文件的原理之后,我们来比较各种访问磁盘数据的方法。
read/write 系统调用
这是最常用、最基本的文件访问方法,它们的使用方法相信读者已经比较熟悉,就不作介绍了。 这对系统调用涉及两个额外的开销:系统调用开销和数据复制开销。
- 每访问一次文件,都需要产生一次系统调用,但是现在的处理器系统调用的开销通常是比较小的;
- 数据需要在用户的缓冲区和 page cache 之间进行复制。
mmap 系统调用
mmap 系统调用的原理是将 page cache 中的页直接映射到用户进程的地址空间中去, 从而进程可以通过访问自身地址空间中的虚拟地址来访问 page cache 中的页,当然, 最后还是需要依靠内核在适当的时候将 page cache 中的内容写回到磁盘。 所以,mmap 和 read/write 之间的区别在于
- 只需映射一次,后续的访问无须系统调用;
- 数据不需要在用户缓冲区和 page cache 之间复制。
需要特别注意的是,对同一个文件而言,mmap 所访问的和 read/write 访问的 是同一个 page cache。
mmap 的原型如下
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
其中 addr 是映射的起始虚拟地址,一般指定为 NULL,这样内核会自动选择合适的地址 并作为结果返回给用户。另一个比较重要的参数是 flags,它可以取两个值, 分别为 MAP_SHARED 和 MAP_PRIVATE。如果指定为 MAP_SHARED 用户对映射的内存区域的更改会反映在 page cache 上,这样所有读写这个文件的 进程相当于共享这片内存区域(read/write 操作也是作用在这个 page cache 上的); 如果指定为 MAP_PRIVATE,当用户修改这片内存区域的时候,内核会复制相应的页(Copy On Write), 用户的更改只会作用在这些复制出来的页上,而不是作用在 page cache 中,这样 其他访问这个文件的进程就不会看到该文件的更改。
使用 mmap 可以提高访问文件的速度,但是这个操作会为内核引入额外的维护开销 (需要维护更多的地址空间结构)。通常如果需要访问整个比较大的结构化的二进制文件, 使用 mmap 可以提高访问的效率,而且便于编程。
直接读写磁盘设备文件
还有一种不太常用的访问磁盘数据的方法是绕开文件系统,直接读写磁盘设备文件(如 /dev/sda1)。 但是从内核的实现来看,这种操作的效率并不比普通的 read/write 系统调用高, 因为内核为磁盘设备文件单独维护一个 page cache,所有读写操作依然需要经过数据复制。
Direct I/O
Direct I/O 的意思是用户进程的读写操作不经过 page cache,而是直接写入磁盘。 使用 Direct I/O 的最常见程序是数据库,由于数据库系统通常会实现自己的缓存策略, 所以 page cache 的缓存对它来说是多余的,而且会影响效率。 要使用 direct I/O 只需要在打开文件的时候设置 O_DIRECT 标志位,或者使用 fcntl 系统调用来设置该标志位。