一,数据的四次拷贝与四次上下文切换
很多应用程序在面临客户端请求时,可以等价为进行如下的系统调用:
- File.read(file, buf, len);
- Socket.send(socket, buf, len);
例如消息中间件 Kafka 就是这个应用场景,从磁盘中读取一批消息后原封不动地写入网卡(NIC,Network interface controller)进行发送。在没有任何优化技术使用的背景下,操作系统为此会进行 4 次数据拷贝,以及 4 次上下文切换,如下图所示:
如果没有优化,读取磁盘数据,再通过网卡传输的场景性能比较差:
4次copy
- CPU 负责将数据从磁盘搬运到内核空间的 Page Cache 中
- CPU 负责将数据从内核空间的 Page Cache 搬运到用户空间的缓冲区
- CPU 负责将数据从用户空间的缓冲区搬运到内核空间的 Socket 缓冲区中
- CPU 负责将数据从内核空间的 Socket 缓冲区搬运到的网络中
4次上下文切换
- read 系统调用时:用户态切换到内核态
- read 系统调用完毕:内核态切换回用户态
- write 系统调用时:用户态切换到内核态
- write 系统调用完毕:内核态切换回用户态
问题分析
- CPU 全程负责内存内的数据拷贝还可以接受,因为效率还算可以接受,但是如果要全程负责内存与磁盘、网络的数据拷贝,这将难以接受,因为磁盘、网卡的速度远小于内存,内存又远远小于 CPU
- 4 次 copy 太多了,4 次上下文切换也太频繁了
二,DMA技术
DMA,英文全称是Direct Memory Access,即直接内存访问。DMA本质上是一块主板上独立的芯片,允许外设设备和内存存储器之间直接进行IO数据传输,其过程不需要CPU的参与。我们一起来看下IO流程,DMA等忙做了什么事情。
1,用户应用进程调用read函数,向操作系统发起IO调用,进入阻塞状态,等待数据返回.
2,CPU收到指令后、对DMA控制器发起指令调度。
3,DMA收到IO请求后,将请求发送给磁盘:
做盘将数圳放入做盘控制缓冲区,并通知DMA
4,DMA将数据从微盘控制器缓冲区揭贝到内核缓冲区。
5,DMA向CPU发山数据读完的信号,把工作交换给CPU,由CPU负责将数据从内核缓冲区拷贝到用户缪冲区。
6,用户应用进程山内核态切换回用户态,解除阻寒状态
可以发现,DMA做的事情很清晰哦,它主要就是帮TCPU转发一下IO请求,以及将贝数据。
为什么需要它的?
主要就是效率,它帮助CPU做事情,这时候,CPU就可以闲下来大做别的事情,提高了CPU的利用效率。大白话解释就是,CPU老哥太忙太器啦,所以他找了个小弟《名叫DMA) ,替他完成一部分的拷贝工作,这样CPU老哥就能着手于做其他事情。
三,零拷贝技术
零拷贝技术是一个思想,指的是指计算机执行操作时,CPU 不需要先将数据从某处内存复制到另一个特定区域。
可见,零拷贝的特点是 CPU 不全程负责内存中的数据写入其他组件,CPU 仅仅起到管理的作用。但注意,零拷贝不是不进行拷贝,而是 CPU 不再全程负责数据拷贝时的搬运工作。如果数据本身不在内存中,那么必须先通过某种方式拷贝到内存中(这个过程 CPU 可以不参与),因为数据只有在内存中,才能被转移,才能被 CPU 直接读取计算。
零拷贝技术的具体实现方式有很多,例如:
- mmap+write
- sendfile
- 带有DMA收集拷贝功能的sendfile
不同的零拷贝技术适用于不同的应用场景,下面依次进行分析。
1,mmap
mmap 的函数原型如下:
addr: 指定映射的虚拟内存地址
length:映射的长度
prot:映射内存的保护模式
fags: 指定映射的类型
fa:进行映射的文件句柄
offset:文件偏移量
前面一小节,零找贝相关的知识点回顾,我们介绍了虚拟内存,可以把内核空间和用户空间的虚拟地址映射到同一个物理地址,从而减少数据拷贝次数! mmap就是用了虚拟内存这个特点,它将内核中的读级冲区与用户空间的缓冲区进行映射,所有的IO都在内核中完成。
1,用户进程通过 mmap方法向操作系统内核发起IO调用,上下文从用户态切换为内核态。
2,CPU利用DMA控制器,把数据认硬盘小拷贝到内核缓冲区。
3,上下文从内核态切换回用户态,mmap方法返回。
4,用户进程通过 write 方法向操作系统内核发起IO调用,上下文从用户态切换为内核态。
5,CPU将内核缓冲区的数据拷贝到的socket缓冲区。
6,CPU利用DMA控制器,把数据从socket缓冲区拷贝到网卡,上下文从内核态切换回用户态,write调用返回。
可以发现, mmap+write 实现的零拷贝,I/0发生了4次用户空间与内核空间的上下文切换,以及3次数据拷贝。其中3次数据拷贝中,包括了2次DMA拷贝和1次CPU拷贝。
mmap 是将读缓冲区的地址和用户缓冲区的地址进行映射,内核缓冲区和应用缓冲区共享,所以节省了一次CPU拷贝并且用户进程内存是虚拟的,只是映射到内核的读缓冲区,可以节省一半的内存空间。
2,sendfile
sendfile 是Linux2.1内核版本后引入的一个系统调用函数,API如下:
out_fd:为待写入内容的文件描述符,一个socket描述符。
in_fd:为待读出内容的文件描述符,必须是真实的文件,不能是socket和管道。
offset: 指定从读入文件的哪个位置开始读,如果为NULL,表示文件的默认起始位置
count:指定在fdout和fdin之间传输的字节数。
sendfile表示在两个文件描述符之间传输数据,它是在操作系统内核中操作的,避免了数据从内核缓冲区和用户缓冲区之间的拷贝操作,因此可以使用它来实现零拷贝。
1,用户进程发起sendfile系统调用,上下文(切换1)从用户态转向内核态
2,DMA控制器,把数据从硬盘中拷贝到内核缓冲区。
3,CPU将读缓冲区中数据拷贝到socket缓冲区
4,DMA控制器,异步把数据从socket缓冲区拷贝到网卡,
5,上下文(切换2)从内核态切换回用户态,sendfile调用返回。
可以发现, sendfile 实现的零拷贝,I/O发生了2次用户空间与内核空间的上下文切换,以及3次数据拷贝。其中3次数据拷贝中,包括了2次DMA拷贝和1次CPU拷贝。那能不能把CPU拷贝的次数减少到0次呢? 有的,即带有DMA收集拷贝功能的sendfile !
3,带有DMA收集拷贝功能的sendfile
linux2.4版本之后,对 sendfile 做了优化升级,引入SG-DMA技术,其实就是对DMA拷贝加入了 scatter/gather 操作,它可以直接从内核空间缓冲区小将数据读收到网卡。使用这个特点搞零拷贝,即还可以多省去一次CPU拷贝。
1,用户进程发起sendfile系统调用,上下文(切换1)从用户态转向内核态
2,DMA控制器,把数据从硬盘中拷贝到内核缓冲区。
3,CPU把内核缓冲区中的文件描述符信息(包括内核缓冲区的内存地址和偏移量)发送到socket缓冲区
4,DMA控制器根据文件描述符信息,直接把数据从内核缓冲区拷贝到网卡
5,上下文(切换2)从内核态切换回用户态,sendfile调用返回。
可以发现, sendfile+DMA scatter/gather 实现的零拷贝,I/0发生了2次用户空间与内核空间的上下文切换,以及2次数据拷贝。其中2次数据拷贝都是包DMA拷贝。这就是真正的零拷贝(Zero-copy)技术,全程都没有通过CPU来搬运数据,所有的数据都是通过DMA来进行传输的。
三,java实现的零拷贝
Java Nio mmap的文持
javaNIO有一个 MappedByteBuffer 的类,可以用米实现内存映射。它的底层是调用了Linux内
核的mmap的API.
Java NIO对sendfile的支持
FileChannel的 transferTo()/transferFrom() ,底层就是sendfile 系统调用函数。Kafka这个开源项目就用到它。