传统 IO 方式
传统的 IO 读写其实就是 read + write 的操作,整个过程会分为如下几步
- 用户调用 read()方法,开始读取数据,此时发生一次上下文从用户态到内核态的切换,也就是图示的切换 1
- 将磁盘数据通过 DMA 拷贝到内核缓存区
- 将内核缓存区的数据拷贝到用户缓冲区,这样用户,也就是我们写的代码就能拿到文件的数据
- read()方法返回,此时就会从内核态切换到用户态,也就是图示的切换 2
- 当我们拿到数据之后,就可以调用 write()方法,此时上下文会从用户态切换到内核态,即图示切换 3
- CPU 将用户缓冲区的数据拷贝到 Socket 缓冲区
- 将 Socket 缓冲区数据拷贝至网卡
- write()方法返回,上下文重新从内核态切换到用户态,即图示切换 4
整个过程发生了 4 次上下文切换和 4 次数据的拷贝,这在高并发场景下肯定会严重影响读写性能。
零拷贝 IO
零拷贝(Zero-Copy)技术是一种高效的 I/O 操作方式,它通过减少数据在计算机系统中的拷贝次数,从而提高数据传输的效率并降低 CPU 的负载。在传统的 I/O 操作中,数据通常需要在用户态和内核态之间多次拷贝,而零拷贝技术通过优化这些操作,减少了不必要的数据拷贝,从而显著提升了性能。
mmap
mmap(memory map)是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。
相较于传统 IO,mmap技术需要 4 次上下文切换与 3 次数据拷贝。
当用户发起 mmap 调用的时候会发生上下文切换 1,进行内存映射,然后数据被拷贝到内核缓冲区,mmap 返回,发生上下文切换 2;随后用户调用 write,发生上下文切换 3,将内核缓冲区的数据拷贝到 Socket 缓冲区,write 返回,发生上下文切换 4。
基于 mmap IO 读写其实就变成 mmap + write 的操作,通过 mmap
系统调用,将文件映射到进程的虚拟内存空间,用户空间和内核空间共享同一块内存区域,数据无需从内核空间拷贝到用户空间,对映射区域的读写操作直接反映到文件上,数据通过内核空间的缓冲区发送到目标(如网络 socket)。
在 java 中可以这样使用 mmap
FileChannel fileChannel = new RandomAccessFile("test.txt", "rw").getChannel();
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, fileChannel.size());
RocketMQ 就是使用的 mmap
sendfile
系统调用直接在内核空间将文件内容从一个文件描述符传输到另一个文件描述符,期间不需要进行上下文切换,也正是因此,使用 sendfile 时应用不具备对于文件的读写能力,一般情况下也不能跨文件系统传输。
数据通过 DMA 直接从磁盘加载到内核缓冲区,然后通过 DMA 发送到网络接口。
如图,用户在发起 sendfile()调用时会发生切换 1,之后数据通过 DMA 拷贝到内核缓冲区,之后再将内核缓冲区的数据 CPU 拷贝到 Socket 缓冲区,最后拷贝到网卡,sendfile()返回,发生切换 2。发生了 3 次拷贝和两次切换
在 java 中可以这样使用 sendfile
FileChannel channel = FileChannel.open(Paths.get("./test.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
//调用transferTo方法向目标数据传输
channel.transferTo(position, len, target);
在如上代码中,并没有文件的读写操作,而是直接将文件的数据传输到 target 目标缓冲区,也就是说,sendfile 是无法知道文件的具体的数据的;但是 mmap 不一样,他是可以修改内核缓冲区的数据的。假设如果需要对文件的内容进行修改之后再传输,只有 mmap 可以满足。
Kafka 中就是使用的 sendfile
DMA
DMA,全称Direct Memory Access,即直接存储器访问。 DMA传输将数据从一个地址空间复制到另一个地址空间,提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。
为什么我们需要 DMA 技术呢,我们知道CPU有转移数据、计算、控制程序转移等很多功能,系统运作的核心就是CPU,CPU无时不刻的在处理着大量的事务,但有些事情却没有那么重要,比方说数据的复制和存储数据,如果我们把这部分的CPU资源拿出来,让CPU去处理其他的复杂计算事务,是不是能够更好的利用CPU的资源呢?
我们希望能有一个硬件来处理数据转移所带来的 cpu 资源消耗,让 cpu 专注于更加重要的操作——计算、控制等。
DMA的作用就是实现数据的直接传输,而去掉了传统数据传输需要CPU寄存器参与的环节,主要涉及四种情况的数据传输,但本质上是一样的,都是从内存的某一区域传输到内存的另一区域(外设的数据寄存器本质上就是内存的一个存储单元)。
- 外设到内存
- 内存到外设
- 内存到内存
- 外设到外设
当DMA控制器准备开始数据传输时,它需要向CPU申请总线的使用权(如果存在 DMA 总线的话,这个步骤可能会跳过)。
- MDA 请求(DMA Request)
- 外设准备好数据后,会向 DMA 控制器发送一个传输请求信号 (DREQ),DMA 接收到请求后,向 CPU 发送一个总线请求信号,请求 CPU 让出总线控制权
- CPU 响应
- CPU 在接收到总线请求信号后,会在当前机器周期结束后释放总线控制权,并向 DMA 控制器发送一个总线相应信号,DMA 控制器接管总线,并开始传输数据。
- DMA 接管总线后,占用至少一个总线周期来完成数据的读写操作,在某些情况下,DMA 控制器可能会使用“周期窃取(Cycle Stealing)的方式,在 CPU 不使用总线时,短暂占用总线并进行数据传输” 当DMA控制器完成数据传输后,它会通知CPU,以便CPU可以处理传输完成后的数据。
- 传输完成通知
- 数据传输完成后,DMA 控制器会释放总线控制权,并向 CPU 发送一个中断请求(IRQ),中断请求信号通知CPU数据传输已经完成,请求 CPU 处理传输后的数据。
- CPU处理中断
- CPU 接收到中断请求后,会暂停当前任务,保存当前状态,并跳转到中断处理程序。
DMA 传输数据主要需要四个参数,分别为 源地址、目标地址、数据传输量、传输模式。 前三个参数都好理解,这里我们需要注意的是它的传输模式
正常模式(DMA_Mode_Normal)是一种单次传输模式。在这种模式下,DMA控制器仅执行一次数据传输,传输完成后自动停止。适用于需要一次性传输固定数据量的场景,如文件读写、单次数据采集等。传输完成后,DMA控制器停止传输,并可能触发一个中断通知CPU。
循环模式(DMA_Mode_Circular)是一种连续传输模式。在这种模式下,当一次数据传输完成后,DMA控制器会自动重置传输量寄存器,并重新开始传输。适用于需要连续传输数据的场景,如音频播放、实时数据采集等。传输完成后,DMA控制器自动重置传输量寄存器,重新开始传输,减少了CPU的干预,提高了数据传输的效率。