记一次 splice 导致 io.Copy 阻塞的排查过程

记一次 splice 导致 io.Copy 阻塞的排查过程

简而言之,net.TCPConn 的 ReadFrom 零拷贝实现 splice1.21.0 - 1.21.4 删除了 SPLICE_F_NONBLOCK 参数,导致在 CentOS7.2(内核版本 3.10.0) 上 splice 被阻塞。

相关的 issuehttps://github.com/golang/go/issues/59041

这个问题在 1.21.5 中被修复,commithttps://github.com/yunginnanet/go/commit/35afad885d5e046a4a14643b5b530b128ca953de

背景

由于环境的问题,需要有一个 TCP 的代理,之前一直用 ncat -vl 10022 -k -c 'ncat -nv 127.0.0.1 22' 方式将 10022 端口的流量代理至 127.0.0.1:22,但是 ncat 是一个连接一个进程,如果要做短连接压测的,代理会成为瓶颈。

所以决定换个代理的软件,因为 Go 写一个代理特别简单,十行代码就能实现一个性能不错的服务,那就直接自己写一个。

go 复制代码
package main

import (
	"flag"
	"fmt"
	"io"
	"net"
	"sync"

	"github.com/sirupsen/logrus"
)

func main() {
	f := flag.String("from", "", "source addr")
	t := flag.String("to", "", "dest addr")
	flag.Parse()

	if *f == "" || *t == "" {
		fmt.Println("Invalid from/to address")
		return
	}
	logrus.WithFields(logrus.Fields{"from": *f, "to": *t}).Info("Setup proxy server")

	lis, err := net.Listen("tcp", *f)
	if err != nil {
		panic(err)
	}
	logrus.WithField("addr", lis.Addr()).Info("Listen on")

	for {
		conn, err := lis.Accept()
		if err != nil {
			panic(err)
		}
		go handleConn(conn, *t)
	}
}

func handleConn(uConn net.Conn, to string) {
	logrus.WithField("addr", uConn.RemoteAddr()).Info("New conn")
	defer uConn.Close()

	rConn, err := net.Dial("tcp", to)
	if err != nil {
		logrus.WithError(err).Error("Fail to net.DialTCP")
		return
	}
	logrus.WithField("local", rConn.LocalAddr()).Info("Start proxy conn")

	wg := sync.WaitGroup{}
	wg.Add(2)
	go func() {
		defer wg.Done()
		io.Copy(uConn, rConn)
		rConn.Close()
		uConn.Close()
	}()
	go func() {
		defer wg.Done()
		io.Copy(rConn, uConn)
		uConn.Close()
		rConn.Close()
	}()
	wg.Wait()
}

编译操作系统为 Debian12,Go 版本为 1.21.1

因为默认路由的原因,我把这个服务部署在了一个 CentOS7.2 的虚拟机里面,压测发现QPS总是上不去。

用 tcpdump 抓包定位到是这边的代理程序有问题,流量没有被正确的进行转发。

为避免出现敏感数据,用下面的图来做模拟,在 A 使用 scpB 发送文件,中间经过了个我们写的服务 PROXY

shell 复制代码
+----------------+      +-------------------------------+
|  (A) Debian12  |      |  (B) CentOS7.2                |
|                | <--> | 192.168.32.251:10022          |
| 192.168.32.251 |      |        └─> PROXY              |
|                |      |              └─> 127.0.0.1:22 |
+----------------+      +-------------------------------+

# 生成一个大的文件
#   dd if=/dev/zero of=/tmp/1.txt bs=1M count=1024
# 使用命令模拟压测
#   scp -P 10022 /tmp/1.txt root@192.168.32.245:/tmp/

排查

ps 看到这个进程还在运行,所以不是进程退出导致的。

top 观察进程 CPU 占用也不高,所以不是代码写出死循环来了。

由于程序没有加日志,通过 strace -p $(pidof PROXY) 来分析一下当前哪些系统调用在执行,看起来是 epoll_pwait 没有就绪事件返回。

shell 复制代码
[pid 26877] splice(14, NULL, 18, NULL, 1048576, 0) = -1 EAGAIN (Resource temporarily unavailable)
[pid 26790] epoll_pwait(5,  <unfinished ...>
[pid 26788] nanosleep({tv_sec=0, tv_nsec=20000},  <unfinished ...>
[pid 26790] <... epoll_pwait resumed>[], 128, 0, NULL, 0) = 0
[pid 26877] epoll_pwait(5,  <unfinished ...>
[pid 26790] epoll_pwait(5,  <unfinished ...>
[pid 26877] <... epoll_pwait resumed>[], 128, 0, NULL, 0) = 0
[pid 26877] futex(0xc000040d48, FUTEX_WAIT_PRIVATE, 0, NULL <unfinished ...>
[pid 26788] <... nanosleep resumed>NULL) = 0
[pid 26788] futex(0x5ea8a0, FUTEX_WAIT_PRIVATE, 0, {tv_sec=60, tv_nsec=0} <unfinished ...>
[pid 26790] <... epoll_pwait resumed>[{EPOLLIN|EPOLLOUT, {u32=2345140225, u64=9221451948300435457}}], 128, -1, NULL, 0) = 1
[pid 26790] epoll_pwait(5, [], 128, 0, NULL, 0) = 0
[pid 26790] epoll_pwait(5, [{EPOLLIN|EPOLLOUT, {u32=2345140225, u64=9221451948300435457}}], 128, -1, NULL, 0) = 1
         多条 epoll_pwait 省略

看看连接缓冲区里面有没有数据 netstat -ntp | grep 10022,在接受缓冲区内还有 1666120 个字节的数据没有被读出

shell 复制代码
tcp6  1666120      0 192.168.32.245:10022    192.168.32.251:49440    ESTABLISHED 26787/PROXY

当时想着看看重启能不能复现,在重启之前先 kill -3 把堆栈打印出来,拿到了一个关键的栈信息。

go 复制代码
goroutine 19 [syscall]:
syscall.Syscall6(0x7ff92db08be8?, 0xc000068c88?, 0x45fca5?, 0xc000068c98?, 0x48ed3c?, 0xc000068cb0?, 0x48eea7?)
        /usr/local/go1.21/src/syscall/syscall_linux.go:91 +0x30 fp=0xc000068c60 sp=0xc000068bd8 pc=0x481b50
syscall.Splice(0xc000102000?, 0xc000068d08?, 0x0?, 0x4e70c0?, 0x4e70c0?, 0xc000068d20?)
        /usr/local/go1.21/src/syscall/zsyscall_linux_amd64.go:1356 +0x45 fp=0xc000068cc0 sp=0xc000068c60 pc=0x480d05
internal/poll.splice(...)
        /usr/local/go1.21/src/internal/poll/splice_linux.go:155
internal/poll.spliceDrain(0xc000102100?, 0xc000102000, 0x5a800?)
        /usr/local/go1.21/src/internal/poll/splice_linux.go:92 +0x185 fp=0xc000068d68 sp=0xc000068cc0 pc=0x4917c5
internal/poll.Splice(0x0?, 0x0?, 0x7fffffffffffffff)
        /usr/local/go1.21/src/internal/poll/splice_linux.go:42 +0x173 fp=0xc000068e00 sp=0xc000068d68 pc=0x491413
net.splice(0x0?, {0x53bca8?, 0xc000106000?})
        /usr/local/go1.21/src/net/splice_linux.go:39 +0xdf fp=0xc000068e60 sp=0xc000068e00 pc=0x4cc29f
net.(*TCPConn).readFrom(0xc000106008, {0x53bca8, 0xc000106000})
        /usr/local/go1.21/src/net/tcpsock_posix.go:48 +0x28 fp=0xc000068e90 sp=0xc000068e60 pc=0x4cd0c8
net.(*TCPConn).ReadFrom(0xc000106008, {0x53bca8?, 0xc000106000?})
        /usr/local/go1.21/src/net/tcpsock.go:130 +0x30 fp=0xc000068ed0 sp=0xc000068e90 pc=0x4cc770
io.copyBuffer({0x53bd68, 0xc000106008}, {0x53bca8, 0xc000106000}, {0x0, 0x0, 0x0})
        /usr/local/go1.21/src/io/io.go:416 +0x147 fp=0xc000068f50 sp=0xc000068ed0 pc=0x47d587
io.Copy(...)
        /usr/local/go1.21/src/io/io.go:389
main.handleConn.func2()
        /home/devel/demo/app/demo/main.go:73 +0xb2 fp=0xc000068fe0 sp=0xc000068f50 pc=0x4db672
runtime.goexit()
        /usr/local/go1.21/src/runtime/asm_amd64.s:1650 +0x1 fp=0xc000068fe8 sp=0xc000068fe0 pc=0x464641
created by main.handleConn in goroutine 17
        /home/devel/demo/app/demo/main.go:71 +0x368

分析看到在 io.Copy 这条路线有问题,先看看 io.Copy 的源码

分析 io.Copy

io.Copy 内部有这么一段代码,优先于 read/write 调用,上面的堆栈打印看起来也是这个 ReadFrom 里面有问题。

go 复制代码
if wt, ok := src.(WriterTo); ok {
	return wt.WriteTo(dst)
}
// Similarly, if the writer has a ReadFrom method, use it to do the copy.
if rt, ok := dst.(ReaderFrom); ok {
	return rt.ReadFrom(src)
}

OK 先跳过这个 ReadFrom 看看能不能行呢,于是把 io.Copy 里面的 WriteTo/ReadFrom 注释,并且直接放到外面来,使用一般的 read/write 调用。

编译运行,可行!!!

那么问题就只能在这个 ReadFrom 里面了,照着上面的堆栈,一路追到了 poll.Splice 内,但是之前没有用过 splice 这个函数,只知道是一个零拷贝相关的函数。好吧,Go 在这里还做了一些优化。

那看来还是得研究一下,这个 splice 系统调用。

分析 poll.Splice

在这之前先搜索了一些文档看了一下,这个 splice文档写的相当好,很快就能够理解。

文章里面的的这张图清晰的描述了两次 splice 就能通过 pipe 在内核就将数据发送出去,没有把数据从内核空间拷贝至用户空间。

为了减少语言的干扰,使用 C 照着 poll.Splice 重写了一遍,代码如下。在 splice_readfrom 内部,每次循环调用两次 splice,一次将源 sockfd 的数据放至 pipe 中,一次将 pipe 中的数据写入目的 sockfd 中。

cpp 复制代码
#define _GNU_SOURCE 1
#include <arpa/inet.h>
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <netinet/in.h>
#include <poll.h>
#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include <thread>

static ssize_t splice_drain(int fd, int pipefd, size_t max) {
  while (1) {
    ssize_t n = splice(fd, NULL, pipefd, NULL, max, 0);
    if (n >= 0)
      return n;

    // error handle
    if (errno == EINTR)
      continue;
    else if (errno != EAGAIN)
      return -1;
  }
}

static ssize_t splice_pump(int pipefd, int fd, size_t in_pipe) {
  ssize_t written = 0;
  while (in_pipe > 0) {
    ssize_t n = splice(pipefd, NULL, fd, NULL, in_pipe, 0);
    if (n >= 0) {
      in_pipe -= n;
      written += n;
      continue;
    }

    if (errno != EAGAIN)
      return -1;
  }
  return written;
}

static const size_t kMaxSpliceSize = 1 << 20;

ssize_t splice_readfrom(int dstfd, int srcfd) {
  int pipefd[2];
  if (pipe2(pipefd, 0) < 0)
    return -1;

  ssize_t written = 0;
  ssize_t remain = INT64_MAX;
  while (remain > 0) {
    size_t max = kMaxSpliceSize;
    if (max > (size_t)remain)
      max = remain;

    ssize_t in_pipe = splice_drain(srcfd, pipefd[1], max);
    if (in_pipe < 0)
      return -1;
    else if (in_pipe == 0)
      break;

    ssize_t n = splice_pump(pipefd[0], dstfd, in_pipe);
    if (n > 0) {
      remain -= n;
      written += n;
    }
  }
  close(pipefd[0]);
  close(pipefd[1]);
  return written;
}

int main(int argc, char **argv) {
  int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  if (sockfd < 0) {
    perror("Fail to socket");
    return -1;
  }

  int opt = 1;
  setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const void *)&opt, sizeof(opt));

  fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFL, NULL) | O_NONBLOCK);

  struct sockaddr_in addr;
  addr.sin_family = AF_INET;
  addr.sin_port = htons(10022);
  addr.sin_addr.s_addr = htonl(INADDR_ANY);

  if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
    perror("Fail to bind");
    return -1;
  }

  if (listen(sockfd, 10) < 0) {
    perror("Fail to listen");
    return -1;
  }
  printf("listen on\n");

  int timeout = 3000;
  struct pollfd fds = {sockfd};
  fds.events |= POLLIN;

  while (1) {
    int ret = poll(&fds, 1, timeout);
    if (ret > 0) {
      struct sockaddr_in in;
      socklen_t len = sizeof(in);
      int connfd = accept(sockfd, (struct sockaddr *)&in, &len);
      if (connfd < 0) {
        perror("Fail to accept");
        return -1;
      }

      fcntl(connfd, F_SETFL, fcntl(connfd, F_GETFL, NULL) | O_NONBLOCK);

      std::thread t(
          [](struct sockaddr_in addr, int u_connfd) {
            int sockfd = socket(AF_INET, SOCK_STREAM, 0);
            if (sockfd < 0) {
              perror("Fail to socket");
              return;
            }

            struct sockaddr_in dst_addr;
            dst_addr.sin_family = AF_INET;
            dst_addr.sin_port = htons(10022);
            dst_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);

            if (connect(sockfd, (struct sockaddr *)&dst_addr,
                        sizeof(dst_addr)) < 0) {
              perror("Fail to connect");
              return;
            }

            char dst_txt[INET_ADDRSTRLEN];
            char src_txt[INET_ADDRSTRLEN];

            inet_ntop(AF_INET, &addr.sin_addr, src_txt, sizeof(src_txt));
            printf("New conn from %s:%d\n", src_txt, ntohs(addr.sin_port));

            std::thread t1([&]() { splice_readfrom(sockfd, u_connfd); });
            std::thread t2([&]() { splice_readfrom(u_connfd, sockfd); });
            t1.join();
            t2.join();

            close(sockfd);
            close(u_connfd);
          },
          in, connfd);
      t.detach();
    }
  }

  return 0;
}

测试下来,和 Go 版本表现一样,也是被阻塞,不过现在问题就更清晰一些,splice 的使用有问题。

于是仔细看了一下文档,里面有一个参数 SPLICE_F_NONBLOCK,要不加上试一下看看,加上之后程序是正常运行的。

所以会是这个参数的问题?在 Go 的实现里面,spliceflags 参数是为 0 的,也就是意味着是没有设置为非阻塞状态的。

想到我们之前的代理程序都没有出现这个情况,难道是 Go 版本的原因?于是使用 Go1.18 对 PROXY 进行编译运行,正常运行!

看了两个版本的实现,果然 Go1.18 是含有这个 SPLICE_F_NONBLOCK 参数的,在之后的版本内被删除了;继续搜索,发现了有人提了个上面的 issue。

对代码追踪发现 受影响版本为 1.21.0 - 1.21.4

扩展分析

issue 里面 Go 的开发者说所有的 case 都正常能跑过,所以把这个参数删除了。既然开发者测试没有问题,但是实际使用又有问题,那就有可能是环境不一致导致的。

分析未在不同内核上splice表现不一致

在上面的排查过程中,我还把 PROXY (Go1.21.1) 放到 A 中运行,代理至 192.168.32.245:22 上,表现也是正常的。经过测试,io.Copy 在不同的系统上的影响如下:

Kernel\Go 1.18.0 1.21.1
3.10 正常 不正常
6.1 正常 正常

1.18 是没有 BUG 的版本,也就是增加了 SPLICE_F_NONBLOCK 参数。那为何 1.21.1 版本没有增加这个参数的可以在 6.1 的内核上运行呢。

没有很好的头绪,难道是 pipe 导致的问题吗,pipe 太小了?于是调整 pipe 大小

c 复制代码
fcntl(pipefd[0], F_SETPIPE_SZ, 1 << 20);
fcntl(pipefd[1], F_SETPIPE_SZ, 1 << 20);

使用 Go1.21.1 版本进行编译,并且进行测试,结果如下:

Kernel\Go 1.21.1
3.10 正常
6.1 正常

pipe 太小,那测试数据小于默认大小 65536 的看看会不会有问题

shell 复制代码
dd if=/dev/zero of=/tmp/1.txt bs=1 count=65536

测试结果如下:

测试数据大小 测试结果
65536 不正常
32768 不正常
25000 不正常
16384 正常

splice 还有一个参数 len ,为从 fd_in 到 pipe_w 中的字节数,如果我减少这个大小,那么结果会如何。测试下来 和调整 pipe 大小带来的结果相同

splice 在不同内核上表现的结果不同这个问题,可以缩小一些排查的范围了:和 pipe 相关

不同内核的 splice 实现

看代码之前确认要关注的点:在哪里存在阻塞的动作

splice 实现位于 fs/splice.c 中,下面的代码取自 kernel-6.1(3.10 的内核代码也相似,主体逻辑没有变化)

c 复制代码
SYSCALL_DEFINE6(splice, ...) // fs/splice.c
  -> __do_splice
    -> do_splice
      -> splice_file_to_pipe  // 将 sockfd 的数据传输至 pipe 中,走这条路径
        -> do_splice_to
          -> tcp_splice_read (in->f_op->splice_read) // net/ipv4/tcp.c
            -> __tcp_splice_read
              -> tcp_read_sock
                -> tcp_splice_data_recv
                  -> skb_splice_bits                 // net/core/ipv4/skbuff.c
                    -> splice_to_pipe                // fs/splice.c

经过 TCP 的读取,兜兜转转又回到 fs/splice.c 中。

kernel-6.1 的实现

在 kernel-6.1 的实现中,spclie_to_pipe 的实现没有阻塞

c 复制代码
ssize_t splice_to_pipe(struct pipe_inode_info *pipe,
		       struct splice_pipe_desc *spd)
{
  // ....
	while (!pipe_full(head, tail, pipe->max_usage)) {
		struct pipe_buffer *buf = &pipe->bufs[head & mask];

		buf->page = spd->pages[page_nr];
		buf->offset = spd->partial[page_nr].offset;
		buf->len = spd->partial[page_nr].len;
		buf->private = spd->partial[page_nr].private;
		buf->ops = spd->ops;
		buf->flags = 0;

		head++;
		pipe->head = head;
		page_nr++;
		ret += buf->len;

		if (!--spd->nr_pages)
			break;
	}
	if (!ret)
		ret = -EAGAIN;

out:
	while (page_nr < spd_pages)
		spd->spd_release(spd, page_nr++);

	return ret;
}

向上回溯,在 splice_file_to_pipe 中,wait_for_space 中如果 pipe 满了则进行等待 pipe_wait_writable(pipe)

c 复制代码
long splice_file_to_pipe(struct file *in,
			 struct pipe_inode_info *opipe,
			 loff_t *offset,
			 size_t len, unsigned int flags)
{
	long ret;

	pipe_lock(opipe);
	ret = wait_for_space(opipe, flags);
	if (!ret)
		ret = do_splice_to(in, offset, opipe, len, flags);
	pipe_unlock(opipe);
	if (ret > 0)
		wakeup_pipe_readers(opipe);
	return ret;
}

static int wait_for_space(struct pipe_inode_info *pipe, unsigned flags)
{
	for (;;) {
		if (unlikely(!pipe->readers)) {
			send_sig(SIGPIPE, current, 0);
			return -EPIPE;
		}
		if (!pipe_full(pipe->head, pipe->tail, pipe->max_usage))
			return 0;
		if (flags & SPLICE_F_NONBLOCK)
			return -EAGAIN;
		if (signal_pending(current))
			return -ERESTARTSYS;
		pipe_wait_writable(pipe);
	}
}

调整测试代码,对 pipe 只生产而不不消费数据。

c 复制代码
ssize_t splice_readfrom(int dstfd, int srcfd) {
    ...
    ssize_t in_pipe = splice_drain(srcfd, pipefd[1], max);

    sleep(1);
    written += in_pipe;
    printf("+%ld written=%ld\n", in_pipe, written);
    continue;

    ssize_t n = splice_pump(pipefd[0], dstfd, in_pipe);
}

为避免 ssh 的元数据干扰,不再使用 sshd 127.0.0.1:22 作为最后点,转而写了一个 io.Discard 的 Go 服务。

测试客户端为 ncat -nv 192.168.32.251 10022 < /tmp/1.txt

在 Debian12(kernel-6.1) 上进行测试,结果如下

c 复制代码
+65509 written=65509
+57344 written=122853
+49152 written=172005
+36864 written=208869
+28672 written=237541
+20480 written=258021
+16384 written=274405
+8192 written=282597
+4096 written=286693
// 之后阻塞

pipe 的大小为 65536(PAGE_SIZE * 16),但是写入的数据大于了 pipe 的缓冲区后,还能够继续写入,这点和可能和 skbuff/pipe 的 PAGE 有关,这里先跳过,直接测试一下在 CentOS7.2 上表现如何,结果直接阻塞,第一个 splice 都没有返回,好吧看看代码。

kernel-3.10 的实现

同样找到关键的 splice_to_pipe 函数

c 复制代码
ssize_t splice_to_pipe(struct pipe_inode_info *pipe, struct splice_pipe_desc *spd)
{
  // ...
	for (;;) {
		if (pipe->nrbufs < pipe->buffers) {
			int newbuf = (pipe->curbuf + pipe->nrbufs) & (pipe->buffers - 1);
			struct pipe_buffer *buf = pipe->bufs + newbuf;

			buf->page = spd->pages[page_nr];
			buf->offset = spd->partial[page_nr].offset;
			buf->len = spd->partial[page_nr].len;
			buf->private = spd->partial[page_nr].private;
			buf->ops = spd->ops;
			if (spd->flags & SPLICE_F_GIFT)
				buf->flags |= PIPE_BUF_FLAG_GIFT;

			pipe->nrbufs++;
			page_nr++;
			ret += buf->len;

			if (!--spd->nr_pages)
				break;
			if (pipe->nrbufs < pipe->buffers)
				continue;

			break;
		}

		if (spd->flags & SPLICE_F_NONBLOCK) {
			if (!ret)
				ret = -EAGAIN;
			break;
		}

		pipe->waiting_writers++;
		pipe_wait(pipe);
		pipe->waiting_writers--;
	}
	return ret;
}

代码删除了和信号相关的逻辑,整个循环内的关键路径

  • if (!--spd->nr_pages) 为数据页都被挂在 pipe 后退出循环
  • if (pipe->nrbufs < pipe->buffers) 为 pipe 中还有空间则继续运行
  • if (spd->flags & SPLICE_F_NONBLOCK) 为 pipe 没有空间但是设置了非阻塞,则直接返回
  • pipe_wait 为数据没有读完,但是 pipe 已经没有空间则直接被挂起

在上面分析未在不同内核上splice表现不一致的结果中,可以看到 16K 的数据是能够返回的,数据的大小大一些就被阻塞了。

对比分析

kernel-6.1 对 splice 的实现相较 kernel-3.10 做了关键的两点变化:

  1. 提前做了 pipe 的空判断,这样数据挂载函数 splice_to_pipe 内部就不用进行阻塞了,而 3.10 将空判断和数据的转移放在一起做了

    c 复制代码
    static int wait_for_space(struct pipe_inode_info *pipe, unsigned flags)
    {
    	for (;;) {
    		if (unlikely(!pipe->readers)) {
    			send_sig(SIGPIPE, current, 0);
    			return -EPIPE;
    		}
    		if (!pipe_full(pipe->head, pipe->tail, pipe->max_usage))
    			return 0;
    		if (flags & SPLICE_F_NONBLOCK)
    			return -EAGAIN;
    		if (signal_pending(current))
    			return -ERESTARTSYS;
    		pipe_wait_writable(pipe);
    	}
    }
  2. 限制了单次 splice 读取的大小

    c 复制代码
    static long do_splice_to(struct file *in, loff_t *ppos,
    			 struct pipe_inode_info *pipe, size_t len,
    			 unsigned int flags)
    {
    	/* Don't try to read more the pipe has space for. */
    	p_space = pipe->max_usage - pipe_occupancy(pipe->head, pipe->tail);
    	len = min_t(size_t, len, p_space << PAGE_SHIFT);
    
    	return in->f_op->splice_read(in, ppos, pipe, len, flags);
    }

结合代码和测试程序进行分析一下
kernel-3.10 里面的实现可能在 splice_to_pipe 中就被阻塞了,pipe 可容纳的空间小于 skbuff 中的数据
kernel-6.1 由于每次都会判断是否为空,只向 pipe 中写入可容纳的数据,所以只要有空间就不会被阻塞。

那么就遗留另外一个问题,pipe 的可容纳大小在不同版本内核上的不一样,和文档里面的 65536 都有一些明显出入,但是测试 pipe 的 write,则是准确的 65536. 据查资料得到的结论,fd -> pipe -> fd 这个过程只是 skbuff 的 PAGE 变化,内核不会再进行额外的内存分配。

上面的分析还需要通过调试来进行证明,那可以再写一篇文章通过kprobe分析 splice 了,这里再挖一个坑。

结论

这个问题只在低版本的内核上有问题,在高版本 Debian12 是正常的,在 Go1.21.5 中已经修复,建议使用 Go1.21.5 及以上的版本。

TODO

通过 kprobe 来分析 splice 下的 pipe 空间变化

参考

相关推荐
AndyFrank22 分钟前
mac crontab 不能使用问题简记
linux·运维·macos
筱源源38 分钟前
Kafka-linux环境部署
linux·kafka
算法与编程之美1 小时前
文件的写入与读取
linux·运维·服务器
xianwu5432 小时前
反向代理模块
linux·开发语言·网络·git
Amelio_Ming2 小时前
Permissions 0755 for ‘/etc/ssh/ssh_host_rsa_key‘ are too open.问题解决
linux·运维·ssh
Ven%3 小时前
centos查看硬盘资源使用情况命令大全
linux·运维·centos
TeYiToKu3 小时前
笔记整理—linux驱动开发部分(9)framebuffer驱动框架
linux·c语言·arm开发·驱动开发·笔记·嵌入式硬件·arm
dsywws3 小时前
Linux学习笔记之时间日期和查找和解压缩指令
linux·笔记·学习
yeyuningzi4 小时前
Debian 12环境里部署nginx步骤记录
linux·运维·服务器
上辈子杀猪这辈子学IT4 小时前
【Zookeeper集群搭建】安装zookeeper、zookeeper集群配置、zookeeper启动与关闭、zookeeper的shell命令操作
linux·hadoop·zookeeper·centos·debian