El envío de datos con PACKET_MMAP y PACKET_TX_RING es más lento que "normal"(sin)
performance sockets (1)
Estoy escribiendo un generador de tráfico en C usando la opción de socket PACKET_MMAP para crear un búfer de anillo para enviar datos a través de un socket en bruto. El búfer de anillo se llena con tramas de Ethernet para enviar y se envía sendto
. El contenido completo del búfer de anillo se envía a través del zócalo, lo que debería proporcionar un mayor rendimiento que tener un búfer en la memoria y llamar a sendto
repetidamente para cada trama en el búfer que se debe enviar.
Cuando no se usa PACKET_MMAP, al enviar el envío a un solo cuadro se copia desde el búfer en la memoria de la zona de usuario a un SK buf en la memoria del kernel, entonces el kernel debe copiar el paquete a la memoria a la que accede la NIC para DMA y señalar la NIC a DMA el marco en sus propios buffers de hardware y en cola para su transmisión. Cuando se utiliza la opción PACKET_MMAP, la memoria asignada a mmapped es asignada por la aplicación y vinculada al socket sin formato. La aplicación coloca los paquetes en el búfer de mmapped, llama a sendto
y en lugar de que el Kernel tenga que copiar los paquetes en un búfer SK, puede leerlos directamente desde el búfer de mmapped. También se pueden leer "bloques" de paquetes desde el búfer de anillo en lugar de paquetes / marcos individuales. Por lo tanto, el aumento de rendimiento es una llamada al sistema para copiar varios marcos y una acción de copia menos para que cada marco ingrese a los buffers de hardware NIC.
Cuando estoy comparando el rendimiento de un socket utilizando PACKET_MMAP con un socket "normal" (un búfer de caracteres con un solo paquete) no hay ningún beneficio en el rendimiento. ¿Por qué es esto? Al usar PACKET_MMAP en modo Tx, solo se puede poner un cuadro en cada bloque de anillo (en lugar de múltiples cuadros por bloque de anillo como en el modo Rx). Sin embargo, estoy creando 256 bloques, por lo que deberíamos enviar 256 cuadros en un solo envío, ¿no?
Rendimiento con PACKET_MMAP, main()
llama a 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
Rendimiento sin PACKET_MMAP, main()
llama a 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
La función packet_tx()
es ligeramente más rápida que la función packet_tx_mmap()
, pero también es un poco más corta, así que creo que el aumento mínimo de rendimiento es simplemente las líneas de código presentes en packet_tx
. Me parece que ambas funciones tienen prácticamente el mismo rendimiento, ¿por qué? ¿Por qué PACKET_MMAP no es mucho más rápido, ya que entiendo que debería haber muchas menos llamadas y copias de sistema?
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;
}
Tenga en cuenta que la siguiente función llama a setup_socket()
y no a 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;
}
}
La única diferencia en las funciones de configuración del zócalo se pega a continuación, pero esencialmente son los requisitos para configurar un SOCKET_RX_RING o 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;
}
}
Actualización 1: Resultado contra la (s) interfaz (es) física (s) Se mencionó que una razón por la que podría no estar viendo una diferencia de rendimiento al usar PACKET_MMAP fue porque estaba enviando tráfico a la interfaz de bucle invertido (que, por un lado, no tiene un QDISC ). Dado que la ejecución de cualquiera de las packet_tx_mmap()
o packet_tx()
puede generar más de 10Gbps y solo tengo interfaces de 10Gbps a mi disposición, he unido dos y estos son los resultados, que muestran casi lo mismo que arriba, hay un mínimo Diferencia de velocidad entre las dos funciones:
packet_tx()
a 20G bond0
- 1 hilo: Promedio 10.77Gbps ~ / 889kfps ~
- 2 subprocesos: Promedio 19.19Gbps ~ / 1.58Mfps ~
- 3 subprocesos: Promedio de 19.67 Gbps ~ / 1.62Mfps ~ (esto es tan rápido como irá el enlace)
packet_tx_mmap()
a 20G bond0:
- 1 hilo: Promedio 11.08Gbps ~ / 913kfps ~
- 2 hilos: Promedio 19.0Gbps ~ / 1.57Mfps ~
- 3 subprocesos: Promedio 19.66Gbps ~ / 1.62Mfps ~ (esto es tan rápido como el enlace se ejecutará)
Esto fue con marcos de 1514 bytes de tamaño (para mantenerlo igual que las pruebas de bucle de retorno originales anteriores).
En todas las pruebas anteriores, el número de IRQ suaves fue aproximadamente el mismo (medido utilizando este script ). Con un hilo ejecutando packet_tx()
hubo alrededor de 40k interrupciones por segundo en un núcleo de CPU. Con 2 y 3 hilos corriendo allí 40k en 2 y 3 núcleos respectivamente. Los resultados cuando se usa packet_tx_mmap()
iguales. Circa 40k IRQs suaves para un solo hilo en un núcleo de CPU. 40k por núcleo cuando se ejecutan 2 y 3 hilos.
Actualización 2: Código fuente completo
He cargado el código fuente completo ahora, todavía estoy escribiendo esta aplicación, por lo que probablemente tenga muchas fallas, pero están fuera del alcance de esta pregunta: https://github.com/jwbensley/EtherateMT
Muchas interfaces para el kernel de Linux no están bien documentadas. O incluso si parecen estar bien documentados, pueden ser bastante complejos y eso puede dificultar la comprensión de cuáles son las propiedades funcionales o, a menudo, aún más difíciles, de la interfaz.
Por esta razón, mi consejo para cualquier persona que desee una comprensión sólida de las API del kernel o que necesite crear aplicaciones de alto rendimiento utilizando las API del kernel debe poder participar con el código del kernel para tener éxito.
En este caso, el interrogador desea comprender las características de rendimiento del envío de marcos en bruto a través de una interfaz de memoria compartida (paquete mmap) al kernel.
La documentación de linux está here . Tiene un enlace a un "cómo", que ahora se puede encontrar here e incluye una copia de packet_mmap.c
(Tengo una versión ligeramente diferente disponible here .
La documentación está orientada en gran medida a la lectura, que es el caso de uso típico del uso de paquetes mmap: leer de manera eficiente marcos sin procesar desde una interfaz, por ejemplo , obtener eficientemente una captura de paquetes de una interfaz de alta velocidad con poca o ninguna pérdida.
Sin embargo, el OP está interesado en la escritura de alto rendimiento, que es un caso de uso mucho menos común, pero potencialmente útil para un simulador / generador de tráfico, que parece ser lo que el OP quiere hacer con él. Afortunadamente, el "cómo" se trata de escribir marcos.
Aun así, se proporciona muy poca información acerca de cómo funciona esto realmente, y nada de ayuda obvia para responder a la pregunta de OP sobre por qué el uso de paquetes mmap no parece ser más rápido que no usarlo y, en su lugar, enviar un cuadro a la vez.
Afortunadamente, la fuente del núcleo es de código abierto y está bien indexada, por lo que podemos recurrir a la fuente para ayudarnos a obtener la respuesta a la pregunta.
Para encontrar el código de kernel relevante, puede buscar varias palabras clave, pero PACKET_TX_RING
destaca como una opción de socket exclusiva de esta función. Al buscar entre las interwebs "PACKET_TX_RING linux cross reference" aparece un pequeño número de referencias, incluyendo af_packet.c
, que con una pequeña inspección parece ser la implementación de toda la funcionalidad AF_PACKET
, incluido el paquete mmap.
Mirando a través de af_packet.c
, parece que el núcleo del trabajo para transmitir con el paquete mmap tiene lugar en tpacket_snd()
. Pero es esto correcto? ¿Cómo podemos saber si esto tiene algo que ver con lo que creemos que tiene?
Una herramienta muy poderosa para obtener información como esta del núcleo es SystemTap . (El uso de esto requiere la instalación de símbolos de depuración para su kernel. Estoy usando Ubuntu, y this es una receta para hacer que SystemTap funcione en Ubuntu).
Una vez que tiene SystemTap funcionando, puede usar SystemTap en combinación con packet_mmap.c
para ver si se tpacket_snd()
instalando una sonda en la función del kernel tpacket_snd
, y luego ejecutando packet_mmap
para enviar un marco a través de un anillo de TX compartido:
$ 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! Estamos en algo; tpacket_snd
está siendo llamado. Pero nuestra victoria será de corta duración. Si continuamos tratando de obtener más información de una compilación de kernel de stock, SystemTap se quejará de que no puede encontrar las variables que queremos inspeccionar y los argumentos de función se imprimirán con valores como ?
o ERROR
. Esto se debe a que el kernel se compila con optimización y toda la funcionalidad para AF_PACKET
se define en la unidad de traducción única af_packet.c
; Muchas de las funciones están integradas por el compilador, perdiendo efectivamente variables y argumentos locales.
Para extraer más información de af_packet.c
, vamos a tener que compilar una versión del kernel donde af_packet.c
se construya sin optimización. Busque here alguna orientación. Esperaré.
Bien, espero que no haya sido tan difícil y que haya arrancado con éxito un kernel del que SystemTap puede obtener mucha información buena. Tenga en cuenta que esta versión del núcleo es solo para ayudarnos a descubrir cómo funciona el paquete mmap. No podemos obtener ninguna información de rendimiento directo de este núcleo porque af_packet.c
se af_packet.c
sin optimización. Si resulta que necesitamos obtener información sobre cómo se comportaría la versión optimizada, podemos crear otro kernel con af_packet.c
compilado con optimización, pero con algún código de instrumentación agregado que exponga la información a través de variables que no se optimizarán. que SystemTap puede verlos.
Así que vamos a usarlo para obtener alguna información. Echa un vistazo a 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);
}
Esto define una función ( print_ts
para imprimir el tiempo de la época de Unix con una resolución de microsegundos) y una serie de sondas.
Primero, definimos sondas para imprimir información cuando los paquetes en tx_ring tienen su estado establecido o leído. A continuación, definimos las sondas para la llamada y el retorno de tpacket_snd
y en los puntos dentro del do {...} while (...)
procesamos los paquetes en el tx_ring. Finalmente agregamos una sonda al destructor skb.
Podemos iniciar el script SystemTap con sudo stap status.stp
. Luego ejecute sudo packet_mmap -c 2 <interface>
para enviar 2 marcos a través de la interfaz. Aquí está la salida que obtuve del script 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)
Y aquí está la captura de red:
Hay mucha información útil en la salida de SystemTap. Podemos ver tpacket_snd
obtener el estado del primer fotograma en el anillo ( TP_STATUS_SEND_REQUEST
es 1) y luego establecerlo en TP_STATUS_SENDING
(2). Hace lo mismo con el segundo. El siguiente fotograma tiene el estado TP_STATUS_AVAILABLE
(0), que no es una solicitud de envío, por lo que llama a schedule()
para ceder, y continúa el ciclo. Dado que no hay más marcos para enviar ( ph==NULL
) y se ha solicitado el bloqueo ( msg->msg_flags ==
MSG_DONTWAIT
) el MSG_DONTWAIT
do {...} while (...)
termina, y tpacket_snd
devuelve 300
, el número de bytes en cola para la transmisión.
A continuación, packet_mmap
llama a packet_mmap
nuevamente (a través del código "bucle hasta que la cola está vacía"), pero no hay más datos para enviar en el timbre tx, y se solicita el no bloqueo, por lo que inmediatamente devuelve 0, ya que no hay datos en cola . Tenga en cuenta que el cuadro en el que se verificó el estado es el mismo cuadro en el que se verificó por última vez en la llamada anterior --- no comenzó con el primer cuadro en el anillo tx, verificó la head
(que no está disponible en la zona de usuario).
Asincrónicamente, se llama al destructor, primero en el primer cuadro, estableciendo el estado del cuadro en TP_STATUS_AVAILABLE
y disminuyendo el conteo pendiente, y luego en el segundo cuadro. Tenga en cuenta que si no se solicitó el no bloqueo, la prueba al final del ciclo do {...} while (...)
esperará hasta que todos los paquetes pendientes se hayan transferido a la NIC (asumiendo que admite datos dispersos). ) antes de volver. Puede ver esto ejecutando packet_mmap
con la opción -t
para "subprocesos", que utiliza el bloqueo de E / S (hasta que llegue a "bucle hasta que la cola esté vacía").
Un par de cosas a anotar. Primero, las marcas de tiempo en la salida de SystemTap no aumentan: no es seguro inferir el orden temporal de la salida de SystemTap. En segundo lugar, tenga en cuenta que las marcas de tiempo en la captura de red (realizadas localmente) son diferentes. FWIW, la interfaz es un 1G barato en una computadora de torre barata.
Entonces, en este punto, creo que más o menos sabemos cómo af_packet
está procesando el timbre de tx compartido. Lo que viene a continuación es cómo los marcos en el anillo tx encuentran su camino hacia la interfaz de red. Podría ser útil revisar esta sección (sobre cómo se maneja la transmisión de la capa 2) de una overview del flujo de control en el kernel de red de Linux.
De acuerdo, si tiene una comprensión básica de cómo se maneja la transmisión de la capa 2, parecería que esta interfaz de mmap de paquete debería ser una enorme manguera de incendios; cargue un anillo tx compartido con paquetes, llame a sendto()
con MSG_DONTWAIT
, y luego tpacket_snd
recorrerá la cola tx creando skb''s y los pondrá en la qdisc. De forma asíncrona, los skb se eliminarán de la qdisc y se enviarán al hardware tx ring. Los skb no deben ser non-linear por lo que harán referencia a los datos en el anillo tx en lugar de copiar, y una NIC moderna agradable debería ser capaz de manejar datos dispersos y hacer referencia a los datos en los anillos tx también. Por supuesto, cualquiera de estas suposiciones podría estar equivocada, por lo que tratemos de descargar un montón de daño en una qdisc con esta manguera de incendios.
Pero primero, un hecho que no se entiende comúnmente sobre cómo funcionan las qdisc. Contienen una cantidad limitada de datos (generalmente contados en el número de cuadros, pero en algunos casos podrían medirse en bytes) y si intenta encolar un cuadro a una qdisc completa, el cuadro generalmente se eliminará (dependiendo de lo que Enqueuer decide hacer). Por lo tanto, daré la pista de que mi hipótesis original era que el OP estaba usando paquetes mmap para convertir los marcos en una qdisc tan rápido que muchos se estaban eliminando. Pero no te aferres demasiado a esa idea; te lleva en una dirección, pero siempre mantén una mente abierta. Vamos a intentarlo para averiguar qué sucede.
El primer problema al intentar esto es que la qdisc predeterminada pfifo_fast no mantiene estadísticas. Así que vamos a reemplazar eso con el qdisc pfifo
que hace. Por defecto, pfifo
limita la cola a los marcos de TXQUEUELEN
(que por lo general el valor predeterminado es 1000). Pero como queremos mostrar una qdisc abrumadora, configurémosla explícitamente en 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
También tpacket_snd
cuánto tiempo lleva procesar los marcos en tpacket_snd
con el script SystemTap 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("tpacket_snd@net/packet/af_packet.c:2213") {
print_ts();
printf("tpacket_snd: return(%d)/n", $err);
}
Inicie el script SystemTap con sudo stap call-return.stp
y luego sudo stap call-return.stp
8096 marcos de 1500 bytes en ese qdisc con una capacidad de 50 cuadros:
$ 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)
Así que vamos a ver cuántos paquetes ha descartado la 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
¿Qué? ¿Ha eliminado uno de los 8096 cuadros descargados en una qdisc de 50 cuadros? Vamos a comprobar la salida de 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)
¿Qué? ¿Se necesitaron casi 100 ms para procesar 8096 marcos en tpacket_snd
? Vamos a ver cuánto tiempo se tardaría en transmitir; eso es 8096 cuadros a 1500 bytes / cuadro a 1gigabit / s ~ = 97ms. ¿Qué? Huele como si algo estuviera bloqueando.
Echemos un vistazo más de cerca a tpacket_snd
. Gemido:
skb = sock_alloc_send_skb(&po->sk,
hlen + tlen + sizeof(struct sockaddr_ll),
0, &err);
Ese 0
parece bastante inocuo, pero ese es realmente el argumento del noblock. Debe ser msg->msg_flags & MSG_DONTWAIT
(resulta que esto está arreglado en 4.1 ). Lo que está sucediendo aquí es que el tamaño de la qdisc no es el único recurso limitante. Si la asignación de espacio para el skb excede el tamaño del límite sndbuf del socket, entonces esta llamada bloqueará para esperar a que se libere el skb o devolverá -EAGAIN
a un llamante no bloqueante. En la corrección en V4.1, si se solicita el no bloqueo, devolverá el número de bytes escritos si no es cero, de lo contrario, -EAGAIN
para la persona que llama, que casi parece que alguien no quiere que averigües cómo usar esto ( por ejemplo , rellena un anillo de tx con MSG_DONTWAIT
de datos, llama a MSG_DONTWAIT
con MSG_DONTWAIT
, y obtienes un resultado que enviaste 150KB en lugar de EWOULDBLOCK
).
Entonces, si está ejecutando un kernel anterior a 4.1 (creo que el OP se está ejecutando> 4.1 y no está afectado por este error), deberá aplicar af_packet.c
parche a af_packet.c
y construir un nuevo kernel o actualizar a un kernel 4.1 o superior.
Ahora he arrancado una versión parcheada de mi kernel, ya que la máquina que estoy usando está ejecutando 3.13. Si bien no bloquearemos si el sndbuf está lleno, aún regresaremos con -EAGAIN
. Hice algunos cambios en packet_mmap.c
para aumentar el tamaño predeterminado de sndbuf y usar SO_SNDBUFFORCE
para anular el máximo del sistema por socket si es necesario (parece que necesita aproximadamente 750 bytes + el tamaño de trama para cada cuadro). También hice algunas adiciones a call-return.stp
para registrar el tamaño máximo de sndbuf ( sk_sndbuf
), la cantidad utilizada ( sk_wmem_alloc
), cualquier error devuelto por sock_alloc_send_skb
y cualquier error devuelto por dev_queue_xmit
al enviar el skb al qdisc. Aquí está la nueva versión:
# 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);
}
Intentemoslo de nuevo:
$ 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
Y aquí está la salida de 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
Ahora las cosas están funcionando como se espera; hemos solucionado un error que nos ha impedido bloquear el límite de sndbuf y hemos ajustado el límite de sndbuf para que no sea una restricción, y ahora vemos que los marcos del anillo tx se ponen en cola en la qdisc hasta que esté completo , en cuyo momento nos devuelven ENOBUFS
.
El siguiente problema ahora es cómo seguir publicando de manera eficiente en la qdisc para mantener la interfaz ocupada. Tenga en cuenta que la implementación de packet_poll
es inútil en el caso de que rellenemos la qdisc y volvamos ENOBUFS
, porque solo pregunta si el encabezado es TP_STATUS_AVAILABLE
, lo que en este caso permanecerá TP_STATUS_SEND_REQUEST
hasta que una llamada posterior sendto
tenga éxito en poner la trama en la qdisc. Una conveniencia simple (actualizada en packet_mmap.c) es hacer un bucle en el envío hasta que sea un error o un error distinto de ENOBUFS
o EAGAIN
.
De todos modos, sabemos mucho más que suficiente para responder la pregunta de los OP ahora, incluso si no tenemos una solución completa para evitar que la NIC se quede sin hambre.
Por lo que hemos aprendido, sabemos que cuando el OP llama a sendto con un timbre tx en modo de bloqueo, tpacket_snd
comenzará a colocar skbs en el qdisc hasta que se exceda el límite de sndbuf (y el valor predeterminado es generalmente muy pequeño, aproximadamente 213K, y además, descubrió que los datos de trama a los que se hace referencia en el anillo de tx compartido se cuentan para esto cuando se bloqueará (mientras se mantiene pg_vec_lock
). A medida que se libere skb, se pondrán en cola más fotogramas, y quizás se vuelva a superar el sndbuf y se volverá a bloquear. Eventualmente, todos los datos se habrán puesto en cola en la qdisc pero tpacket_snd
se seguirán bloqueando hasta que se hayan transmitido todas las tramas (no se puede marcar una trama en el anillo de tx como disponible hasta que la NIC la haya recibido, como un skb en el anillo del conductor hace referencia a un cuadro en el anillo tx) mientras se mantiene presionadopg_vec_lock
. En este punto, el NIC está muerto de hambre, y el bloqueo ha bloqueado a cualquier otro escritor de socket.
Por otro lado, cuando OP publica un paquete a la vez, será manejado por el packet_snd
cual se bloqueará si no hay espacio en el sndbuf y luego encolar el marco en la qdisc, y volverá inmediatamente. No espera a que se transmita la trama. A medida que la qdisc se está drenando, se pueden poner en cola marcos adicionales. Si el editor puede mantenerse al día, la NIC nunca estará muerta de hambre.
Además, el operador está copiando en el anillo tx para cada envío a llamada y comparándolo con pasar un búfer de marco fijo cuando no se usa un anillo tx. No verá una aceleración por no copiar de esa manera (aunque esa no es la única ventaja de usar el anillo tx).