使用 PACKET_MMAP 和 PACKET_TX_RING 发送数据比 "normal" 慢(没有)

Sending data with PACKET_MMAP and PACKET_TX_RING is slower than "normal" (without)

我正在使用 PACKET_MMAP 套接字选项用 C 编写流量生成器,以创建环形缓冲区以通过原始套接字发送数据。环形缓冲区中充满了要发送的以太网帧,并调用了 sendto。环形缓冲区的全部内容通过套接字发送,这比在内存中有一个缓冲区并为缓冲区中需要发送的每个帧重复调用 sendto 提供更高的性能。

当不使用PACKET_MMAP时,在调用sendto时,单个帧从用户态内存中的缓冲区复制到内核内存中的 SK buf,然后内核必须复制数据包到 NIC 为 DMA 访问的内存,并向 NIC 发出信号,将帧 DMA 到它自己的硬件缓冲区中,并将其排队等待传输。当使用 PACKET_MMAP 套接字选项时,映射内存由应用程序分配并链接到原始套接字。应用程序将数据包放入映射缓冲区,调用 sendto,内核不必将数据包复制到 SK 缓冲区,它可以直接从映射缓冲区读取它们。此外,"blocks" 个数据包可以从环形缓冲区而不是单个 packets/frames 中读取。因此,性能提升是通过一个系统调用来复制多个帧,并为每个帧减少一个复制操作以将其放入 NIC 硬件缓冲区。

当我将使用 PACKET_MMAP 的套接字的性能与“普通”套接字(其中包含单个数据包的字符缓冲区)进行比较时,根本没有性能优势。这是为什么? 在 Tx 模式下使用 PACKET_MMAP 时,每个环形块只能放入一帧(而不是像 Rx 模式那样每个环形块有多个帧)但是我创建了 256 个块所以我们应该在单个 sendto 调用中发送 256 帧,对吗?

性能 PACKET_MMAP,main() 调用 packet_tx_mmap()

bensley@ubuntu-laptop:~/C/etherate10+$ sudo taskset -c 1 ./etherate_mt -I 1
Using inteface lo (1)
Running in Tx mode
1. Rx Gbps 0.00 (0) pps 0   Tx Gbps 17.65 (2206128128) pps 1457152
2. Rx Gbps 0.00 (0) pps 0   Tx Gbps 19.08 (2385579520) pps 1575680
3. Rx Gbps 0.00 (0) pps 0   Tx Gbps 19.28 (2409609728) pps 1591552
4. Rx Gbps 0.00 (0) pps 0   Tx Gbps 19.31 (2414260736) pps 1594624
5. Rx Gbps 0.00 (0) pps 0   Tx Gbps 19.30 (2411935232) pps 1593088

性能没有​​ PACKET_MMAP,main() 调用 packet_tx()

bensley@ubuntu-laptop:~/C/etherate10+$ sudo taskset -c 1 ./etherate_mt -I 1
Using inteface lo (1)
Running in Tx mode
1. Rx Gbps 0.00 (0) pps 0   Tx Gbps 18.44 (2305001412) pps 1522458
2. Rx Gbps 0.00 (0) pps 0   Tx Gbps 20.30 (2537520018) pps 1676037
3. Rx Gbps 0.00 (0) pps 0   Tx Gbps 20.29 (2535744096) pps 1674864
4. Rx Gbps 0.00 (0) pps 0   Tx Gbps 20.26 (2533014354) pps 1673061
5. Rx Gbps 0.00 (0) pps 0   Tx Gbps 20.32 (2539476106) pps 1677329

packet_tx() 函数看起来比 packet_tx_mmap() 函数稍快,但它也稍微短一些,所以我认为最小的性能提升只是 packet_tx。所以在我看来这两个函数的性能几乎相同,这是为什么呢?为什么 PACKET_MMAP 没有快很多,据我所知,系统调用和副本应该少得多?

void *packet_tx_mmap(void* thd_opt_p) {

    struct thd_opt *thd_opt = thd_opt_p;
    int32_t sock_fd = setup_socket_mmap(thd_opt_p);
    if (sock_fd == EXIT_FAILURE) exit(EXIT_FAILURE);

    struct tpacket2_hdr *hdr;
    uint8_t *data;
    int32_t send_ret = 0;
    uint16_t i;

    while(1) {

        for (i = 0; i < thd_opt->tpacket_req.tp_frame_nr; i += 1) {

            hdr = (void*)(thd_opt->mmap_buf + (thd_opt->tpacket_req.tp_frame_size * i));
            data = (uint8_t*)(hdr + TPACKET_ALIGN(TPACKET2_HDRLEN));

            memcpy(data, thd_opt->tx_buffer, thd_opt->frame_size);
            hdr->tp_len = thd_opt->frame_size;
            hdr->tp_status = TP_STATUS_SEND_REQUEST;

        }

        send_ret = sendto(sock_fd, NULL, 0, 0, NULL, 0);
        if (send_ret == -1) {
            perror("sendto error");
            exit(EXIT_FAILURE);
        }

        thd_opt->tx_pkts  += thd_opt->tpacket_req.tp_frame_nr;
        thd_opt->tx_bytes += send_ret;

    }

    return NULL;

}

请注意下面的函数调用 setup_socket() 而不是 setup_socket_mmap():

void *packet_tx(void* thd_opt_p) {

    struct thd_opt *thd_opt = thd_opt_p;

    int32_t sock_fd = setup_socket(thd_opt_p); 

    if (sock_fd == EXIT_FAILURE) {
        printf("Can't create socket!\n");
        exit(EXIT_FAILURE);
    }

    while(1) {

        thd_opt->tx_bytes += sendto(sock_fd, thd_opt->tx_buffer,
                                    thd_opt->frame_size, 0,
                                    (struct sockaddr*)&thd_opt->bind_addr,
                                    sizeof(thd_opt->bind_addr));
        thd_opt->tx_pkts += 1;

    }

}

下面粘贴了套接字设置函数的唯一区别,但本质上是设置 SOCKET_RX_RING 或 SOCKET_TX_RING 的要求:

// Set the TPACKET version, v2 for Tx and v3 for Rx
// (v2 supports packet level send(), v3 supports block level read())
int32_t sock_pkt_ver = -1;

if(thd_opt->sk_mode == SKT_TX) {
    static const int32_t sock_ver = TPACKET_V2;
    sock_pkt_ver = setsockopt(sock_fd, SOL_PACKET, PACKET_VERSION, &sock_ver, sizeof(sock_ver));
} else {
    static const int32_t sock_ver = TPACKET_V3;
    sock_pkt_ver = setsockopt(sock_fd, SOL_PACKET, PACKET_VERSION, &sock_ver, sizeof(sock_ver));
}

if (sock_pkt_ver < 0) {
    perror("Can't set socket packet version");
    return EXIT_FAILURE;
}


memset(&thd_opt->tpacket_req, 0, sizeof(struct tpacket_req));
memset(&thd_opt->tpacket_req3, 0, sizeof(struct tpacket_req3));

//thd_opt->block_sz = 4096; // These are set else where
//thd_opt->block_nr = 256;
//thd_opt->block_frame_sz = 4096;

int32_t sock_mmap_ring = -1;
if (thd_opt->sk_mode == SKT_TX) {

    thd_opt->tpacket_req.tp_block_size = thd_opt->block_sz;
    thd_opt->tpacket_req.tp_frame_size = thd_opt->block_sz;
    thd_opt->tpacket_req.tp_block_nr = thd_opt->block_nr;
    // Allocate per-frame blocks in Tx mode (TPACKET_V2)
    thd_opt->tpacket_req.tp_frame_nr = thd_opt->block_nr;

    sock_mmap_ring = setsockopt(sock_fd, SOL_PACKET , PACKET_TX_RING , (void*)&thd_opt->tpacket_req , sizeof(struct tpacket_req));

} else {

    thd_opt->tpacket_req3.tp_block_size = thd_opt->block_sz;
    thd_opt->tpacket_req3.tp_frame_size = thd_opt->block_frame_sz;
    thd_opt->tpacket_req3.tp_block_nr = thd_opt->block_nr;
    thd_opt->tpacket_req3.tp_frame_nr = (thd_opt->block_sz * thd_opt->block_nr) / thd_opt->block_frame_sz;
    thd_opt->tpacket_req3.tp_retire_blk_tov   = 1;
    thd_opt->tpacket_req3.tp_feature_req_word = 0;

    sock_mmap_ring = setsockopt(sock_fd, SOL_PACKET , PACKET_RX_RING , (void*)&thd_opt->tpacket_req3 , sizeof(thd_opt->tpacket_req3));
}

if (sock_mmap_ring == -1) {
    perror("Can't enable Tx/Rx ring for socket");
    return EXIT_FAILURE;
}


thd_opt->mmap_buf = NULL;
thd_opt->rd = NULL;

if (thd_opt->sk_mode == SKT_TX) {

    thd_opt->mmap_buf = mmap(NULL, (thd_opt->block_sz * thd_opt->block_nr), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_LOCKED | MAP_POPULATE, sock_fd, 0);

    if (thd_opt->mmap_buf == MAP_FAILED) {
        perror("mmap failed");
        return EXIT_FAILURE;
    }


} else {

    thd_opt->mmap_buf = mmap(NULL, (thd_opt->block_sz * thd_opt->block_nr), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_LOCKED | MAP_POPULATE, sock_fd, 0);

    if (thd_opt->mmap_buf == MAP_FAILED) {
        perror("mmap failed");
        return EXIT_FAILURE;
    }

    // Per bock rings in Rx mode (TPACKET_V3)
    thd_opt->rd = (struct iovec*)calloc(thd_opt->tpacket_req3.tp_block_nr * sizeof(struct iovec), 1);

    for (uint16_t i = 0; i < thd_opt->tpacket_req3.tp_block_nr; ++i) {
        thd_opt->rd[i].iov_base = thd_opt->mmap_buf + (i * thd_opt->tpacket_req3.tp_block_size);
        thd_opt->rd[i].iov_len  = thd_opt->tpacket_req3.tp_block_size;
    }


}

更新 1:针对物理接口的结果 有人提到我在使用 PACKET_MMAP 时可能看不到性能差异的一个原因是因为我正在将流量发送到环回接口(一方面,它没有 QDISC)。由于 运行 packet_tx_mmap()packet_tx() 例程中的任何一个都可以生成超过 10Gbps 的数据,而我只有 10Gbps 的接口可供使用,所以我将两个接口绑定在一起,这些是结果,显示了很多同上,两个函数之间的速度差异很小:

packet_tx() 到 20G bond0

packet_tx_mmap() 到 20G bond0:

这是 1514 字节大小的帧(以保持与上面的原始环回测试相同)。

在上述所有测试中,软 IRQ 的数量大致相同(使用 this script 测量)。对于一个线程 运行 packet_tx(),CPU 核心每秒大约有 40k 次中断。对于 2 和 3 线程 运行,2 核和 3 核分别有 40k。使用 packet_tx_mmap() 时的结果相同。一个 CPU 核心上的单线程大约 40k 软 IRQ。 运行 2 和 3 线程时每个内核 40k。

更新 2:完整源代码

我已经上传了完整的源代码,我还在写这个应用程序,所以它可能有很多缺陷,但它们不在这个问题的范围内:https://github.com/jwbensley/EtherateMT

linux 内核的许多接口都没有很好的文档记录。或者即使它们看起来有很好的文档记录,它们也可能非常复杂,这会让人很难理解接口的功能性属性,或者通常更难理解的非功能性属性是什么。

出于这个原因,我建议任何想要深入了解内核 API 或需要使用内核 API 创建高性能应用程序的人都需要能够使用内核代码才能取得成功。

在这种情况下,提问者想了解通过共享内存接口(数据包 mmap)向内核发送原始帧的性能特征。

linux 文档是 here. It has a stale link to a "how to," which can now be found here and includes a copy of packet_mmap.c (I have a slightly different version available here

文档主要面向阅读,这是使用数据包 mmap 的典型用例:有效地从接口读取原始帧,例如, 有效地从中获取数据包捕获几乎没有或没有损失的高速接口。

然而,OP 对高性能 写作 感兴趣,这是一个不太常见的用例,但可能对流量 generator/simulator 有用,这似乎是什么OP 想要处理它。值得庆幸的是,"how to" 就是写框架。

即便如此,关于这实际上是如何工作的提供的信息很少,并且没有明显帮助回答 OP 的问题,即为什么使用数据包 mmap 似乎并不比不使用它而是发送一个更快一次一帧。

谢天谢地,内核源代码是开源的并且索引完善,所以我们可以求助于源代码来帮助我们找到问题的答案。

为了找到相关的内核代码,您可以搜索几个关键字,但 PACKET_TX_RING 作为此功能独有的套接字选项脱颖而出。在互联网上搜索 "PACKET_TX_RING linux cross reference" 会找到少量参考资料,包括 af_packet.c,稍微检查一下似乎是所有 AF_PACKET 功能的实现,包括数据包 mmap。

翻看af_packet.c,似乎用packet mmap传输的工作核心发生在tpacket_snd()。但这是正确的吗?我们如何判断这是否与我们认为的有任何关系?

从内核中获取此类信息的一个非常强大的工具是 SystemTap. (Using this requires installing debugging symbols for your kernel. I happen to be using Ubuntu, and this 是让 SystemTap 在 Ubuntu 上工作的方法。)

让 SystemTap 工作后,您可以将 SystemTap 与 packet_mmap.c 结合使用,以查看是否通过在内核函数 tpacket_snd 上安装探测器来调用 tpacket_snd(),然后运行ning packet_mmap 通过共享 TX 环发送帧:

$ sudo stap -e 'probe kernel.function("tpacket_snd") { printf("W00T!\n"); }' &
[1] 19961
$ sudo ./packet_mmap -c 1 eth0
[...]
STARTING TEST:
data offset = 32 bytes
start fill() thread
send 1 packets (+150 bytes)
end of task fill()
Loop until queue empty (0)
END (number of error:0)
W00T!
W00T!

W00T!我们正在做某事; tpacket_snd 实际上正在被调用。但我们的胜利将是短暂的。如果我们继续尝试从库存内核构建中获取更多信息,SystemTap 将抱怨它找不到我们想要检查的变量,并且函数参数将打印出 ? 或 [=34= 的值].这是因为内核是经过优化编译的,AF_PACKET 的所有功能都定义在单个翻译单元 af_packet.c 中;许多函数被编译器内联,实际上丢失了局部变量和参数。

为了从 af_packet.c 中获取更多信息,我们将不得不构建一个未优化构建 af_packet.c 的内核版本。查看 here 以获得一些指导。我等。

好的,希望这不是太难,并且您已经成功启动了一个内核,SystemTap 可以从中获得很多有用的信息。请记住,这个内核版本只是为了帮助我们弄清楚 packet mmap 是如何工作的。我们无法从这个内核中获得任何直接的性能信息,因为 af_packet.c 是在 没有 优化的情况下构建的。如果事实证明我们需要获取有关优化版本的行为方式的信息,我们可以构建另一个内核,其中 af_packet.c 已优化编译,但添加了一些检测代码,这些代码通过不会得到优化的变量公开信息出来以便 SystemTap 可以看到它们。

所以让我们用它来获取一些信息。看看 status.stp:

# This is specific to net/packet/af_packet.c 3.13.0-116

function print_ts() {
  ts = gettimeofday_us();
  printf("[%10d.%06d] ", ts/1000000, ts%1000000);
}

#  325 static void __packet_set_status(struct packet_sock *po, void *frame, int status)
#  326 {
#  327  union tpacket_uhdr h;
#  328 
#  329  h.raw = frame;
#  330  switch (po->tp_version) {
#  331  case TPACKET_V1:
#  332      h.h1->tp_status = status;
#  333      flush_dcache_page(pgv_to_page(&h.h1->tp_status));
#  334      break;
#  335  case TPACKET_V2:
#  336      h.h2->tp_status = status;
#  337      flush_dcache_page(pgv_to_page(&h.h2->tp_status));
#  338      break;
#  339  case TPACKET_V3:
#  340  default:
#  341      WARN(1, "TPACKET version not supported.\n");
#  342      BUG();
#  343  }
#  344 
#  345  smp_wmb();
#  346 }

probe kernel.statement("__packet_set_status@net/packet/af_packet.c:334") {
  print_ts();
  printf("SET(V1): %d (0x%.16x)\n", $status, $frame);
}

probe kernel.statement("__packet_set_status@net/packet/af_packet.c:338") {
  print_ts();
  printf("SET(V2): %d\n", $status);
}

#  348 static int __packet_get_status(struct packet_sock *po, void *frame)
#  349 {
#  350  union tpacket_uhdr h;
#  351 
#  352  smp_rmb();
#  353 
#  354  h.raw = frame;
#  355  switch (po->tp_version) {
#  356  case TPACKET_V1:
#  357      flush_dcache_page(pgv_to_page(&h.h1->tp_status));
#  358      return h.h1->tp_status;
#  359  case TPACKET_V2:
#  360      flush_dcache_page(pgv_to_page(&h.h2->tp_status));
#  361      return h.h2->tp_status;
#  362  case TPACKET_V3:
#  363  default:
#  364      WARN(1, "TPACKET version not supported.\n");
#  365      BUG();
#  366      return 0;
#  367  }
#  368 }

probe kernel.statement("__packet_get_status@net/packet/af_packet.c:358") { 
  print_ts();
  printf("GET(V1): %d (0x%.16x)\n", $h->h1->tp_status, $frame); 
}

probe kernel.statement("__packet_get_status@net/packet/af_packet.c:361") { 
  print_ts();
  printf("GET(V2): %d\n", $h->h2->tp_status); 
}

# 2088 static int tpacket_snd(struct packet_sock *po, struct msghdr *msg)
# 2089 {
# [...]
# 2136  do {
# 2137      ph = packet_current_frame(po, &po->tx_ring,
# 2138              TP_STATUS_SEND_REQUEST);
# 2139 
# 2140      if (unlikely(ph == NULL)) {
# 2141          schedule();
# 2142          continue;
# 2143      }
# 2144 
# 2145      status = TP_STATUS_SEND_REQUEST;
# 2146      hlen = LL_RESERVED_SPACE(dev);
# 2147      tlen = dev->needed_tailroom;
# 2148      skb = sock_alloc_send_skb(&po->sk,
# 2149              hlen + tlen + sizeof(struct sockaddr_ll),
# 2150              0, &err);
# 2151 
# 2152      if (unlikely(skb == NULL))
# 2153          goto out_status;
# 2154 
# 2155      tp_len = tpacket_fill_skb(po, skb, ph, dev, size_max, proto,
# 2156                    addr, hlen);
# [...]
# 2176      skb->destructor = tpacket_destruct_skb;
# 2177      __packet_set_status(po, ph, TP_STATUS_SENDING);
# 2178      atomic_inc(&po->tx_ring.pending);
# 2179 
# 2180      status = TP_STATUS_SEND_REQUEST;
# 2181      err = dev_queue_xmit(skb);
# 2182      if (unlikely(err > 0)) {
# [...]
# 2195      }
# 2196      packet_increment_head(&po->tx_ring);
# 2197      len_sum += tp_len;
# 2198  } while (likely((ph != NULL) ||
# 2199          ((!(msg->msg_flags & MSG_DONTWAIT)) &&
# 2200           (atomic_read(&po->tx_ring.pending))))
# 2201      );
# 2202 
# [...]
# 2213  return err;
# 2214 }

probe kernel.function("tpacket_snd") {
  print_ts();
  printf("tpacket_snd: args(%s)\n", $$parms);
}

probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2140") {
  print_ts();
  printf("tpacket_snd:2140: current frame ph = 0x%.16x\n", $ph);
}

probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2141") {
  print_ts();
  printf("tpacket_snd:2141: (ph==NULL) --> schedule()\n");
}

probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2142") {
  print_ts();
  printf("tpacket_snd:2142: flags 0x%x, pending %d\n", 
     $msg->msg_flags, $po->tx_ring->pending->counter);
}

probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2197") {
  print_ts();
  printf("tpacket_snd:2197: flags 0x%x, pending %d\n", 
     $msg->msg_flags, $po->tx_ring->pending->counter);
}

probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2213") {
  print_ts();
  printf("tpacket_snd: return(%d)\n", $err);
}

# 1946 static void tpacket_destruct_skb(struct sk_buff *skb)
# 1947 {
# 1948  struct packet_sock *po = pkt_sk(skb->sk);
# 1949  void *ph;
# 1950 
# 1951  if (likely(po->tx_ring.pg_vec)) {
# 1952      __u32 ts;
# 1953 
# 1954      ph = skb_shinfo(skb)->destructor_arg;
# 1955      BUG_ON(atomic_read(&po->tx_ring.pending) == 0);
# 1956      atomic_dec(&po->tx_ring.pending);
# 1957 
# 1958      ts = __packet_set_timestamp(po, ph, skb);
# 1959      __packet_set_status(po, ph, TP_STATUS_AVAILABLE | ts);
# 1960  }
# 1961 
# 1962  sock_wfree(skb);
# 1963 }

probe kernel.statement("tpacket_destruct_skb@net/packet/af_packet.c:1959") {
  print_ts();
  printf("tpacket_destruct_skb:1959: ph = 0x%.16x, ts = 0x%x, pending %d\n",
     $ph, $ts, $po->tx_ring->pending->counter);
}

这定义了一个函数(print_ts 以微秒分辨率打印出 unix 纪元时间)和一些探测器。

首先,我们定义探测器以在 tx_ring 中的数据包设置或读取其状态时打印出信息。接下来,我们为 tpacket_snd 的调用和 return 以及 do {...} while (...) 循环中处理 tx_ring 中的数据包的点定义探测器。最后我们给skb的析构函数添加一个探针。

我们可以用 sudo stap status.stp 启动 SystemTap 脚本。然后运行sudo packet_mmap -c 2 <interface>通过接口发送2帧。这是我从 SystemTap 脚本得到的输出:

[1492581245.839850] tpacket_snd: args(po=0xffff88016720ee38 msg=0x14)
[1492581245.839865] GET(V1): 1 (0xffff880241202000)
[1492581245.839873] tpacket_snd:2140: current frame ph = 0xffff880241202000
[1492581245.839887] SET(V1): 2 (0xffff880241202000)
[1492581245.839918] tpacket_snd:2197: flags 0x40, pending 1
[1492581245.839923] GET(V1): 1 (0xffff88013499c000)
[1492581245.839929] tpacket_snd:2140: current frame ph = 0xffff88013499c000
[1492581245.839935] SET(V1): 2 (0xffff88013499c000)
[1492581245.839946] tpacket_snd:2197: flags 0x40, pending 2
[1492581245.839951] GET(V1): 0 (0xffff88013499e000)
[1492581245.839957] tpacket_snd:2140: current frame ph = 0x0000000000000000
[1492581245.839961] tpacket_snd:2141: (ph==NULL) --> schedule()
[1492581245.839977] tpacket_snd:2142: flags 0x40, pending 2
[1492581245.839984] tpacket_snd: return(300)
[1492581245.840077] tpacket_snd: args(po=0x0 msg=0x14)
[1492581245.840089] GET(V1): 0 (0xffff88013499e000)
[1492581245.840098] tpacket_snd:2140: current frame ph = 0x0000000000000000
[1492581245.840093] tpacket_destruct_skb:1959: ph = 0xffff880241202000, ts = 0x0, pending 1
[1492581245.840102] tpacket_snd:2141: (ph==NULL) --> schedule()
[1492581245.840104] SET(V1): 0 (0xffff880241202000)
[1492581245.840112] tpacket_snd:2142: flags 0x40, pending 1
[1492581245.840116] tpacket_destruct_skb:1959: ph = 0xffff88013499c000, ts = 0x0, pending 0
[1492581245.840119] tpacket_snd: return(0)
[1492581245.840123] SET(V1): 0 (0xffff88013499c000)

下面是网络抓包:

SystemTap 输出中有很多有用的信息。我们可以看到 tpacket_snd 得到环中第一帧的状态 (TP_STATUS_SEND_REQUEST is 1) and then set it to TP_STATUS_SENDING (2). It does the same with the second. The next frame has status TP_STATUS_AVAILABLE (0), which is not a send request, so it calls schedule() to yield, and continues the loop. Since there are no more frames to send (ph==NULL) and non-blocking has been requested (msg->msg_flags ==MSG_DONTWAIT) do {...} while (...) 循环终止,并且 tpacket_snd returns 300, 排队等待传输的字节数。

接下来,packet_mmap再次调用sendto(通过"loop until queue empty"代码),但是tx环中没有更多的数据要发送,请求非阻塞,所以它立即 returns 0,因为没有数据排队。请注意,它检查状态的帧与上一次调用中最后检查的帧相同——它不是从 tx 环中的第一帧开始,它检查了 head(这在用户区)。

异步调用析构函数,首先在第一帧上将帧的状态设置为 TP_STATUS_AVAILABLE 并递减挂起计数,然后在第二帧上调用。请注意,如果未请求非阻塞,do {...} while (...) 循环结束时的测试将等到所有待处理的数据包在 return 之前都已传输到 NIC(假设它支持分散数据)荷兰国际集团你可以通过 运行ning packet_mmap 和 "threaded" 的 -t 选项来观看这个,它使用阻塞 I/O(直到它到达 "loop until queue empty")。

有几点需要注意。首先,SystemTap 输出上的时间戳没有增加:从 SystemTap 输出推断时间顺序是不安全的。其次,请注意网络捕获(在本地完成)的时间戳不同。 FWIW,该接口是廉价塔式计算机中的廉价 1G。

所以在这一点上,我想我们或多或少知道 af_packet 是如何处理共享 tx 环的。接下来是 tx 环中的帧如何找到到达网络接口的路径。查看 linux 网络内核中的控制流 this section (on how layer 2 transmission is handled) of an overview 可能会有所帮助。

好的,如果您对第 2 层传输的处理方式有基本的了解,那么这个数据包 mmap 接口似乎应该是一个巨大的消防水带;用数据包加载一个共享的 tx 环,用 MSG_DONTWAIT 调用 sendto(),然后 tpacket_snd 将遍历 tx 队列创建 skb 并将它们排入 qdisc。异步地,skb 将从 qdisc 出列并发送到硬件 tx 环。 skb 应该是 non-linear 所以它们将引用 tx 环中的数据而不是复制,并且一个不错的现代 NIC 应该能够处理分散的数据并引用 tx 环中的数据。当然,这些假设中的任何一个都可能是错误的,所以让我们尝试用这个消防水带将大量伤害倾倒在 qdisc 上。

但首先,关于 qdiscs 如何工作的一个不为人所知的事实。它们包含有限数量的数据(通常以帧数计算,但在某些情况下可以以字节为单位)并且如果您尝试将帧排队到完整的 qdisc,该帧通常会被丢弃(取决于排队者决定这样做)。因此,我将给出一个提示,即我最初的假设是 OP 使用数据包 mmap 将帧爆炸到 qdisc 中的速度如此之快,以至于许多帧都被丢弃了。但是不要太坚持这个想法;它带你走向一个方向,但始终保持开放的心态。让我们试一试,看看会发生什么。

尝试这个的第一个问题是默认的 qdisc pfifo_fast 不保留统计信息。所以让我们用 qdisc pfifo 代替它。默认情况下 pfifo 将队列限制为 TXQUEUELEN 帧(通常默认为 1000)。但是因为我们想要展示压倒性的 qdisc,让我们明确地将它设置为 50:

$ sudo tc qdisc add dev eth0 root pfifo limit 50
$ tc -s -d qdisc show dev eth0
qdisc pfifo 8004: root refcnt 2 limit 50p
 Sent 42 bytes 1 pkt (dropped 0, overlimits 0 requeues 0) 
 backlog 0b 0p requeues 0 

让我们也测量使用 SystemTap 脚本 call-return.stp:

处理 tpacket_snd 中的帧需要多长时间
# This is specific to net/packet/af_packet.c 3.13.0-116

function print_ts() {
  ts = gettimeofday_us();
  printf("[%10d.%06d] ", ts/1000000, ts%1000000);
}

# 2088 static int tpacket_snd(struct packet_sock *po, struct msghdr *msg)
# 2089 {
# [...]
# 2213  return err;
# 2214 }

probe kernel.function("tpacket_snd") {
  print_ts();
  printf("tpacket_snd: args(%s)\n", $$parms);
}

probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2213") {
  print_ts();
  printf("tpacket_snd: return(%d)\n", $err);
}

使用 sudo stap call-return.stp 启动 SystemTap 脚本,然后让我们将 8096 个 1500 字节的帧加载到只有 50 帧容量的 qdisc 中:

$ sudo ./packet_mmap -c 8096 -s 1500 eth0
[...]
STARTING TEST:
data offset = 32 bytes
start fill() thread
send 8096 packets (+12144000 bytes)
end of task fill()
Loop until queue empty (0)
END (number of error:0)

所以让我们检查一下 qdisc 丢弃了多少数据包:

$ tc -s -d qdisc show dev eth0
qdisc pfifo 8004: root refcnt 2 limit 50p
 Sent 25755333 bytes 8606 pkt (dropped 1, overlimits 0 requeues 265) 
 backlog 0b 0p requeues 265 

WAT?将 8096 帧中的一帧丢弃到 50 帧的 qdisc 上?让我们检查 SystemTap 输出:

[1492603552.938414] tpacket_snd: args(po=0xffff8801673ba338 msg=0x14)
[1492603553.036601] tpacket_snd: return(12144000)
[1492603553.036706] tpacket_snd: args(po=0x0 msg=0x14)
[1492603553.036716] tpacket_snd: return(0)

WAT? It took nearly 100ms to process 8096 frames in tpacket_snd? Let's check how long that would actually take to transmit; that's 8096 frames at 1500 bytes/frame at 1gigabit/s ~= 97ms. WAT?闻起来好像有东西堵住了。

让我们仔细看看tpacket_snd。呻吟:

skb = sock_alloc_send_skb(&po->sk,
                 hlen + tlen + sizeof(struct sockaddr_ll),
                 0, &err);

0 看起来很无伤大雅,但这实际上是 noblock 的论点。应该是msg->msg_flags & MSG_DONTWAIT(原来这是fixed in 4.1)。这里发生的是 qdisc 的大小不是唯一的限制资源。如果为 skb 分配 space 会超过套接字的 sndbuf 限制的大小,则此调用将阻塞以等待 skb 被释放或 return -EAGAIN 到非阻塞呼叫者。在 V4.1 的修复中,如果请求非阻塞,如果非零,它将 return 写入的字节数,否则 -EAGAIN 给调用者,这似乎有人不想要你弄清楚如何使用这个(例如你用80MB的数据填充一个tx环,用MSG_DONTWAIT调用sendto,你得到一个你发送150KB的结果而不是 EWOULDBLOCK).

因此,如果您 运行ning 4.1 之前的内核(我相信 OP 是 运行ning >4.1 并且不受此错误的影响),您将需要修补 af_packet.c 并构建新内核或升级到内核 4.1 或更高版本。

我现在已经启动了内核的补丁版本,因为我使用的机器是 运行ning 3.13。虽然如果 sndbuf 已满我们不会阻塞,但我们仍然会 return 和 -EAGAIN。我对 packet_mmap.c 进行了一些更改以增加 sndbuf 的默认大小,并在必要时使用 SO_SNDBUFFORCE 覆盖每个套接字的系统最大值(它似乎需要大约 750 字节 + 每个帧的帧大小).我还对 call-return.stp 进行了一些补充,以记录 sndbuf 最大大小 (sk_sndbuf)、使用的数量 (sk_wmem_alloc)、由 sock_alloc_send_skb 编辑的任何错误 return以及将 skb 排队到 qdisc 时从 dev_queue_xmit 编辑的任何错误 return。这是新版本:

# This is specific to net/packet/af_packet.c 3.13.0-116

function print_ts() {
  ts = gettimeofday_us();
  printf("[%10d.%06d] ", ts/1000000, ts%1000000);
}

# 2088 static int tpacket_snd(struct packet_sock *po, struct msghdr *msg)
# 2089 {
# [...]
# 2133  if (size_max > dev->mtu + reserve + VLAN_HLEN)
# 2134      size_max = dev->mtu + reserve + VLAN_HLEN;
# 2135 
# 2136  do {
# [...]
# 2148      skb = sock_alloc_send_skb(&po->sk,
# 2149              hlen + tlen + sizeof(struct sockaddr_ll),
# 2150              msg->msg_flags & MSG_DONTWAIT, &err);
# 2151 
# 2152      if (unlikely(skb == NULL))
# 2153          goto out_status;
# [...]
# 2181      err = dev_queue_xmit(skb);
# 2182      if (unlikely(err > 0)) {
# 2183          err = net_xmit_errno(err);
# 2184          if (err && __packet_get_status(po, ph) ==
# 2185                 TP_STATUS_AVAILABLE) {
# 2186              /* skb was destructed already */
# 2187              skb = NULL;
# 2188              goto out_status;
# 2189          }
# 2190          /*
# 2191           * skb was dropped but not destructed yet;
# 2192           * let's treat it like congestion or err < 0
# 2193           */
# 2194          err = 0;
# 2195      }
# 2196      packet_increment_head(&po->tx_ring);
# 2197      len_sum += tp_len;
# 2198  } while (likely((ph != NULL) ||
# 2199          ((!(msg->msg_flags & MSG_DONTWAIT)) &&
# 2200           (atomic_read(&po->tx_ring.pending))))
# 2201      );
# [...]
# 2213  return err;
# 2214 }

probe kernel.function("tpacket_snd") {
  print_ts();
  printf("tpacket_snd: args(%s)\n", $$parms);
}

probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2133") {
  print_ts();
  printf("tpacket_snd:2133: sk_sndbuf =  %d sk_wmem_alloc = %d\n", 
     $po->sk->sk_sndbuf, $po->sk->sk_wmem_alloc->counter);
}

probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2153") {
  print_ts();
  printf("tpacket_snd:2153: sock_alloc_send_skb err = %d, sk_sndbuf =  %d sk_wmem_alloc = %d\n", 
     $err, $po->sk->sk_sndbuf, $po->sk->sk_wmem_alloc->counter);
}

probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2182") {
  if ($err != 0) {
    print_ts();
    printf("tpacket_snd:2182: dev_queue_xmit err = %d\n", $err);
  }
}

probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2187") {
  print_ts();
  printf("tpacket_snd:2187: destructed: net_xmit_errno = %d\n", $err);
}

probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2194") {
  print_ts();
  printf("tpacket_snd:2194: *NOT* destructed: net_xmit_errno = %d\n", $err);
}

probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2213") {
  print_ts();
  printf("tpacket_snd: return(%d) sk_sndbuf =  %d sk_wmem_alloc = %d\n", 
     $err, $po->sk->sk_sndbuf, $po->sk->sk_wmem_alloc->counter);
}

我们再试一次:

$ sudo tc qdisc add dev eth0 root pfifo limit 50
$ tc -s -d qdisc show dev eth0
qdisc pfifo 8001: root refcnt 2 limit 50p
 Sent 2154 bytes 21 pkt (dropped 0, overlimits 0 requeues 0) 
 backlog 0b 0p requeues 0 
$ sudo ./packet_mmap -c 200 -s 1500 eth0
[...]
c_sndbuf_sz:       1228800
[...]
STARTING TEST:
data offset = 32 bytes
send buff size = 1228800
got buff size = 425984
buff size smaller than desired, trying to force...
got buff size = 2457600
start fill() thread
send: No buffer space available
end of task fill()
send: No buffer space available
Loop until queue empty (-1)
[repeated another 17 times]
send 3 packets (+4500 bytes)
Loop until queue empty (4500)
Loop until queue empty (0)
END (number of error:0)
$  tc -s -d qdisc show dev eth0
qdisc pfifo 8001: root refcnt 2 limit 50p
 Sent 452850 bytes 335 pkt (dropped 19, overlimits 0 requeues 3) 
 backlog 0b 0p requeues 3 

这是 SystemTap 的输出:

[1492759330.907151] tpacket_snd: args(po=0xffff880393246c38 msg=0x14)
[1492759330.907162] tpacket_snd:2133: sk_sndbuf =  2457600 sk_wmem_alloc = 1
[1492759330.907491] tpacket_snd:2182: dev_queue_xmit err = 1
[1492759330.907494] tpacket_snd:2187: destructed: net_xmit_errno = -105
[1492759330.907500] tpacket_snd: return(-105) sk_sndbuf =  2457600 sk_wmem_alloc = 218639
[1492759330.907646] tpacket_snd: args(po=0x0 msg=0x14)
[1492759330.907653] tpacket_snd:2133: sk_sndbuf =  2457600 sk_wmem_alloc = 189337
[1492759330.907688] tpacket_snd:2182: dev_queue_xmit err = 1
[1492759330.907691] tpacket_snd:2187: destructed: net_xmit_errno = -105
[1492759330.907694] tpacket_snd: return(-105) sk_sndbuf =  2457600 sk_wmem_alloc = 189337
[repeated 17 times]
[1492759330.908541] tpacket_snd: args(po=0x0 msg=0x14)
[1492759330.908543] tpacket_snd:2133: sk_sndbuf =  2457600 sk_wmem_alloc = 189337
[1492759330.908554] tpacket_snd: return(4500) sk_sndbuf =  2457600 sk_wmem_alloc = 196099
[1492759330.908570] tpacket_snd: args(po=0x0 msg=0x14)
[1492759330.908572] tpacket_snd:2133: sk_sndbuf =  2457600 sk_wmem_alloc = 196099
[1492759330.908576] tpacket_snd: return(0) sk_sndbuf =  2457600 sk_wmem_alloc = 196099

现在一切正常;我们已经修复了一个导致我们阻塞的错误,因为超过了 sndbuf 限制,我们已经调整了 sndbuf 限制,使其不再是一个约束,现在我们看到来自 tx 环的帧被排入 qdisc 队列,直到它已满, 此时我们得到 returned ENOBUFS.

下一个问题是如何有效地持续发布到 qdisc 以保持界面繁忙。请注意,packet_poll 的实现在我们填充 qdisc 并返回 ENOBUFS 的情况下是无用的,因为它只是查询头部是否为 TP_STATUS_AVAILABLE,在这种情况下将保持 TP_STATUS_SEND_REQUEST 直到对 sendto 的后续调用成功将帧排队到 qdisc。一个简单的权宜之计(在 packet_mmap.c 中更新)是在 sendto 上循环直到成功或除 ENOBUFSEAGAIN 之外的错误。

无论如何,我们现在知道的足以回答 OP 问题,即使我们没有完整的解决方案来有效地防止 NIC 被饿死。

据我们了解,我们知道当 OP 在阻塞模式下使用 tx ring 调用 sendto 时,tpacket_snd 将开始将 skbs 排队到 qdisc 上,直到超过 sndbuf 限制(通常默认为相当小,大约 213K,此外,我发现共享 tx 环中引用的帧数据被计入此)当它阻塞时(同时仍保持 pg_vec_lock)。随着 skb 的释放,更多的帧将被加入队列,也许 sndbuf 将再次被超出,我们将再次阻塞。最终,所有数据都将排队到 qdisc,但 tpacket_snd 将继续阻塞,直到所有帧都已传输 (您不能在tx ring 在 NIC 收到它之前一直可用,因为驱动程序环中的 skb 引用 tx ring 中的帧)同时仍然保持 pg_vec_lock。此时网卡饿死,其他任何socket writer都被锁阻塞了。

另一方面,当 OP 一次发布一个数据包时,它将由 packet_snd 处理,如果 sndbuf 中没有空间,它将阻塞,然后将帧排队到 qdisc 上,并且立即return。它不等待帧被传输。随着 qdisc 被耗尽,额外的帧可以被排队。如果发布者能跟上,网卡永远不会饿死。

此外,op 正在为每个 sendto 调用复制到 tx 环中,并将其与不使用 tx 环时传递固定帧缓冲区进行比较。你不会看到不以这种方式复制的加速(尽管这不是使用 tx ring 的唯一好处)。