所以应用程序在通过网络收发数据时,其实都是在和 Socket 缓冲区打交道,具体的发送和接收任务都是由内核来做的 , 因为只有内核才能操作硬件设备 。用户空间的代码要想与硬件设备交互,必须通过系统调用或操作系统提供的其它接口,然后由内核代为执行 。
所以通过网络发送数据 , 会涉及一次数据的拷贝,以及用户空间和内核空间的切换 。因为 CPU 要将数据从用户空间搬运到内核空间的 Socket 缓冲区中 。
5) 内核要将 Socket 缓冲区里的数据通过网卡发送出去,于是再将数据从 Socket 缓冲区搬到网卡的缓冲区里面,而这一步搬运是由 DMA 来做的 。只要不涉及用户空间,大部分的数据搬运都可以由 DMA 来做,而一旦涉及到用户空间,数据搬运就必须由 CPU 来做 。
6) 发送完毕之后 , 再从内核空间切换到用户空间,应用程序继续干其它事情 。
如果想要提升性能 , 那么关键就在于减少上下文切换的次数和数据拷贝的次数,因为用户空间和内核空间的切换是需要成本的,至于数据拷贝就更不用说了 。
而整个过程涉及了 4 次的上下文切换,因为用户空间没有权限操作磁盘或网卡,这些操作都需要交由操作系统内核来完成 。而通过内核去完成某些任务的时候,需要使用操作系统提供的系统调用函数 。而一次系统调用必然会发生两次上下文切换:首先从用户态切换到内核态,当内核执行完任务后,再切换回用户态交由应用程序执行其它代码 。
然后是数据拷贝,这个数据也被拷贝了 4 次,其中两次拷贝由 DMA 负责,另外两次由 CPU 负责 。但很明显 , CPU 的两次拷贝没有太大必要 , 先将数据从 PageCache 拷贝到用户空间,然后再从用户空间拷贝到 Socket 缓冲区 。既然这样的话,那直接从 PageCache 拷贝到 Socket 缓冲区不行吗 。
如果文件在读取之后不对它进行操作,或者说不对文件数据进行加工,只是单纯地通过网卡发送出去,那么就没必要到用户空间这里绕一圈 。

文章插图
此时的 4 次上下文切换就变成了 2 次,因为系统调用只有 1 次 。数据搬运也由 4 次变成了 3 次,所以总共减少了两次上下文切换和一次数据拷贝 。
而这种减少数据拷贝(特别是在用户和内核之间的数据拷贝)的技术,便称之为零拷贝 。
Linux 内核提供了一个系统调用函数 sendfile(),便可以实现上面这个过程 。
#include <sys/sendfile.h>ssize_t sendfile(int out_fd, int in_fd,off_t *offset, size_t count);out_fd 和 in_fd 均为文件描述符,分别代表要写入的文件和要读取的文件,offset 表示从文件的哪个位置开始读 , count 表示写入多少个字节 。返回值是实际写入的长度 。当然像 Python/ target=_blank class=infotextkey>Python、JAVA 都对 sendfile 进行了封装,我们在使用 Python 进行 Socket 编程时,便可以使用该方法 。

文章插图
当然该方法会调用 os.sendfile(),它和 C 的 sendfile() 是一致的,如果是 Linux 系统 , 那么不存在问题 。如果是 windows 系统,os.sendfile() 则不可用,此时 Socket 的 sendfile 会退化为 send 方法 。
然而目前来说,虽然实现了零拷贝,但还不是零拷贝的终极形态 。我们看到 CPU 还是进行了一次拷贝,并且此时虽然不涉及用户空间 , 但数据搬运依旧是 CPU 来做的 。因为 DMA 主要负责硬件(例如磁盘或网卡)和内存的数据传输,但不适用于内存到内存的数据拷贝 。
那么问题来了 , 数据文件从磁盘读到 PageCache 之后,可不可以直接搬到网卡缓冲区里面呢?如果你的网卡支持 SG-DMA 技术,那么通过 CPU 将数据从 PageCache 拷贝到 socket 缓冲区这一步也可以省略 。
你可以通过以下命令,查看网卡是否支持 SG(scatter-gather)特性:
[root@satori ~]# ethtool -k eth0 | grep scatter-gatherscatter-gather: on tx-scatter-gather: on tx-scatter-gather-fraglist: off [fixed]Linux 内核从 2.4 版本开始起,对于那些支持 SG-DMA 技术的网卡,会进一步优化 sendfile() 系统调用的过程 , 优化后的过程如下:- DMA 将数据从磁盘拷贝到 PageCache;
- 将描述符和数据长度发送到 Socket 缓冲区,网卡的 SG-DMA 控制器基于该信息直接将 PageCache 的数据拷贝到网卡缓冲区中;
推荐阅读
- 八个提升编程体验的VS Code插件
- Python的集合模块,使用数据容器处理数据集合
- 基于Kubernetes网关API策略的流量管理
- 深入理解实践场景下的DNS隧道通信
- RabbitMQ发送和接收消息的几种方式
- 一文搞定双链表,让你彻底弄懂线性表的链式实现
- 不吃饭也要掌握的Synchronized锁升级过程
- Java中的泛型,看完这个还不会,我倒立洗头!
- Springboot之把外部依赖包纳入Spring容器管理的两种方式
- 探索Java的HTTP请求与响应处理机制
