[翻译] 非阻塞缓存式读操作
非阻塞缓存式读操作
译文作者:zhangzl2013
译文链接:http://www.cnblogs.com/zhangzl2013/p/non_blocking_buffered_file_read_operations.html
原文作者:Jonathan Corbet
原文链接:Non-blocking buffered file read operations
本文有可能会被转载,从而导致评论留言的碎片化。想参与评论和探讨的同学,请找到原文或译文的原始地址,与原文或译文作者互动讨论。
类Unix系统中的缓存式IO是异步的,这种想法是再自然不过的了。在Linux中,页面高速缓存(page cache)的存在,使得进程级的IO请求和发往存储介质的物理请求是不同的事情。所以,实际上有些操作是同步的;特别是读操作,它只有在数据真正的读入到页面高速缓存中才算完成。对普通文件调用read()操作通常都是阻塞型的;通常这种阻塞不会造成什么障碍,但对于响应时效高的程序来说会有一点问题。现在,对于这个问题有了一个不完全的解决办法,不过代价是要加入四个新的系统调用。
阻塞型缓存式读取并不是一个新问题,应用程序已经有了很多应对它的办法。最一般的方法是创建一些专门用于缓存式IO的线程。这些线程在阻塞的时候其他线程还是正常工作。这个方法还算高效,但不可避免的要加一些线程间通信的额外开销,特别是当数据本身就已经存在于页面高速缓存中,read()操作本可以马上完成的时候,这种开销就完全是没必要的了。
Milosz Tanski最近提交的补丁集试图用另一种方式解决这个问题。他的方法是允许程序仅仅用一个read()调用就可以发起一个非阻塞型请求。可惜当前的read()及其变体都没有flags参数可供使用。所以Milosz加了两组新的读写函数:
int readv2(unsigned long fd, struct iovec *vec, unsigned long vlen, int flags); int writev2(unsigned long fd, struct iovec *vec, unsigned long vlen, int flags); int preadv2(unsigned long fd, struct iovec *vec, unsigned long vlen, unsigned long pos_l, unsigned long pos_h, int flags); int pwritev2(unsigned long fd, struct iovec *vec, unsigned long vlen, unsigned long pos_l, unsigned long pos_h, int flags);
除了添加了一个flags参数之外,每组函数都跟原来差不多。当一个读请求带有这个标记,如果请求的数据已经在页面高速缓存中,它就会直接返回成功;否则它会返回EAGAIN。对于当前的补丁而言,如果无法满足非阻塞读取,它也不会开始进行预取操作。新的写操作不支持非阻塞特性;调用写函数时flags参数必须为0。给写操作添加非阻塞特性也不是不可以;只要页面高速缓存中的数据对应的内存是立即可用的,写操作才返回成功就行。但是这个实现要留到以后。
替代方案
这个补丁相对来说比较简单和直接,但人们会提出疑问:内核早就支持这个非阻塞的模式了,只要在open()或者fcntl()函数中设置O_NONBLOCK标记就行了,还有必要添加新的系统调用吗?有两个理由不希望在普通文件上实现非阻塞IO,第一个是它将破坏已有的应用程序。
因为非阻塞IO这个特性是可选的,要使用它必须明确指定,所以看起来对普通文件支持非阻塞IO不会有什么问题。然而实际上,给open()传递O_NONBLOCK时会发生两个事:一是open()本身就不会阻塞了,二是后续的IO也不会阻塞。有些程序利用了第一点;例如Samba用它来防止文件被锁住时open()函数阻塞。由于缓存型读无论有没有O_NONBLOCK都会阻塞,所以程序不在乎是否在调用read()之前,先调用fcntl()重置了此标识。如果read()函数返回了EAGAIN,而程序没有考虑到这种情况,就会出错。
人们会觉得这是使用的问题,但这么用已经有几十年了;改变内核从而导致这些程序不可用是没法被接受的。Samba并不是唯一一个,squid和GQview也这样。所以这是个很现实的问题。
除此之外,Volker Lendecke还解释道,完全的非阻塞行为也不能符合Samba这类程序所期望的用法。期望的用法是试着读取非阻塞模式的数据;如果数据还不可用,请求会移交给线程池从而同步执行。如果线程池正在使用同一个文件描述符,那么阻塞式读取的尝试将会失败。如果使用了不同的文件描述符,那么就会产生由于POSIX文件锁导致的奇怪问题。所以需要一种基于每次读取的非阻塞操作。
另一个办法是增加一个fincore()系统调用,供进程向内核查询所请求的数据是否已经在页面高速缓存中。至少2010年时就有添加fincore()函数的补丁了。但是fincore()是解决这个问题的一个比较迂回的办法,而且在你调用fincore()和read()之间可能页面高速缓存又发生了变化。而直接在read()中支持非阻塞的行为是可以避免这面这种竟态条件的。
最后,还可以考察一下内核的异步IO子系统,它可以是应用程序获得基于请求的非阻塞特性。但是异步IO是没法支持缓存式IO的,而且这种尝试也因为问题的复杂性而停滞了。跟异步IO不同,read()函数在不满足条件时可以直接返回失败,所以说给read()加入非阻塞特性更简单。
结论就是我们有了一组新的API来使普通文件也有了非阻塞读的特性。这个补丁集的更新有点慢,本文写作时,最新的补丁中已经去掉了readv2()和writev2()这两个函数。想要进入3.18开发周期可能有点晚了,但是进去3.19应该没问题。
-- 结束 --