tipos sistema procesos llamadas llamada arbol c linux performance sockets tcp

procesos - ¿Por qué desacelera el socket TCP si se hace en múltiples llamadas al sistema?



llamada al sistema exec (1)

Interesante. Estás siendo víctima del algoritmo de Nagle junto con confirmaciones retrasadas de TCP .

El algoritmo de Nagle es un mecanismo utilizado en TCP para diferir la transmisión de segmentos pequeños hasta que se hayan acumulado suficientes datos que hagan que valga la pena construir y enviar un segmento a través de la red. Del artículo de wikipedia:

El algoritmo de Nagle funciona combinando una cantidad de pequeños mensajes salientes y enviándolos todos a la vez. Específicamente, mientras haya un paquete enviado para el cual el emisor no haya recibido acuse de recibo, el remitente deberá mantener el almacenamiento intermedio de su salida hasta que tenga el valor de salida de un paquete completo, de modo que la salida pueda enviarse de una sola vez.

Sin embargo, TCP generalmente emplea algo conocido como confirmaciones de retraso TCP , que es una técnica que consiste en acumular un lote de respuestas ACK (porque TCP usa ACKS acumulativo) para reducir el tráfico de red.

Ese artículo de wikipedia menciona esto más adelante:

Con ambos algoritmos habilitados, las aplicaciones que realizan dos escrituras sucesivas en una conexión TCP, seguidas de una lectura que no se cumplirá hasta que los datos de la segunda escritura hayan llegado al destino, experimentan un retraso constante de hasta 500 milisegundos, el " Retardo ACK " .

(Énfasis mío)

En su caso específico, dado que el servidor no envía más datos antes de leer la respuesta, el cliente está causando el retraso: si el cliente escribe dos veces , la segunda escritura se retrasará .

Si la parte emisora ​​usa el algoritmo de Nagle, el remitente pondrá los datos en cola hasta que se reciba un ACK. Si el remitente no envía suficientes datos para llenar el tamaño máximo de segmento (por ejemplo, si realiza dos escrituras pequeñas seguidas de una lectura de bloqueo), la transferencia se detendrá hasta el tiempo de espera de retardo de ACK.

Entonces, cuando el cliente hace 2 llamadas de escritura, esto es lo que sucede:

  1. El cliente emite la primera escritura.
  2. El servidor recibe algunos datos. No lo reconoce con la esperanza de que lleguen más datos (por lo que puede agrupar un montón de ACK en un único ACK).
  3. El cliente emite la segunda escritura. La escritura anterior no ha sido confirmada, por lo que el algoritmo de Nagle aplaza la transmisión hasta que lleguen más datos (hasta que se hayan recopilado suficientes datos para hacer un segmento) o cuando la escritura anterior sea ACKed.
  4. El servidor está cansado de esperar y después de 500 ms reconoce el segmento.
  5. El cliente finalmente completa la 2da escritura.

Con 1 escritura, esto es lo que sucede:

  1. El cliente emite la primera escritura.
  2. El servidor recibe algunos datos. No lo reconoce con la esperanza de que lleguen más datos (por lo que puede agrupar un montón de ACK en un único ACK).
  3. El servidor escribe en el socket. Un ACK es parte del encabezado TCP, por lo tanto, si está escribiendo, puede reconocer el segmento anterior sin costo adicional. Hazlo.
  4. Mientras tanto, el cliente escribió una vez, por lo que ya estaba esperando en la siguiente lectura, no había una segunda escritura esperando el ACK del servidor .

Si desea seguir escribiendo dos veces en el lado del cliente, debe desactivar el algoritmo de Nagle. Esta es la solución propuesta por el autor del algoritmo:

La solución de nivel de usuario es evitar las secuencias de escritura, escritura y lectura en los sockets. write-read-write-read está bien. escribir-escribir-escribir está bien. Pero escribir-escribir-leer es un asesino. Por lo tanto, si puede, almacene sus pequeñas escrituras en TCP y envíelas todas a la vez. El uso del paquete de E / S UNIX estándar y la escritura de vaciado antes de cada lectura generalmente funciona.

( Ver la cita en Wikipedia )

Como lo menciona David Schwartz en los comentarios , esta puede no ser la mejor idea por varias razones, pero ilustra el punto y muestra que esto de hecho está causando la demora.

Para deshabilitarlo, debe configurar la opción TCP_NODELAY en los sockets con setsockopt(2) .

Esto se puede hacer en tcpConnectTo() para el cliente:

int tcpConnectTo(const char* server, const char* port) { struct sockaddr_in sa; if(getsockaddr(server,port,(struct sockaddr*)&sa)<0) return -1; int sock=tcpConnect(&sa); if(sock<0) return -1; int val = 1; if (setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &val, sizeof(val)) < 0) perror("setsockopt(2) error"); return sock; }

Y en tcpAccept() para el servidor:

int tcpAccept(const char* port) { int listenSock, sock; listenSock = tcpListenAny(port); if((sock=accept(listenSock,0,0))<0) return fprintf(stderr,"Accept failed/n"),-1; close(listenSock); int val = 1; if (setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &val, sizeof(val)) < 0) perror("setsockopt(2) error"); return sock; }

Es interesante ver la gran diferencia que esto hace.

Si prefiere no meterse con las opciones de socket, es suficiente para asegurarse de que el cliente escribe una vez, y solo una vez, antes de la próxima lectura. Todavía puede hacer que el servidor lea dos veces:

for(i=0;i<4000;++i) { if(amServer) { writeLoop(sock,buf,10); //readLoop(sock,buf,20); readLoop(sock,buf,10); readLoop(sock,buf,10); }else { readLoop(sock,buf,10); writeLoop(sock,buf,20); //writeLoop(sock,buf,10); //writeLoop(sock,buf,10); } }

¿Por qué el siguiente código es lento? Y por lento quiero decir 100x-1000x lento. Simplemente realiza lectura / escritura repetidamente directamente en un socket TCP. La parte curiosa es que sigue siendo lento solo si uso dos llamadas de función para leer y escribir, como se muestra a continuación. Si cambio el código del servidor o del cliente para usar una sola llamada de función (como en los comentarios), se vuelve superrápido.

Fragmento de código:

int main(...) { int sock = ...; // open TCP socket int i; char buf[100000]; for(i=0;i<2000;++i) { if(amServer) { write(sock,buf,10); // read(sock,buf,20); read(sock,buf,10); read(sock,buf,10); }else { read(sock,buf,10); // write(sock,buf,20); write(sock,buf,10); write(sock,buf,10); } } close(sock); }

Nos topamos con esto en un programa más grande, que en realidad estaba usando el buffer de stdio. Se volvió misteriosamente lento cuando el tamaño de la carga útil excedía el tamaño del búfer por un pequeño margen. Luego hice algunas excavaciones con strace , y finalmente strace el problema a esto. Puedo resolver esto haciendo tonterías con la estrategia de almacenamiento en búfer, pero realmente me gustaría saber qué demonios está pasando aquí. En mi máquina, va de 0.030 a más de un minuto en mi máquina (probado tanto localmente como en máquinas remotas) cuando cambio las dos llamadas de lectura a una sola.

Estas pruebas se realizaron en varias distribuciones de Linux y varias versiones de kernel. Mismo resultado.

Código totalmente ejecutable con repetición de red:

#include <netdb.h> #include <stdbool.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include <netinet/ip.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/tcp.h> static int getsockaddr(const char* name,const char* port, struct sockaddr* res) { struct addrinfo* list; if(getaddrinfo(name,port,NULL,&list) < 0) return -1; for(;list!=NULL && list->ai_family!=AF_INET;list=list->ai_next); if(!list) return -1; memcpy(res,list->ai_addr,list->ai_addrlen); freeaddrinfo(list); return 0; } // used as sock=tcpConnect(...); ...; close(sock); static int tcpConnect(struct sockaddr_in* sa) { int outsock; if((outsock=socket(AF_INET,SOCK_STREAM,0))<0) return -1; if(connect(outsock,(struct sockaddr*)sa,sizeof(*sa))<0) return -1; return outsock; } int tcpConnectTo(const char* server, const char* port) { struct sockaddr_in sa; if(getsockaddr(server,port,(struct sockaddr*)&sa)<0) return -1; int sock=tcpConnect(&sa); if(sock<0) return -1; return sock; } int tcpListenAny(const char* portn) { in_port_t port; int outsock; if(sscanf(portn,"%hu",&port)<1) return -1; if((outsock=socket(AF_INET,SOCK_STREAM,0))<0) return -1; int reuse = 1; if(setsockopt(outsock,SOL_SOCKET,SO_REUSEADDR, (const char*)&reuse,sizeof(reuse))<0) return fprintf(stderr,"setsockopt() failed/n"),-1; struct sockaddr_in sa = { .sin_family=AF_INET, .sin_port=htons(port) , .sin_addr={INADDR_ANY} }; if(bind(outsock,(struct sockaddr*)&sa,sizeof(sa))<0) return fprintf(stderr,"Bind failed/n"),-1; if(listen(outsock,SOMAXCONN)<0) return fprintf(stderr,"Listen failed/n"),-1; return outsock; } int tcpAccept(const char* port) { int listenSock, sock; listenSock = tcpListenAny(port); if((sock=accept(listenSock,0,0))<0) return fprintf(stderr,"Accept failed/n"),-1; close(listenSock); return sock; } void writeLoop(int fd,const char* buf,size_t n) { // Don''t even bother incrementing buffer pointer while(n) n-=write(fd,buf,n); } void readLoop(int fd,char* buf,size_t n) { while(n) n-=read(fd,buf,n); } int main(int argc,char* argv[]) { if(argc<3) { fprintf(stderr,"Usage: round {server_addr|--} port/n"); return -1; } bool amServer = (strcmp("--",argv[1])==0); int sock; if(amServer) sock=tcpAccept(argv[2]); else sock=tcpConnectTo(argv[1],argv[2]); if(sock<0) { fprintf(stderr,"Connection failed/n"); return -1; } int i; char buf[100000] = { 0 }; for(i=0;i<4000;++i) { if(amServer) { writeLoop(sock,buf,10); readLoop(sock,buf,20); //readLoop(sock,buf,10); //readLoop(sock,buf,10); }else { readLoop(sock,buf,10); writeLoop(sock,buf,20); //writeLoop(sock,buf,10); //writeLoop(sock,buf,10); } } close(sock); return 0; }

EDITAR: Esta versión es ligeramente diferente del otro fragmento en que lee / escribe en un bucle. Entonces, en esta versión, dos escrituras separadas provocan automáticamente dos llamadas de read() separadas read() , incluso si readLoop se llama solo una vez. Pero, de lo contrario, el problema aún persiste.