普通的IO

我们以读取一个本地磁盘文件的内容并通过网络发送为例,我们先要说说一个普通的IO操作是怎样做的

  1. 系统接收到网络用户读取文件的请求
  2. 应用程序发起系统调用, 从用户态切换到内核态(第一次上下文切换)
  3. 内核态中把数据从硬盘文件读取到内核中间缓冲区(kernel buf)
  4. 数据从内核中间缓冲区(kernel buf)复制到(用户态)应用程序缓冲区(app buf),从内核态切换回到用户态(第二次上下文切换),Read操作完毕。
  5. 应用程序开始发送数据到网络上
  6. 应用程序发起系统调用,从用户态切换到内核态(第三次上下文切换)
  7. 内核中再把数据从socket的缓冲区(socket buf)发送的网卡的缓冲区(NIC buf)上
  8. 从内核态切换回到用户态(第四次上下文切换),Write操作完毕

由上诉流程得知, 一次read-send涉及到了四次拷贝:

  1. 硬盘拷贝到内核缓冲区(DMA COPY)
  2. 内核缓冲区拷贝到应用程序缓冲区(CPU COPY)
  3. 应用程序缓冲区拷贝到socket缓冲区(CPU COPY)
  4. socket buf拷贝到网卡的缓冲区(DMA COPY)

DMA COPY和CPU COPY
简单来说,DMA COPY是有硬盘的芯片完成的,不需要CPU参与,把CPU的算力解放出来了。

其中涉及到2次cpu中断, 还有4次的上下文切换

很明显,第2次和第3次的的copy只是把数据复制到app buffer又原封不动的复制回来, 为此带来了两次的cpu copy和两次上下文切换, 是完全没有必要的.那么有没有什么办法能够解决掉这种没有意义的复制呢?

MMap

能不能说应用程序缓冲区共享内核缓冲区呢,这样发送的时候就不需要把数据从内核缓存区拷贝到应用程序缓存区了,直接可以让socket buf读取内核缓冲区的数据,减少一次copy.

其实是可以的,这就是MMap(内存映射文件)技术:即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。

应用程序调用 mmap ,磁盘文件中的数据通过 DMA 拷贝到内核缓冲区,接着操作系统会将这个缓冲区与应用程序共享,这样就不用往用户空间拷贝。应用程序调用write ,操作系统直接将数据从内核缓冲区拷贝到 socket 缓冲区,最后再通过 DMA 拷贝到网卡发出去。

MMap除了在发送的时候有应用,在写入的时候也是有应用的,比如Kafka的落盘技术就用到了MMap。kafka数据写入、是写入这块内存空间,但实际这块内存和OS内核内存有映射,也就是相当于写在内核内存空间了,且这块内核空间、内核直接能够访问到,直接落入磁盘。也就是说Kafka写入数据的时候并不是直接落盘的。

MMap写入的时候也有一个很明显的缺陷——不可靠,写到mmap中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用flush的时候才把数据真正的写到硬盘。Kafka提供了一个参数——producer.type来控制是不是主动flush;如果Kafka写入到mmap之后就立即flush然后再返回Producer叫同步(sync);写入mmap之后立即返回Producer不调用flush叫异步(async)。

在Java中,也可以使用MMap,相关连接

零拷贝(Zero-Copy)

虽然有MMap技术,但是还是会有好几次copy.

  1. 硬盘拷贝到内核缓冲区(DMA COPY)
  2. 内核缓冲区拷贝到socket缓冲区(CPU COPY)
  3. socket buf拷贝到网卡的缓冲区(DMA COPY)

如果能够直接从内核缓存区copy到网卡缓存区就好了。

linux内核2.1开始引入一个叫sendFile系统调用
在内核2.4以后的版本中, linux内核对socket缓冲区描述符做了优化. 通过这次优化,sendFile系统调用可以在只复制kernel buffer的少量元信息的基础上, 把数据直接从kernel buffer 复制到网卡的buffer中去.从而避免了从“内核缓冲区”拷贝到“socket缓冲区”的这一次拷贝.

这个优化后的sendFile, 我们称之为支持scatter-gather特性的sendFile

在支持scatter-gather特性的sendFile的支撑下, 我们的read-send模型可以优化为:

  1. 应用程序开始读文件的操作
  2. 应用程序发起系统调用, 从用户态进入到内核态(第一次上下文切换)
  3. 内核态中把数据从硬盘文件读取到内核中间缓冲区
  4. 内核态中把数据在内核缓冲区的位置(offset)和数据大小(size)两个信息追加(append)到socket的缓冲区中去
  5. 网卡的buf上根据socekt缓冲区的offset和size从内核缓冲区中直接拷贝数据
  6. 从内核态返回到用户态(第二次上下文切换)

最后数据拷贝变成只有两次DMA COPY:

  1. 硬盘拷贝到内核缓冲区(DMA COPY)
  2. 内核缓冲区拷贝到网卡的缓冲区(DMA COPY)

这里明明还有2次copy,为什么叫零拷贝?因为这两次COPY都是DMA copy,是不需要CPU参与的。

JAVA代码实现零拷贝

File file = new File("test.zip");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
FileChannel fileChannel = raf.getChannel();
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("", 1234));
// 直接使用了transferTo()进行通道间的数据传输
fileChannel.transferTo(0, fileChannel.size(), socketChannel);
FileChannel.transferTo()transferTo()FileChanneltransferTo0()sendfile()
关于MMap和SendFile总结

这两个严格来说是不同的东西,只是恰巧都可以用来提高IO的性能,但他们的实现原理是完全不同的。
MMap是内存映射技术,通过这个技术可以让应用程序写入文件时直接操作内存,提高写入性能。
RocketMQ 在消费消息时,使用了 mmap。而kafka 使用了 sendFile,kafka的落盘技术也用到了mmap。