基础知识篇
每个进程中都有一个用户文件描述符表,表项指向一个全局文件表中的某个表项,全局文件表的表项有一个指向内存的索引节点指针(inode,包括偏移量等信息),每个inode 唯一标识一个文件。如果同时有多个进程打开同一文件,它们的用户文件描述符表的表项指向不同的全局文件表的表项,但是这些全局文件表的表项会指向同一个inode。

页缓存(pagecache)
页缓存是操作系统实现的一种主要的磁盘缓存,以此用来减少对磁盘 I/O 的操作。
当一个进程准备读取磁盘上的文件内容时,操作系统会先查看待读取的数据所在的页(page)是否在页缓存(pagecache)中,如果存在(命中)则直接返回数据,从而避免了对物理磁盘的I/O 操作;如果没有命中,则操作系统会向磁盘发起读取请求并将读取的数据页存入页缓存,之后再将数据返回给进程。同样,如果一个进程需要将数据写入磁盘,那么操作系统也会检测数据对应的页是否在页缓存中,如果不存在,则会先在页缓存中添加相应的页,最后将数据写入对应的页。被修改过后的页也就变成了脏页,操作系统会在合适的时间把脏页中的数据写入磁盘,以保持数据的一致性。
系统的所有文件 I/O 请求,操作系统都是通过 page cache 机制实现的。对于操作系统来说,磁盘文件都是由一系列的数据页顺序组成,数据页的大小由操作系统本身而决定,x86 的 Linux 中一个标准页面大小是 4KB。为了提高页的查询速度,同时节省 page cache 数据结构占用的内存,Linux 内核使用树来保存 page cache 中的页。
对一个进程而言,它会在进程内部缓存处理所需的数据,然而这些数据有可能还缓存在操作系统的页缓存中,因此同一份数据有可能被缓存了两次。并且,除非使用Direct I/O 的方式,否则页缓存很难被禁止。
pagecache 会对数据文件进行预读取,对于每个文件的第一个读请求操作,系统在读入所请求页的同时会读入紧随其后的少数几个页。因此,想要提高 page cache 的命中率(尽量让访问的页在物理内存中),从硬件的角度来说肯定是物理内存越大越好。从操作系统层面来说,访问 page cache 时,即使只访问 1k 的消息,系统也会提前预读取更多的数据,在下次读取消息时, 就很可能可以命中内存。
但是系统上文件非常多,即使是多余的 page cache 也是非常宝贵的资源, os 不可能将 page cache 随机分配给任何文件,Linux 底层就提供了 mmap 将一个程序指定的文件映射进虚拟内存(Virtual Memory),对文件的读写就变成了对内存的读写,能充分利用 page cache。不过,文件 IO 仅仅用到了 page cache 还是不够的,如果对文件进行随机读写,会使虚拟内存产生很多缺页(Page Fault)中断。

对于数据文件的写入,OS 会先写入至 page cache 内,随后通过异步的方式由 pdflush 内核线程将 page cache 内的数据刷盘至物理磁盘上。
写时复制(copy on write)
如果多个程序同时访问同一块数据,那么每个程序都拥有指向这块数据的指针,在每个程序看来,自己都是独立拥有这块数据的,只有当程序需要对数据内容进行修改时,才会把数据内容拷贝到程序自己的应用空间里去,这时候,数据才成为该程序的私有数据。如果程序不需要对数据进行修改,那么永远都不需要拷贝数据到自己的应用空间里。这样就减少了数据的拷贝。写时复制的内容可以再写一篇文章了。