如果只是普通地以O_RDWR的flag去open一个文件朝里write(不考虑创建、扩增),那默认内核会把文件的这个页面读进来缓存在内核里的,也即所谓的page cache。随后再发起新的write syscall写相同的页面时,只要写在page cache里就可以结束syscall直接返回了。
内核的这个page cache有很多好处,比如你的程序对io还没到需要自己做用户态的读写缓存,那内核的这个机制就帮你省去了很多工作,毕竟page cache是在内存里的,而且可以拿来做read hit,相比于每次read/write都要访问磁盘,带来的性能优势还是很不错的,算是惠及大部分普通程序。
fwrite是用户态的glibc库,相当于把write的系统调用封装了一下,关键一点在于,他在用户态又多加了一个buffer,只有当你的fwrite写入量够多或者你主动fflush才会真的发起一个write syscall。
所以fwrite的好处是对于小量的写,减少syscall的次数,毕竟如果你每写一个字节都要发起一个syscall,然后特权级切换到内核,这是比较耗性能的。
程序的最终目的是要把数据写到磁盘上, 但是系统从通用性和性能角度,尽量提供一个折中的方案来保证这些。让我们来看一个最常用的写文件典型example,也是路径最长的IO。
{
char *buf = malloc(MAX_BUF_SIZE);
strncpy(buf, src, , MAX_BUF_SIZE);
fwrite(buf, MAX_BUF_SIZE, 1, fp);
fclose(fp);
}
这里malloc的buf对于图层中的application buffer,即应用程序的buffer;调用fwrite后,把数据从application buffer 拷贝到了 CLib buffer,即C库标准IObuffer。fwrite返回后,数据还在CLib buffer,如果这时候进程core掉。这些数据会丢失。没有写到磁盘介质上。当调用fclose的时候,fclose调用会把这些数据刷新到磁盘介质上。除了fclose方法外,还有一个主动刷新操作fflush 函数,不过fflush函数只是把数据从CLib buffer 拷贝到page cache 中,并没有刷新到磁盘上,从page cache刷新到磁盘上可以通过调用fsync函数完成。
有人说,我不想通过fwrite+fflush这样组合,我想直接写到page cache。这就是我们常见的文件IO调用read/write函数。这些函数基本上是一个函数对应着一个系统调用,如sys_read/sys_write。调用write函数,是直接通过系统调用把数据从应用层拷贝到内核层,从application buffer 拷贝到 page cache 中。
最后是O_DIRECT,如果你在open一个文件的时候加上这个flag,那么后续对这个文件的所有read/write syscall都会bypass掉内核的page cache。也就是read/write直接发起disk io,不会在内核中缓存。
到这你会发现,O_DIRECT相当于把前面提到的各种buffer全扔了,直进直出,不存在什么locality、复用。
buffer,或者缓存虽然有好处,但也有适用条件以及额外的开销:
- 如果你的程序的文件读写几乎没有locality或者什么热点,加缓存不会带来cache hit方面的性能提升。
- 反而如果你写入的数据量很大,那用fwrite时,会发生你程序的buffer到glibc buffer一次拷贝。glibc buffer发起write syscall到page cache一次拷贝;而如果是普通write或O_DIRECT的write),只会发生你程序的buffer到page cache(disk的buffer cache)一次拷贝。
-注意:2.4之后的内核page cache和disk buffer cache合并,所以page cache的内容直接可以移交给disk buffer cache用于写回。
所以从性能的角度,fwrite并不适合大量写的场景。然而光从这个角度并不能看出O_DIRECT的有无带来什么作用。O_DIRECT适用于,数据读写性能、一致性、locality、写回时机等等对你的程序已经重要到全都要你自己管理,这时内核自带的page cache那种粗粒度、不太可控的设施已经不能满足你的需求了。
使用直接IO需要遵守的一些限制:
- 用于传递数据的缓冲区,其内存边界必须对齐为块大小的整数倍。
- 数据传输的开始点,即文件和设备的偏移量,必须是块大小的整数倍
- 待传递数据的长度必须是块大小的整数倍。
不遵守上述任一限制均将导致EINVAL错误。
最典型的就是数据库应用:
对数据库而言,依赖于page cache会带来非常多问题,比如:
- writeback,也即写回磁盘同步的时机不可控。page cache可能在任何时候写回,包括你的事务做到一半,进程遭到调度,内核擅自把部分page cache上的内容写回磁盘,造成预期外的数据不一致。
那么此时最好的解决方案是,所有对文件内容的cache,自己在用户态管理,bypas掉内核的page cache,而这就要借助O_DIRECT。什么时候写回,写回哪些,自己说了算。
最后提一下O_SYNC的问题,因为往往会和O_DIRECT一起用。从数据库的例子里可以看出,O_DIRECT既然控制写回,就和事务、数据一致性脱不了关系。一致性很重要的一个问题就是事情的发生顺序:写回真的做完了吗?
单靠O_DIRECT并不能保证写回的“完成”,因为O_DIRECT只是bypass掉page cache,保证数据直接提交到disk buffer cache,然而此时write即便返回,也不能保证disk buffer cache上的数据真的进入存储介质中(比如block layer的调度)。也即数据仍然在内存中,我们怎么才能保证write返回时数据就已经进入存储介质呢?答案就是O_SYNC。
所以O_DIRECT和O_SYNC一起使用,可以确保每一次写返回时,数据以bypass page cache的方式写,且已经进入存储介质。