聊聊文件IO

  • Post author:
  • Post category:其他



前言

在消息队列中,消息消费利用零拷贝减少拷贝次数、采用内存映射技术来使文件的访问像访问内存那样。那到底零拷贝原理是什么呢?怎么实现内存映射的?


零拷贝

为什么使用零拷贝?使用零拷贝有什么好处?零拷贝的意思是说不需要将数据从某处复制到特定的某一个区域,可以减少CPU在数据复制的消耗还有内存内存带宽。

例子:对于文件下载功能实现来说,很多情况下很不断地读取文件内容到应用程序空间,再不断地将数据写到网卡发送给客户端。

while((n = read(diskfd, buf, BUF_SIZE)) > 0)    
    write(sockfd, buf , n);复制代码

对于以上的IO操作实际已经发生了多次的数据拷贝。

数据拷贝流程:

1、DMA将磁盘文件拷贝到内核缓存

2、CPU控制将内核缓存数据拷贝到应用程序空间

3、CPU控制将应用程序数据拷贝到套接字的buffer

4、DMA将socket的buffer拷贝到网卡设备发送出去

其实这种情况下需要消耗两次的CPU、DMA完成拷贝工作;对于应用程序来说,CPU资源是相对昂贵的,应该尽可能节省,是否减少利用cpu资源完成拷贝?

采用sendfile减少内核空间到用户空间的拷贝:

数据拷贝流程:

1、利用DMA拷贝磁盘数据到内核的buffer

2、在套接字buffer上追加当前要发送的数据在kernel buffer的位置还有偏移量

3、根据套接字buffer的位置还有偏移量信息,DMA将kernel buffer的数据拷贝到网卡发送出去

可以看到采用sendfile只需要完成两次拷贝,不需要拷贝到用户空间,而且并不消耗cpu资源,可以大幅提高程序的效率。但是由于此次IO的整个流程都在内核空间完成,这种效率是最高的,但是应用程序无法对文件资源进行操作,那该怎么解决呢?有没有什么方式可以即减少数据的拷贝,同时可以操作数据呢?

采用mmap内存映射

mmap(内存映射)是一个比sendfile昂贵但优于传统I/O的方法。

我们的程序发起一次系统调用,将一个文件(或者文件的一部分)映射到虚拟地址空间的一部分,注意这时候没有分配和映射到具体的物理内存空间,而是到第一次加载这个文件的时候,通过MMU把之前虚拟地址换算成物理地址,把文件加载进物理内存。

数据拷贝流程:

1、调用mmap,发生用户空间到内核空间的上下文切换(第一次切换),通过DMA将磁盘数据拷贝到内核空间

2、mmap调用返回,发生内核空间到用户空间的切换(第二次切换),并且用户空间和内核空间共享这部分缓存区,用户空间可以像操作缓冲区的数据一样操作这部分数据

3、调用write,发生用户空间到内核空间的切换(第三次上下文切换),CPU拷贝内核空间的缓冲区数据到套接字的buffer

4、write调用返回,发生内核空间到用户空间的切换(第四次上下文切换),并且通过DMA将套接字的buffer拷贝到网卡发送出去

可以看到采用mmap发送数据,可以操作数据,发生四次的上下文切换还有3次的数据拷贝,但是明显优于传统的IO。

实现零复制的软件通常依靠基于直接存储器访问(

D

irect

M

emory

A

ccess,

DMA

)的复制,以及通过内存管理单元(MMU)的内存映射。这些功能需要特定硬件的支持,并通常涉及到特定存储器的对齐。

很多同学在看消息队列的刷盘时候会看到按照4k的page cache来实现异步刷盘,那page cache具体实现是什么呢?为什么它可以提高刷盘效率?

Page Cache

page cache的目的是通过将数据存储在物理内存使磁盘IO最小化。page cache的大小是动态的,可以增大到消耗所有的free memory, 可以缩小来减轻内存压力。在page cache中的page可以包含许多不连续的物理disk blocks。

在 Linux 内核中,文件的每个数据块最多只能对应一个 Page Cache 项,它通过两个数据结构来管理这些 Cache 项,一个是 radix tree,另一个是双向链表。Radix tree 是一种搜索树,Linux 内核利用这个数据结构来通过文件内偏移快速定位 Cache 项。

实际使用page cache预读取来提高顺序读的效率,利用预先加载数据减少磁盘io,但是对于随机读显然采用page cache是不那么合适的,因为多次的预加载反而降低读取的效率。

在前面的分析中,只针对数据拷贝效率进行说明,而对于文件数据怎么读取到内核空间呢?block io layer 对外提供通用的磁盘访问接口,而block layer再往下就是某个device具体的driver用于加载磁盘的数据。

在调用filechannel的read加载的时候会触发page cache预读IO的方式,随着读取预读范围扩大直到占满空闲内存,这种方式能提高顺序读取的效率,page cache很有效果。而对于随机读取的方式,依旧存在其价值,减少了 Block IO Layed(近似理解为磁盘) 到 Page Cache 的 overhead。

欢迎关注【路上小栈】,优质内容持续更新!