Netty章节十七:Zero-copy,零拷贝

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传输步骤:

  1. 能向CPU发出系统保持(HOLD)信号,提出总线接管请求
  2. 当CPU发出允许接管信号后,负责对总线的控制,进入DMA方式
  3. 能对存储器寻址及能修改地址指针,实现对内存的读写
  4. 能决定本次DMA传送的字节数,判断DMA传送是否结束
  5. 发出DMA结束信号,使CPU恢复正常工作状态

Netty章节十七:Zero-copy,零拷贝

用户态、内核态: 从下图可以更进一步对内核所做的事有一个“全景式”的印象。主要表现为:向下控制硬件资源,向内管理操作系统资源:包括进程的调度和管理、内存的管理、文件系统的管理、设备驱动程序的管理以及网络资源的管理,向上则向应用程序提供系统调用的接口。从整体上来看,整个操作系统分为两层:用户态和内核态,这种分层的架构极大地提高了资源管理的可扩展性和灵活性,而且方便用户对资源的调用和集中式的管理,带来一定的安全性。

Netty章节十七:Zero-copy,零拷贝

传统数据传输方式(IO拷贝过程)

整体示意图

Netty章节十七:Zero-copy,零拷贝

简单示意图及解释

Netty章节十七:Zero-copy,零拷贝

进行了4上下文切换,和4次copy操作

其它图示及解释

拷贝过程

Netty章节十七:Zero-copy,零拷贝

上下文切换

Netty章节十七:Zero-copy,零拷贝

涉及的步骤
  1. 用户发出 read 指令,触发第一次上下文切换:从用户上下文切换到内核上下文;然后DMA copy ,执行第一次复制,从磁盘将数据copy到内核空间缓冲区中
  2. 用户 read 指令返回,触发第二次上下文切换:从内核上下文切换到用户上下文;CPU copy,执行第二次复制,将数据从内核空间缓冲区copy到用户空间缓冲区。
  3. 用户发出 write 指令,触发第三次上下文切换:从用户上下文切换到内核上下文;CPU copy,执行第三次复制,将数据从用户空间缓存复制到socket缓冲区。
  4. 用户 writes 指令返回,触发第四次上下文切换:从内核上下文切换到用户上下文;DMA copy,执行第四次拷贝被执行这次拷贝独立地、异步地发生,将socket缓冲区的数据拷贝到NIC缓冲区。

以上是没有错误的理想方案。如果传输的文件大于内核空间缓冲区和socket缓冲区,则拷贝和上下文切换的数量将更多。总之,传统的数据传输方法至少具有4个上下文切换和4个拷贝。

Zero-copy

通过组合传统的数据传输方法,不难发现,如果它是纯静态数据,则完全不需要第二和第三副本。从内核空间缓冲区到socket缓冲区画一条线。并且此过程是在内核上下文中发生的,很明显,此功能的实现需要底层操作系统的支持。

第一次优化

整体示意图

Netty章节十七:Zero-copy,零拷贝

简单示意图

拷贝过程

Netty章节十七:Zero-copy,零拷贝

上下文切换

Netty章节十七:Zero-copy,零拷贝

涉及的步骤
  1. 用户发出一个 transfer(传输)指令 transferTo()方法,触发第一次上下文切换:从用户上下文切换到内核上下文;用户接收transfer(传输)指令的响应,触发第二次上下文切换:从内核上下文切换到用户上下文。
  2. 在 transferTo()方法 期间涉及三次拷贝,第一次从磁盘拷贝到内核缓冲区(DMA copy)、第二次从内核缓冲区拷贝到socket缓冲区(CPU copy)、第三次从socket缓冲区拷贝到NIC缓冲区(DMA copy)。

此时,减少了2次上下文切换和1次拷贝操作,性能得到提高。从图中的流中我们可以很容易的看出,内核缓冲区到socket缓冲区这个拷贝,似乎没有必要。如果NIC支持从内核缓冲区直接读取,那么我们就可以省略拷贝到socket缓冲区的步骤从而减少1次拷贝。事实上,已经有了相关的硬件支持,这种操作称为网卡的收集操作。

第二次优化

整体示意图

Netty章节十七:Zero-copy,零拷贝

简单示意图

Netty章节十七:Zero-copy,零拷贝

涉及的步骤
  1. DMA copy 将文件内容拷贝到内核缓冲区;
  2. 仅将数据位置和长度的信息描述符附加到socket缓冲区。DMA直接将数据从内核缓冲区拷贝到NIC缓冲区;因此省去了内核缓冲区拷贝到socket缓冲区(CPU拷贝);

总之,结合硬件(底层网络接口卡)和底层操作系统的支持,可以将数据传输优化为两个上下文切换器,两个DMA拷贝(实际上,元数据传输仍然可以忽略不计:位置数据描述符),然后将信息描述符的长度附加到socket缓冲区中)。

零拷贝是一种有效的磁盘到网卡传输方法,它依赖于底层操作系统:零拷贝是kafka的有效保证之一,这意味着java和Scala已经受支持并且已经成熟。在nginx的http配置中,有一个选项sendfile。On可以打开零拷贝,作为c语言编写的nginx对底层系统的调用,它有一个独特的优势。

最完善的Zero-copy示意图

在Linux2.4版本之后就采用了这种实现了真正的Zero-copy

Netty章节十七:Zero-copy,零拷贝

这里的protocol engine(协议引擎)会真正的完成数据发送,在发送数据的时候它会从两个buffer中读取数据(gather搜集操作)。从socket buffer里确认了位置以及长度之后,直接把kernel buffer里的数据发送到网络

性能测试

Netty章节十七:Zero-copy,零拷贝

Netty章节十七:Zero-copy,零拷贝

上一篇:页面Http请求自动变成了Https请求


下一篇:Oracle 存储过程部分说明