我们对于 I/O 设备的大量操作,多数类似文件复制,把硬盘数据拷贝到内存,再把内存数据传输到I/O设备。这种情况,如果所有数据都要经过 CPU,实在有点太浪费时间了,因为 CPU 速度很快,大部分都在傻等硬盘读写。

因此,计算机工程师们,发明了 DMA 技术,即直接内存访问(Direct Memory Access)技术,来减少 CPU 的等待。

DMAC 协处理器

DMA 技术就是在主板上放一块独立的芯片,当进行内存和 I/O 设备的数据传输时,不再通过 CPU 来控制传输,而直接通过 DMA 控制器(DMAC), 这块芯片,就可以称之为协处理器。

DMAC 最大的作用在于,当要传输的数据特别大,速度特别快,或传输的数据特别小,速度特别慢的时候。

比如,我们用千兆网卡或者硬盘传输大量数据的时候,如果都用 CPU,那可定忙不过来。而当我们数据传输很慢,DMAC 就可以等数据到齐了,再发送信号,给到 CPU 处理。

再说说为什么 DMAC 是一块「协处理器芯片」呢?

因为 DMAC 实在协助 CPU ,完成对应数据的传输工作,写死 DMAC 控制数据传输的过程中,我们还是需要 CPU 的。

此外,DMAC 是一个特殊的 I/O 设备,一般连接到总线上的设备,有两种类型,一种是主设备,一种是从设备。只有主设备可以主动发起数据传输,从设备只能接受数据传输。从设备一般通过发送控制信号告诉 CPU ,让 CPU 主动去读或写。

而 DMAC 既是主设备,又是从设备。对于 CPU 来说,它是从设备,对于硬盘来说,它是主设备。

来看看使用 DMAC 进行数据传输的具体过程。

  1. CPU 向 DMAC 设备发起请求。其实就是在 DMAC 里修改配置寄存器。
  2. CPU 修改 DMAC 的配置时候,会告诉 DMAC 几个信息
    • 源地址的初始值,以及传输的时候地址增减方式。
    • 目标地址初始值,以及传输的时候地址增减方式。
    • 要传输的数据长度。
  3. 设置完这些信息,DMAC 就好变成一个空闲的状态。
  4. 如果我们要从硬盘往内存加载数据,硬盘就会像 DMAC 发起一个数据传输请求。这个请求不通过总线,而是一个额外的连线。
  5. 然后我们的 DMAC 需要在通过一个额外的连线响应这个申请。
  6. 于是,DMAC这个芯片,就向硬盘的接口发起要总线读的传输请求。数据就从硬盘里面,读到了DMAC的控制器里面。
  7. 然后,DMAC再向我们的内存发起总线写的数据传输请求,把数据写入到内存里面。
  8. DMAC会反复进行上面第6、7步的操作,直到DMAC的寄存器里面设置的数据长度传输完成。
  9. 数据传输完成之后,DMAC重新回到第3步的空闲状态。

所以整个数据传输过程,我们不是通过 CPU 来搬运数据,而是有DMAC 来搬运。但是 CPU 在这个过程也是必不可少的。因为传输什么数据,从哪传到哪,还是由 CPU 设置。这也是为什么,DMAC 被称为协处理器。

kafka 与 DMA

kafka 是一个用来处理实时数据的管道,常用来做消息队列,或者收集和落地海量的日志。作为实时数据和日志的管道,瓶颈自然在 I/O 层面。

有两种常见的情况,一是从网络中接收上游的数据,落地到本地磁盘上;二是从本地磁盘读取出来,通过网络发送出去。

先来看有一种情况,从磁盘读数据发送到网络上去。如果我们自己写程序,最直观的就是用一个文件读操作,从磁盘把数据读到内存里来,然后再用一个 socket,把数据发送到网络上去。

1
2
File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);

这个过程,一共发生了四次数据传输,两次是 DMA 传输,另外两次,是 CPU 控制的传输,来看下这个过程。

  1. 从硬盘读到操作系统内核的缓冲区里,通过 DMA 搬运
  2. 从内核缓冲区复制到应用内存里,通过 CPU 搬运
  3. 从应用内存里,再写到操作系统的 Socket 缓冲区里,通过 CPU 搬运
  4. 从 Socket 缓冲区写到网卡的缓冲区里,通过 DMA 搬运

我们只是搬运一份数据,却经过了四次操作。而且 2、3 两步相当于绕了一圈,特没效率。

对于 kafka 这种专注搬运数据的事儿,就需要尽可能少的搬运。kafka 将数据搬运从 4 次减少为 2 次,并且只有 DMA 来进行数据搬运,不需要 CPU。

1
2
3
4
@Override
public long transferFrom(FileChannel fileChannel, long position, long count) throws IOException {
return fileChannel.transferTo(position, count, socketChannel);
}

kafka 通过调用 Java 的 NIO 库,具体是 FileChannel 里的 transferTo 方法,数据没有读到中间的应用内存里,而是直接通过 channel,写到对应的网络设备里。并且对于 Socket 操作,也不是写到 Socket Buffer 里,而是直接通过描述符写到网卡的缓冲区里面。

第一次,是通过DMA,从硬盘直接读到操作系统内核的读缓冲区里面。第二次,则是根据Socket的描述符信息,直接从读缓冲区里面,写入到网卡的缓冲区里面。

这个方法,没有在内存层面去复制数据,因此也称为零拷贝(Zero-Copy)

在使用了这样的零拷贝的方法之后呢,我们传输同样数据的时间,可以缩减为原来的1/3,相当于提升了3倍的吞吐率。

3 > /proc/sys/vm/drop_caches

  1. 第一次读取文件的耗时如下

    1
    2
    3
    4
    5
    sh-4.4# time cat /home/dd.out &> /dev/null

    real 0m1.774s
    user 0m0.020s
    sys 0m0.970s
  2. 再次读取文件的耗时如下

    1
    2
    3
    4
    5
    sh-4.4# time cat /home/dd.out &> /dev/null

    real 0m0.212s
    user 0m0.020s
    sys 0m0.180s

可以看到,第二次读取文件的耗时远小于第一次的耗时,这是因为第一次是从磁盘来读取的内容,磁盘I/O是比较耗时的,而第二次读取的时候由于文件内容已经在第一次读取时被读到内存了,所以是直接从内存读取的数据,内存相比磁盘速度是快很多的。这就是Page Cache存在的意义:减少I/O,提升应用的I/O速度。