文章目录
前言
最近一直在做Rocksdb以及存储相关的事情,借这个机会总结一下Linux的I/O栈。这次写写Linux中不同读写接口的区别以及性能差异。
Linux系统I/O
read() / write()
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
这是Linux最最基础的文件I/O系统调用。以read()
为例,它从fd当前的offset开始读n个byte,然后下一次则从新的offset开始读(假设单线程执行,那么下一次就是offset + n),write()
同理。
pread() / pwrite()
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
pread()
效果上等同于lseek() + read()
,将两次系统调用化为一次,并保证调用前后offset不会被改变。同时,因为没有移动offset,所以对多线程场景更加友好,不会出现多线程调用read()
出现的offset错乱问题。
readv() / writev()
上面两组系统调用,都是将读到的数据存到一块连续的内存中。如果我们想将结果存到内存中的不同地方,可以使用readv()解决这个问题,将对每一块内存进行的多次系统调用减少到一次。
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
要想理解readv() / writev()
,我们首先要知道iovec是做什么的。
struct iovec
{
void __user *iov_base;
__kernel_size_t iov_len;
};
可以看到,一个iovec本身代表的就是文件读取的偏移量与长度。readv()
的第二个参数是一个iovec数组,内部的实现方式是从数组的第一个iovec开始填充,填充满了之后再去填充第二个,所以并不是一个真正意义上的向量化操作。同样的,readv的调用是原子性的,保证了读取的一致性。
C语言库I/O
下面的这几组接口是C语言库中提供的接口。不同于上面Linux原生的接口,他们都是与struct FILE
交互。在这里我们说的buffer也指的是用户态FILE
结构体里的buffer,与内核中的page cache有所区别。
fread() / fwrite()
size_t fread(void *buf, size_t size, size_t count, FILE *fp)
size_t fwrite(const void * buf, size_t size, size_t count, FILE *fp)
这一组接口就是FILE
结构体中的buffer与函数传入的buffer来做交互。对于写入就是buf
写入到fp->buffer
;对于读取就是fp->buffer
写入到buf
。
各个系统调用怎么选择?
总体来说,单线程的情况下,如果操作中不涉及对offset的相关操作,那么使用普通的read()/write()
足矣;如果涉及到在某一offset的读写操作,既可以用lseek() + read()/write()
的方式,也可以直接使用pread()/pwrite()
,(推荐使用后者,因为少一次系统调用,并且不变更offset,方便全局维护)。而在多线程的情况下,pread()/pwrite()
的原子性以及对于offset的不变更带来的好处使得它几乎成为唯一的选择。最后,如果想要将多次读写操作合为一次,那么可以通过readv()/writev()
的接口来结合业务实现。
对于fread()/fwrite()
与read()/write()
的选择,首先我们来想一个问题。一次I/O需要几次数据拷贝?在Linux系统提供的I/O中,一次读操作中数据会从磁盘拷贝到page cache,然后page cache再拷贝到应用内存;写操作反向同理。在C库的I/O接口中,一次读操作会从磁盘拷贝到page cache,page cache拷贝到FILE中的buffer区域,然后buffer区域再拷贝到用户程序的内存;写操作反向同理。所以我们可以看到,使用Linux原生的I/O会产生两次的数据拷贝,比C库的I/O少了一次。
但是这并不意味着read()/write()
就一定比fread()/fwrite()
要快,因为系统调用的次数在不同场景下也是不同的,他们之间的选择也要根据实际的硬件资源与业务逻辑进行判断。例如我们读取一个128K的文件,但是分配的缓存只有4K,那么使用read()
的话只能通过调用128/4=次来读取完整的文件;但是对于fread()
来说,因为用户态的buffer由C库管理自动分配,只需要一次调用就可以读出全部内容。所以在这种情况下,fread()
明显好得多。对于写入来说,fwrite()
可以通过在用户态buffer攒批数据然后下层调用write()
刷到page cache中,这一点对于大批量的写入很友好;而write()
则每次都需要陷入内核存到page cache,频繁发生系统调用。