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

传统 I/O 方式
为了更好的理解零拷贝解决的问题,我们首先了解一下传统 I/O 方式存在的问题 。
在 Linux 系统中,传统的访问方式是通过 write() 和 read() 两个系统调用实现的,通过 read() 函数读取文件到到缓存区中,然后通过 write() 方法把缓存中的数据输出到网络端口 。
伪代码如下:

  1. read(file_fd, tmp_buf, len);
  2. write(socket_fd, tmp_buf, len);
下图分别对应传统 I/O 操作的数据读写流程,整个过程涉及 2 次 CPU 拷贝、2 次 DMA 拷贝,总共 4 次拷贝,以及 4 次上下文切换 。
 
 
下面简单地阐述一下相关的概念:
  • 上下文切换:当用户程序向内核发起系统调用时,CPU 将用户进程从用户态切换到内核态;当系统调用返回时,CPU 将用户进程从内核态切换回用户态 。
  • CPU 拷贝:由 CPU 直接处理数据的传送,数据拷贝时会一直占用 CPU 的资源 。
  • DMA 拷贝:由 CPU 向DMA磁盘控制器下达指令,让 DMA 控制器来处理数据的传送,数据传送完毕再把信息反馈给 CPU,从而减轻了 CPU 资源的占有率 。
传统读操作
当应用程序执行 read 系统调用读取一块数据的时候,如果这块数据已经存在于用户进程的页内存中,就直接从内存中读取数据 。
如果数据不存在,则先将数据从磁盘加载数据到内核空间的读缓存(read buffer)中,再从读缓存拷贝到用户进程的页内存中 。
  1. read(file_fd, tmp_buf, len);
基于传统的 I/O 读取方式,read 系统调用会触发 2 次上下文切换,1 次 DMA 拷贝和 1 次 CPU 拷贝 。
发起数据读取的流程如下:
  • 用户进程通过 read() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space) 。
  • CPU 利用 DMA 控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer) 。
  • CPU 将读缓冲区(read buffer)中的数据拷贝到用户空间(user space)的用户缓冲区(user buffer) 。
  • 上下文从内核态(kernel space)切换回用户态(user space),read 调用执行返回 。
传统写操作
当应用程序准备好数据,执行 write 系统调用发送网络数据时,先将数据从用户空间的页缓存拷贝到内核空间的网络缓冲区(socket buffer)中,然后再将写缓存中的数据拷贝到网卡设备完成数据发送 。
  1. write(socket_fd, tmp_buf, len);
基于传统的 I/O 写入方式,write() 系统调用会触发 2 次上下文切换,1 次 CPU 拷贝和 1 次 DMA 拷贝 。
用户程序发送网络数据的流程如下:
  • 用户进程通过 write() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space) 。
  • CPU 将用户缓冲区(user buffer)中的数据拷贝到内核空间(kernel space)的网络缓冲区(socket buffer) 。
  • CPU 利用 DMA 控制器将数据从网络缓冲区(socket buffer)拷贝到网卡进行数据传输 。
  • 上下文从内核态(kernel space)切换回用户态(user space),write 系统调用执行返回 。
零拷贝方式
在 Linux 中零拷贝技术主要有 3 个实现思路:
  • 用户态直接 I/O:应用程序可以直接访问硬件存储,操作系统内核只是辅助数据传输 。
这种方式依旧存在用户空间和内核空间的上下文切换,硬件上的数据直接拷贝至了用户空间,不经过内核空间 。因此,直接 I/O 不存在内核空间缓冲区和用户空间缓冲区之间的数据拷贝 。
  • 减少数据拷贝次数:在数据传输过程中,避免数据在用户空间缓冲区和系统内核空间缓冲区之间的 CPU 拷贝,以及数据在系统内核空间内的 CPU 拷贝,这也是当前主流零拷贝技术的实现思路 。
  • 写时复制技术:写时复制指的是当多个进程共享同一块数据时,如果其中一个进程需要对这份数据进行修改,那么将其拷贝到自己的进程地址空间中,如果只是数据读取操作则不需要进行拷贝操作 。
用户态直接 I/O
用户态直接 I/O 使得应用进程或运行在用户态(user space)下的库函数直接访问硬件设备 。
数据直接跨过内核进行传输,内核在数据传输过程除了进行必要的虚拟存储配置工作之外,不参与任何其他工作,这种方式能够直接绕过内核,极大提高了性能 。
 
 
用户态直接 I/O 只能适用于不需要内核缓冲区处理的应用程序,这些应用程序通常在进程地址空间有自己的数据缓存机制,称为自缓存应用程序,如数据库管理系统就是一个代表 。
其次,这种零拷贝机制会直接操作磁盘 I/O,由于 CPU 和磁盘 I/O 之间的执行时间差距,会造成大量资源的浪费,解决方案是配合异步 I/O 使用 。


推荐阅读