使用PACKET_MMAP和PACKET_TX_RING发送数据比“普通”(不使用)方式更慢。

30
我正在使用C语言编写一个流量生成器,使用PACKET_MMAP套接字选项创建环形缓冲区通过原始套接字发送数据。环形缓冲区填充以太网帧以进行发送,并调用sendto。整个环形缓冲区的内容都会被发送到套接字上,这应该比在内存中使用缓冲区并为需要发送的缓冲区中的每个帧重复调用sendto来获得更高的性能。
当不使用PACKET_MMAP时,在调用sendto时,单个帧从用户空间内存中的缓冲区复制到内核内存中的SK buf,然后内核必须将数据包复制到NIC进行DMA访问的内存中,并向NIC发出信号以将帧传输到自己的硬件缓冲区并排队进行传输。当使用PACKET_MMAP套接字选项时,应用程序分配了mmapped内存并将其链接到原始套接字。应用程序将数据包放入mmapped缓冲区,调用sendto,内核无需将数据包复制到SK buf中即可直接从mmapped缓冲区读取它们。此外,可以从环形缓冲区中读取“块”而不是单个数据包/帧。因此,性能提高了一次系统调用以复制多个帧,并且每个帧都少了一个复制操作,以将其放入NIC硬件缓冲区中。
当我比较使用PACKET_MMAP的套接字和“普通”套接字(一个带有单个数据包的char缓冲区)的性能时,根本没有任何性能优势。为什么会这样?在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

  • 1线程:平均10.77Gbps / 889kfps~
  • 2线程:平均19.19Gbps / 1.58Mfps~
  • 3线程:平均19.67Gbps / 1.62Mfps~(这是bond能够达到的最快速度)

packet_tx_mmap() 发送至20G bond0:

  • 1线程:平均11.08Gbps / 913kfps~
  • 2线程:平均19.0Gbps / 1.57Mfps~
  • 3线程:平均19.66Gbps / 1.62Mfps~(这是连接速度的最大值)

上述所有测试中,帧大小为1514字节(与上面的原始环回测试保持一致)。

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

更新2:完整源代码

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


1
你的网络速度有多快?你的帧大小有多大?也许你只是饱和了你的链接?你检查过实际(自动协商)比特率了吗? - maxy
帧大小为1514个八位字节,包括头部。根据输出结果,我正在向环回接口lo发送流量。我选择向环回接口发送流量是为了排除网卡作为问题源的可能性。 - jwbensley
我的理解是,因为packet_tx_mmap函数应该与内核共享缓冲区,这意味着在单个sendto()系统调用中从用户空间复制多个数据包到内核空间,所以向回路接口发送流量意味着我们专门测试该方面,而不用担心将数据包DMA到NIC上,因为这对于packet_txpacket_tx_mmap来说都是相同的处理过程,因为它们都在内核栈中处于较低层。 - jwbensley
2
每帧少了一次复制操作 - 在我看来,复制操作只是从内核移动到用户空间,因为您在程序中执行了memcpy - kfx
1
你让我对此产生了兴趣,我花了一点时间继续探索。如果我们想要使用MSG_DONTWAIT,则需要了解如何同步用户空间和内核空间之间的共享环形缓冲区的访问。在内核中,设置和获取数据包状态分别使用写入和读取屏障(参见此处),因此我们在用户空间中也需要小心处理。屏障曾经在<asm/system.h>中定义,但现在已不再如此。我正在使用liburcu。今天就这些。 - JimD.
显示剩余12条评论
1个回答

45
许多与Linux内核的接口并没有得到很好的文档支持。即使它们似乎有良好的文档支持,它们可能非常复杂,这使得理解接口的功能属性或者更难的非功能属性变得困难。
因此,我建议任何想要深入了解内核API或需要使用内核API创建高性能应用程序的人都需要能够参与内核代码才能成功。
在这种情况下,提问者想要了解通过共享内存接口(packet mmap)发送原始帧的性能特征。
Linux文档在此处。它有一个过时的链接指向“how to”,现在可以在此处找到,并包括packet_mmap.c的副本(我有一个稍微不同版本的此处可用)。
文档主要针对阅读进行了优化,这是使用数据包mmap的典型用例:从接口有效地读取原始帧,例如:从高速接口有效地获取数据包捕获时几乎不会丢失。然而,OP对高性能的写入更感兴趣,这是一个不太常见的用例,但对于流量生成器/模拟器可能非常有用,这似乎是OP想要做的事情。幸运的是,“如何”都是关于写入帧的。即便如此,提供的关于实际工作方式的信息很少,并且没有明显的帮助来回答OP的问题,即为什么使用数据包mmap似乎不比不使用它并单独发送一帧快。幸运的是,内核源代码是开源的,索引良好,因此我们可以查看源代码来帮助我们回答问题。
为了找到相关的内核代码,你可以搜索几个关键字,但是PACKET_TX_RING作为一个唯一的套接字选项更加突出。在互联网上搜索“PACKET_TX_RING linux交叉引用”会出现少量参考资料,包括af_packet.c,这个实现了所有AF_PACKET功能的文件,包括数据包mmap。
浏览af_packet.c,看起来使用数据包mmap进行传输的核心工作发生在tpacket_snd()中。但是这是否正确?我们如何确定这与我们想象的有关?
一个非常强大的从内核获取此类信息的工具是SystemTap。(使用此工具需要安装内核的调试符号。我碰巧正在使用Ubuntu,this是在Ubuntu上让SystemTap工作的方法。)
一旦您已经成功安装了SystemTap,您可以结合使用packet_mmap.c来使用SystemTap,以查看是否通过在内核函数tpacket_snd上安装探针并运行packet_mmap来发送一帧,从而调用了tpacket_snd()函数。请注意保留HTML标签。
$ 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会抱怨无法找到我们想要检查的变量,并且函数参数将打印出值为?ERROR。这是因为内核是使用优化编译的,并且所有AF_PACKET的功能都在单个翻译单元af_packet.c中定义;许多函数由编译器内联,有效地丢失了局部变量和参数。
为了从af_packet.c中获取更多信息,我们需要构建一个版本的内核,在该版本中,af_packet.c没有进行优化构建。请点击这里获取一些指导。我会等待。

好的,希望这不会太难,您已经成功启动了一个内核,SystemTap可以从中获取大量有用的信息。请记住,此内核版本仅帮助我们确定如何使用数据包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以及处理tx_ring中的数据包的do {...} while (...)循环中的某些点的探针。最后,我们添加了一个探针到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)

这里是网络抓包:

network capture of first run of packet_mmap

SystemTap输出中有很多有用的信息。我们可以看到tpacket_snd获取环中第一个帧的状态(TP_STATUS_SEND_REQUEST为1),然后将其设置为TP_STATUS_SENDING(2)。它对第二个帧执行相同的操作。下一个帧的状态为TP_STATUS_AVAILABLE(0),这不是发送请求,因此它调用schedule()进行让步,并继续循环。由于没有更多的帧需要发送(ph==NULL)并且已请求非阻塞(msg->msg_flags ==MSG_DONTWAIT),所以do {...} while (...)循环终止,tpacket_snd返回排队等待传输的字节数300

接下来,packet_mmap再次调用sendto(通过“循环直到队列为空”的代码),但在tx环中没有更多要发送的数据,并且请求非阻塞,因此它立即返回0,因为没有数据排队。请注意,它检查状态的帧与上一次调用时检查的帧相同---它没有从tx环中的第一帧开始,而是检查了head(在用户空间不可用)。
异步地,析构函数首先在第一帧上被调用,将帧的状态设置为TP_STATUS_AVAILABLE并递减挂起计数,然后在第二帧上被调用。请注意,如果没有请求非阻塞,则在do {...} while (...)循环的末尾进行的测试将等待所有挂起的数据包传输到NIC(假设它支持散布的数据)才返回。您可以通过使用“线程化”选项(使用阻塞I/O,直到到达“循环直到队列为空”)运行packet_mmap来观察这一点。
请注意以下几点。首先,SystemTap输出的时间戳不是递增的:不能从SystemTap输出中推断时间顺序是安全的。其次,请注意本地捕获的网络数据包的时间戳是不同的。顺便说一下,接口是一个廉价的1G塔式计算机。
所以目前为止,我认为我们更或多或少知道了af_packet如何处理共享的tx环。接下来的问题是tx环中的帧如何到达网络接口。可能有助于查看此部分(有关如何处理第2层传输)以及Linux网络内核控制流概述

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

首先,关于qdisc的工作原理有一个不太被人们所理解的事实。它们可以容纳一定量的数据(通常以帧数计算,但在某些情况下也可以以字节为单位),如果您尝试将帧排队到已满的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 脚本,然后让我们向具有仅 50 帧容量的 qdisc 发送 8096 个 1500 字节帧:

$ 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? 处理8096帧在tpacket_snd中花费了近100毫秒? 让我们检查一下实际传输需要多长时间; 这是以1千兆比特/秒的速度传输的1500字节/帧的8096帧,约为97毫秒。 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(事实证明这在4.1中已经得到修复)。发生的情况是qdisc的大小不是唯一的限制资源。如果为skb分配空间会超过套接字sndbuf限制的大小,则此调用将阻止等待skb被释放或对非阻塞呼叫者返回-EAGAIN。在V4.1中的修复中,如果请求非阻塞,则返回写入的字节数(如果非零),否则对呼叫者返回-EAGAIN,这似乎几乎像是有人不想让你弄清楚如何使用它(例如,你使用80MB的数据填充tx环,使用MSG_DONTWAIT调用sendto,你会得到一个结果,说明你发送了150KB而不是EWOULDBLOCK)。

如果你正在运行4.1之前的内核(我相信OP正在运行>4.1,并且不受此错误影响),则需要修补af_packet.c并构建新的内核或升级到4.1或更高版本的内核。
我现在启动了一个已修补的内核版本,因为我使用的机器运行3.13。虽然我们不会在sndbuf已满时阻止,但我们仍将返回-EAGAIN。我对packet_mmap.c进行了一些更改,增加了sndbuf的默认大小,并使用SO_SNDBUFFORCE来覆盖每个套接字的系统最大值(似乎每个帧需要大约750字节+帧大小)。我还添加了一些内容到call-return.stp,以记录sndbuf的最大大小(sk_sndbuf)、所使用的数量(sk_wmem_alloc)、sock_alloc_send_skb返回的任何错误以及将skb排队到qdisc时dev_queue_xmit返回的任何错误。这是新版本:
# 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环的帧被排入队列,直到它满为止,此时我们会得到返回值ENOBUFS
接下来的问题是如何有效地继续向qdisc发布以保持接口忙碌。请注意,在我们填满qdisc并收到ENOBUFS后,packet_poll的实现是无用的,因为它只查询头是否为TP_STATUS_AVAILABLE,在这种情况下,它将保持TP_STATUS_SEND_REQUEST,直到随后的sendto调用成功将帧排队到qdisc。一种简单的方法(在packet_mmap.c中更新)是循环执行sendto,直到成功或出现除ENOBUFSEAGAIN之外的错误。
总之,我们现在知道的比足以回答OP的问题要多得多,即使我们没有一个完整的解决方案来有效地防止NIC饥饿。
根据我们所了解的,当 OP 以阻塞模式调用带有 tx 环的 sendto 时,tpacket_snd 将开始将 skbs 排队到 qdisc 中,直到超过 sndbuf 限制(默认情况下通常很小,约为 213K,并且我发现在共享的 tx 环中引用的帧数据也计入其中),此时它将被阻塞(同时仍然持有 pg_vec_lock)。随着 skb 的释放,更多的帧将被排队,可能会再次超过 sndbuf 并再次阻塞。最终,所有数据都将被排队到 qdisc,但 tpacket_snd 将继续阻塞,直到所有帧都已传输(你不能将 tx 环中的帧标记为可用,直到 NIC 已经接收到它,因为驱动程序环中的 skb 引用了 tx 环中的帧),同时仍然持有 pg_vec_lock。此时,NIC 被饥饿,任何其他套接字编写器都被锁定。

另一方面,当发件人一次发布一个数据包时,它将由 packet_snd 处理。如果sndbuf中没有空间,则会阻塞并将帧排队到qdisc上,然后立即返回。它不会等待帧传输完成。随着qdisc的排空,可以排队添加其他帧。如果发布者跟得上,NIC将永远不会饥饿。

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


感谢你迄今为止的所有帮助,我现在正在构建一个带有调试符号和未经优化的 af_packet.c 版本的内核。在我们等待期间,只是一些思考食物。我没有在我的应用程序中使用 MSG_DONTWAIT 标志。我不尝试使用非阻塞调用,所以你上面的代码片段 sock_alloc_send_skb(x,x,0,x) - 即使我们已经发现了第三个参数传递不正确的 bug,它也应该是零,对吗? - jwbensley
2
@jwbensley 有一个套接字选项可以绕过QDISC层直接进行传输 无论您是发布到qdisc还是直接发布到NIC环,下一个问题是如何管理您的发布速度(到目前为止,您一直在意外地依赖阻塞来解决此问题)。我建议从qdsic开始,因为它很容易管理其大小(TXQUEUELEN),并且如果您发布得太快,则很容易查看诸如丢包等统计信息(至少如果您没有使用默认的pfifo_fast)。我认为驱动程序环大小是动态自适应的,但您可能能够控制其大小。这需要一些研究。 - JimD.
1
@jwbensley,这里有一些关于驱动程序队列的潜在有趣信息:https://www.coverfire.com/articles/queueing-in-the-linux-network-stack/ - JimD.
1
@jwbensley 虽然我们发现第三个参数没有正确传递的错误,但是它应该无论如何都应该是零对吧?我认为您正在运行已经修复这个问题的内核版本,是的,我现在明白如果要阻止操作,它应该为零,因为当sndbuf限制超过时,此操作会阻塞以等待空闲空间。 - JimD.
1
@jwbensley 我在这个回答的末尾总结了我认为阻塞 tx 环比不使用 tx 环更慢的原因。我已经没有空间了(30k 字符)。最有效的方法可能是非阻塞的(就像我在这个答案中所追求的那样)。 - JimD.
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接