零拷贝技术

注意事项:除Direct I/O,与磁盘相关的文件的读写都有使用到Page cache技术。

Netty、Kafka和Mysql等开源组件都用到了零拷贝这个核心技术。

1、数据的四次拷贝和四次上下文切换

很用应用程序在面临客户端请求时,可以等价为进行如下的系统调用:

1 File.read(file, buf, len);
2 Socket.send(socket, buf, len);

例如,消息中间件Kafka就是这个应用场景,从磁盘中读取一批消息后原封不动地写入网卡(NIC,Network interface controler)进行发送。

在没有任何优化技术使用的背景下,操作系统为此将进行4次数据拷贝和四次上下文切换,如下图所示:

如果没有优化,读取磁盘数据,在通过网卡输送的场景性能比较差:

1.1 4次拷贝

  • 物理设备<->内存:
    • CPU负责将数据从磁盘搬运到内核空间的Page Cache中。
    • CPU负责将数据从内核空间的Socket缓冲区搬运到网络中。
  • 内存内部拷贝:
    • CPU负责将数据从内核空间的Page Cache搬运到用户空间缓冲区。
    • CPU负责将数据从用户空间的缓冲区搬运到内核空间的Socket缓冲区。

1.2 4次上下文切换

  1. read系统调用时:用户态切换到内核态;
  2. read系统调用完毕时:内核态切换回用户态;
  3. write系统调用时:用户态切换内切换到内核态;
  4. write系统调用完毕时:内核态切换回用户态;

这时遇到的问题:

  1. CPU全程负责内存内部的数据拷贝还可以接受,因为内存的数据拷贝效率还行(不过还是比CPU慢很多),但是如果要CPU全程负责内存与磁盘、内存与网卡的数据拷贝,这将难以接受,因为磁盘、网卡的I/O速度远小于内存。
  2. 4次拷贝太多了,4次上下文切换太过频繁。

2、DMA参与下的四次数据拷贝

DMA技术就是在主板上放一块独立的芯片。在进行内存和I/O设备的数据传输的时候,我们不再通过CPU来控制数据传输,而直接通过DMA控制器(DMA Controller,简称DMAC)。这块芯片,我们可以认为它其实就是一个协处理器(Co-Processor)。

 DMAC的价值在下列情况下尤其明显:

  • 传输的数据特别大、速度特别快。
  • 传输的数据特别小,速度特别慢。

比如,用千兆网卡或者硬盘传输大量数据时,如果都用CPU来搬运的话,肯定忙不过来,所以可以选择DMAC。而当传输很慢的时候,DMAC可以等数据到齐了,再发送信号,给到CPU去处理,而不是让CPU在哪里忙等。

注意:这里的“协”字,DMA是在“协助”CPU,完成对应对数据传输工作。在DMAC控制数据传输的过程中,DMAC还是被CPU控制,只是数据的拷贝行为不再由CPU来完成。

原本计算机所有组件之间的数据拷贝(流动)必须经过CPU。以磁盘读写为例,如下图所示:

现在,DMAC代替了CPU负责内存磁盘、内存与网卡之间的数据搬运,CPU作为DMAC的控制者,如下图所示:

但是DMAC有其局限性,DMAC仅仅能用于设备间数据交换时进行数据拷贝,但是设备内部之间的数据拷贝还需要CPU来亲力亲为。例如,CPU需要负责内核空间与用户空间之间的数据拷贝(内存内部的拷贝),如下图所示:

 

上图中的read buffer也是page cache,socket buffer也就是socket缓冲区。

3、零拷贝技术

3.1 什么是零拷贝技术

零拷贝技术是一种思想,指的是计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定的区域。

可见,零拷贝的特点就是CPU不全程负责内存中的数据写入其他组件,CPU仅仅起到管理的作用。但注意,零拷贝不是不进行拷贝,而是CPU不再全程负责数据拷贝的搬运工作。如果数据本身不在内存中,那么必须先通过某种方式拷贝到内存中(这个过程CPU可以仅仅负责管理,DMAC来负责具体数据拷贝),因为数据只有在内存中,才能被转移,才能被CPU直接读取计算。

零拷贝技术的具体实现方式有很多,例如:

  • sendfile
  • mmap
  • 直接Direct I/O
  • splice

不同的零拷贝技术适用不同的应用场景,下面依次进行sendfile、mmap、Direct I/O的分析。

这里先做一个前瞻性的技术总结:

  • DMA技术:DMA负责内存与其他组件之间的数据拷贝,CPU仅需负责管理,而无需负责全程的数据拷贝。
  • 使用page cache的zero copy:
    • sendfile:一次代替read/write系统调用,通过DMA技术以及传递文件描述符,实现zero copy
    • mmap:仅代替read系统调用,将内核空间地址映射为用户空间地址,write操作直接作用于内核空间。通过DMA技术以及地址映射技术,用户空间与内核空间无须数据拷贝,实现了zero copy
  • 不使用page cache的Direct I/O:读写操作直接在磁盘上进行,不使用page cache机制,通常结合用户空间的用户缓存使用。通过DMA技术直接与磁盘/网卡进行数据交互,实现zero copy。

3.2 sendfile

sendfile的应用场景是:用户从磁盘读起一些文件数据后,不需要经过任何计算与处理就通过网络传输出去。此场景的典型应用就是消息队列。

在传统I/O下,正如第一节所示,上述应用场景的一次数据传输需要四次CPU全权负责的拷贝与上下文切换。

sendfile主要使用到了两个技术:

  • DMA技术
  • 传递文件描述符代替数据拷贝

下面依次讲解这两个技术的作用:

  • 利用DMA技术

sendfile依赖DM啊技术,将四次CPU全程负责的拷贝与四次上下文切换减少到两次,如下图所示:

 

利用DMA技术减少2次CPU全程参与到拷贝

DMA负责磁盘到内核空间中Page Cache(Read Buffer)的数据拷贝以及从内核空间中socket buffer到网卡到数据拷贝。

 

  • 传递文件描述符代替数据拷贝 

传递文件描述可以代替数据拷贝,这是由于两个原因:

  1. page cache以及socket buffer都在内核空间中;
  2. 数据在传输中没有被更新;

利用传递文件描述符代替内核中的数据拷贝

注意事项:只有网卡支持SG-DMA(The Scatter-Gather Direct Memory Access)技术才可以通过传递文件描述符的方式避免内核空间的一次CPU拷贝。这意味着此优化取决于Linux系统的物理网卡是否支持(Linux在内核2.4版本里引入了DMA的scatter/gather-分散/收集功能,只要确保Linux版本高于2.4即可)

  • 一次系统调用代替两次系统调用

由于sendfile仅仅对应一次系统调用,而传统文件操作则需要使用read以及write两个系统调用。正因为如此,sendfile能够将用户态与内核态之间的上下文切换从4次降到2次。

sendfile系统调用仅仅需要两次上下文切换

另一方面,我们需要注意sendfile系统调用的局限性。如果应用程序需要对从磁盘读取的数据进行写操作,例如解密或加密,那么sendfile系统调用就无法使用。这是因为用户线程根本就不能够通过sendfile系统调用到传输到数据。

3.3 mmap

  • mmap基础概念

mmap即memory map,也就是内存映射。

mmap是一种内存映射到方法,即将一个文件或者其他对象映射到进程的地址空间,实现文件的磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写这段内存,而系统会自动回血脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read、write等系统调用函数。相反,内核空间对这段区域对修改也直接反映到用户空间,从而实现不同进程间的文件共享。如下图所示:

mmap具有如下特点:

  1. mmap向应用程序提供内存访问接口的内存地址是连续的,但是对应的磁盘文件的block可以不是地址连续的;
  2. mmap提供的内存空间是虚拟空间(虚拟内存),而不是物理空间(物理内存),因此完全可以分配远远大于物理内存大小的虚拟空间(例如16G内存主机分配1000G的mmap内存空间);
  3. mmap负责映射文件逻辑上一段连续的数据(物理上可以不连续存储)映射为连续内存,而这里的文件可以是磁盘文件、驱动假造出的文件(例如DMA技术)以及设备;
  4. mmap由操作系统负责管理,对同一个文件地址的映射将被所有线程共享,操作系统确保线程安全以及线程可见性;

mmap的设计很有启发性。基于磁盘的读写单位是block(一般大小为4KB),而基于内存的读写单位是地址(虽然内存的管理和分配单位是4KB)。换言之,CPU进行一次磁盘读写操作涉及的数量至少是4KB,但是进行一次内存操作涉及的数据是基于地址的,也就是通常的64bit(64位操作系统)。mmap下进程可以采用指针的方式进行读写操作,着是值得注意的。

  • mmap的I/O模型

mmap也是一种零拷贝技术,其I/O模型如下图所示:

mmap技术有如下特点:

  1. 利用DMA技术来取代CPU在内存与其他组件之间的数据拷贝,例如从磁盘到内存,从内存到网卡;
  2. 用户空间的mmap file使用虚拟内存,实际上不占物理内存,只有在内核空间的kernel buffer cache才占据实际的物理内存;
  3. mmap函数需要配合write()系统调用进行操作,这与sendfile()函数有所不同,后者一次性替代了read()/wirte();因此mmap也至少需要4次上下文切换;
  4. mmap仅仅能够避免内核空间到用户空间的全程CPU负责的数据拷贝,但是内核空间内部还是需要全程CPU负责的数据拷贝;

利用mmap()替换read(),配合write()调用的整个流程如下:

  1. 用户进程调用mmap(),从用户态陷入内核态,将内核缓冲区映射到用户缓冲区;
  2. DMA控制器将数据从硬盘拷贝到内核缓冲区(可见其使用了page cache机制);
  3. mmap()返回,上下文从内核态切回用户态;
  4. 用户进程调用write(),尝试把文件数据写到内核的套接字缓冲区,再次陷入内核态;
  5. CPU将内核缓冲区的数据拷贝到套接字缓冲区;
  6. DMA控制器将数据从套接字缓冲区拷贝到网卡发送Ringbuffer中,完成数据传输。
  7. write()返回,上下文从内核态切回用户态。
  • mmap的优势

1)简化用户进程编程 

在用户空间看来,通过mmap机制以后,磁盘上的文件仿佛直接就在内存中,把访问磁盘文件简化为按地址访问内存。这样一来,应用程序自然不需要使用文件系统write(写入)、读取(read)、fsync(同步)等系统调用,因为现在只要面向内存等虚拟空间进行开发。

但是,这并不意外这我们不再需要进行这些系统调用,而是说这些系统调用由操作系统在mmap机制等内部封装好了。

基于缺页异常等懒加载

出于节约物理内存以及mmap方法快速返回的目的,mmap映射采用懒加载机制。具体来说,通过mmap申请1000G内存可能仅仅占用了100MB的虚拟内存空间,甚至没有分配实际的物理内存空间。当你访问香港内存地址时,才会进行真正的write、read等系统调用。CPU会通过陷入缺页异常等方式来将磁盘上的数据加载到物理内存中,此时才会发生真正的物理内存分配。

数据一致性由OS确保

当发生数据修改时,内存出现脏页,与磁盘文件出现不一致。mmap机制下由操作系统自动完成内存数据落盘(脏页回刷),用户进程通常并不需要手动管理数据落盘。

2)读写效率提高:避免内核空间到用户空间的数据拷贝

简而言之,mmap被认为快的原因是因为建立了页到用户进程的虚拟空间的映射,以读取文件为例,避免了页从内核空间拷贝到用户空间。

3)避免只读操作时到swap操作

虚拟内存带来了种种好处,但是一个最大的问题在于所有 进程的虚拟内存大小总和可能大于物理内存大小,因此当操作系统物理内存不够用时,就会把一部分内存swap到磁盘上。

在mmap下,如果虚拟空间没有发生读写操作,那么由于通过mmap操作得到的内存数据完全可以通过再次调用mmap操作映射文件得到。但是,通过其他方式分配的内存,在没有发生写操作的情况下,操作系统并不知道如何简单地从现有文件中(除非其重新执行一遍应用程序,但是代价很大)恢复内存数据,因此必须将内存swap到磁盘上。

4)节约内存

由于用户空间与内核空间实际上共用一份数据,因此在大文件场景下实际物理内存占用上有优势。

  • mmap不是银弹

mmap不是银弹,这意味着mmap也有其缺陷,在相关场景下的性能存在缺陷:

  1. 由于mmap使用时必须事先指定好内存映射的大小,因此mmap不适合变长文件;
  2. 如果更新文件的操作比较多,mmap避免两态拷贝的优势就被摊还,最终还是落在了大量的脏页回xie及由此引发的随机I/O上,所以在随机写很多的情况下,mmap方式在效率上不一定比带缓冲区的一般写快。
  3. 读/写小文件(例如16K以下的文件),mmap与通过read系统调用相比有着更高的开销与延迟;同时mmap的刷盘由系统全权控制,但是在小数据量大情况下由应用本身手动控制更好。
  4. mmap受限于操作系统内存大小:例如在32bits操作系统上,虚拟内存总大小也就2GB,但由于mmap必须要在内存中找到一块连续的地址块,此时你就无法对4GB大小的文件完全进行mmap,在这种情况下你必须分配多块分别进行mmap,但是此时内存地址已经不再连续,使用mmap的意义大打折扣,而且引入了额外的复杂性。
  • mmap的适用场景

mmap的适用场景实际上非常受限,在如下场合下可以选择使用mmap机制:

  1. 多个线程以只读的方式同时访问一个文件,这是因为mmap机制下多线程共享了同一个物理内存空间,因此节约了内存。案例:多进程可能依赖同一个动态连接库,利用mmap可以实现内存仅仅价值一份动态连接库,多个进程共享此动态连接库。
  2. mmap非常适合用于进程间通信,这是因为对同一对应的mmap分配的物理内存天然多线程共享,并可以依赖于操作系统同步原语;
  3. mmap虽然比sendfile等机制多了一次CPU全程参与的内存拷贝,但是用户空间语内核空间并不需要数据拷贝,因此在正确使用情况下并不比sendfile效率差。

3.4 Direct I/O

 Direct I/O即直接I/O。其名字中的“直接”二字用于区分使用page cache机制的缓存I/O。

  • 缓存文件I/O:用户空间要读写一个文件并不直接与磁盘交互,而是中间夹层缓存,即page cache;
  • 直接文件I/O:用户空间读取的文件直接与磁盘交互,中间没有page catch层;

“直接”在这里还有另一层语义:其他所有技术中,数据至少需要在内核空间存储一份,但是在Direct I/O技术中,数据直接存储在用户空间中,绕过了内核。

Direct I/O模式如下图所示:

此时,用户空间直接通过DMA的方式与磁盘以及网卡进行数据拷贝。

事实上,即使Direct I/O还是可能需要使用操作系统的fsync系统调用的。因为虽然文件的数据本身没有使用任何缓存,但是文件的元数据仍然需要缓存,包括VFS中的inode cache和dentry cache等。

在部分操作系统中,在Direct I/O模式下进行write系统调用能够确保文件数据落盘,但是文件元数据不一定落盘。如果在此类操作系统上,那么还需要执行一次fsync系统调用却不文件元数据页落盘。否则,可能会导致文件异常,元数据缺失等情况。MySQL等O_DIRECT与O_DIRECT_NO_FSYNC配置是一个具体的案例。

1)优点

  1. Linux中的直接I/O技术省略掉缓存I/O技术中操作系统内核缓冲区的使用,数据直接在应用程序地址空间和磁盘之间进行传输,从而使得自缓存应用程序可以省略掉复杂的系统级别的缓存结构,而执行程序自己定义的数据读写管理,从而降低系统级别的管理对应用程序访问数据的影响;
  2. 与其他零拷贝技术一样,避免了内核空间到用户空间的数据拷贝,如果要传输的数据量很大,使用直接I/O的方式进行数据传输,而不需要操作系统内核地址空间数据拷贝操作的参与,这将会大大提高性能。

2)缺点

  1. 由于设备之间的数据传输是通过DMA完成的,因此用户空间的数据缓冲区内存必须进行page pinning(页锁定),这是为了防止其物理页框地址被交换到磁盘或者被移动到新的地址而导致DMA去拷贝数据的时候再指定的地址找不到内存页从而引发缺页错误,而页锁定的开销并不必CPU拷贝小,所以为了避免频繁的页锁定系统调用,应用程序必须分配和注册一个持久的内存池,用于数据缓冲;
  2. 如果访问的数据不在应用程序缓存中,那么每次数据都会直接从磁盘进行加载,这种直接加载会非常缓慢。
  3. 在应用层引入直接I/O需要应用自己管理,这带来了额外的系统复杂性;

谁会使用Direct I/O?

自缓存应用程序(self-caching applications)可以选择使用Direct I/O。

自缓存应用程序

对于某些应用程序来说,它会有它自己的数据缓存机制,比如,它会将数据缓存在应用程序地址空间,这类应用程序完全不需要使用操作系统内核中的高速缓冲存储器,这类应用程序就被称作是自缓存应用程序(self-caching applications)。

例如,应用内部维护一个缓存空间,当有读取操作时,首先读取应用层的缓存数据,如果没有,那么就通过Direct I/O直接通过磁盘I/O来读取数据。缓存仍然在应用,只不过应用觉得自己实现一个缓存比操作系统的缓存更高效。

数据库管理系统就是这类应用程序的一个代表。自缓存应用程序倾向于使用数据的逻辑表达式,而非物理表达式;当系统内存较低的时候,自缓存应用程序会让这种数据的逻辑缓存被换出,而并非时磁盘上实际的数据被换出。自缓存应用程序对要操作的数据的语义了如执掌,所以它可以采用更加高效的缓存替换算法。自缓存应用程序有可能会在多台主机之间共享一块内存,那么自缓存应用程序就需要提供一种能够有效地将用户地址空间的缓存数据置为无效的机制,从而确保应用程序地址空间缓存数据的一致性。

page cache 是Linux为所有应用程序提供的缓存机制,但是数据库应用太特殊了,page cache影响了数据对特性的追求。

另一方面,目前Linux上的异步IO库,其依赖于文件使用O_DIRECT模式打开,它们通常一起配合使用。

如何使用Direct I/O?

用户应用需要实现用户空间内的缓存区,读/写操作应当尽量通过此缓存区提供。如果有性能上的考虑,那么尽量避免频繁地基于 Direct I/O 进行读/写操作。

4、经典案例

4.1 Kafka

Kafka 作为一个消息队列,涉及到磁盘 I/O 主要有两个操作:

  • Provider 向 Kakfa 发送消息,Kakfa 负责将消息以日志的方式持久化落盘;
  • Consumer 向 Kakfa 进行拉取消息,Kafka 负责从磁盘中读取一批日志消息,然后再通过网卡发送;

Kakfa 服务端接收 Provider 的消息并持久化的场景下使用 mmap 机制,能够基于顺序磁盘 I/O 提供高效的持久化能力,使用的 Java 类为 java.nio.MappedByteBuffer。
Kakfa 服务端向 Consumer 发送消息的场景下使用 sendfile 机制,这种机制主要两个好处:

  • sendfile 避免了内核空间到用户空间的 CPU 全程负责的数据移动;
  • sendfile 基于 Page Cache 实现,因此如果有多个 Consumer 在同时消费一个主题的消息,那么由于消息一直在 page cache 中进行了缓存,因此只需一次磁盘 I/O,就可以服务于多个 Consumer;

使用 mmap 来对接收到的数据进行持久化,使用 sendfile 从持久化介质中读取数据然后对外发送是一对常用的组合。但是注意,你无法利用 sendfile 来持久化数据,利用 mmap 来实现 CPU 全程不参与数据搬运的数据拷贝。

4.2 MySQL

MySQL 的具体实现比 Kakfa 复杂很多,这是因为支持 SQL 查询的数据库本身比消息队列对复杂很多。可以参考MySQL 的零拷贝技术。

5、总结

DMA 技术使得内存与其他组件,例如磁盘、网卡进行数据拷贝时,CPU 仅仅需要发出控制信号,而拷贝数据的过程则由 DMAC 负责完成。

Linux 的零拷贝技术有多种实现策略,但根据策略可以分为如下几种类型:

  • 减少甚至避免用户空间和内核空间之间的数据拷贝:在一些场景下,用户进程在数据传输过程中并不需要对数据进行访问和处理,那么数据在 Linux 的 Page Cache 和用户进程的缓冲区之间的传输就完全可以避免,让数据拷贝完全在内核里进行,甚至可以通过更巧妙的方式避免在内核里的数据拷贝。这一类实现一般是是通过增加新的系统调用来完成的,比如 Linux 中的 mmap(),sendfile() 以及 splice() 等。
  • 绕过内核的直接 I/O:允许在用户态进程绕过内核直接和硬件进行数据传输,内核在传输过程中只负责一些管理和辅助的工作。这种方式其实和第一种有点类似,也是试图避免用户空间和内核空间之间的数据传输,只是第一种方式是把数据传输过程放在内核态完成,而这种方式则是直接绕过内核和硬件通信,效果类似但原理完全不同。
  • 内核缓冲区和用户缓冲区之间的传输优化:这种方式侧重于在用户进程的缓冲区和操作系统的页缓存之间的 CPU 拷贝的优化。这种方法延续了以往那种传统的通信方式,但更灵活。
posted @ 2022-05-05 13:08  钟齐峰  阅读(458)  评论(0编辑  收藏  举报