Я пишу генератор трафика в 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