之前听F老师说,手动修改代码后,本机 Qemu 热升级打开零拷贝支持后效果非常好。因此拿内核代码树里的测试代码试了试,
却发现几乎没啥区别:并且每次都报 zc=n
, 也就是并没有成功做零拷贝。
在F老师的帮助下,perf
粗粗地看了看,相关内核路径确实也跑到了。奇怪。前几天看 SNIA SDC EMEA 2022 视频的时候,里面提到打开零拷贝效果明显,忍不住又探究了一番。
MSG_ZEROCOPY 🔗
语义 🔗
MSG_ZEROCOPY只是一个提示(hint),内核会避免做拷贝并入队一个通知,但并不能保证一定会消除拷贝开销。重要的是,该参数实际上也改变了send()
系统调用的语义:
- 内核会锁定相关页面,通过共享内存的方式,把待发送的数据共享给网络栈;
- 因此发送过程中用户不能修改
buf
– 虽然此时send(fd, buf, ...)
可能已返回; buf
完成使用后,内核通过发送 zc completion notification 通知用户。
BUG 🔗
测试代码曾经有 BUG, 会导致代码其实没有去读取 completion 通知。字节的同学提交了一个补丁。
Typically, it will start the receiving process after around 30+ sendmsgs. However, as the introduction of commit dfa2f0483360 (“tcp: get rid of sysctl_tcp_adv_win_scale”), the sender is always writable and does not get any chance to run recv notifications. The selftest always exits with OUT_OF_MEMORY because the memory used by opt_skb exceeds the net.core.optmem_max.
我用的测试代码已经包含了及时读取 notification 的修复,但仍然报 zc=n
.
再探 🔗
没有免费午餐 🔗
再次仔细阅读文档, 得到如下信息:
- 网络设备需要支持 scatter-gather I/O;
- 收到 zc completion 不代表发送完成;
- 内核通过在
ee_code
中设置SO_EE_CODE_ZEROCOPY_COPIED
告知用户当前设备无法使用零拷贝,后续不必设置MSG_ZEROCOPY
.
无法使用零拷贝却又做了相关工作,还得回退到拷贝模式,反而会有性能损耗。
$ ethtool -k bond1 | grep scat
scatter-gather: on
tx-scatter-gather: on
tx-scatter-gather-fraglist: off [requested on]
zc=n
的原因 🔗
还是在 StackOverflow 找到了答案:
After tracing kernel stack, i found that skb_copy_ubufs lead to the result, which was called by dev_queue_xmit_nit. It means that MSG_ZEROCOPY notification will return SO_EE_CODE_ZEROCOPY_COPIED if there are network taps in use. For my case, they are dhclient and lldpd.service. After killing them, the code disappeared.
默认使用最大 payload 进行测试,在一个相对空闲的环境进行测试,成功使用零拷贝时得出的数据比较有波动,并且不具有性能优势。
# 不使用零拷贝
$ time ./msg_zerocopy -C 39 -4 tcp -D 10.10.3.4
tx=129909 (8106 MB) txc=0 zc=n
real 0m4.201s
user 0m0.049s
sys 0m1.988s
# 设置使用零拷贝,实际未能成功零拷贝
$ time ./msg_zerocopy -C 39 -4 tcp -D 10.10.3.4 -z
tx=90078 (5621 MB) txc=90078 zc=n
real 0m4.242s
user 0m0.022s
sys 0m2.211s
# 关掉 lldpd 设置使用零拷贝,实际成功进行了零拷贝
$ time ./msg_zerocopy -C 39 -4 tcp -D 10.10.3.4 -z
tx=108938 (6798 MB) txc=108938 zc=y
real 0m4.201s
user 0m0.023s
sys 0m1.108s
$ time ./msg_zerocopy -C 39 -4 tcp -D 10.10.3.4 -z
tx=70444 (4395 MB) txc=70444 zc=y
real 0m4.202s
user 0m0.018s
sys 0m0.841s
用 bpftrace
跟踪了一下,打开零拷贝,由于 lldpd 服务而未能进行零拷贝时,内核代码会走 skb_copy_ubufs()
. 成功进行零拷贝不会进入该函数。
$ bpftrace -e 'kretfunc:skb_copy_ubufs{printf("%s\n", kstack); exit()}'
Attaching 1 probe...
exit_misc_binfmt+53365
bpf_get_stackid_raw_tp+82
exit_misc_binfmt+53365
exit_misc_binfmt+60989
skb_copy_ubufs+5
dev_queue_xmit_nit+451
dev_hard_start_xmit+106
__dev_queue_xmit+2060
ip_finish_output2+621
ip_output+112
__ip_queue_xmit+365
__tcp_transmit_skb+2561
tcp_write_xmit+1085
tcp_sendmsg_locked+677
tcp_sendmsg+39
sock_sendmsg+66
____sys_sendmsg+495
___sys_sendmsg+124
__sys_sendmsg+87
do_syscall_64+91
entry_SYSCALL_64_after_hwframe+97
测试环境:kernel-4.18.0. Stack Overflow 的回答中用的 5.4.0