网络框架netpoll中的SO_ZEROCOPY

作者 : admin 本文共2334个字,预计阅读时间需要6分钟 发布时间: 2024-06-16 共1人阅读

背景了解

https://www.163.com/dy/article/FS6AS7SS0518R7MO.html
https://docs.kernel.org/networking/msg_zerocopy.html

send() with MSG_ZEROCOPY

kernel v4.14 版本接受了来自 Google 工程师 Willem de Bruijn 在 TCP 网络报文的通用发送接口 send() 中实现的 zero-copy 功能
用户进程就能够把用户缓冲区的数据通过零拷贝的方式经过内核空间发送到网络套接字中去

因为前面几种零拷贝技术都是要求用户进程不能处理加工数据而是直接转发到目标文件描述符中去
Willem de Bruijn 在他的论文里给出的压测数据:
采用 netperf 大包发送测试,性能提升 39%,而线上环境的数据发送性能则提升了 5%~8%
官方文档陈述说这个特性通常只在发送 10KB 左右大包的场景下才会有显著的性能提升
一开始这个特性只支持 TCP,到内核 v5.0 版本之后才支持 UDP

这个功能的使用模式如下:
if (setsockopt(socket_fd, SOL_SOCKET, SO_ZEROCOPY, &one, sizeof(one)))
error(1, errno, “setsockopt zerocopy”);
ret = send(socket_fd, buffer, sizeof(buffer), MSG_ZEROCOPY);
// https://docs.kernel.org/networking/msg_zerocopy.html

第一步,先给要发送数据的 socket 设置一个 SOCK_ZEROCOPY option
第二步,在调用 send() 发送数据时再设置一个 MSG_ZEROCOPY option
其实理论上来说只需要调用 setsockopt() 或者 send() 时传递这个 zero-copy 的 option 即可
两者选其一,但是这里却要设置同一个 option 两次
官方的说法是为了兼容 send() API 以前的设计上的一个错误:send() 以前的实现会忽略掉未知的 option
为了兼容那些可能已经不小心设置了 MSG_ZEROCOPY option 的程序,设计成两步设置

猜测其他可能:
给使用者提供更灵活的使用模式,因为这个新功能只在大包场景下才可能会有显著的性能提升
现实场景很复杂:不仅仅是全部大包或者全部小包的场景,有可能是大包小包混合的场景
因此使用者可以先调用 setsockopt() 设置 SOCK_ZEROCOPY option
然后再根据实际业务场景中的网络包尺寸选择是否要在调用 send() 时使用 MSG_ZEROCOPY 进行 zero-copy 传输

注意:
send() 可能是异步发送数据,因此使用 MSG_ZEROCOPY 时,调用 send() 之后不能立刻重用或释放 buffer
因为 buffer 中的数据不一定已经被内核读走了,所以还需要从 socket 关联的队列里读取一下通知消息
看看 buffer 中的数据是否已经被内核读走

底层原理
这个技术是基于 redhat 红帽在 2010 年给 Linux 内核提交的 virtio-net zero-copy 技术之上实现的
通过 send() 把数据在用户缓冲区中的分段指针发送到 socket 中去
利用 page pinning 锁住用户缓冲区的内存页
然后利用 DMA 直接在用户缓冲区通过内存地址指针进行数据读取,实现零拷贝
具体的细节可以通过阅读 Willem de Bruijn 的论文 (PDF)

主要缺陷:
(1) 只适用于大文件 (10KB 左右) 的场景,小文件场景因为 page pinning 页锁定和等待缓冲区释放的通知消息这些机制
甚至可能比直接 CPU 拷贝更耗时
(2) 因为可能异步发送数据,需要额外调用 poll() 和 recvmsg() 系统调用等待 buffer 被释放的通知消息,增加代码复杂度
以及会导致多次用户态和内核态的上下文切换;
(3) MSG_ZEROCOPY 目前只支持发送端,接收端暂不支持

绕过内核的直接 I/O
前面种种的 zero-copy 的方法,都是在想方设法地优化减少或者去掉用户态和内核态之间以及内核态和内核态之间的数据拷贝
为了实现避免这些拷贝可谓是八仙过海,各显神通,采用了各种各样的手段
那如果换个思路:这么费劲地去消除这些拷贝不就是因为有内核在掺和吗?

netpoll中的实现

初始化时的fd option设置

const (
	SO_ZEROCOPY       = 60
	SO_ZEROBLOCKTIMEO = 69
	MSG_ZEROCOPY      = 0x4000000
)

func setZeroCopy(fd int) error {
   
	return syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, SO_ZEROCOPY, 1)
}

func setBlockZeroCopySend(fd int, sec, usec int64) error {
   
	return syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, SO_ZEROBLOCKTIMEO, &syscall.Timeval{
   
		Sec:  sec,
		Usec: usec,
	})
}

func (c *connection) init(conn Conn, opts *options) (err error) {
   
	... ...
	if setZeroCopy(c.fd) == nil && setBlockZeroCopySend(c.fd, defaultZeroCopyTimeoutSec, 0) == nil {
   
		c.supportZeroCopy = true
	}
	... ...
}

poll wait中的使用

仅关注 triggerWrite

// Wait implements Poll.
func (p 
本站无任何商业行为
个人在线分享 » 网络框架netpoll中的SO_ZEROCOPY
E-->