Linux的IO及零拷贝技术

  在我们平时的开发工作中包括使用的各种中间件,经常会涉及到IO操作(磁盘文件IO、网络socketIO)。因此我们有必要了解IO操作的底层原理及其优化技术。

一、Linux的IO操作流程


  上图是用户发起IO读请求的处理流程。流程比较清晰,在此不多介绍。
  DMA(直接存储器访问)是为了解放CPU,代替CPU完成从外设缓冲区读取数据到内核缓冲区中。

二、数据拷贝


  上图是一次读请求的数据拷贝过程。
  缓冲区是所有IO的基础,外设、内核、进程都有自己的IO缓冲区,数据拷贝到自己的缓冲区后才可进行操作。
  读请求涉及的数据拷贝过程为:外设->内核read缓冲区,内核read缓冲区->进程缓冲区。
  其实数据从外设读到内核后,在物理内存中已经存储了该数据,当再次拷贝到进程缓冲区时,又在物理内存中存储了一份数据。

三、虚拟内存


  现代操作系统都会用到虚拟内存。虚拟内存就是对物理内存的映射。也就是说一块物理内存可以对应多块虚拟内存。利用这一特性,当数据拷贝到内核缓冲区后,进程只需将虚拟内存和内核映射到同一块物理内存上,即可减少数据拷贝。

四、零拷贝技术

1、mmap+write


  使用mmap+write代替原来的read+write方式,mmap是一种内存映射文件的方法,即操作系统将内核缓冲区的数据地址映射到进程地址空间中,进程便可操作内核缓冲区中的数据。但是当write时,内核还是需要将read缓冲区数据拷贝到内核socket缓冲区中。

2、sendFile


  sendFile系统调用在内核版本2.1中被引入,目的是简化通过网络在两个通道之间进行的数据传输过程。不仅减少了数据复制,还减少了用户态和内核态的切换次数。
  可以看出,该技术依然存在一次内核中的数据拷贝,Linux2.4内核中做了改进,将内核缓冲区中对应的数据描述信息(内存地址、偏移量)记录到相应的socket缓冲区中,这样就省去了内核read缓冲区到内核socket缓冲区的拷贝。
  局限性:sendFile只能将文件传递到套接字上,反之不行。

五、java零拷贝

  java的IO操作过程中,还涉及到一步堆内内存到临时本地内存(堆外内存)的拷贝,即外设->内核->临时本地内存->堆内存->临时本地内存->内核->外设。也就是内核缓冲区数据拷贝到用户空间中后,还需要再次拷贝到JVM中才能使用。

1、DirectByteBuffer

  DirectByteBuffer实现了在jvm中直接使用堆外内存,可以省去临时本地内存到堆内存的拷贝过程。但是内核空间到用户空间的拷贝依然存在且多余。
  DirectBuffer 申请的是非 JVM 的物理内存,所以创建和销毁的代价很高。DirectBuffer 申请的内存并不是直接由 JVM 负责垃圾回收,但在 DirectBuffer 包装类被回收时,会通过 Java Reference 机制来释放该内存块。

2、MappedByteBuffer

  MappedByteBuffer通过操作系统的mmap功能实现了虚拟内存地址映射,即返回一个虚拟地址,该地址和内核空间中的虚拟地址对应同一块物理内存地址,并且最后返回的是DirectBuffer,这样既减少了内核空间到用户空间的拷贝也减少了堆外内存到堆内内存的拷贝。

public class MappedByteBufferTest {

    public static void main(String[] args) throws Exception {
        File file = new File("D://db.txt");
        long len = file.length();
        byte[] ds = new byte[(int) len];
        MappedByteBuffer mappedByteBuffer = new FileInputStream(file).getChannel().map(FileChannel.MapMode.READ_ONLY, 0,
                len);
        for (int offset = 0; offset < len; offset++) {
            byte b = mappedByteBuffer.get();
            ds[offset] = b;
        }
        Scanner scan = new Scanner(new ByteArrayInputStream(ds)).useDelimiter(" ");
        while (scan.hasNext()) {
            System.out.print(scan.next() + " ");
        }
    }
}

  MappedByteBuffer可以在一个打开的文件和MappedByteBuffer之间建立一个虚拟内存映射,返回一个地址address,这样就无需调用read或write方法对文件进行读写,通过address就能够操作文件。底层采用unsafe.getByte方法,通过(address + 偏移量)获取指定内存的数据。MappedByteBuffer继承于ByteBuffer,类似于一个基于内存的缓冲区,只不过该对象的数据元素存储在磁盘的一个文件中;当首次调用get,访问address所指向的内存区域,会导致缺页中断,中断响应函数会在交换区中查找相对应的页面,如果找不到(也就是该文件从来没有被读入内存的情况),则从硬盘上将文件指定页读取到物理内存中(非jvm堆内存)。

六、总结

  零拷贝其实就是共享内存空间,通过虚拟地址和内存映射的技术实现用户空间和内核空间共享同一块物理内存。

猜你喜欢

转载自www.cnblogs.com/ssl-bl/p/12909679.html