【问题标题】:Why the more data in the `read()`, the lower the `read()` latency?为什么 `read()` 中的数据越多,`read()` 延迟越低?
【发布时间】:2021-12-20 12:21:06
【问题描述】:

在生产环境中发现一个不直观的现象:

  1. 我有一个服务器来接受客户端请求。
  2. 服务器每次最多读取 16K 的数据(即read(fd, buf, 16 * 1024))。

我发现read()系统调用通过strace -c返回16KB时延迟为2µs,而read() systemc调用时延迟为5µs返回10B。 (注意:服务器每次 read() 总是读取 16KB,区别在于客户端。一个客户端一次写入 16KB,另一个写入 10B。)

我觉得这很反直觉,read()write() 16KB 应该比 10B 慢,但是我对 linux 内核和网络堆栈了解不多。

我写了一个最小的可重现示例:

服务器端代码:

#include <assert.h>
#include <unistd.h>
#include <netinet/in.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>

const int BUFSIZE = 16 * 1024;

int main() {
    // make a socket
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    assert(fd > 0);

    // bind to 0.0.0.0:7731
    struct sockaddr_in sock;
    memset(&sock, 0, sizeof(sock));
    sock.sin_family = AF_INET;
    sock.sin_addr.s_addr = INADDR_ANY;
    sock.sin_port = htons(7731);
    bind(fd, (struct sockaddr*)&sock, sizeof(sock));
    assert(listen(fd, 128) == 0);

    // accept a peer
    struct sockaddr_in peer_sock;
    socklen_t socklen;
    int peer_fd = accept(fd, (struct sockaddr*)&peer_sock, &socklen);
    assert(peer_fd > 0);

    // read & write
    char buf[BUFSIZE];
    int nread = 0;
    while (nread = read(peer_fd, buf, sizeof(buf)), nread > 0) {
        int sum = 0;
        for (int i = 0; i < nread; ++i) {
            sum += buf[i];
        }
        assert(write(peer_fd, buf, nread) == nread);
    }
    return 0;
}

客户端代码(由 golang 编写):

package main

import (
    "log"
    "net"
)

func main() {
    conn, err := net.Dial("tcp", "127.0.0.1:7731")
    if err != nil {
        log.Fatal(err)
    }

    // 16KB or 10B buf
    buf := make([]byte, 16*1024)
    for i := 0; i < 1000000; i++ {
        nwrite := 0
        for nwrite != len(buf) {
            n, err := conn.Write(buf)
            if err != nil {
                log.Fatal(err)
            }
            nwrite += n
        }

        nread := 0
        readBuf := make([]byte, len(buf))
        for nread != len(buf) {
            n, err := conn.Read(readBuf)
            if err != nil {
                log.Fatal(err)
            }
            nread += n
        }
        log.Print(i)
    }
}

当客户端一次写入10B(注意:服务器每次读取16KB。即read(fd, buf, 16384)),服务器的stracestrace -c -side 输出是:

$ strace ./server
write(4, "\0\0\0\0\0\0\0\0\0\0", 10)    = 10
read(4, "\0\0\0\0\0\0\0\0\0\0", 16384)  = 10
write(4, "\0\0\0\0\0\0\0\0\0\0", 10)    = 10
read(4, "\0\0\0\0\0\0\0\0\0\0", 16384)  = 10
write(4, "\0\0\0\0\0\0\0\0\0\0", 10)    = 10
read(4, "\0\0\0\0\0\0\0\0\0\0", 16384)  = 10
write(4, "\0\0\0\0\0\0\0\0\0\0", 10)    = 10
read(4, "\0\0\0\0\0\0\0\0\0\0", 16384)  = 10
write(4, "\0\0\0\0\0\0\0\0\0\0", 10)    = 10
read(4, "\0\0\0\0\0\0\0\0\0\0", 16384)  = 10
write(4, "\0\0\0\0\0\0\0\0\0\0", 10)    = 10
read(4, "\0\0\0\0\0\0\0\0\0\0", 16384)  = 10
write(4, "\0\0\0\0\0\0\0\0\0\0", 10)    = 10
read(4, "\0\0\0\0\0\0\0\0\0\0", 16384)  = 10
write(4, "\0\0\0\0\0\0\0\0\0\0", 10)    = 10
read(4, "\0\0\0\0\0\0\0\0\0\0", 16384)  = 10
write(4, "\0\0\0\0\0\0\0\0\0\0", 10)    = 10
read(4, "\0\0\0\0\0\0\0\0\0\0", 16384)  = 10
write(4, "\0\0\0\0\0\0\0\0\0\0", 10)    = 10
read(4, "\0\0\0\0\0\0\0\0\0\0", 16384)  = 10
write(4, "\0\0\0\0\0\0\0\0\0\0", 10)    = 10
read(4, "\0\0\0\0\0\0\0\0\0\0", 16384)  = 10
write(4, "\0\0\0\0\0\0\0\0\0\0", 10)    = 10
read(4, "\0\0\0\0\0\0\0\0\0\0", 16384)  = 10
write(4, "\0\0\0\0\0\0\0\0\0\0", 10)    = 10
read(4, "\0\0\0\0\0\0\0\0\0\0", 16384)  = 10
write(4, "\0\0\0\0\0\0\0\0\0\0", 10)    = 10
read(4, "\0\0\0\0\0\0\0\0\0\0", 16384)  = 10
write(4, "\0\0\0\0\0\0\0\0\0\0", 10)    = 10
read(4, "\0\0\0\0\0\0\0\0\0\0", 16384)  = 10
write(4, "\0\0\0\0\0\0\0\0\0\0", 10)    = 10

$ strace -c ./a.out
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 65.00    0.233234           9     24596           write
 35.00    0.125599           5     24598           read
  0.00    0.000000           0         2           open
  0.00    0.000000           0         2           close
  0.00    0.000000           0         2           fstat
  0.00    0.000000           0         5           mmap
  0.00    0.000000           0         4           mprotect
  0.00    0.000000           0         1           munmap
  0.00    0.000000           0         1           brk
  0.00    0.000000           0         3         3 access
  0.00    0.000000           0         1           socket
  0.00    0.000000           0         1           accept
  0.00    0.000000           0         1           bind
  0.00    0.000000           0         1           listen
  0.00    0.000000           0         1           execve
  0.00    0.000000           0         1           arch_prctl
------ ----------- ----------- --------- --------- ----------------
100.00    0.358833                 49220         3 total

客户端一次写入16KB时,服务器端输出的stracestrace -c为:

$ strace ./server
write(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 16384) = 16384
read(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 16384) = 16384
write(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 16384) = 16384
read(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 16384) = 16384
write(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 16384) = 16384
read(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 16384) = 16384
write(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 16384) = 16384
read(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 16384) = 16384
write(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 16384) = 16384
read(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 16384) = 16384
write(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 16384) = 16384
read(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 16384) = 16384
write(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 16384) = 16384
read(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 16384) = 16384
write(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 16384) = 16384
read(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 16384) = 16384
write(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 16384) = 16384
read(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 16384) = 16384
write(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 16384) = 16384
read(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 16384) = 16384
write(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 16384) = 16384
read(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 16384) = 16384
write(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 16384) = 16384
read(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 16384) = 16384

$  strace -c ./server
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 61.26    0.063949           3     20692           write
 38.74    0.040441           2     20694           read
  0.00    0.000000           0         2           open
  0.00    0.000000           0         2           close
  0.00    0.000000           0         2           fstat
  0.00    0.000000           0         5           mmap
  0.00    0.000000           0         4           mprotect
  0.00    0.000000           0         1           munmap
  0.00    0.000000           0         1           brk
  0.00    0.000000           0         3         3 access
  0.00    0.000000           0         1           socket
  0.00    0.000000           0         1           accept
  0.00    0.000000           0         1           bind
  0.00    0.000000           0         1           listen
  0.00    0.000000           0         1           execve
  0.00    0.000000           0         1           arch_prctl
------ ----------- ----------- --------- --------- ----------------
100.00    0.104390                 41412         3 total

【问题讨论】:

  • 我猜它正在等待更多数据到达
  • 不确定,但如果我不得不猜测,我的猜测是,由于您的缓冲区独立于操作系统使用的数据通信缓冲区,因此缓冲区越大意味着您最终发出的系统调用越少从操作系统的缓冲区中读取。我认为系统调用很昂贵。
  • @AlanBirtles 我认为如果读取 16KB 应该等待更长的时间
  • @Galik 是的,系统调用很昂贵。但我认为 16KB 系统调用会比 10B 系统调用慢,但事实恰恰相反
  • 我的第一个猜测是,这是由于 Nagel 的算法等待合并小数据包。分布是怎样的?中位数与平均值不同是因为有几个阻塞读取吗?

标签: c++ c linux linux-kernel network-programming


【解决方案1】:

在 linux 内部,从缓冲区读取时会调用此函数。这是非常直接的,即使一个人想要读取比缓冲区大小更多的字节,也不应该影响读取它所花费的时间。所以从 10k 缓冲区读取 16k 应该与从 10k 缓冲区读取 10k 所用的时间相同。

/**
 * simple_read_from_buffer - copy data from the buffer to user space
 * @to: the user space buffer to read to
 * @count: the maximum number of bytes to read
 * @ppos: the current position in the buffer
 * @from: the buffer to read from
 * @available: the size of the buffer
 *
 * The simple_read_from_buffer() function reads up to @count bytes from the
 * buffer @from at offset @ppos into the user space address starting at @to.
 *
 * On success, the number of bytes read is returned and the offset @ppos is
 * advanced by this number, or negative value is returned on error.
 **/
ssize_t simple_read_from_buffer(void __user *to, size_t count, loff_t *ppos,
                const void *from, size_t available)
{
    loff_t pos = *ppos;
    size_t ret;

    if (pos < 0)
        return -EINVAL;
    if (pos >= available || !count)
        return 0;
    if (count > available - pos)
        count = available - pos;
    ret = copy_to_user(to, from + pos, count);
    if (ret == count)
        return -EFAULT;
    count -= ret;
    *ppos = pos + count;
    return count;
}
EXPORT_SYMBOL(simple_read_from_buffer);

因此,如果我们要讨论简单的缓冲区,您的假设是正确的,即从 16k 缓冲区读取 10k 应该比从同一个 16k 缓冲区读取 16k 花费的时间更少。

但是!!!!!!!!!

如果我们看一下套接字文件操作,我们可以看到套接字定义了 read_iter 操作。

static const struct file_operations socket_file_ops = {
    .owner =    THIS_MODULE,
    .llseek =   no_llseek,
    .read_iter =    sock_read_iter,
    .write_iter =   sock_write_iter,
    .poll =     sock_poll,
    .unlocked_ioctl = sock_ioctl,
#ifdef CONFIG_COMPAT
    .compat_ioctl = compat_sock_ioctl,
#endif
    .mmap =     sock_mmap,
    .release =  sock_close,
    .fasync =   sock_fasync,
    .sendpage = sock_sendpage,
    .splice_write = generic_splice_sendpage,
    .splice_read =  sock_splice_read,
    .show_fdinfo =  sock_show_fdinfo,
};

所以当这个函数被调用时:

ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
    ssize_t ret;

    if (!(file->f_mode & FMODE_READ))
        return -EBADF;
    if (!(file->f_mode & FMODE_CAN_READ))
        return -EINVAL;
    if (unlikely(!access_ok(buf, count)))
        return -EFAULT;

    ret = rw_verify_area(READ, file, pos, count);
    if (ret)
        return ret;
    if (count > MAX_RW_COUNT)
        count =  MAX_RW_COUNT;

    if (file->f_op->read)
        ret = file->f_op->read(file, buf, count, pos);
    else if (file->f_op->read_iter)
        ret = new_sync_read(file, buf, count, pos);
    else
        ret = -EINVAL;
    if (ret > 0) {
        fsnotify_access(file);
        add_rchar(current, ret);
    }
    inc_syscr(current);
    return ret;
}

new_sync_read 被调用,而不是 read,如果我们要讨论常规缓冲区,则会被调用。在行尾,调用了文件的read_iter,之前在struct中设置为sock_read_iter。这调用了sock_recvmsg 函数,该函数进行了INET 调用inet_recvmsg,这会归结为像tcp_recvmsg 这样的TCP 实现,然后调用其他TCP 函数,然后当然会执行一堆涉及标志的黑魔法,例如 MSG_WAITALL表示这里也有一些等待。

关键是,如果我们谈论“常规”缓冲区,您的假设是正确的。但是使用 TCP 和网络,事情变得更加复杂。不难想象,从套接字读取 16k 的最简单方法是 16k 消息到达并出现在该套接字中。否则会变得复杂。

PS:我不是专家,这是我第一次深入研究 linux 内核实现。尽管如此,这很有趣。但我所说的可能有错误。

【讨论】:

    【解决方案2】:

    这个问题是缓存失效问题。

    读取 16k 缓冲区时,CPU 请求 16Kib 内存块,在高延迟内存访问期间,将比请求更多的内存(比如 48Kib)读入缓存,具体取决于硬件缓存大小和使用的算法。如果该区域的内存发生变化,则缓存无效,必须从内存中重新加载。


    背景资料:

    这就是网络速度发挥作用的地方:

    以太网最大传输单元 (MTU) 为 1500 字节 - TCP(IPV4) 标头为 40 字节 = 1460 字节或 IPV6 为 1440,可能会更少,具体取决于选项。

    所以你的 10Kib 传输需要 8 个数据包,而 16Kib 需要 12 个数据包。

    让我们从客户端看一下进程延迟:

    从内存复制到网络接口卡 (NIC) 和输出到线路所需的时间对于两种大小几乎相同,因为时间是由网络传输时间设置的。假设数据持续可用。

    在接收端:

    在接收到数据时,数据被接收并直接内存访问 (DMA) 到内存中的内核缓冲区。 DMA 传输使上次内存读取的缓存部分无效,需要缓慢地重新加载缓存数据。


    由于 10 Kib 数据更频繁地发生缓存失效,因此与 16 Kib 数据相比,进程在内存访问刷新时停止。导致显着更多的内存延迟。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-09-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-08-28
      • 1970-01-01
      相关资源
      最近更新 更多