web-dev-qa-db-ja.com

PACKET_MMAPとPACKET_TX_RINGを使用したデータの送信が「通常」よりも遅い(なし)

CでPACKET_MMAPソケットオプションを使用してトラフィックジェネレータを作成し、Rawソケットを介してデータを送信するためのリングバッファを作成しています。リングバッファは送信するイーサネットフレームで満たされ、sendtoが呼び出されます。リングバッファの内容全体がソケットを介して送信されます。これにより、メモリにバッファを配置し、送信が必要なバッファ内のフレームごとにsendtoを繰り返し呼び出すよりも高いパフォーマンスが得られます。

PACKET_MMAPを使用しない場合、sendtoを呼び出すと、単一のフレームがユーザーランドメモリのバッファーからカーネルメモリのSK bufにコピーされ、カーネルはNICをDMAに送信し、NICにDMAにフレームを送信して、独自のハードウェアバッファに入れ、送信のためにキューに入れます。 PACKET_MMAPソケットオプションを使用する場合、マッピングされたメモリはアプリケーションによって割り当てられ、rawソケットにリンクされます。アプリケーションはmmappedバッファーにパケットを置き、sendtoを呼び出します。カーネルがパケットをSK bufにコピーする代わりに、mmappedバッファーから直接読み取ることができます。また、個々のパケット/フレームの代わりに、パケットの「ブロック」をリングバッファーから読み取ることができます。したがって、パフォーマンスの向上は、複数のフレームをコピーする1つのsys-callであり、それをNICハードウェアバッファーに入れる各フレームのコピーアクションが1つ少なくなります。

PACKET_MMAPを使用するソケットのパフォーマンスを「通常の」ソケット(単一のパケットが含まれるcharバッファー)と比較すると、パフォーマンス上の利点はまったくありません。これはなぜですか?TxモードでPACKET_MMAPを使用すると、各リングブロックに配置できるフレームは1つだけです(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を使用しているときにパフォーマンスの違いが見られない理由の1つは、ループバックインターフェイスへのトラフィック(これには、QDISCがありません)。 packet_tx_mmap()またはpacket_tx()ルーチンのいずれかを実行すると10Gbpsを超える可能性があり、自由に使用できるインターフェイスは10Gbpsしかないため、2つを結合しましたが、これらは結果であり、かなりの結果を示しています上記と同じように、2つの関数の速度の違いは最小限です。

packet_tx()〜20G bond0

  • 1スレッド:平均10.77Gbps〜/ 889kfps〜
  • 2スレッド:平均19.19Gbps〜/ 1.58Mfps〜
  • 3スレッド:平均19.67Gbps〜/ 1.62Mfps〜(これはボンドが行くのと同じくらい速いです)

packet_tx_mmap() to 20G bond0:

  • 1スレッド:平均11.08Gbps〜/ 913kfps〜
  • 2スレッド:平均19.0Gbps〜/ 1.57Mfps〜
  • 3スレッド:平均19.66Gbps〜/ 1.62Mfps〜(これはボンドが行くのと同じくらい速いです)

これは、サイズが1514バイトのフレームでした(上記の元のループバックテストと同じに保つため)。

上記のすべてのテストで、ソフトIRQの数はほぼ同じでした( このスクリプト を使用して測定)。 1つのスレッドがpacket_tx()を実行している場合、CPUコアで1秒あたり約40kの割り込みがありました。 2スレッドと3スレッドを実行すると、それぞれ2コアと3コアで40kが実行されます。 packet_tx_mmap()を使用した場合の結果は同じです。 1つのCPUコアのシングルスレッド用の約40kのソフトIRQ。 2スレッドと3スレッドを実行する場合、コアあたり40k。

更新2:完全なソースコード

完全なソースコードをアップロードしましたが、まだこのアプリケーションを作成しているため、おそらく多くの欠陥がありますが、これらはこの質問の範囲外です: https://github.com/jwbensley/EtherateMT =

23
jwbensley

Linuxカーネルへの多くのインターフェースは十分に文書化されていません。または、それらが十分に文書化されているように見えても、それらはかなり複雑になる可能性があり、そのため、インターフェースの機能的、または多くの場合さらに非機能的なプロパティが何であるかを理解するのが難しくなる可能性があります。

このため、カーネルAPIをしっかり理解したい、またはカーネルAPIを使用して高性能アプリケーションを作成する必要のある人への私のアドバイスは、成功するためにカーネルコードに関与できる必要があります。

この場合、質問者は、共有メモリインターフェイス(パケットmmap)を介してカーネルにrawフレームを送信するパフォーマンス特性を理解したいと考えています。

Linuxのドキュメントは here です。 「ハウツー」への古いリンクがあり、これは here にあり、packet_mmap.cのコピーが含まれています(使用可能なバージョンが少し異なります here

ドキュメントは、主に読み取りに向けられています。これは、パケットmmapを使用する一般的な使用例です。インターフェイスから生のフレームを効率的に読み取り、eg高速インターフェースからパケットキャプチャを効率的にほとんどまたはまったく取得しません。損失。

ただし、OPは高いパフォーマンス書き込みに関心があります。これはあまり一般的ではありませんが、OPがそれを実行したいように見えるトラフィックジェネレーター/シミュレーターに潜在的に役立ちます。ありがたいことに、「ハウツー」はすべてフレームの記述に関するものです。

それでも、これが実際にどのように機能するかについて提供される情報はほとんどなく、パケットmmapを使用するほうが、使用せずに1フレームずつ送信するよりも高速に見えない理由に関するOPの質問に答える明白な助けはありません。

ありがたいことに、カーネルソースはオープンソースであり、インデックスが作成されているので、ソースを参照して質問への回答を得ることができます。

関連するカーネルコードを見つけるために検索できるキーワードがいくつかありますが、PACKET_TX_RINGはこの機能に固有のソケットオプションとして際立っています。インターウェブで「PACKET_TX_RING linux cross reference」を検索すると、af_packet.cを含む少数の参照が見つかります。これは、少し調べれば、パケットmmapを含むすべてのAF_PACKET機能の実装のようです。

af_packet.cを見ると、パケットmmapを使用して送信する作業の中心はtpacket_snd()で行われているようです。しかし、これは正しいですか?これが私たちの考えていることと何か関係があるかどうかはどうすればわかりますか?

カーネルからこのような情報を取得するための非常に強力なツールは SystemTap です。 (これを使用するには、カーネルにデバッグシンボルをインストールする必要があります。私はたまたまUbuntuを使用しています。 this は、SystemTapをUbuntuで動作させるためのレシピです。)

SystemTapが機能したら、packet_mmap.cと組み合わせてSystemTapを使用して、カーネル関数tpacket_sndにプローブをインストールし、packet_mmapを実行して共有TXリングを介してフレームを送信することで、tpacket_snd()が呼び出されるかどうかを確認できます。

$ 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);
}

これは、関数(マイクロ秒の解像度でUNIXエポック時間を出力するprint_ts)といくつかのプローブを定義します。

最初に、tx_ring内のパケットのステータスが設定または読み取られたときに情報を出力するプローブを定義します。次に、tpacket_sndの呼び出しと戻り、およびdo {...} while (...)ループ内のポイントでtx_ringのパケットを処理するためのプローブを定義します。最後に、skbデストラクタにプローブを追加します。

SystemTapスクリプトをSudo stap status.stpで開始できます。次に、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 is 1)、それをTP_STATUS_SENDING(2)に設定するのを確認できます。 2番目の場合も同じです。次のフレームのステータスはTP_STATUS_AVAILABLE(0)であり、これは送信要求ではないため、schedule()を呼び出して生成し、ループを続行します。送信するフレームがなくなったため(ph==NULL)、非ブロッキングが要求された(msg->msg_flags ==MSG_DONTWAIT )ため、do {...} while (...)ループは終了し、tpacket_snd300を返します。キューに入れられたバイト数伝染;感染。

次に、packet_mmapsendtoを再度呼び出します(「キューが空になるまでループする」コードを介して)が、txリングで送信するデータがなく、非ブロッキングが要求されているため、データがキューに入れられていないため、すぐに0を返します。 。ステータスを確認したフレームは、前回の呼び出しで最後に確認したフレームと同じであることに注意してください。txリングの最初のフレームではなく、headを確認しました(ユーザーランドでは利用できません)。

非同期的に、最初に最初のフレームでデストラクタが呼び出され、フレームのステータスがTP_STATUS_AVAILABLEに設定されて保留中のカウントが減らされ、次に2番目のフレームで呼び出されます。ノンブロッキングが要求されなかった場合、do {...} while (...)ループの最後のテストは、保留中のパケットがすべてNICに転送されるまで待機します(分散データをサポートしていると想定) )戻る前に。これを見るには、ブロッキングI/Oを使用する「スレッド化された」に対してpacket_mmapオプションを指定して-tを実行します(「キューが空になるまでループする」まで)。

注意すべき点がいくつかあります。まず、SystemTap出力のタイムスタンプは増加していません。SystemTapの出力から時間的な順序を推測するのは安全ではありません。次に、ネットワークキャプチャ(ローカルで行われる)のタイムスタンプが異なることに注意してください。 FWIW、インターフェイスは安価なタワーコンピュータの安価な1Gです。

したがって、この時点で、af_packetが共有txリングをどのように処理しているか、多かれ少なかれ知っていると思います。次に来るのは、txリング内のフレームがネットワークインターフェイスに到達する方法を見つける方法です。 Linuxネットワークカーネルの制御フローの 概要このセクション (レイヤー2送信の処理方法について)を確認すると役立つ場合があります。

よし、レイヤー2の送信がどのように処理されるかを基本的に理解している場合、このパケットmmapインターフェイスは巨大な消防ホースであるように思われるでしょう。共有txリングをパケットでロードし、MSG_DONTWAITを指定してsendto()を呼び出します。その後、tpacket_sndがtxキューを反復処理してskbを作成し、qdiscにエンキューします。非同期で、skbはqdiscからデキューされ、ハードウェア送信リングに送信されます。 skbは non-linear である必要があるため、コピーではなくtxリング内のデータを参照し、Nice modern 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スクリプトtpacket_sndcall-return.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);
}

# 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フレーム容量で8096 1500バイトフレームをその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 

[〜#〜]ワット[〜#〜] ? 50フレームのqdiscにダンプされた8096フレームの1つをドロップしましたか? 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)

[〜#〜]ワット[〜#〜]tpacket_sndで8096フレームを処理するには、ほぼ100msかかりましたか?実際に送信にかかる時間を確認してみましょう。これは、1ギガビットで1,500バイト/フレームの8096フレームで、約97ミリ秒です。 [〜#〜]ワット[〜#〜] ?何かが詰まっているようなにおいがします。

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を返します。これは、誰かがこれを使用する方法を理解したくないようです。 (eg txリングを80MBのデータで満たし、sendtoをMSG_DONTWAITで呼び出し、EWOULDBLOCKではなく150KBを送信したという結果を返します)。

したがって、4.1より前のカーネルを実行している場合(OPは4.1を超えており、このバグの影響を受けないと思います)、af_packet.cにパッチを適用して新しいカーネルをビルドするか、カーネル4.1以降にアップグレードする必要があります。

私が使用しているマシンは3.13を実行しているので、パッチを当てたバージョンのカーネルを起動しました。 sndbufがいっぱいの場合はブロックしませんが、それでも-EAGAINで戻ります。 packet_mmap.cにいくつかの変更を加えて、sndbufのデフォルトサイズを増やし、SO_SNDBUFFORCEを使用して、必要に応じてソケットごとのシステムの最大値をオーバーライドしました(約750バイト+各フレームのフレームサイズが必要と思われます)。また、sndbufの最大サイズ(call-return.stp)、使用量(sk_sndbuf)、sk_wmem_allocから返されたエラー、およびskbをqdiscにエンキューしたときにsock_alloc_send_skbから返されたエラーをログに記録するために、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リングからのフレームがいっぱいになるまでqdiscにエンキューされます。 、その時点でENOBUFSが返されます。

次の問題は、インターフェースをビジー状態に保つために、qdiscへのパブリッシュを効率的に維持する方法です。 packet_pollの実装は、qdiscを埋めてENOBUFSを取得する場合には役に立たないことに注意してください。これは、ヘッドがTP_STATUS_AVAILABLEであるかどうかを照会するだけであり、この場合、sendtoへの後続の呼び出しがキューイングに成功するまでTP_STATUS_SEND_REQUESTのままです。 qdiscへのフレーム。簡単な方法(packet_mmap.cで更新)は、成功するか、ENOBUFSまたはEAGAIN以外のエラーが発生するまでsendtoでループすることです。

とにかく、NICが効率的に不足しないようにする完全なソリューションがない場合でも、OPの質問に答えるには十分以上の知識があります。

私たちが学んだことから、OPがブロッキングモードでtxリングを使用してsendtoを呼び出すと、tpacket_sndは、sndbufの制限を超えるまでskbをqdiscにエンキューし始めます(そして、デフォルトは一般に非常に小さく、約213Kであり、さらに、 (pg_vec_lockを保持したまま)ブロックするときに、共有txリングで参照されるフレームデータがこれにカウントされることを発見しました。 skbが解放されると、さらに多くのフレームがキューに入れられ、おそらくsndbufが再び超過して、再びブロックされます。最終的に、すべてのデータはqdiscのキューに入れられますが、tpacket_sndすべてのフレームが送信されるまでブロックされ続けます(_になるまで、txリングのフレームを使用可能としてマークできませんNICはそれを受信しました。ドライバリングのskbがtxリングのフレームを参照しているため)、pg_vec_lockを保持したままです。この時点でNICは不足しており、他のソケットライターはロックによってブロックされています。

一方、OPが一度にパケットをパブリッシュすると、パケットはpacket_sndによって処理され、sndbufにスペースがない場合はブロックされ、フレームがqdiscにエンキューされ、すぐに返されます。フレームが送信されるのを待ちません。 qdiscが排出されると、追加のフレームをキューに入れることができます。パブリッシャーが追いつくことができれば、NICが決して飢えることはありません。

さらに、opはすべてのsendto呼び出しについてtxリングにコピーし、txリングを使用していないときに固定フレームバッファーを渡すことと比較しています。そのようにコピーしないことによるスピードアップは見られません(それがtxリングを使用する唯一の利点ではありません)。

29
Jim D.