操作系统之零拷贝原理和实现方式

传统拷贝的流程

首先要了解零拷贝需要先了解传统拷贝的一个过程。附图下

(考虑到Java多了一个堆外内存)

一般会经历以下步骤:
1:用户空间到内核空间:应用程序发起系统调用,操作系统将数据从磁盘读取到内核空间的缓冲区(DMA 搬运)。
2:内核空间到用户空间:操作系统把内核缓冲区的数据拷贝到用户空间的缓冲区,此时应用程序才能访问这些数据。
3:用户空间到内核空间(socket 缓冲区):应用程序将用户空间缓冲区的数据再次拷贝到内核空间的 socket 缓冲区。
4:内核空间到网络设备:最后,操作系统将 socket 缓冲区的数据发送到网络设备,通过网络传输。

可以发现:

在这个过程中,如果不考虑用户态的内存拷贝和物理设备到驱动的数据拷贝,我们会发现,这其中会涉及4次数据拷贝。同时也会涉及到4次进程上下文的切换。这种方式存在多次上下文切换和数据拷贝,效率较低。上下文切换会消耗 CPU 资源,多次数据拷贝也会增加内存带宽的占用和时间开销。但是对于零拷贝来说,是通过各种方式减少拷贝的次数或者cpu的拷贝次数实现的。

常见的零拷贝方式有mmap,sendfile,dma,directI/O等。

SendFile:

本质就是只做文件传输,而不通过用户态进行干预。。来减少拷贝次数,有局限性,属于纵向优化了。

FileChannel 是 Java NIO 中用于文件操作的通道类,transferTo() 和 transferFrom() 方法是基于操作系统的 sendfile 机制实现的。sendfile 允许在内核空间直接将文件数据从磁盘读取到内核缓冲区,然后直接将内核缓冲区的数据发送到网络设备,避免了数据在用户空间的拷贝。

数据拷贝变成了2次,上下文切换减少到了2次。

以下是一个demo

import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;

public class ZeroCopyTransferToExample {
    public static void main(String[] args) {
        try (FileInputStream fis = new FileInputStream("source.txt");
             FileChannel fileChannel = fis.getChannel();
             SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 8080))) {

            // 将文件通道的数据传输到套接字通道
            long transferred = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
            System.out.println("Transferred bytes: " + transferred);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

对于 UDP 数据传输也可以实现SendFIie,可以使用 DatagramChannel 结合 FileChannel.transferTo() 实现零拷贝。同样是利用 transferTo() 方法的特性,直接在内核空间完成数据从文件到网络的传输。

import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.DatagramChannel;
import java.nio.channels.FileChannel;

public class ZeroCopyUDPExample {
    public static void main(String[] args) {
        try (FileInputStream fis = new FileInputStream("source.txt");
             FileChannel fileChannel = fis.getChannel();
             DatagramChannel datagramChannel = DatagramChannel.open()) {

            // 绑定本地地址
            datagramChannel.bind(new InetSocketAddress(9090));

            // 设置目标地址
            InetSocketAddress targetAddress = new InetSocketAddress("localhost", 9091);

            // 将文件通道的数据传输到 UDP 通道
            long transferred = fileChannel.transferTo(0, fileChannel.size(), datagramChannel);
            System.out.println("Transferred bytes: " + transferred);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

mmap

将内核态和用户态的内存映射到一起,避免来回拷贝,实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用 read、write 等系统调用函数。

MappedByteBuffer 是 Java NIO 中用于内存映射文件的类,它使用 mmap 系统调用将文件映射到用户空间的地址空间,这样内核和用户空间可以共享这块内存,避免了从内核空间到用户空间的一次拷贝。

数据拷贝变成了2次,上下文切换减少到了2次。

以下是demo

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class ZeroCopyMappedByteBufferExample {
    public static void main(String[] args) {
        try (FileInputStream fis = new FileInputStream("source.txt");
             FileChannel inChannel = fis.getChannel();
             FileOutputStream fos = new FileOutputStream("destination.txt");
             FileChannel outChannel = fos.getChannel()) {

            // 获取文件大小
            long fileSize = inChannel.size();

            // 将输入文件映射到内存
            MappedByteBuffer mappedByteBuffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileSize);

            // 将映射的内存数据写入输出文件
            outChannel.write(mappedByteBuffer);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

直接IO

之前的mmap可以让用户态和内核态共用一个内存空间来减少拷贝,其实还有一个方式,就是硬件数据不经过内核态的空间,直接到用户态的内存中,这种方式就是Direct I/O。换句话说,Direct I/O不会经过内核态,而是用户态和设备的直接交互,用户态的写入就是直接写入到磁盘,不会再经过操作系统刷盘处理。这样确实拷贝次数减少,读取速度会变快,但是因为操作系统不再负责缓存之类的管理,这就必须交由应用程序自己去做,譬如MySql就是自己通过Direct I/O完成的,同时MySql也有一套自己的缓存系统同时,虽然direct I/O可以直接将文件写入磁盘中,但是文件相关的元信息还是要通过fsync缓存到内核空间中。

Java并没有这方面的api,需要调用c底层的代码来实现。也跟操作系统底层设计有关系。不同操作系统实现的方式不一样。考虑的点比较多。很麻烦。。。

这种方式的数据拷贝变成了1次,上下文切换减少到了2次。

DMA外部实现

DMA 是一种允许外部设备(如磁盘、网卡等)直接与系统内存进行数据传输,而DMA 控制器可以直接控制数据在内存和外部设备之间的传输,CPU 只需要发起 DMA 传输请求,之后就可以去处理其他任务,当 DMA 传输完成后会通过中断通知 CPU。面从原理和不同场景下的实现方式来详细介绍。主要有俩种方式:网络到网络的拷贝,磁盘到网络的拷贝

磁盘到网络的拷贝:这种方式本质是调用sendfile机制 + DMA控制器即可,绕过了cpu处理。

网络到网络的拷贝:本质也是通过DMA直接将数据写入到内核缓冲区中,并有DMA进行转发和读,也会绕过cpu处理。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值