Отправка данных с помощью PACKET_MMAP и PACKET_TX_RING происходит медленнее, чем "нормальный" (без)

Я пишу генератор трафика в C с помощью опции сокета PACKET_MMAP для создания кольцевого буфера для отправки данных по сырому сокету. Буфер звонка заполняется кадрами Ethernet для отправки и вызывается sendto. Все содержимое кольцевого буфера отправляется через сокет, который должен обеспечивать более высокую производительность, чем наличие буфера в памяти, и многократно называть sendto для каждого кадра в буфере, который нуждается в отправке.

Если вы не используете PACKET_MMAP, при вызове sendto один кадр копируется из буфера в памяти пользовательского пространства в буфер SK в памяти ядра, тогда ядро ​​должно скопировать пакет в память, к которому обращается NIC для DMA и сигнализировать NIC о DMA кадре в его собственные аппаратные буферы и поставить его в очередь для передачи. При использовании опции сокета PACKET_MMAP mmapped memory выделяется приложением и привязывается к необработанному сокету. Приложение помещает пакеты в mmapped-буфер, вызывает sendto, и вместо того, чтобы ядро ​​должно было копировать пакеты в SK buf, оно может напрямую их считывать из mmapped-буфера. Также "блоки" пакетов могут считываться из кольцевого буфера вместо отдельных пакетов/кадров. Таким образом, увеличение производительности - это один sys-вызов для копирования нескольких кадров и еще одно действие копирования для каждого кадра, чтобы получить его в аппаратных буферах NIC.

Когда я сравниваю производительность сокета с помощью PACKET_MMAP с "обычным" сокетом (буфером char с одним пакетом в нем), вообще-то не приносит пользы. Почему это? При использовании PACKET_MMAP в режиме Tx в каждый кольцевой блок может быть помещен только один кадр (а не несколько кадров на один кольцевой блок, как в режиме Rx), однако я создаю 256 блоков, поэтому мы должны отправлять 256 кадры в одном правильном вызове sendto?

Производительность с PACKET_MMAP, main() вызывает packet_tx_mmap():

[email protected]:~/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():

[email protected]:~/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 намного быстрее, поскольку я понимаю, что должно быть гораздо меньше sys-вызовов и копий?

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, заключалась в том, что я отправлял трафик на интерфейс loopback (что, с одной стороны, не имеет QDISC). Поскольку запуск любой из подпрограмм packet_tx_mmap() или packet_tx() может генерировать более 10 Гбит/с, и у меня есть только 10 Гбит/с интерфейсы в моем распоряжении, я связал два вместе, и это результаты, которые показывают почти то же самое, что и выше, минимальные разность между двумя функциями:

packet_tx() до 20G bond0

  • 1 thread: Average 10.77Gbps ~/889kfps ~
  • 2 потока: средний 19.19Gbps ​​~/1.58Mfps ~
  • 3 потока: средний 19.67Gbps ~/1.62Mfps ~ (это как быстро, как облигация будет идти)

packet_tx_mmap() до 20G bond0:

  • 1 поток: средний 11.08Gbps ~/913kfps ~
  • 2 потока: средний 19.0 Гбит/с ~/1.57Mfps ~
  • 3 потока: средний 19.66Gbps ~/1.62Mfps ~ (это как быстро, как облигация будет идти)

Это было с кадрами размером 1514 байт (чтобы они были такими же, как и исходные тесты на loopback выше).

Во всех вышеперечисленных тестах количество мягких IRQ было примерно одинаковым (измерено с помощью этого script). С одним потоком, выполнявшимся packet_tx(), в ядре процессора было около 40 тыс. Прерываний в секунду. С 2 и 3 потоками, работающими там 40k на 2 и 3 ядрах соответственно. Результаты при использовании packet_tx_mmap(), где то же самое. Около 40k мягких IRQ для одного потока на одном ядре процессора. 40k на ядро ​​при запуске 2 и 3 потоков.

Обновление 2: Полный исходный код

Я загрузил полный исходный код сейчас, я все еще пишу это приложение, поэтому у него, вероятно, много недостатков, но они не входят в сферу этого вопроса: https://github.com/jwbensley/EtherateMT

Ответы

Ответ 1

Многие интерфейсы к ядру Linux плохо документированы. Даже если они выглядят хорошо документированными, они могут быть довольно сложными, и это может затруднить понимание того, что функциональные или, зачастую, даже более сложные, нефункциональные свойства интерфейса.

По этой причине мой совет любому, кто хочет твердо понимать API ядра или должен создавать высокопроизводительные приложения с использованием API-интерфейсов ядра, должен иметь возможность взаимодействовать с кодом ядра, чтобы быть успешным.

В этом случае вопросник хочет понять характеристики производительности отправки необработанных кадров через интерфейс общей памяти (пакетный mmap) в ядро.

Документация по linux здесь. Он имеет устаревшую ссылку на "как", который теперь можно найти здесь и включает в себя копию packet_mmap.c (у меня есть немного доступна другая версия здесь.

Документация в значительной степени ориентирована на чтение, что является типичным прецедентом использования пакета mmap: эффективное считывание необработанных кадров из интерфейса для, например, эффективно получать захват пакетов с высокоскоростного интерфейса с небольшой потерей или без потерь.

Однако OP заинтересован в высокопроизводительной записи, которая является гораздо менее распространенным вариантом использования, но потенциально полезна для генератора трафика/симулятора, который, по-видимому, стремится к тому, что OP хочет сделать с ним. К счастью, "как" все о написании кадров.

Тем не менее, очень мало информации о том, как это работает, и ничего очевидного ответа на вопрос OP о том, почему использование пакета mmap, похоже, не быстрее, чем не использовать его, и вместо этого отправляет один кадр в время.

К счастью, исходный код ядра с открытым исходным кодом и хорошо проиндексирован, поэтому мы можем обратиться к источнику, чтобы помочь нам получить ответ на вопрос.

Чтобы найти соответствующий код ядра, есть несколько ключевых слов, которые вы могли бы искать, но PACKET_TX_RING выделяется как опция сокета, уникальная для этой функции. Поиск в interwebs для "PACKET_TX_RING linux cross reference" приводит к небольшому количеству ссылок, включая af_packet.c, который с небольшой проверкой представляется реализацией всех функций AF_PACKET, включая пакетный mmap.

Просматривая af_packet.c, кажется, что ядро ​​работы для передачи с пакетом mmap происходит в tpacket_snd(). Но верно ли это? Как мы можем сказать, имеет ли это какое-либо отношение к тому, что мы думаем?

Очень мощный инструмент для получения такой информации из ядра - SystemTap. (Для этого требуется установить отладочные символы для вашего ядра. Я использую Ubuntu, а это - это рецепт для работы SystemTap на Ubuntu.)

Как только вы работаете с SystemTap, вы можете использовать SystemTap в сочетании с packet_mmap.c, чтобы узнать, даже если tpacket_snd() даже вызывается, установив пробник на функцию ядра tpacket_snd, а затем запустив 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 будет жаловаться, что он не сможет найти переменные, которые мы хотим проверить, а аргументы функции будут распечатываться со значениями как ? или 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("[email protected]/packet/af_packet.c:334") {
  print_ts();
  printf("SET(V1): %d (0x%.16x)\n", $status, $frame);
}

probe kernel.statement("[email protected]/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("[email protected]/packet/af_packet.c:358") { 
  print_ts();
  printf("GET(V1): %d (0x%.16x)\n", $h->h1->tp_status, $frame); 
}

probe kernel.statement("[email protected]/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("[email protected]/packet/af_packet.c:2140") {
  print_ts();
  printf("tpacket_snd:2140: current frame ph = 0x%.16x\n", $ph);
}

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

probe kernel.statement("[email protected]/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("[email protected]/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("[email protected]/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("[email protected]/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 и в точках цикла do {...} while (...), обрабатывающих пакеты в tx_ring. Наконец, мы добавляем зонд в деструктор skb.

Мы можем запустить SystemTap script с помощью sudo stap status.stp. Затем запустите sudo packet_mmap -c 2 <interface>, чтобы отправить 2 кадра через интерфейс. Вот результат, который я получил из SystemTap script:

[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)

И вот сетевой захват:

сетевой захват первого запуска пакета_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 (через код "loop until queue empty" ), но больше нет данных для отправки в tx-кольцо, и запрашивается неблокировка, поэтому он немедленно возвращает 0, поскольку никакие данные не были поставлены в очередь. Обратите внимание, что кадр, в котором он проверял статус, - это тот же самый кадр, который он проверил последним в предыдущем вызове, - он не начинался с первого кадра в кольце tx, он проверял head (который недоступен в пользовательской области).

Асинхронно, деструктор вызывается сначала в первом кадре, устанавливая статус кадра на TP_STATUS_AVAILABLE и уменьшая ожидающий счет, а затем на втором кадре. Обратите внимание, что если неблокирование не запрашивалось, тест в конце цикла do {...} while (...) будет ждать, пока все возвращенные пакеты не будут перенесены в NIC (при условии, что он поддерживает разбросанные данные) перед возвратом. Вы можете посмотреть это, запустив packet_mmap с опцией -t для "threaded", которая использует блокировку ввода-вывода (до тех пор, пока она не завершится до "loop до очереди пуста" ).

Несколько замечаний. Во-первых, временные метки на выходе SystemTap не увеличиваются: небезопасно выводить временный порядок из выпадающего меню SystemTap. Во-вторых, обратите внимание, что метки времени при захвате сети (выполняются локально) различны. FWIW, интерфейс дешевый 1G в дешевом башенном компьютере.

Итак, на данный момент, я думаю, мы более или менее знаем, как AF_PACKET обрабатывает общее tx-кольцо. Далее следует, как кадры в кольце tx находят свой путь к сетевому интерфейсу. Возможно, было бы полезно рассмотреть этот раздел (о том, как обрабатывается передача 2-го уровня) в потоке управления в сетевом ядре linux.

ОК, поэтому, если у вас есть базовое понимание того, как обрабатывается передача 2-го уровня, похоже, что этот интерфейс mmap пакета должен быть огромным пожарным шлангом; загрузите общее tx-кольцо с пакетами, вызовите sendto() с помощью MSG_DONTWAIT, а затем tpacket_snd будет перебирать очередь tx, создавая skb и размещая их на qdisc. Асинхронно, skb будет выгружен из qdisc и отправлен на аппаратное кольцо tx. Skb должен быть нелинейнымпоэтому они будут ссылаться на данные в кольце tx, а не на копирование, а красивый современный сетевой адаптер должен иметь возможность обрабатывать разбросанные данные и ссылаться на данные в tx-кольцах. Конечно, любое из этих предположений может быть неправильным, поэтому давайте попытаемся свалить много повреждений на qdisc с помощью этого пожарного шланга.

Но во-первых, не совсем понятный факт о том, как работают qdiscs. Они содержат ограниченный объем данных (как правило, подсчитывается по количеству кадров, но в некоторых случаях он может быть измерен в байтах), и если вы попытаетесь вставить рамку в полный qdisc, кадр вообще будет отброшен (в зависимости от того, что enqueuer решает сделать). Поэтому я дам подсказку, что моя первоначальная гипотеза заключалась в том, что 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 

Пусть также измеряет, сколько времени требуется для обработки кадров в tpacket_snd с помощью SystemTap script call-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("[email protected]/packet/af_packet.c:2213") {
  print_ts();
  printf("tpacket_snd: return(%d)\n", $err);
}

Запустите SystemTap script с помощью sudo stap call-return.stp, а затем включите 8096 1500 байтовых кадров в этот qdisc с малой мощностью кадра 50:

$ 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? Потребовалось около 100 мс для обработки 8096 кадров в tpacket_snd? Позвольте проверить, сколько времени потребуется на передачу; что 8096 кадров при 1500 байт/кадр при 1gigabit/s ~ = 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 для вызывающего, что почти похоже на то, что кто-то не хочет, чтобы вы выяснили, как используйте это (например, вы заполняете кольцо tx с 80 МБ данных, вызываете sendto с MSG_DONTWAIT, и вы возвращаете результат, который вы отправили 150 КБ, а не 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 max (sk_sndbuf), используемой суммы (sk_wmem_alloc), любой ошибки, возвращаемой sock_alloc_send_skb, и любой ошибки, возвращенной из dev_queue_xmit при вставке skb в qdisc. Вот новая версия:

# 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 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("[email protected]/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("[email protected]/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("[email protected]/packet/af_packet.c:2182") {
  if ($err != 0) {
    print_ts();
    printf("tpacket_snd:2182: dev_queue_xmit err = %d\n", $err);
  }
}

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

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

probe kernel.statement("[email protected]/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, которая в этом случае останется TP_STATUS_SEND_REQUEST, пока не будет последующий вызов sendto преуспевает в очередности кадра в qdisc. Простая целесообразность (обновленная в пакете_mmap.c) заключается в том, чтобы зацикливать на sendto до достижения успеха или ошибки, отличной от ENOBUFS или EAGAIN.

В любом случае, мы знаем, что более чем достаточно, чтобы ответить на вопрос OPs сейчас, даже если у нас нет полного решения для эффективного предотвращения сбоя сетевого адаптера.

Из того, что мы узнали, мы знаем, что когда OP вызывает sendto с tx-кольцом в режиме блокировки, tpacket_snd запустит enqueuing skbs на qdisc до тех пор, пока предел sndbuf не будет превышен (а значение по умолчанию обычно невелико, 213K, и, кроме того, я обнаружил, что данные кадра, на которые ссылаются в общем tx-кольце, подсчитываются по этому), когда он будет блокироваться (при сохранении pg_vec_lock). Поскольку skb освобождается, больше кадров будет выставлено в очередь, и, возможно, sndbuf будет снова превышен, и мы снова заблокируем. В конце концов все данные будут поставлены в очередь на qdisc, но tpacket_snd будет продолжать блокироваться до тех пор, пока все кадры не будут переданы (вы не можете пометить кадр в кольце tx как доступно до тех пор, пока NIC не получит его, поскольку skb в кольце драйвера ссылается на кадр в кольце tx), сохраняя при этом pg_vec_lock. На данный момент NIC голодает, и любые другие производители сокетов заблокированы блокировкой.

С другой стороны, когда OP публикует пакет за раз, он обрабатывается packet_snd, который будет блокироваться, если в sndbuf нет места, а затем помещает кадр в qdisc и немедленно возвращается. Он не ждет передачи кадра. Когда qdisc сливается, дополнительные кадры могут быть выставлены в очередь. Если издатель может идти в ногу, сетевой адаптер никогда не будет голодать.

Кроме того, op копирует в tx-кольцо для каждого вызова sendto и сравнивает это с передачей буфера фиксированного кадра, когда не используется tx-кольцо. Вы не увидите ускорения от того, чтобы не копировать этот путь (хотя это не единственное преимущество использования кольца tx).