2012-10-04 1235 views
21

Tengo un servidor multiproceso (grupo de subprocesos) que maneja una gran cantidad de solicitudes (hasta 500/seg para un nodo), usando 20 subprocesos. Hay un hilo de escucha que acepta conexiones entrantes y las pone en cola para que procesen los hilos del manejador. Una vez que la respuesta está lista, los hilos escriben al cliente y cierran el socket. Todo parecía estar bien hasta hace poco, un programa cliente de prueba comenzó a colgar al azar después de leer la respuesta. Después de una gran cantidad de excavaciones, parece que el cerrar() del servidor no desconecta realmente el socket. He agregado algunas impresiones de depuración al código con el número de descriptor de archivo y obtengo este tipo de salida.close() no está cerrando el socket correctamente

Processing request for 21 
Writing to 21 
Closing 21 

El valor de retorno de close() es 0 o se imprimirá otra instrucción de depuración. Después de esta salida con un cliente que se cuelga, lsof muestra una conexión establecida.

SERVER 8160 21u raíz IPv4 TCP 32754237 localhost: 9980-> localhost: 47530 (establecido)

CLIENTE 17747 12u raíz IPv4 TCP localhost 32754228: 47530-> localhost: 9980 (establecido)

Es tan si el servidor nunca envía la secuencia de apagado al cliente, y este estado se cuelga hasta que se mata al cliente, dejando el servidor en un estado de espera cerrada

SERVIDOR 8160 raíz 21u IPv4 32754237 TCP localhost: 9980-> localhost: 47530 (CLOSE_WAIT)

Además, si el cliente tiene un tiempo de espera especificado, se agotará el tiempo de espera en lugar de colgar. También puedo ejecutar manualmente

call close(21) 

en el servidor de gdb, y el cliente se desconectará. Esto sucede quizás una vez cada 50,000 solicitudes, pero puede que no suceda por períodos prolongados. versión

Linux: 2.6.21.7-2.fc8xen Centos versión: 5.4

acciones de socket (final) son los siguientes

SERVER:

int client_socket; struct sockaddr_in client_addr; socklen_t client_len = sizeof (client_addr);

while(true) { 
    client_socket = accept(incoming_socket, (struct sockaddr *)&client_addr, &client_len); 
    if (client_socket == -1) 
    continue; 
    /* insert into queue here for threads to process */ 
} 

Luego, el hilo toma el socket y genera la respuesta.

/* get client_socket from queue */ 

/* processing request here */ 

/* now set to blocking for write; was previously set to non-blocking for reading */ 
int flags = fcntl(client_socket, F_GETFL); 
if (flags < 0) 
    abort(); 
if (fcntl(client_socket, F_SETFL, flags|O_NONBLOCK) < 0) 
    abort(); 

server_write(client_socket, response_buf, response_length); 
server_close(client_socket); 

server_write and server_close.

void server_write(int fd, char const *buf, ssize_t len) { 
    printf("Writing to %d\n", fd); 
    while(len > 0) { 
     ssize_t n = write(fd, buf, len); 
     if(n <= 0) 
     return;// I don't really care what error happened, we'll just drop the connection 
     len -= n; 
     buf += n; 
    } 
    } 

void server_close(int fd) { 
    for(uint32_t i=0; i<10; i++) { 
     int n = close(fd); 
     if(!n) {//closed successfully                                 
     return; 
     } 
     usleep(100); 
    } 
    printf("Close failed for %d\n", fd); 
    } 

CLIENTE:

lado del cliente está utilizando libcurl v 7.27.0

CURL *curl = curl_easy_init(); 
CURLcode res; 
curl_easy_setopt(curl, CURLOPT_URL, url); 
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); 
curl_easy_setopt(curl, CURLOPT_WRITEDATA, write_tag); 

res = curl_easy_perform(curl); 

Nada especial, simplemente una conexión básica rizo. El cliente se bloquea en tranfer.c (en libcurl) porque el socket no se percibe como cerrado. Está esperando más datos del servidor.

Cosas que he probado hasta ahora:

apagado antes del cierre

shutdown(fd, SHUT_WR);                                    
char buf[64];                                      
while(read(fd, buf, 64) > 0);                                   
/* then close */ 

Configuración SO_LINGER a cerrar por la fuerza en 1 segundo

struct linger l; 
l.l_onoff = 1; 
l.l_linger = 1; 
if (setsockopt(client_socket, SOL_SOCKET, SO_LINGER, &l, sizeof(l)) == -1) 
    abort(); 

Estos han hecho ninguna diferencia. Cualquier idea sería muy apreciada.

EDITAR - Esto terminó siendo un problema de seguridad de subprocesos dentro de una biblioteca de colas causando que el socket se maneje de manera inapropiada por varios subprocesos.

+0

¿Es 100% positivo que no haya ningún otro subproceso que pueda estar utilizando el socket cuando llama 'close'? ¿Cómo haces tus lecturas sin bloqueo? –

+0

Me temo que acabo de iniciar sesión aquí y recordé este problema. Más tarde descubrí que había un problema de seguridad de subprocesos en una cola utilizada para pasar las conexiones. No hubo error aquí. Perdón por la desinformación. – DavidMFrey

Respuesta

54

Aquí hay un código que he usado en muchos sistemas Unix (por ejemplo SunOS 4, SGI IRIX, HP-UX 10.20, CentOS 5, Cygwin) para cerrar un socket:

int getSO_ERROR(int fd) { 
    int err = 1; 
    socklen_t len = sizeof err; 
    if (-1 == getsockopt(fd, SOL_SOCKET, SO_ERROR, (char *)&err, &len)) 
     FatalError("getSO_ERROR"); 
    if (err) 
     errno = err;    // set errno to the socket SO_ERROR 
    return err; 
} 

void closeSocket(int fd) {  // *not* the Windows closesocket() 
    if (fd >= 0) { 
     getSO_ERROR(fd); // first clear any errors, which can cause close to fail 
     if (shutdown(fd, SHUT_RDWR) < 0) // secondly, terminate the 'reliable' delivery 
     if (errno != ENOTCONN && errno != EINVAL) // SGI causes EINVAL 
      Perror("shutdown"); 
     if (close(fd) < 0) // finally call close() 
     Perror("close"); 
    } 
} 

Pero lo anterior hace no garantiza que se envíen escrituras con búfer.

Agraciado cerca: Me llevó aproximadamente 10 años descubrir cómo cerrar una toma de corriente. Pero durante otros 10 años simplemente llamé con pereza a usleep(20000) por un ligero retraso para 'asegurar' que el buffer de escritura se vaciara antes del cierre. Obviamente, esto no es muy inteligente, porque:

  • La demora fue demasiado larga la mayor parte del tiempo.
  • La demora fue demasiado corta en algunas ocasiones, ¡tal vez!
  • Una señal como SIGCHLD podría ocurrir al final usleep() (pero usualmente llamé usleep() dos veces para manejar este caso - un truco).
  • No hay indicación de si esto funciona. Pero esto quizás no sea importante si a) los reinicios duros están perfectamente bien, y/ob) usted tiene control sobre ambos lados del enlace.

Pero hacer un correcto lavado es sorprendentemente difícil. Usando SO_LINGER es aparentemente no el camino a seguir; Véase, por ejemplo:

Y SIOCOUTQ parece ser específico de Linux.

Nota shutdown(fd, SHUT_WR) no dejar de escribir, contrariamente a su nombre, y tal vez al contrario de man 2 shutdown.

Este código flushSocketBeforeClose() espera hasta una lectura de cero bytes, o hasta que el temporizador expire. La función haveInput() es una envoltura simple para seleccionar (2), y está configurada para bloquear por hasta 1/100 de segundo.

bool haveInput(int fd, double timeout) { 
    int status; 
    fd_set fds; 
    struct timeval tv; 
    FD_ZERO(&fds); 
    FD_SET(fd, &fds); 
    tv.tv_sec = (long)timeout; // cast needed for C++ 
    tv.tv_usec = (long)((timeout - tv.tv_sec) * 1000000); // 'suseconds_t' 

    while (1) { 
     if (!(status = select(fd + 1, &fds, 0, 0, &tv))) 
     return FALSE; 
     else if (status > 0 && FD_ISSET(fd, &fds)) 
     return TRUE; 
     else if (status > 0) 
     FatalError("I am confused"); 
     else if (errno != EINTR) 
     FatalError("select"); // tbd EBADF: man page "an error has occurred" 
    } 
} 

bool flushSocketBeforeClose(int fd, double timeout) { 
    const double start = getWallTimeEpoch(); 
    char discard[99]; 
    ASSERT(SHUT_WR == 1); 
    if (shutdown(fd, 1) != -1) 
     while (getWallTimeEpoch() < start + timeout) 
     while (haveInput(fd, 0.01)) // can block for 0.01 secs 
      if (!read(fd, discard, sizeof discard)) 
       return TRUE; // success! 
    return FALSE; 
} 

Ejemplo de uso:

if (!flushSocketBeforeClose(fd, 2.0)) // can block for 2s 
     printf("Warning: Cannot gracefully close socket\n"); 
    closeSocket(fd); 

En lo anterior, mi getWallTimeEpoch() es similar a time(), y Perror() es un contenedor de perror().

Editar: Algunos comentarios:

  • Mi primera admisión es un poco embarazosa. El OP y Nemo desafiaron la necesidad de borrar el so_error interno antes de cerrar, pero ahora no puedo encontrar ninguna referencia para esto. El sistema en cuestión era HPUX 10.20. Después de un error connect(), simplemente al llamar al close() no se lanzó el descriptor de archivo, porque el sistema deseaba entregarme un error pendiente. Pero yo, como la mayoría de las personas, nunca me molesté en comprobar el valor de retorno de close.. Finalmente me quedé sin los descriptores de archivos (ulimit -n), que finalmente me llamaron la atención.

  • (punto muy secundario) Un comentarista se opuso a los argumentos numéricos codificados a shutdown(), en lugar de, p. Ej. SHUT_WR para 1. La respuesta más simple es que Windows usa diferentes # define/enums, p. Ej. SD_SEND. Y muchos otros escritores (por ejemplo, Beej) usan constantes, al igual que muchos sistemas heredados.

  • Además, siempre, siempre configuro FD_CLOEXEC en todos mis enchufes, ya que en mis aplicaciones nunca quiero que se transmitan a un niño y, más importante aún, no quiero que un niño colgado me impacte.

Código de ejemplo para establecer CLOEXEC:

static void setFD_CLOEXEC(int fd) { 
     int status = fcntl(fd, F_GETFD, 0); 
     if (status >= 0) 
     status = fcntl(fd, F_SETFD, status | FD_CLOEXEC); 
     if (status < 0) 
     Perror("Error getting/setting socket FD_CLOEXEC flags"); 
    } 
+5

Ojalá pudiera votar esto dos veces. Esta es solo la segunda muestra de un socket correctamente cerrado que he visto en la naturaleza. – grieve

+1

+1 para 'getsockopt()' ing 'SO_ERROR'. – alk

+0

@JosephQuinsey - ¿Tiene una referencia para "errores ... causará que close() sea ignorado"? Preferiblemente de la especificación POSIX? – Nemo

0

Esto me suena como un error en la distribución de Linux.

El GNU C library documentation dice:

Cuando haya terminado de usar un enchufe, sólo tiene que cerrar su descriptor de archivo con close

nada acerca de la eliminación de cualquier indicador de error o en espera de los datos de ser enrojecido o algo por el estilo.

Su código está bien; su O/S tiene un error.

+0

Inclinándose hacia esta respuesta. Tomará algo de trabajo conseguir otro sistema operativo para probar. Volveré a visitar esto una vez que lo haya probado. Quiero agregar este enlace desde @Nemo ya que parece relevante para la pregunta. y la respuesta a la que fue adjunta ha sido eliminada. https://sites.google.com/site/michaelsafyan/software-engineering/checkforeintrwheninvokingclosethinkagain – DavidMFrey

+0

Estoy aceptando esta respuesta, ya que cambiar mi cola de seguridad de subprocesos para usar semáforos en lugar de pthread las condiciones inexplicablemente (para mí de todos modos) resolvió el problema. – DavidMFrey

+3

'Nada sobre borrar los indicadores de error o esperar a que se vacíen los datos o algo similar. Posiblemente," esperar a que se vacíen los datos "caiga bajo" cuando haya terminado de usar un socket ". –

2

Gran respuesta de Joseph Quinsey. Tengo comentarios sobre la función haveInput. Preguntándose qué tan probable es que select devuelva un fd que no incluyó en su conjunto. Esta sería una gran falla del sistema operativo en mi humilde opinión. Ese es el tipo de cosas que verificaría si escribo pruebas unitarias para la función select, no en una aplicación común.

if (!(status = select(fd + 1, &fds, 0, 0, &tv))) 
    return FALSE; 
else if (status > 0 && FD_ISSET(fd, &fds)) 
    return TRUE; 
else if (status > 0) 
    FatalError("I am confused"); // <--- fd unknown to function 

Mi otro comentario se refiere al manejo de EINTR. En teoría, podría quedarse atascado en un bucle infinito si select seguía devolviendo EINTR, ya que este error permite que el bucle vuelva a comenzar. Dado el tiempo de espera muy corto (0.01), parece muy poco probable que suceda. Sin embargo, creo que la forma adecuada de tratar con esto sería devolver los errores a la persona que llama (flushSocketBeforeClose).La persona que llama puede seguir llamando al haveInput, ya que su tiempo de espera no ha expirado y declarar la falla de otros errores.

ADEMÁS # 1

flushSocketBeforeClose no va a salir rápidamente en caso de read devolver un error. Seguirá funcionando hasta que expire el tiempo de espera. No puede confiar en el select dentro de haveInput para anticipar todos los errores. read tiene errores propios (por ejemplo, EIO).

 while (haveInput(fd, 0.01)) 
     if (!read(fd, discard, sizeof discard)) <-- -1 does not end loop 
      return TRUE; 
Cuestiones relacionadas