传统IO
基于传统的IO方式,底层实际上通过调用read()和write()来实现。通过read()把数据从硬盘读取到内核缓冲区,再复制到用户缓冲区;然后再通过write()写入到socket缓冲区,最后写入网卡设备。整个过程发生了4次用户态和内核态的上下文切换
和4次拷贝
,具体流程如下:
- 用户进程通过read()方法向操作系统发起调用,此时
上下文从用户态转向内核态
- DMA控制器把
数据从硬盘中拷贝到读缓冲区
- CPU把
读缓冲区数据拷贝到应用缓冲区
,上下文从内核态转为用户态
,read()返回 - 用户进程通过write()方法发起调用,
上下文从用户态转为内核态
- CPU将
应用缓冲区中数据拷贝到socket缓冲区
- DMA控制器把
数据从socket缓冲区拷贝到网卡
,上下文从内核态切换回用户态
,write()返回
几种常见的零拷贝技术
mmap+write
mmap主要实现方式是将读缓冲区的地址和用户缓冲区的地址进行映射,内核缓冲区和应用缓冲区共享
,从而减少了从读缓冲区到用户缓冲区的一次CPU拷贝。整个过程发生了4次用户态和内核态的上下文切换
和3次拷贝
,具体流程如下:
- 用户进程通过mmap()方法向操作系统发起调用,
上下文从用户态转向内核态
- DMA控制器把
数据从硬盘中拷贝到读缓冲区
上下文从内核态转为用户态
,mmap调用返回- 用户进程通过write()方法发起调用,
上下文从用户态转为内核态
CPU将读缓冲区中数据拷贝到socket缓冲区
- DMA控制器把
数据从socket缓冲区拷贝到网卡
,上下文从内核态切换回用户态
,write()返回
由于用户进程中的内存是虚拟的,只是映射到内核的读缓冲区,所以可以节省一半的内存空间,比较适合大文件的传输。
sendfile
sendfile是Linux2.1内核版本后引入的一个系统调用函数,通过使用sendfile数据可以直接在内核空间进行传输
,因此避免了用户空间和内核空间的拷贝,同时由于使用sendfile替代了read+write从而节省了一次系统调用,也就是2次上下文切换。整个过程发生了2次用户态和内核态的上下文切换
和3次拷贝
,具体流程如下:
- 用户进程通过sendfile()方法向操作系统发起调用,
上下文从用户态转向内核态
- DMA控制器把
数据从硬盘中拷贝到读缓冲区
- CPU将
读缓冲区中数据拷贝到socket缓冲区
- DMA控制器把
数据从socket缓冲区拷贝到网卡
,上下文从内核态切换回用户态
,sendfile调用返回
sendfile+DMA Scatter/Gather
DMA Scatter/Gather 分散/收集功能: 它将读缓冲区中的数据描述信息–内存地址和偏移量记录到socket缓冲区,由 DMA 根据这些将数据从读缓冲区拷贝到网卡,相比之前版本减少了一次CPU拷贝的过程。整个过程发生了2次用户态和内核态的上下文切换
和2次拷贝
,其中更重要的是完全没有CPU拷贝
,具体流程如下:
- 用户进程通过sendfile()方法向操作系统发起调用,
上下文从用户态转向内核态
- DMA控制器利用scatter把
数据从硬盘中拷贝到读缓冲区离散存储
- CPU把读缓冲区中的文件描述符和数据长度发送到socket缓冲区
- DMA控制器根据文件描述符和数据长度,使用scatter/gather把
数据从内核缓冲区拷贝到网卡
- sendfile()调用返回,
上下文从内核态切换回用户态
Ref
https://juejin.cn/post/6844903949359644680#heading-19
https://www.cnblogs.com/xiaolincoding/p/13719610.html