29 May 2015

学习自Zero Copy I: User-Mode Perspective

什么是zero copy


让我们了解下一个作为通过存储在文件中的数据提供服务的服务器守护进程在传输给客户端时,守护进程的简单处理过程:

1
2
read(file, tmp_buf, len);
write(socket, tmp_buf, len);

在这两个系统调用发生的复制:

Copying in Two Sample System Calls

  1. read系统调用发生了一次上下文切换(user mode到kernel mode)。第一次复制是由DMA engine产生的,它从磁盘读取文件内容到内核地址空间缓冲区。
  2. 数据从内核缓冲复制到用户缓冲区,read系统调用就返回了。这又发生了一次上下文切换(kernel mode到user mode)。
  3. write系统调用发生了一次上下文切换(user mode到kernel mode)第三次复制是数据从用户缓冲区复制到了内核中与socket相关的缓冲区。
  4. write系统调用的返回发生了第四次上下文切换。第四次复制发生在DMA engine将数据从内核缓冲区传输到协议引擎,这个过程是独立和异步的。为何是独立和异步的?因为当write返回时,并不意味着开始传输数据给客户端了。只是表明Ethernet driver的队列中有空闲的描述符以及接收数据的传输请求。数据通常以先进先出的方式被传输,除非驱动器/硬件实现了优先级队列。

正如所见,这里发生了许多不必要的数据复制,有些复制是可以被消除的。有些硬件具有高级特性,能够绕开内存在设备之间直接传输数据。对于操作系统来说,我们能够在内核和用户缓冲区之间减少复制。

一个减少复制的方法就是调用mmap代替read,例如:

1
2
tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);

在这两个系统调用发生的复制:

Calling mmap

  1. mmap系统调用使得文件内容被DMA engine复制到了内核缓冲区。该缓冲区与用户进程共享。
  2. 写系统调用使得内核将数据从原来的内核缓冲区复制到与socket相关的内核缓冲区。
  3. 第三次复制发生在DMA engine将数据从与socket相关的内核缓冲区复制到协议引擎。

通过使用mmap代替read,我们消除了一次复制。然而,mmap+write的方法存在一个陷阱,当你在内存映射一个文件并调用write时,另一个进程truncate了这个文件。write系统调用将会因为bus error的信号SIGBUS而中断,因为遇到了非法的内存访问。SIGBUS的默认动作是杀死进程并且dump core——这通常不是一个网络服务器所希望的。有两个方法可以处理这个问题:

一是设置信号处理函数,简单的调用return。如果这样做,write系统调用会在它被中断前返回已写的字节数,并且errno被设置为success。但这不是个好方法,因为进程接收到SIGBUS表明已经发生了非法的内存访问,所以并没有处理好真正发生的问题。

二是使用file leasing。这是正确的方法来解决这个问题。通过使用file leasing,当其他进程试图truncate正在传输的文件时,内核将向你发送一个实时信号RT_SIGNAL_LEASE。它告诉你内核阻止了你在leasing file上的读/写。write系统调用会在进程发生非法地址访问前被中断并返回已写的字节数,并且errno设置为success。

1
2
3
4
5
6
7
8
9
if(fcntl(fd, F_SETSIG, RT_SIGNAL_LEASE) == -1) {
    perror("kernel lease set signal");
    return -1;
}
/* l_type can be F_RDLCK F_WRLCK */
if(fcntl(fd, F_SETLEASE, l_type)){
    perror("kernel lease set type");
    return -1;
}

你应该在映射文件前设置lease,并且释放lease在完成工作后。

sendfile


在内核2.1版本引入了sendfile系统调用,用于优化网络和本地文件之间的数据传输,不仅减少复制,而且减少了上下文切换。

调用sendfile:

Replacing Read and Write with Sendfile

  1. sendfile系统调用使得文件内容被DMA engine复制到了内核缓冲区。之后,数据被复制到与socket相关的内核缓冲区。
  2. 第三次复制发生在DMA engine将数据从与socket相关的内核缓冲区传输到协议引擎。

当另一个进程truncate我们通过sendfile传输的文件时,如果我们没注册任何信号处理时,sendfile会返回已传输的字节数在他被中断前,并且errno设置为success。

如果我们在sendfile调用前设置lease,那么sendfile的行为和返回值并不会改变,但是会收到RT_SIGNAL_LEASE信号,在sendfile返回前。

到目前为止,在内核空间中仍然存在一次复制。这需要硬件支持才能消除。我们需要一块支持收集操作的网卡。这意味着等待传输的数据在内存中无须连续存储,即消除存放数据的内核缓冲区到与socket相关的内核缓冲区的复制。在内核2.4,socket缓冲区描述符被修改以适合这个需求——这就是所谓linux下的zero copy。

硬件支持收集操作,能够将分散在内存中的数据进行传输,减少了一次复制:

Replacing Read and Write with Sendfile

  1. sendfile系统调用使得文件内容被DMA engine复制到内核缓冲区。
  2. 没有数据被复制到socket内核缓冲区。取而代之,仅仅包含数据地址和长度信息的描述符被附加到socket内核缓冲区。DMA engine能够直接从分散的内核缓冲区传送数据到协议引擎。

从操作系统角度,即内核空间和用户空间,这称之为zero copy。

可移植性问题


sendfile系统调用存在的问题就是缺乏标准的实现,在不同系统中实现可能不同:

  • 一是Linux sendfile能够在两个文件描述符之间传输数据。
  • 二是Linux sendfile没有实现vectored transfers(不知道这是啥…)。