文件I/O:Linux下的不同I/O接口(pread/pwrite, read/write, readv/writev, fread/fwrite)

前言

最近一直在做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,频繁发生系统调用。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dabtwice

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值