【操作系统】操作系统IO技术底层机制和ZeroCopy

本文涉及的产品
数据传输服务 DTS,数据迁移 small 3个月
推荐场景:
MySQL数据库上云
数据传输服务 DTS,数据同步 small 3个月
推荐场景:
数据库上云
数据传输服务 DTS,数据同步 1个月
简介: 【操作系统】操作系统IO技术底层机制和ZeroCopy

1.DMA技术详解

(1)应用程序 从 磁盘读写数据 的时序图(未用DMA技术前)

af443a556dda4d7f987fb9e68956dd34.jpg

(2)什么是DMA 技术 (Direct Memory Access)

  • 直接内存访问,直接内存访问是计算机科学中的一种内存访问技术。

DMA之前:要把外设的数据读入内存或把内存的数据传送到外设,一般都要通过CPU控制完成,利用中断技术。

允许某些硬件系统能够独立于CPU直接读写操作系统的内存,不需要中处理器(CPU)介入处理。

数据传输操作在一个DM控制器(DMAC)的控制下进行,在传输过程中CPU可以继续进行其他的工作。

在大部分时间CPU和I/O操作都处于并行状态,系统的效率更高。


982b8290dd124161866fd8208073c70b.jpg


(3)应用程序的读写数据

3a44d88ce01c4f7a819436b8437156f3.jpg

  • 读本地磁盘
  • 操作系统检查内存缓冲区读取,如果存在则直接把内核空间的数据copy到用户空间(CPU负责),应用程序即可使用。
  • 上步没数据,则从磁盘中读取到内核缓冲(DMA负责),再把内核空间的数据copy到用户空间(CPU负责),应用程序即可使用
  • 硬盘->内核缓冲区->用户缓冲区

写操作本地磁盘

  • 根据操作系统的写入方式不一样,buffer IO 和 direct IO ,写入磁盘时机不一样。
  • buffer IO
  • 应用程序把数据从用户空间copy到内核空间的缓冲区(CPU负责),再把内核缓冲区的数据写到磁盘(DMA负责)。
  • direct IO
  • 应用程序把数据直接从用户态地址空间写入到磁盘中,直接跳过内核空间缓冲区。
  • 减少操作系统缓冲区和用户地址空间的拷贝次数,降低了CPU和内存开销。
  • 用户缓冲区->内核缓冲区->硬盘
  • 读网络数据
  • 网卡Socket(类似磁盘)中读取客户端发送的数据到内核空间(DMA负责)。
  • 把内核空间的数据copy到用户空间(CPU负责),然后应用程序即可使用。
  • 写网络数据
  • 用户缓冲区中的数据copy到内核缓冲区的Socket Buffer 中(CPU负责)
  • 将内核空间中的Socket Buffer 拷贝到Socket协议栈(网卡设备)进行传输(DMA负责)

(4)DMA的工作总结

  • 从磁盘的缓冲区到内核缓冲区的拷贝工作。
  • 从网卡设备到内核的socket buffer 的拷贝工作。
  • 从内核缓冲区到磁盘缓冲区的拷贝工作。
  • 从内核的socket buffer到网卡设备的拷贝工作。
  • 注意:内核缓冲区到用户缓冲区之间的拷贝工作仍然由CPU负责

(5)DMA技术带来的性能损耗


a4db950c99174194a33f62be21a3b6b6.jpg

  • 上图应用程序从磁盘读取数据发送到网络上的损耗,程序需要两个命令 先read读取,再write写出
  • 四次内核态和用户态的切换
  • 四次缓冲区的拷贝(2次DMA拷贝、2次CPU拷贝)
  • 读取:磁盘缓冲区到内核缓冲区(DMA)
  • 读取:内核缓冲区到用户缓冲区(CPU)
  • 写出:用户缓冲区到内核缓冲区Socket Buffer(CPU)
  • 写出:内核缓冲区的Socket Buffer到网卡设备(DMA)

为了解决这种性能的损耗所以就诞生了零拷贝。

2.ZeroCopy零拷贝技术简介

(1)什么是零拷贝ZeroCopy

减少不必要的内核缓冲区跟用户缓冲区之间的拷贝工作,从而减少CPU的开销和减少kernel和user模式的上下文切换,达到性能的提升。从磁盘中读取文件通过网络发送出去,只需要拷贝2\3次和2\4的内核态和用户态的切换即可。

ZeroCopy技术实现方式有两种(内核态和用户态切换次数不一样)

  • 方式一:mmap+write
  • 方式二:sendfile

(2)ZeroCopy的实现底层 mmap + write

  • 操作系统都使用虚拟内存,虚拟地址通过多级页表映射物理地址。
  • 多个虚拟内存可以指向同一个物理地址,虚拟内存的总空间远大于物理内存空间。
  • 如果把内核空间和用户空间的虚拟地址映射到同一个物理地址,就不需要来回复制数据。
  • mmap系统调用函数会直接把内核缓冲区的数据映射到用户空间,内核空间和用户空间就不需要在进行数据拷贝的操作了,节省了CPU开销。
  • mmap()负责读取,write()负责写出
  • 执行流程
  • 应用程序先调用mmap()方法,将数据从磁盘拷贝到内核缓冲区,返回结束(DMA负责)。在调用write(),内核缓冲区的数据直接拷贝到内核socket buffer (CPU负责),然后把内核缓冲区的Socket Buffer 给直接拷贝给Socket协议线,即网卡设备中,返回结束(DMA负责)

a3d514aced394ffd9cfa532b13caf968.jpg

  • 采用mmap之后,CPU用户态和内核态上下文切换依旧是4次和全程有3次数据拷贝
  • 2次DMA拷贝、1次CPU拷贝、4次内核态用户态切换,减少了1次CPU拷贝

(3)ZeroCopy的实现底层 sendfile

  • Linux kernal 2.1新增发送文件的系统调用函数sendfile()。
  • 执行流程
  • 替代read()和write()两个系统调用,减少一次系统调用,即减少2次CPU上下文切换的开销,调用sendfile(),从磁盘读取到内核缓冲区,然后直接把内核缓冲区的数据拷贝到socket buffer缓冲区里,再把内核缓冲区的SocketBuffer给直接拷贝给Socket协议栈,即网卡设备中(DMA负责)。

496ab18cb4cc4544a88d30ea2e543db2.jpg

采用sendfile后,CPU用户态和内核态上下文切换是2次 和 全程3次的数据拷贝,2次DMA拷贝、1次的CPU拷贝、2次内核态用户态切换。

Linux 2.4+ 版本之后改进sendfile,利用DMA Gather(带有收集功能的DMA),变成了真正的零拷贝(没有CPU Copy)

应用程序先调用sendfile()方法,将数据从磁盘拷贝到内核缓冲区(DMA负责)


把内存地址、偏移量的缓冲区fd描述符拷贝到Socket Buffer中去 拷贝很少的数据,可忽略

  • 本质和虚拟内存的解决方法思路一样,就是内存地址的记录
  • 然后把内核缓冲区的Socket Buffer给直接拷贝给Socket协议栈 即网卡设备中,返回结束(DMA负责)

27aa1535891f4b709969342eeb6908a0.jpg

3.Java和主流中间件里的零拷贝技术

(1)Java中有哪些零拷贝技术

  • Java NIO对mmap的实现 fileChannel.map()
  • Java NIO对sendfile的实现 fileChannel.transferTo()fileChannel.transferFrom()
  • (2)什么是FileChannel
  • FileChannel是一个连接到文件的通道,可以通过文件通道读写文件,该常被用于搞笑的网络/文件的数据传输和大文件拷贝
  • 应用程序使用FileChannel写完以后,数据是在PageCache上的,操作系统不定时的把PageCache的数据写入到磁盘。为了避免宕机数据丢失,使用channel.force(true) 把文件相关的数据强制刷入磁盘上去。
  • 使用之前必须先打开它,但是无法直接new一个FileChannel。
  • 常规通过使用一个InputStream、OutputStream或者RandomAccessFile来获取一个FileChannel实例。
RandomAccessFile randomAccessFile = new RandomAccessFile("文件路径","rw");
FileChannel inChannel = randomAccessFile.getChannel();

(3)mmap方式实现

  • map方法,把文件映射成内存映射文件
  • MappedByteBuffer,是抽象类也是ByteBuffer的子类,具体实现子类是DirectByteBuffer,可被通道进行读写。
  • 一次map大小要限制在2G内,过大map会增加虚拟内存回收和重新分配的压力,直接报错。
  • FileChannel.java中的map对long size 进行了限制,不能大于Integer.MAX_VALUE,否则就报错


bc41b7d455b845b299b9eaef0f5fbc1d.jpg

  • JDK层做限制是因为底层C++的类型,无符号int类型最大是2^31 -1, 2^31 -1 字节就是 2GB - 1B。
MappedByteBuffer map(int mode,long position,long size)
position:文件开始位置
size:映射文件区域大小
mode:访问该内存映射文件的方式,READ_ONLY(只读)、READ_WRITE(读写)、PRIVATE(创建一个读写副本)

(4)sendfile方式实现

  • fileChannel.transferTo(long postition,long count,WritableByteChannel target)
  • 将字节从此通道的文件传输到给定的可写入字节通道。
  • 返回值为真实拷贝的size,最大拷贝2G,超出2G的部分将丢弃。
position:文件中的位置,从此位置开始传输,必须非负数
count:要传输的最大字节数,必须非负数
target:目标通道
返回:实际已传输的字节数,可能为零
  • fileChannel.transferFrom(ReadableByteChannel src, long position, long count)
  • 将字节从给定的可读取字节通道传输到此通道的文件中
  • 对比 从源通道读取并将内容写入此通道的循环语句相比,此方法更高效
src:源通道
position:文件中的位置,从此位置开始传输,必须非负数
count:要传输的最大字节数, 必须非负数
返回:实际已传输的字节数,可能为零
  • transferFrom允许将一个通道连接到另一个通道,不需要在用户态和内核态来回复制,同时通道的内核态数据也无需复制,transferTo只有源为FileChannel才支持transfer这种搞笑的复制方式,其他如SocketChannel都不支持transfer模式。
  • 一般可以做FileChannel->FileChannel->FileChannel 和 FileChannel->SocketChannel的transfer零拷贝

4.文件IO性能对比实战

实现一个文件拷贝,对比不同IO方式性能差异,文件大小 200MB~5GB

编码实现:

  • 普通java的io流
  • 普通java的带buffer的io
  • 零拷贝实现之mmap的io
  • 零拷贝实现之sendfile的io
  • 运行环境准备
  • Linux CentOS7.X
  • 安装JDK11 配置全局环境变量 vi /etc/profile
JAVA_HOME=/usr/local/jdk11
CLASSPATH=$JAVA_HOME/lib/
PATH=$PATH:$JAVA_HOME/bin
export PATH JAVA_HOME CLASSPATH
  • 环境变量立刻生效
  • source /etc/profile
  • 查看安装情况 java -version

202b5f15cc874ae8ab39d64f7857e2ae.jpg

  • 准备1.34G测试文件


3088fd7d61a34c7091f9fa29681d5596.jpg

(1)普通java的io验证

public class IOTest {
    public static void main(String[] args) {
        String inputFilePath = args[0];
        String outputFilePath = args[1];
        long start = System.currentTimeMillis();
        try (
                FileInputStream fis = new FileInputStream(inputFilePath);
                FileOutputStream fos = new FileOutputStream(outputFilePath)
        ) {
            byte[] buf = new byte[1];
            while(fis.read(buf) != -1){
                fos.write(buf);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("普通IO耗时:"+(end-start));
    }
测试:java IOTest.java "/usr/local/music.zip" "/usr/local/io-music.zip"

e946028f6c35484fa26b473ef90ad583.jpg(2)普通java的带buffer的io

public class BufferIOTest {
    public static void main(String[] args) {
        String inputFilePath = args[0];
        String outputFilePath = args[1];
        long start = System.currentTimeMillis();
        try (
                BufferedInputStream bis = new BufferedInputStream(new FileInputStream(inputFilePath));
                BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(outputFilePath))
        ) {
            byte[] buf = new byte[1];
            while(bis.read(buf) != -1){
                bos.write(buf);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("Buffer IO耗时:"+(end-start));
    }
}

0bc3a4af9fab49ebb53ad303bf977149.jpg

(3)零拷贝实现之mmap的io

  • 一次 map 最大支持2GB,超过2GB会报错
public class MmapIOTest {
    public static void main(String[] args) {
        String inputFilePathStr = args[0];
        String outputFilePathStr = args[1];
        long start = System.currentTimeMillis();
        try (
                FileChannel channelIn = new FileInputStream(inputFilePathStr).getChannel();
                FileChannel channelOut = new RandomAccessFile(outputFilePathStr, "rw").getChannel()
        ) {
            long size = channelIn.size();
            System.out.println("mappedFile:"+size);
            MappedByteBuffer mbbi = channelIn.map(FileChannel.MapMode.READ_ONLY, 0, size);
            MappedByteBuffer mbbo = channelOut.map(FileChannel.MapMode.READ_WRITE, 0, size);
            for (int i = 0; i < size; i++) {
                byte b = mbbi.get(i);
                mbbo.put(i,b);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("mmap 零拷贝 IO 耗时:"+(end-start));
    }
}

67b8ef3847224395a2bcb6210ad0a3ff.jpg

(4)零拷贝实现之sendfile的io

  • 最大拷贝2G,超出2G的部分将丢弃,最终拷贝的文件大小只有2GB多点,超过2GB可以考虑多次执行
public class SendFileIOTest {
    public static void main(String[] args) {
        String inputFilePathStr = args[0];
        String outputFilePathStr = args[1];
        long start = System.currentTimeMillis();
        try (
                FileChannel channelIn = new FileInputStream(inputFilePathStr).getChannel();
                FileChannel channelOut = new FileOutputStream(outputFilePathStr).getChannel()
        ) {
            // 代码一:针对小于2GB的问题,返回值为真实拷贝的size,最大拷贝2G,超出2G的部分将丢弃,最终拷贝文件大小只有2GB
            //channelIn.transferTo(0,channelIn.size(),channelOut);
            // 代码二:针对大于2GB的文件
            long size = channelIn.size();
            for (long left = size;left>0;){
                //transferSize所拷贝过去的真实长度,size - left 计算出下次要拷贝的位置
                long transferSize = channelIn.transferTo((size - left),left,channelOut);
                System.out.println("总大小:"+size+",拷贝大小:"+transferSize);
                //left剩余字节多少
                left = left - transferSize;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("sendfile 零拷贝 IO 耗时:"+(end-start));
    }
}

55337b6453c7409eadcee08536be19cd.jpg

(5)测试结果分析

  • 1~2GB的文件
  • 普通拷贝
  • 普通java的io流【慢】3973924秒
  • 普通java的带buffer的io【快】33196秒
  • 零拷贝
  • 零拷贝实现之mmap的io【快】7131秒
  • 零拷贝实现之sendfile的io【快】1784秒
  • 分析原因之前,我们先来了解一下局部性原理
局部性原理:指计算机在执行某个程序时,倾向于使用最近使用的数据
时间局部性:如果程序中的某条指令一旦被执行,则不久的将来该指令可能再次被执行
空间局部性:一旦程序访问了某个存储单元,在不久的将来附近的存储单元也有可能被访问
  • 普通的IO和Buffer IO,为什么带有Buffer的IO要比普通的IO性能高?
每次读取数据的时候,系统根据局部性原理,通过DMA会读入更多的数据到内核缓冲区里面
OS根据局部性原理会在一次read(),系统调用过程中预读更多的文件数据缓存在内核IO缓冲区中
当继续访问的文件数据在缓冲区中时便直接拷贝数据到进程缓冲区,避免了再次的抵消磁盘IO操作
OS已经帮减少磁盘IO操作次数,提高了性能
  • 两种零拷贝的方式对比
(1)sendfile
无法在调用过程中修改数据,只适用于应用程序不需要对所访问数据进行处理修改情况,适合静态文件传输,MQ的Broker发送消息给消费者。适合大文件传输,2次上下文切换,最少2次数据拷贝。
(2)mmap
在mmap调用可以在应用程序中直接修改Page Cache中的数据,使用的是mmap+write两步。调用比sendfile成本高,但由于传统的拷贝方式,适用于多个线程以只读的方式同时访问同一个文件,mmap机制下多线程共享同一个物理内存空间,节约内存。适合小数据量续写,4次上下文切换,3次数据拷贝。

5.主流中间件中用到的ZeroCopy技术

(1)Nginx使用的是sendfile 零拷贝

  • WebServer处理静态页面请求时,是从磁盘中读取网页的内容,因为sendfile不能在应用程序中修改数据,所以最适合静态文件服务器或者是直接转发数据的代理服务器。
  • (2)rocketmq主要是mmap,也有小部分使用sendfile
  • rocketMQ在消息存盘和网络发送使用mmap, 单个CommitLog文件大小默认1GB
  • 要在用户进程内处理数据,然后再发送出去的话,用户空间和内核空间的数据传输就是不可避免的
  • (3)Kafka主要是sendfile,也有小部分使用mmap
  • kafka 在客户端和 broker 进行数据传输时,broker 使用 sendfile 系统调用,类似
  • 【FileChannel.transferTo】 API,将磁盘文件读到 OS 内核缓冲区后,直接转到 socket buffer 进行网络发送,即 Linux 的 sendfile。

中读取网页的内容,因为sendfile不能在应用程序中修改数据,所以最适合静态文件服务器或者是直接转发数据的代理服务器。

2)rocketmq主要是mmap,也有小部分使用sendfile

  • rocketMQ在消息存盘和网络发送使用mmap, 单个CommitLog文件大小默认1GB
  • 要在用户进程内处理数据,然后再发送出去的话,用户空间和内核空间的数据传输就是不可避免的
  • (3)Kafka主要是sendfile,也有小部分使用mmap
  • kafka 在客户端和 broker 进行数据传输时,broker 使用 sendfile 系统调用,类似
  • 【FileChannel.transferTo】 API,将磁盘文件读到 OS 内核缓冲区后,直接转到 socket buffer 进行网络发送,即 Linux 的 sendfile。
相关文章
|
3月前
|
存储 安全 物联网
操作系统的心脏:深入理解现代操作系统架构与核心技术
本文旨在为读者提供一个关于现代操作系统(OS)架构和核心技术的全面概述。通过分析OS的主要组件、功能以及它们如何协同工作,本文揭示了操作系统在计算机系统中的核心地位及其复杂性。我们将探讨进程管理、内存管理、文件系统和输入/输出(I/O)等关键技术,并讨论它们对系统性能的影响。此外,本文还将涵盖一些最新的操作系统趋势和技术,如云计算、虚拟化和物联网(IoT)。通过阅读本文,读者将获得对操作系统内部运作方式的深刻理解,这对于软件开发人员、IT专业人士以及对计算机科学感兴趣的任何人来说都是宝贵的知识。
|
13天前
|
运维 Linux 测试技术
操作系统迁移技术
本次分享的主题是操作系统迁移方案,由阿里云路延文分享。 1. 迁移背景:CentOS停更危机 2. KeyarchOS迁移方案 3. 操作系统测试 4. 迁移评估
|
14天前
|
安全 大数据 Linux
云上体验最佳的服务器操作系统 - Alibaba Cloud Linux | 飞天技术沙龙-CentOS 迁移替换专场
本次方案的主题是云上体验最佳的服务器操作系统 - Alibaba Cloud Linux ,从 Alibaba Cloud Linux 的产生背景、产品优势以及云上用户使用它享受的技术红利等方面详细进行了介绍。同时,通过国内某社交平台、某快递企业、某手机客户大数据业务 3 大案例,成功助力客户实现弹性扩容能力提升、性能提升、降本增效。 1. 背景介绍 2. 产品介绍 3. 案例分享
|
1月前
|
Unix Linux Docker
CentOS停更沉寂,RHEL巨变限制源代:Docker容器化技术的兴起助力操作系统新格局
操作系统是计算机系统的核心软件,管理和控制硬件与软件资源,为用户和应用程序提供高效、安全的运行环境。Linux作为开源、跨平台的操作系统,具有高度可定制性、稳定性和安全性,广泛应用于服务器、云计算、物联网等领域。其发展得益于庞大的社区支持,多种发行版如Ubuntu、Debian、Fedora等满足不同需求。
69 4
|
2月前
|
机器学习/深度学习 人工智能 Android开发
移动应用开发与操作系统的协同进化:探索现代技术融合之道###
随着移动互联网的迅猛发展,移动应用已成为人们日常生活中不可或缺的一部分。本文深入探讨了移动应用开发的最新趋势、关键技术以及移动操作系统的发展如何相互促进,共同推动移动互联网的创新与变革。通过分析当前市场动态和技术挑战,本文旨在为开发者提供有价值的见解和指导,帮助他们在竞争激烈的市场中脱颖而出。
|
1月前
|
机器学习/深度学习 人工智能 物联网
操作系统的心脏——深入理解内核机制
在本文中,我们揭开操作系统内核的神秘面纱,探索其作为计算机系统核心的重要性。通过详细分析内核的基本功能、类型以及它如何管理硬件资源和软件进程,我们将了解内核是如何成为现代计算不可或缺的基础。此外,我们还会探讨内核设计的挑战和未来趋势,为读者提供一个全面的内核知识框架。
|
1月前
|
消息中间件 安全 Linux
深入探索Linux操作系统的内核机制
本文旨在为读者提供一个关于Linux操作系统内核机制的全面解析。通过探讨Linux内核的设计哲学、核心组件、以及其如何高效地管理硬件资源和系统操作,本文揭示了Linux之所以成为众多开发者和组织首选操作系统的原因。不同于常规摘要,此处我们不涉及具体代码或技术细节,而是从宏观的角度审视Linux内核的架构和功能,为对Linux感兴趣的读者提供一个高层次的理解框架。
|
2月前
|
存储 消息中间件 算法
深入探索操作系统的心脏——内核机制解析
本文旨在揭示操作系统核心——内核的工作原理,通过剖析其关键组件与机制,为读者提供一个清晰的内核结构图景。不同于常规摘要的概述性内容,本文摘要将直接聚焦于内核的核心概念、主要功能以及其在系统管理中扮演的角色,旨在激发读者对操作系统深层次运作原理的兴趣与理解。
|
2月前
|
安全 Linux 数据安全/隐私保护
深入探索Linux操作系统的多用户管理机制
【10月更文挑战第21天】 本文将详细解析Linux操作系统中的多用户管理机制,包括用户账户的创建与管理、权限控制以及用户组的概念和应用。通过具体实例和命令操作,帮助读者理解并掌握Linux在多用户环境下如何实现有效的资源分配和安全管理。
|
3月前
|
人工智能 Cloud Native Java
云原生技术深度解析:从IO优化到AI处理
【10月更文挑战第24天】在当今数字化时代,云计算已经成为企业IT架构的核心。云原生作为云计算的最新演进形态,旨在通过一系列先进的技术和实践,帮助企业构建高效、弹性、可观测的应用系统。本文将从IO优化、key问题解决、多线程意义以及AI处理等多个维度,深入探讨云原生技术的内涵与外延,并结合Java和AI技术给出相应的示例。
152 1