支撑百万并发的“零拷贝”技术,你了解吗?( 五 )


mmap+write
一种零拷贝方式是使用 mmap+write 代替原来的 read+write 方式,减少了 1 次 CPU 拷贝操作 。
mmap 是 Linux 提供的一种内存映射文件方法,即将一个进程的地址空间中的一段虚拟地址映射到磁盘文件地址,mmap+write 的伪代码如下:

  1. tmp_buf = mmap(file_fd, len);
  2. write(socket_fd, tmp_buf, len);
使用 mmap 的目的是将内核中读缓冲区(read buffer)的地址与用户空间的缓冲区(user buffer)进行映射 。
从而实现内核缓冲区与应用程序内存的共享,省去了将数据从内核读缓冲区(read buffer)拷贝到用户缓冲区(user buffer)的过程 。
然而内核读缓冲区(read buffer)仍需将数据拷贝到内核写缓冲区(socket buffer),大致的流程如下图所示:
 
 
基于 mmap+write 系统调用的零拷贝方式,整个拷贝过程会发生 4 次上下文切换,1 次 CPU 拷贝和 2 次 DMA 拷贝 。
用户程序读写数据的流程如下:
  • 用户进程通过 mmap() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space) 。
  • 将用户进程的内核空间的读缓冲区(read buffer)与用户空间的缓存区(user buffer)进行内存地址映射 。
  • CPU 利用 DMA 控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer) 。
  • 上下文从内核态(kernel space)切换回用户态(user space),mmap 系统调用执行返回 。
  • 用户进程通过 write() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space) 。
  • CPU 将读缓冲区(read buffer)中的数据拷贝到网络缓冲区(socket buffer) 。
  • CPU 利用 DMA 控制器将数据从网络缓冲区(socket buffer)拷贝到网卡进行数据传输 。
  • 上下文从内核态(kernel space)切换回用户态(user space),write 系统调用执行返回 。
mmap 主要的用处是提高 I/O 性能,特别是针对大文件 。对于小文件,内存映射文件反而会导致碎片空间的浪费 。
因为内存映射总是要对齐页边界,最小单位是 4 KB,一个 5 KB 的文件将会映射占用 8 KB 内存,也就会浪费 3 KB 内存 。
mmap 的拷贝虽然减少了 1 次拷贝,提升了效率,但也存在一些隐藏的问题 。
当 mmap 一个文件时,如果这个文件被另一个进程所截获,那么 write 系统调用会因为访问非法地址被 SIGBUS 信号终止,SIGBUS 默认会杀死进程并产生一个 coredump,服务器可能因此被终止 。
Sendfile
Sendfile 系统调用在 Linux 内核版本 2.1 中被引入,目的是简化通过网络在两个通道之间进行的数据传输过程 。
Sendfile 系统调用的引入,不仅减少了 CPU 拷贝的次数,还减少了上下文切换的次数,它的伪代码如下:
  1. sendfile(socket_fd, file_fd, len);
通过 Sendfile 系统调用,数据可以直接在内核空间内部进行 I/O 传输,从而省去了数据在用户空间和内核空间之间的来回拷贝 。
与 mmap 内存映射方式不同的是,Sendfile 调用中 I/O 数据对用户空间是完全不可见的 。也就是说,这是一次完全意义上的数据传输过程 。
基于 Sendfile 系统调用的零拷贝方式,整个拷贝过程会发生 2 次上下文切换,1 次 CPU 拷贝和 2 次 DMA 拷贝 。
用户程序读写数据的流程如下:
  • 用户进程通过 sendfile() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space) 。
  • CPU 利用 DMA 控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer) 。
  • CPU 将读缓冲区(read buffer)中的数据拷贝到的网络缓冲区(socket buffer) 。
  • CPU 利用 DMA 控制器将数据从网络缓冲区(socket buffer)拷贝到网卡进行数据传输 。
  • 上下文从内核态(kernel space)切换回用户态(user space),Sendfile 系统调用执行返回 。
相比较于 mmap 内存映射的方式,Sendfile 少了 2 次上下文切换,但是仍然有 1 次 CPU 拷贝操作 。
Sendfile 存在的问题是用户程序不能对数据进行修改,而只是单纯地完成了一次数据传输过程 。
Sendfile+DMA gather copy
Linux 2.4 版本的内核对 Sendfile 系统调用进行修改,为 DMA 拷贝引入了 gather 操作 。
它将内核空间(kernel space)的读缓冲区(read buffer)中对应的数据描述信息(内存地址、地址偏移量)记录到相应的网络缓冲区( socket buffer)中,由 DMA 根据内存地址、地址偏移量将数据批量地从读缓冲区(read buffer)拷贝到网卡设备中 。
这样就省去了内核空间中仅剩的 1 次 CPU 拷贝操作,Sendfile 的伪代码如下:


推荐阅读