Zero-copy,零拷贝
程序示例
普通IO拷贝
客户端向服务器发送数据,服务器接收
Server
public class OldIOServer {
public static void main(String[] args)throws Exception {
ServerSocket serverSocket = new ServerSocket(8899);
System.out.println("server start");
while (true){
//阻塞,等待连接到来
Socket socket = serverSocket.accept();
DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());
try {
byte[] bytes = new byte[4096];
while (true){
int readCount = dataInputStream.read(bytes);
if(readCount == -1){
break;
}
}
}catch (Exception ex){
ex.printStackTrace();
}
}
}
}
Client
public class OldIOClient {
public static void main(String[] args)throws Exception {
Socket socket = new Socket("localhost", 8899);
String filePath = "/home/tar.gz/pdf-books-master.zip";
FileInputStream fileInputStream = new FileInputStream(filePath);
DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
byte[] bytes = new byte[4096];
int readCount = 0;
//读取数据的总数量
int total = 0;
//开始时间
long startTime = System.currentTimeMillis();
while ((readCount = fileInputStream.read(bytes)) >= 0){
total += readCount;
//向服务端写数据
dataOutputStream.write(bytes);
}
System.out.println("发送总字节数: " + total + ", 耗时: " + (System.currentTimeMillis() - startTime));
dataOutputStream.close();
socket.close();
fileInputStream.close();
}
}
零拷贝方式
客户端向服务器发送数据,服务器接收,零拷贝方式
Server
public class NewIOServer {
public static void main(String[] args)throws Exception {
//创建一个端口映射到8899端口
InetSocketAddress address = new InetSocketAddress(8899);
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
ServerSocket serverSocket = serverSocketChannel.socket();
//如果想绑定到某一个端口号上,但是那个端口号正处于超时状态,则不能绑定成功,此设置可以绑定成功
serverSocket.setReuseAddress(true);
//绑定端口
serverSocket.bind(address);
ByteBuffer byteBuffer = ByteBuffer.allocate(4096);
System.out.println("server start");
while (true){
SocketChannel socketChannel = serverSocketChannel.accept();
//返回来的socketchannel默认就是阻塞的,如果要注册到selector上,必须设置成非阻塞的
socketChannel.configureBlocking(true);
int readCount = 0;
while (readCount != -1){
try {
readCount = socketChannel.read(byteBuffer);
}catch (Exception ex){
ex.printStackTrace();
}
//重新读
byteBuffer.rewind();
}
}
}
}
Client
public class NewIOClient {
public static void main(String[] args)throws Exception {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost",8899));
socketChannel.configureBlocking(true);
String filePath = "/home/tar.gz/pdf-books-master.zip";
FileChannel channel = new FileInputStream(filePath).getChannel();
//系统毫秒数
long startTime = System.currentTimeMillis();
//transferTo(传输的起始位置,传输的最大字节数,目标channel)
long transferCount = channel.transferTo(0, channel.size(), socketChannel);
System.out.println("发送总字节数:" + transferCount +
", 耗时:" + (System.currentTimeMillis() - startTime));
}
}
DMA
DMA: 是计算机科学中的一种内存访问技术。它允许某些计算机内部的硬件子系统(计算机外设),可以独立地直接读写系统内存,而不需*处理器(CPU)介入处理 。在同等程度的处理器负担下,DMA是一种快速的数据传送方式。很多硬件的系统会使用DMA,包含硬盘控制器、绘图显卡、网卡和声卡。
DMA传输步骤:
DMA: 是计算机科学中的一种内存访问技术。它允许某些计算机内部的硬件子系统(计算机外设),可以独立地直接读写系统内存,而不需*处理器(CPU)介入处理 。在同等程度的处理器负担下,DMA是一种快速的数据传送方式。很多硬件的系统会使用DMA,包含硬盘控制器、绘图显卡、网卡和声卡。
DMA传输步骤:
- 能向CPU发出系统保持(HOLD)信号,提出总线接管请求
- 当CPU发出允许接管信号后,负责对总线的控制,进入DMA方式
- 能对存储器寻址及能修改地址指针,实现对内存的读写
- 能决定本次DMA传送的字节数,判断DMA传送是否结束
- 发出DMA结束信号,使CPU恢复正常工作状态
用户态、内核态: 从下图可以更进一步对内核所做的事有一个“全景式”的印象。主要表现为:向下控制硬件资源,向内管理操作系统资源:包括进程的调度和管理、内存的管理、文件系统的管理、设备驱动程序的管理以及网络资源的管理,向上则向应用程序提供系统调用的接口。从整体上来看,整个操作系统分为两层:用户态和内核态,这种分层的架构极大地提高了资源管理的可扩展性和灵活性,而且方便用户对资源的调用和集中式的管理,带来一定的安全性。
传统数据传输方式(IO拷贝过程)
整体示意图
简单示意图及解释
进行了4上下文切换,和4次copy操作
其它图示及解释
拷贝过程
上下文切换
涉及的步骤
- 用户
发出 read 指令
,触发第一次上下文切换:从用户上下文切换到内核上下文;然后DMA copy ,执行第一次复制,从磁盘将数据copy到内核空间缓冲区中 - 用户
read 指令返回
,触发第二次上下文切换:从内核上下文切换到用户上下文;CPU copy,执行第二次复制,将数据从内核空间缓冲区copy到用户空间缓冲区。 - 用户
发出 write 指令
,触发第三次上下文切换:从用户上下文切换到内核上下文;CPU copy,执行第三次复制,将数据从用户空间缓存复制到socket缓冲区。 - 用户
writes 指令返回
,触发第四次上下文切换:从内核上下文切换到用户上下文;DMA copy,执行第四次拷贝被执行这次拷贝独立地、异步地发生,将socket缓冲区的数据拷贝到NIC缓冲区。
以上是没有错误的理想方案。如果传输的文件大于内核空间缓冲区和socket缓冲区,则拷贝和上下文切换的数量将更多。总之,传统的数据传输方法至少具有4个上下文切换和4个拷贝。
Zero-copy
通过组合传统的数据传输方法,不难发现,如果它是纯静态数据,则完全不需要第二和第三副本。从内核空间缓冲区到socket缓冲区画一条线。并且此过程是在内核上下文中发生的,很明显,此功能的实现需要底层操作系统的支持。
第一次优化
整体示意图
简单示意图
拷贝过程
上下文切换
涉及的步骤
- 用户发出一个 transfer(传输)指令 transferTo()方法,触发第一次上下文切换:从用户上下文切换到内核上下文;用户接收transfer(传输)指令的响应,触发第二次上下文切换:从内核上下文切换到用户上下文。
- 在 transferTo()方法 期间涉及三次拷贝,第一次从磁盘拷贝到内核缓冲区(DMA copy)、第二次从内核缓冲区拷贝到socket缓冲区(CPU copy)、第三次从socket缓冲区拷贝到NIC缓冲区(DMA copy)。
此时,减少了2次上下文切换和1次拷贝操作,性能得到提高。从图中的流中我们可以很容易的看出,内核缓冲区到socket缓冲区这个拷贝,似乎没有必要。如果NIC支持从内核缓冲区直接读取,那么我们就可以省略拷贝到socket缓冲区的步骤从而减少1次拷贝。事实上,已经有了相关的硬件支持,这种操作称为网卡的收集操作。
第二次优化
整体示意图
简单示意图
涉及的步骤
- DMA copy 将文件内容拷贝到内核缓冲区;
- 仅将数据位置和长度的信息描述符附加到socket缓冲区。DMA直接将数据从内核缓冲区拷贝到NIC缓冲区;因此省去了内核缓冲区拷贝到socket缓冲区(CPU拷贝);
总之,结合硬件(底层网络接口卡)和底层操作系统的支持,可以将数据传输优化为两个上下文切换器,两个DMA拷贝(实际上,元数据传输仍然可以忽略不计:位置数据描述符),然后将信息描述符的长度附加到socket缓冲区中)。
零拷贝是一种有效的磁盘到网卡传输方法,它依赖于底层操作系统:零拷贝是kafka的有效保证之一,这意味着java和Scala已经受支持并且已经成熟。在nginx的http配置中,有一个选项sendfile。On可以打开零拷贝,作为c语言编写的nginx对底层系统的调用,它有一个独特的优势。
最完善的Zero-copy示意图
在Linux2.4版本之后就采用了这种实现了真正的Zero-copy
这里的protocol engine(协议引擎)会真正的完成数据发送,在发送数据的时候它会从两个buffer中读取数据(gather搜集操作)。从socket buffer里确认了位置以及长度之后,直接把kernel buffer里的数据发送到网络