golang 中使用 writev (sendmsg) 系统调用来一次发送多块数据

发布时间 2023-10-27 17:59:27作者: ahfuzhang

作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢!


writev,或者说 sendmsg 等系统调用,能够发送多个数据块。从节约系统调用次数的角度说,这个 api 非常好。
下面演示如何在 golang 中使用 sendmsg 系统调用:

func sendmsg(conn net.Conn, buf1 []byte, buf2 []byte){
        // 我的应用里正好只有两块数据,所以偷懒了
        // 先获得 socket fd
		rawConn, err := sysconn.SyscallConn()
		if err != nil {
			log.Error(err.Error())
			return
		}
		var sockfd uintptr
		err = rawConn.Control(func(fd uintptr) {
			sockfd = fd
		})
		if err != nil {
			log.Error(err.Error())
			return
		}
        //
		//send msg
		var ivs [2]syscall.Iovec
		start := 0
		total := len(buf1) + len(buf2)
		var cnt uint64 = 2
		for start < total {
			if start < len(buf1) {
				ivs[0].Base = &buf1[start]
				ivs[0].Len = uint64(len(buf1) - start)
				ivs[1].Base = &buf2[0]
				ivs[1].Len = uint64(len(buf2))
			} else {
				cnt = 1
				ivs[0].Base = &buf2[start-len(buf1)]
				ivs[0].Len = uint64(len(buf1)+len(buf2)-start)
			}
			var msghdr = syscall.Msghdr{
				Iov:    &ivs[0],
				Iovlen: cnt,
			}
			var flags uintptr = 0x4000000  // 零拷贝标志
			r, _, e := syscall.RawSyscall(syscall.SYS_SENDMSG, sockfd, uintptr(unsafe.Pointer(&msghdr)), flags)
			if e != 0 {
				log.Error(syscall.Errno(e).Error())
				return
			}
			start += int(r)
		}
}

测试发现:

  1. 通过 sendmsg,减少了 conn.Write() 的调用次数,服务的性能明显上升;
  2. 当 buf1, buf2 对应的缓冲区,来自于 mmap() 调用时,零拷贝就生效了;
  3. 因为我测试的数据只有 2kb,所以使用零拷贝比不使用零拷贝更慢。(可能超过一定规模会更快)