导言
相信那些曾经使用 Go 写过 proxy server 的同学应该对 io.Copy()/io.CopyN()/io.CopyBuffer()/io.ReaderFrom
等接口和方法不陌生,它们是使用 Go 操作各类 I/O 进行数据传输经常需要使用到的 API,其中基于 TCP 协议的 socket 在使用上述接口和方法进行数据传输时利用到了 Linux 的零拷贝技术 sendfile
和 splice
。
我前段时间为 Go 语言内部的 Linux splice
零拷贝技术做了一点优化:为 splice
系统调用实现了一个 pipe pool,复用管道,减少频繁创建和销毁 pipe buffers 所带来的系统开销,理论上来说能够大幅提升 Go 的 io
标准库中基于 splice
零拷贝实现的 API 的性能。因此,我想从这个优化工作出发,分享一些我个人对多线程编程中的一些不成熟的优化思路。
因本人才疏学浅,故行文之间恐有纰漏,望诸君海涵,不吝赐教,若能予以斧正,则感激不尽。
splice
纵观 Linux 的零拷贝技术,相较于mmap
、sendfile
和 MSG_ZEROCOPY
等其他技术,splice
从使用成本、性能和适用范围等维度综合来看更适合在程序中作为一种通用的零拷贝方式。
splice()
系统调用函数定义如下:
#include <fcntl.h>
#include <unistd.h>
int pipe(int pipefd[2]);
int pipe2(int pipefd[2], int flags);
ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);
fd_in 和 fd_out 也是分别代表了输入端和输出端的文件描述符,这两个文件描述符必须有一个是指向管道设备的,这算是一个不太友好的限制。
off_in 和 off_out 则分别是 fd_in 和 fd_out 的偏移量指针,指示内核从哪里读取和写入数据,len 则指示了此次调用希望传输的字节数,最后的 flags 是系统调用的标记选项位掩码,用来设置系统调用的行为属性的,由以下 0 个或者多个值通过『或』操作组合而成:
- SPLICE_F_MOVE:指示
splice()
尝试仅仅是移动内存页面而不是复制,设置了这个值不代表就一定不会复制内存页面,复制还是移动取决于内核能否从管道中移动内存页面,或者管道中的内存页面是否是完整的;这个标记的初始实现有很多 bug,所以从 Linux 2.6.21 版本开始就已经无效了,但还是保留了下来,因为在未来的版本里可能会重新被实现。 - SPLICE_F_NONBLOCK:指示
splice()
不要阻塞 I/O,也就是使得splice()
调用成为一个非阻塞调用,可以用来实现异步数据传输,不过需要注意的是,数据传输的两个文件描述符也最好是预先通过 O_NONBLOCK 标记成非阻塞 I/O,不然splice()
调用还是有可能被阻塞。 - SPLICE_F_MORE:通知内核下一个
splice()
系统调用将会有更多的数据传输过来,这个标记对于输出端是 socket 的场景非常有用。
splice()
是基于 Linux 的管道缓冲区 (pipe buffer) 机制实现的,所以 splice()
的两个入参文件描述符才要求必须有一个是管道设备,一个典型的 splice()
用法是:
int pfd[2];
pipe(pfd);
ssize_t bytes = splice(file_fd, NULL, pfd[1], NULL, 4096, SPLICE_F_MOVE);
assert(bytes != -1);
bytes = splice(pfd[0], NULL, socket_fd, NULL, bytes, SPLICE_F_MOVE | SPLICE_F_MORE);
assert(bytes != -1);
数据传输过程图:
使用 splice()
完成一次磁盘文件到网卡的读写过程如下:
- 用户进程调用
pipe()
,从用户态陷入内核态,创建匿名单向管道,pipe()
返回,上下文从内核态切换回用户态; - 用户进程调用
splice()
,从用户态陷入内核态; - DMA 控制器将数据从硬盘拷贝到内核缓冲区,从管道的写入端"拷贝"进管道,
splice()
返回,上下文从内核态回到用户态; - 用户进程再次调用
splice()
,从用户态陷入内核态; - 内核把数据从管道的读取端"拷贝"到套接字缓冲区,DMA 控制器将数据从套接字缓冲区拷贝到网卡;
splice()
返回,上下文从内核态切换回用户态。
上面是 splice
的基本工作流程和原理,简单来说就是在数据传输过程中传递内存页指针而非实际数据来实现零拷贝。
pipe pool for splice
pipe pool in HAProxy
从上面对 splice
的介绍可知&