对应用程序地址空间和内核之间的数据传输进行优化的零拷贝技术

前面提到的几种零拷贝技术都是通过尽量避免用户应用程序和操作系统内核缓冲区之间的数据拷贝来实现的,使用上面那些零拷贝技术的应用程序通常都要局限于某些特殊的情况:要么不能在操作系统内核中处理数据,要么不能在用户地址空间中处理数据。

而这一小节提出的零拷贝技术保留了传统在用户应用程序地址空间和操作系统内核地址空间之间传递数据的技术,但却在传输上进行优化。

我们知道,数据在系统软件和硬件之间的传递可以通过 DMA 传输来提高效率,但是对于用户应用程序和操作系统之间进行数据传输这种情况来说,并没有类似的工具可以使用。

本节介绍的技术就是针对这种情况提出来的。

利用写时复制

在某些情况下,Linux 操作系统内核中的页缓存可能会被多个应用程序所共享,操作系统有可能会将用户应用程序地址空间缓冲区中的页面映射到操作系统内核地址空间中去。如果某个应用程序想要对这共享的数据调用  write() 系统调用,那么它就可能破坏内核缓冲区中的共享数据,传统的 write() 系统调用并没有提供任何显示的加锁操作,Linux 中引入了写时复制这样一种技术用来保护数据。

什么是写时复制

写时复制是计算机编程中的一种优化策略,它的基本思想是这样的:如果有多个应用程序需要同时访问同一块数据,那么可以为这些应用程序分配指向这块数据的指针,在每一个应用程序看来,它们都拥有这块数据的一份数据拷贝,当其中一个应用程序需要对自己的这份数据拷贝进行修改的时候,就需要将数据真正地拷贝到该应用程序的地址空间中去,也就是说,该应用程序拥有了一份真正的私有数据拷贝,这样做是为了避免该应用程序对这块数据做的更改被其他应用程序看到。

这个过程对于应用程序来说是透明的,如果应用程序永远不会对所访问的这块数据进行任何更改,那么就永远不需要将数据拷贝到应用程序自己的地址空间中去。

这也是写时复制的最主要的优点。

写时复制的实现需要 MMU 的支持,MMU 需要知晓进程地址空间中哪些特殊的页面是只读的,当需要往这些页面中写数据的时候,MMU 就会发出一个异常给操作系统内核,操作系统内核就会分配新的物理存储空间,即将被写入数据的页面需要与新的物理存储位置相对应。

写时复制的最大好处就是可以节约内存。

不过对于操作系统内核来说,写时复制增加了其处理过程的复杂性。

数据传输的实现及其局限性

数据发送端

对于数据传输的发送端来说,实现相对来说是比较简单的,对与应用程序缓冲区相关的物理页面进行加锁,并将这些页面映射到操作系统内核的地址空间,并标识为“ write only ”。

当系统调用返回的时候,用户应用程序和网络堆栈就都可以读取该缓冲区中的数据。在操作系统已经传送完所有的数据之后,应用程序就可以对这些数据进行写操作。

如果应用程序尝试在数据传输完成之前对数据进行写操作,那么就会产生异常,这个时候操作系统就会将数据拷贝到应用程序自己的缓冲区中去,并且重置应用程序端的映射。

数据传输完成之后,对加锁的页面进行解锁操作,并重置 COW 标识。

数据接收端

对于数据接收端来说,该技术的实现则需要处理复杂得多的情况。

如果 read() 系统调用是在数据包到达之前发出的,并且应用程序是被阻塞的,那么 read() 系统调用就会告知操作系统接收到的数据包中的数据应该存放到什么地方去。

在这种情况下,根本没有必要进行页面重映射,网络接口卡可以提供足够的支持让数据直接存入用户应用程序的缓冲区中去。

如果数据接收是异步的,在 read() 系统调用发出之前,操作系统不知道该把数据写到哪里,因为它不知道用户应用程序缓冲区的位置,所以操作系统内核必须要先把数据存放到自己的缓冲区中去。

局限性

写时复制技术有可能会导致操作系统的处理开销很大.所有相关的缓冲区都必须要进行页对齐处理,并且使用的 MMU 页面一定要是整数个的。

对于发送端来说,这不会造成什么问题。

但是对于接收端来说,它需要有能力处理更加复杂的情况。

首先,数据包的尺寸大小要合适,大小需要恰到好处能够覆盖一整页的数据,这就限制了那些 MTU 大小大于系统内存页的网络,比如 FDDI 和 ATM。

其次,为了在没有任何中断的情况下将页面重映射到数据包的流,数据包中的数据部分必须占用整数个页面。

对于异步接收数据的情况来说,为了将数据高效地移动到用户地址空间中去,可以使用这样一种方法:利用网络接口卡的支持,传来的数据包可以被分割成包头和数据两部分,数据被存放在一个单独的缓冲区内,虚拟存储系统然后就会将数据映射到用户地址空间缓冲区去。

使用这种方法需要满足两个先决条件,也就是上面提到过的:一是应用程序缓冲区必须是页对齐的,并且在虚拟存储上是连续的;二是传来的数据有一页大小的时候才可以对数据包进行分割。

事实上,这两个先决条件是很难满足的。如果应用程序缓冲区不是页对齐的,或者数据包的大小超过一个页,那么数据就需要被拷贝。对于数据发送端来说,就算数据在传输的过程中对于应用程序来说是写保护的,应用程序仍然需要避免使用这些忙缓冲区,这是因为写时拷贝操作所带来的开销是很大的。如果没有端到端这一级别的通知,那么应用程序很难会知道某缓冲区是否已经被释放还是仍然在被占用。

这种零拷贝技术比较适用于那种写时复制事件发生比较少的情况,因为写时复制事件所产生的开销要远远高于一次 CPU 拷贝所产生的开销。

实际情况中,大多数应用程序通常都会多次重复使用相同的缓冲区,所以,一次使用完数据之后,不要从操作系统地址空间解除页面的映射,这样会提高效率。

考虑到同样的页面可能会被再次访问,所以保留页面的映射可以节省管理开销,但是,这种映射保留不会减少由于页表往返移动和 TLB 冲刷所带来的开销,这是因为每次页面由于写时复制而进行加锁或者解锁的时候,页面的只读标志都要被更改。

个人收获

  1. copyOnWrite 是并发中也提到的一种并发解决方案。思想都是相通的。

参考资料