Go语言中的零拷贝优化,值得大家收藏

导言

相信那些曾经使用 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 的零拷贝技术,相较于mmapsendfile和 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() 完成一次磁盘文件到网卡的读写过程如下:

  1. 用户进程调用 pipe(),从用户态陷入内核态,创建匿名单向管道,pipe() 返回,上下文从内核态切换回用户态;
  2. 用户进程调用 splice(),从用户态陷入内核态;
  3. DMA 控制器将数据从硬盘拷贝到内核缓冲区,从管道的写入端"拷贝"进管道,splice() 返回,上下文从内核态回到用户态;
  4. 用户进程再次调用 splice(),从用户态陷入内核态;
  5. 内核把数据从管道的读取端"拷贝"到套接字缓冲区,DMA 控制器将数据从套接字缓冲区拷贝到网卡;
  6. splice() 返回,上下文从内核态切换回用户态。

上面是 splice 的基本工作流程和原理,简单来说就是在数据传输过程中传递内存页指针而非实际数据来实现零拷贝。

pipe pool for splice

pipe pool in HAProxy

从上面对 splice 的介绍可知&

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值