当前位置: 首页 > 后端技术 > Java

操作系统IO零拷贝技术

时间:2023-04-02 09:42:48 Java

磁盘可以说是计算机系统中最慢的硬件之一,读写速度与内存相差10倍以上,因此有很多优化磁盘的技术,例如零拷贝、直接I/O、异步I/O等。这些优化的目的是提高系统的吞吐量。另外,操作系统内核中的磁盘缓存区可以有效减少磁盘访问次数。本文研究了I/O的工作原理以及如何优化传输文件的性能。参考博客如下:内容大纲本文将从以下几个方面介绍磁盘的IO技术:DMA直接内存访问之前的IO方式——DMA技术。DMA文件传输问题。如何提高文件传输的性能。零拷贝实现原理分析。PageCache有什么用。如何传输大文件。IObeforeDMA在DMA技术出现之前,操作系统从磁盘读取数据的IO过程如下(以read()接口为例):read(file,tmp_buf,len);用户程序需要读取数据,调用read方法,将读取数据的指令交给CPU执行,线程进入阻塞状态。CPU向磁盘控制器发送指令,告诉磁盘控制器读取什么数据,然后返回;磁盘控制器收到指令后,将指定的数据放入磁盘内部缓冲区,然后以中断的方式通知CPU;接收到中断信号后,开始逐字节读取数据到PageCache缓冲区;CPU从PageCache缓冲区逐字节读取数据到用户缓冲区;用户程序从内存中读取数据,拿到数据后,就可以继续执行后续逻辑了。可见,整个数据传输过程都需要CPU亲自参与数据移动的过程,而在这个过程中,CPU不能做其他事情。简单的移动几个字符的数据是没有问题的,但是如果我们用千兆网卡或者硬盘来传输大量的数据,如果用CPU来移动的话,肯定会忙不过来。计算机科学家发现事情的严重性后,发明了DMA技术,也就是直接内存存取(DirectMemoryAccess)技术。直接内存访问-DMA技术什么是DMA技术?简单的理解就是在I/O设备和内存之间传输数据时,将数据传输的工作全部交给DMA控制器,CPU不再参与任何与数据传输相关的事情,让CPU自己处理其他任务。事务。那么使用DMA控制器进行数据传输的过程到底是怎样的呢?让我们详细看一下。读取(文件,tmp_buf,len);用户程序需要读取数据,调用read方法,将读取数据的指令交给CPU执行。CPU向DMA发送指令,告诉DMA从磁盘读取什么数据,然后返回,线程进入阻塞状态。DMA向磁盘控制器发送IO请求,告诉磁盘控制器读取什么数据,然后返回;磁盘控制器收到IO请求后,读取数据到磁盘缓存区,当磁盘缓存读取完成后,中断DMA;DMA接收到磁盘中断信号,将磁盘缓存区的数据读取到PageCache缓存区,然后中断CPU;CPU响应DMA中断信号,知道数据读取完成,然后将PageCache缓冲区中的数据读入用户缓存;用户程序从内存中读取数据后可以继续执行后续逻辑。可以看出,在整个数据传输过程中,CPU不再参与磁盘数据处理的工作,而是由DMA来完成整个过程,但是CPU在这个过程中也是必不可少的,因为传输什么数据,从哪里到哪里,都需要CPU告诉DMA控制器。早期,DMA只存在于主板上。现在随着I/O设备越来越多,对数据传输的要求也不同,所以每个I/O设备都有自己的DMA控制器。DMA文件传输的问题如果服务端要提供文件传输功能,我们能想到的最简单的方式就是:读取磁盘上的文件,然后通过网络协议发送给客户端。传统I/O的工作方式是数据读写从用户空间到内核空间来回复制,内核空间的数据通过操作系统层面的I/O接口从磁盘读取或写入。代码通常如下。一般需要以下两个系统调用。代码非常简单。虽然只有两行代码,但是里面发生了很多事情。读(文件,tmp_buf,len);写(套接字,tmp_buf,len);用户程序需要读取数据,调用read方法,将读取数据的指令交给CPU执行,线程进入阻塞状态。CPU向磁盘DMA发送指令,告诉磁盘DMA从磁盘读取什么数据,然后返回;磁盘DMA向磁盘控制器发送IO请求,告诉磁盘控制器读取什么数据,然后返回;磁盘控制器收到IO请求后,读取数据到磁盘缓存区,当磁盘缓存读取完成后,中断DMA;DMA接收到磁盘的中断信号,将磁盘缓存区的数据读取到PageCache缓存区,然后中断CPU;CPU响应DMA中断信号,知道数据读取完成,然后将PageCache缓冲区中的数据读入用户缓存;用户程序从内存中读取数据,可以继续执行后续向网卡写入数据的操作;用户需要向网卡设备写入数据,调用write方法,将写数据命令交给CPU执行,线程进入阻塞;CPU将用户缓冲区的数据写入PageCache缓冲区,然后通知网卡DMA写入数据;网卡DMA将数据从PageCache缓存区复制到网卡,给网卡处理数据。网卡开始处理数据,网卡完成数据处理后中断网卡DMA;网卡DMA处理中断,知道数据处理完成,向CPU发送中断;CPU响应DMA中断信号,知道数据处理完成,唤醒用户线程;用户程序执行后续逻辑。这个过程比较复杂,主要有以下问题:发生了4次用户态和内核态的上下文切换,因为发生了两个系统调用,一个是read(),一个是write(),每个系统调用不得不先从用户态切换到内核态,内核完成任务后再从内核态切换回用户态。上下文切换的代价不小。一次切换需要几十纳秒到几微秒。虽然时间看似很短,但在高并发场景下,这种时间很容易被累积放大,从而影响系统的性能。表现。数据拷贝有4个,其中两个是DMA拷贝,另外两个是CPU拷贝的。先说这个过程:第一个copy,将磁盘上的数据复制到操作系统内核的buffer中,复制过程由DMA承载。第二次拷贝是将内核缓冲区中的数据拷贝到用户缓冲区中,这样我们的应用程序就可以使用这部分数据,拷贝过程由CPU来完成。第三次拷贝,将刚刚拷贝到用户缓冲区的数据拷贝到内核的socket缓冲区。这个过程还是由CPU来承载的。第四次拷贝是将内核的socketbuffer中的数据拷贝到网卡的buffer中,这个过程又是由DMA承载的。回顾文件传输过程,我们只移动了一份数据,结果却移动了4次。过多的数据拷贝无疑会消耗CPU资源,大大降低系统性能。这种简单传统的文件传输方式存在冗余的上下文切换和数据拷贝,在高并发系统中是非常糟糕的,增加了很多不必要的开销,严重影响了系统性能。因此,为了提高文件传输的性能,需要减少“用户态和内核态的上下文切换”和“内存拷贝”的次数。如何提高文件传输的性能,减少用户态和内核态的上下文切换次数在读取磁盘数据时,由于用户空间没有操作磁盘或网卡的权限,而内核拥有最高权限,所以会发生上下文切换.这些操作设备的过程需要由操作系统内核来完成,所以一般需要通过内核来完成某些任务时,就需要使用操作系统提供的系统调用函数。而一个系统调用必然会有两次上下文切换:先从用户态切换到内核态,然后在内核执行完任务后切换回用户态由进程代码执行。因此,为了减少上下文切换的次数,就必须减少系统调用的次数。减少数据拷贝次数前面我们知道,传统的文件传输方式会经过4次数据拷贝,这里,“从内核的读缓冲区拷贝到用户缓冲区,再从用户缓冲区拷贝到内核的缓冲区”套接字”,这个过程是不必要的。因为在文件传输的应用场景中,我们并没有对用户空间的数据进行“再加工”,所以实际上并不需要将数据移动到用户空间,所以用户的缓冲区是不需要的。零拷贝实现原理分析零拷贝技术通常有两种实现方式:mmap+writesendfile下面说说它们是如何减少“上下文切换”和“数据拷贝”的次数。mmap+write前面我们知道,read()系统调用会将内核缓冲区中的数据复制到用户缓冲区中,所以为了减少这一步的开销,我们可以将read()系统调用替换为mmap()功能。buf=mmap(file,len);write(sockfd,buf,len);mmap()系统调用函数将内核缓冲区中的数据直接“映射”到用户空间,从而使操作系统内核和用户空间将不需要任何进一步的数据复制操作。具体过程如下:应用进程调用mmap()后,DMA将磁盘数据拷贝到内核缓冲区。然后,应用进程与操作系统内核“共享”这个缓冲区;然后应用进程调用write(),操作系统直接将内核缓冲区中的数据复制到socket缓冲区中。移动数据;最后将内核的socket缓冲区中的数据复制到网卡的缓冲区中,这个过程由DMA承载。我们可以知道,用mmap()代替read(),可以减少一次数据拷贝的过??程。但这并不是理想的零拷贝,因为它仍然需要通过CPU将内核缓冲区中的数据复制到socket缓冲区中,并且仍然需要4次上下文切换,因为系统调用还是2次。在Linux内核2.1版本中,sendfile提供了一个专门用于发送文件的系统调用函数sendfile()。函数形式如下:#includessize_tsendfile(intout_fd,intin_fd,off_t*offset,size_tcount);它的前两个参数分别是目标和源的文件描述符,后两个参数是源的偏移量和复制数据的长度,返回值是实际复制数据的长度。首先,它可以替代之前的两个系统调用,read()和write(),这样可以减少一次系统调用,也减少了两次上下文切换的开销。其次,这个系统调用可以直接将内核缓冲区中的数据复制到套接字缓冲区,而不是复制到用户态,所以只有2次上下文切换和3次数据拷贝。如下图所示:但这并不是真正的零拷贝技术。如果网卡支持SG-DMA(TheScatter-GatherDirectMemoryAccess)技术(区别于普通的DMA),我们可以进一步减少CPU存放在内核缓冲区中的数据量。将数据复制到套接字缓冲区的过程。您可以在您的Linux系统中使用以下命令来检查网卡是否支持scatter-gather特性:$ethtool-keth0|grepscatter-gatherscatter-gather:on因此,从Linux内核2.4版本开始,对于支持SG-DMA技术的网卡,sendfile()系统调用的过程发生了一些变化。具体过程如下:通过DMA将磁盘上的数据拷贝到内核缓冲区;将缓冲区描述符和数据长度传送到socket缓冲区,这样网卡的SG-DMA控制器就可以直接将内核缓冲区中的数据复制到网卡的缓冲区中。这个过程不需要将数据从操作系统内核缓冲区复制到socket缓冲区,减少了一次数据。复制;因此,在这个过程中,只进行了2次数据拷贝,如下图所示:这就是所谓的零拷贝(Zero-copy)技术,因为我们没有在内存层面拷贝数据,也就是也就是说,整个过程并没有通过CPU来携带数据,所有的数据都是通过DMA传输的。.与传统的文件传输方式相比,零拷贝技术的文件传输方式减少了2倍的上下文切换和数据拷贝。只需要2次上下文切换和数据拷贝就可以完成文件传输,而2次数据拷贝过程不需要经过CPU,这2次都是通过DMA承载的。因此,一般来说,零拷贝技术至少可以将文件传输的性能提升一倍以上。使用零拷贝技术的项目事实上,Kafka这个开源项目就是利用“零拷贝”技术极大地提高了I/O吞吐率,这也是Kafka处理海量数据速度如此之快的原因之一。如果回溯Kafka文件传输的代码,你会发现它最终调用了JavaNIO库中的transferTo方法:,count,socketChannel);}如果Linux系统支持sendfile()系统调用,那么transferTo()最后实际上会使用sendfile()系统调用函数。曾经有一个大佬写了个程序,测试了一下。在相同的硬件条件下,传统文件传输和零拷贝文件传输的性能差异,可以看到下面的测试数据图,使用零拷贝可以缩短65%的性能时间,大大提高机器传输的吞吐量数据。此外,Nginx还支持零拷贝技术。一般默认开启零拷贝技术,有利于提高文件传输效率。是否开启零拷贝技术的配置如下:http{...sendfileon...}sendfile的具体配置含义:设置为on表示,使用零拷贝技术传输文件:sendfile,soonly需要2个上下文切换和2个数据副本。设置为off意味着使用传统的文件传输技术:read+write,需要4次上下文切换和4次数据拷贝。当然,要使用sendfile,Linux内核版本必须是2.1或更高版本。页面缓存有什么作用?回顾上面提到的文件传输过程,第一步是将磁盘文件数据复制到“内核缓冲区”中,它实际上是一个磁盘缓存(PageCache)。由于零拷贝使用了PageCache技术,所以零拷贝可以进一步提升性能。接下来让我们看看PageCache是如何做到这一点的。读写磁盘比读写内存慢很多,所以我们应该想办法用“读写内存”代替“读写磁盘”。因此,我们将磁盘中的数据通过DMA移动到内存中,这样就可以用读内存代替读磁盘了。但是内存空间比磁盘小很多,内存注定只能复制磁盘上的一小部分数据。问题来了,应该将哪些磁盘数据复制到内存中呢?我们都知道程序在运行的时候是有“局部性”的,所以通常情况下,刚刚被访问过的数据在短时间内被再次访问的概率很高,所以我们可以使用PageCache来缓存最近访问过的数据访问的数据。当空间不足时,最长时间未被访问的缓存被逐出。因此,读取磁盘数据时,先在PageCache中查找。如果数据存在,则直接返回;如果没有,它将从磁盘中读取,然后缓存在PageCache中。还有一点就是在读取磁盘数据的时候,需要找到数据所在的位置,但是对于机械磁盘来说,是通过磁头的旋转,找到数据所在的扇区,然后开始“顺序”读取数据,但物理操作非常耗时。为了减少它的影响,PageCache使用了“预读功能”。例如,假设read方法一次只读取32KB字节。虽然read一开始只读了0-32KB字节,但是内核也会把后面的32-64KB字节读入PageCache,这样后面读取32-64KB的开销就很低了。如果进程在从PageCache淘汰之前读取了32-64KB,收益会非常大。因此,PageCache的优势主要有两个:缓存最近访问过的数据;预读功能;这两种方法将大大提高读写磁盘的性能。但是在传输大文件(GB级别的文件)时,PageCache就不起作用了,会多浪费一次DMA做的数据拷贝,导致性能下降。即使使用了PageCache的零拷贝,性能也会有所损失。这是因为如果你有很多GB级别的文件要传输,每当用户访问这些大文件时,内核都会将它们加载到PageCache中,所以PageCache空间很快就会被这些大文件填满。另外,由于文件较大,部分文件数据被再次访问的概率可能比较低,这会造成两个问题:由于PageCache长期被大文件占用,其他“热点”》小文件可能无法将PageCache充分利用,因此磁盘读写性能会下降;PageCache中的大文件数据享受不到缓存的好处,需要DMA一次复制到PageCache;因此,对于大文件的传输,不应该使用PageCache,也就是不应该使用零拷贝技术,因为“热”的小文件可能会因为PageCache被大文件占用而无法使用PageCache,这在高并发环境下会造成严重的问题。性能问题。绕过PageCache的I/O称为直接I/O,使用PageCache的I/O称为缓存I/O。通常,对于磁??盘,异步I/O只支持直接I/O。前面说过,大文件的传输不应该使用PageCache,因为PageCache可能被大文件占用,导致“热”的小文件无法使用PageCache。因此,在高并发场景下,对于大文件的传输,应该使用“异步I/O+直接I/O”,而不是零拷贝技术。直接I/O应用场景常见的有两种:应用已经实现了磁盘数据缓存,因此PageCache可能不需要再缓存,减少额外的性能损失。在MySQL数据库中,可以通过参数设置开启直接I/O,默认不开启;传输大文件时,大文件很难命中PageCache缓存,会填满PageCache,导致“热”文件无法充分利用缓存,从而增加性能开销,所以直接I/O应该在这个时候使用。另外,因为directI/O绕过了PageCache,所以无法享受到内核这两点的优化:内核的I/O调度算法会将尽可能多的I/O请求缓存在PageCache中,最后“合并”到1较大的I/O请求发送到磁盘,这是为了减少磁盘寻址操作;内核也会将后续的I/O请求“预读”到PageCache中,同样是为了减少磁盘操作;因此,在传输大文件时,使用“异步I/O+直接I/O”,就可以无阻塞地读取文件。因此,在传输文件时,我们需要根据文件的大小采用不同的方法:传输大文件时,使用“异步I/O+直接I/O”;传输大文件时,使用“异步I/O+直接I/O”;传输小文件时,使用“零拷贝技术”;在nginx中,我们可以通过如下配置,根据文件的大小使用不同的方法:location/video/{sendfileon;aio开启;方向1024m;}当文件大小大于directio的值时,使用“异步I/O+直接I/O”,否则使用“零拷贝技术”。总结一下早期的I/O操作,内存和磁盘之间的数据传输工作是由CPU完成的,此时CPU不能执行其他任务,会浪费CPU资源。因此,为了解决这个问题,DMA技术出现了。每个I/O设备都有自己的DMA控制器。通过这个DMA控制器,CPU只需要告诉DMA控制器我们要传输什么数据,从哪里来,走到哪里,就可以放心离开。后续实际的数据传输工作将由DMA控制器完成,CPU不需要参与数据传输工作。传统的IO方式是从硬盘读取数据,然后通过网卡发送出去。我们需要进行4次上下文切换和4次数据拷贝,其中2次数据拷贝发生在内存中的buffer和对应的硬件设备之间,这是通过DMA完成的,另外2次发生在内核态和用户态之间,此数据移动工作由CPU完成。为了提高文件传输的性能,出现了零拷贝技术,通过系统调用(sendfile方法)将磁盘读取和网络发送两种操作结合??起来,减少了上下文切换的次数。另外,数据拷贝发生在内核中,自然减少了数据拷贝的次数。Kafka和Nginx都实现了零拷贝技术,这将大大提高文件传输的性能。零拷贝技术基于PageCache。PageCache会缓存最近访问过的数据,提高访问缓存数据的性能。同时,为了解决机械硬盘寻址慢的问题,还辅助I/O调度算法实现IO合并和预感知。这就是顺序读取性能优于随机读取的原因。这些优点进一步提高了零拷贝的性能。需要注意的是,零拷贝技术不允许进程对文件内容进行进一步的处理,比如在发送数据之前对数据进行压缩。另外,在传输大文件时,不能使用零拷贝,因为“热点”小文件可能会因为PageCache被大文件占用而无法使用PageCache,大文件的缓存命中率不高,那么你需要使用“异步IO+直接IO”的方式。在Nginx中,可以通过配置设置一个文件大小阈值,对大文件使用异步IO和直接IO,对小文件使用零拷贝。欢迎关注玉壶神的微信公众号
参考文档原8张图,你就能理解“零拷贝”用户态介绍、系统调用和库函数、文件IO和标准IO、缓冲区、等版权所有,禁止转载!