close () no está cerrando el socket correctamente

11 minutos de lectura

avatar de usuario
davidfrey

Tengo un servidor de subprocesos múltiples (grupo de subprocesos) que maneja una gran cantidad de solicitudes (hasta 500/seg para un nodo), utilizando 20 subprocesos. Hay un subproceso de escucha que acepta las conexiones entrantes y las pone en cola para que las procesen los subprocesos del controlador. Una vez que la respuesta está lista, los subprocesos escriben en el cliente y cierran el socket. Todo parecía estar bien hasta hace poco, un programa cliente de prueba comenzó a colgarse al azar después de leer la respuesta. Después de mucho investigar, parece que close() del servidor en realidad no está desconectando el zócalo. Agregué algunas impresiones de depuración al código con el número de descriptor de archivo y obtengo este tipo de salida.

Processing request for 21
Writing to 21
Closing 21

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

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

CLIENTE 17747 root 12u IPv4 32754228 TCP localhost:47530->localhost:9980 (ESTABLECIDO)

Es como si el servidor nunca enviara la secuencia de apagado al cliente, y este estado se cuelga hasta que el cliente muere, dejando el lado del servidor en un estado de espera cerrado.

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á en lugar de colgarse. 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 es posible que no suceda durante períodos prolongados.

Versión de Linux: 2.6.21.7-2.fc8xen Versión de Centos: 5.4 (Final)

las acciones del socket son las siguientes

SERVIDOR:

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 subproceso 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);

servidor_escribir y servidor_cerrar.

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:

El lado del cliente está usando 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 lujoso, solo una conexión de rizo básica. El cliente se cuelga 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:

Apagar antes de cerrar

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

Configuración de SO_LINGER para cerrar a 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 no 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, lo que provocó que varios subprocesos manejaran el socket de manera inapropiada.

  • ¿Está 100% seguro de que ningún otro subproceso podría estar usando el socket cuando llama? close ¿en eso? ¿Cómo haces tus lecturas sin bloqueo?

    –David Schwartz

    23 de diciembre de 2014 a las 16:34

  • 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 había ningún error aquí. Lo siento por la desinformación.

    – DavidMFrey

    15/03/2016 a las 15:46

avatar de usuario
jose quinsey

Aquí hay un código que he usado en muchos sistemas similares a Unix (por ejemplo, SunOS 4, SGI IRIX, HPUX 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 no garantiza que se envíen escrituras almacenadas en búfer.

Cierre elegante: me tomó alrededor de 10 años descubrir cómo cerrar un enchufe. Pero durante otros 10 años simplemente llamé perezosamente usleep(20000) para un ligero retraso para ‘garantizar’ que el búfer de escritura se vació antes del cierre. Esto obviamente no es muy inteligente, porque:

  • La demora era demasiado larga la mayor parte del tiempo.
  • La demora fue demasiado corta algunas veces, ¡tal vez!
  • Una señal como SIGCHLD podría ocurrir para terminar usleep() (pero normalmente llamo usleep() dos veces para manejar este caso, un truco).
  • No hubo indicación de si esto funciona. Pero esto quizás no sea importante si a) los restablecimientos completos funcionan perfectamente y/o b) tiene control sobre ambos lados del enlace.

Pero hacer una descarga adecuada es sorprendentemente difícil. Utilizando SO_LINGER es aparentemente no el camino a seguir; ver por ejemplo:

Y SIOCOUTQ parece ser específico de Linux.

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

este codigo flushSocketBeforeClose() espera hasta una lectura de cero bytes, o hasta que expire el temporizador. La función haveInput() es un envoltorio simple para select(2), y está configurado para bloquear 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 parecido a time(), y Perror() es un envoltorio para perror().

Editar: Algunos comentarios:

  • Mi primera admisión es un poco embarazosa. El OP y Nemo desafiaron la necesidad de despejar el interior so_error 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 fallido connect()solo llamando close() no publicó el descriptor del archivo porque el sistema deseaba enviarme un error pendiente. Pero yo, como la mayoría de la gente, nunca me molesté en comprobar el valor de retorno de close. Así que finalmente me quedé sin descriptores de archivos (ulimit -n), que finalmente llamó mi atención.

  • (punto muy menor) Un comentarista se opuso a los argumentos numéricos codificados para shutdown()en lugar de, por ejemplo, SHUT_WR para 1. La respuesta más simple es que Windows usa diferentes #defines/enumeraciones, por ejemplo 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 sockets, ya que en mis aplicaciones nunca quiero que se pasen a un niño y, lo que es más importante, no quiero que un niño colgado me impacte.

Código de muestra para configurar 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");
   }

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

    – afligir

    4 oct 2012 a las 15:44

  • creo shutdown debe operarse con las macros correspondientes SHUT_RD etc.

    – Jens Gusted

    4 oct 2012 a las 16:15

  • Lea sobre el glorioso FINWAIT característica de TCP.

    – Steve-o

    4 oct 2012 a las 18:15


  • Su código solucionó un problema en mi cliente, donde no podía volver a conectarse inmediatamente después de ser desconectado por el servidor, porque el cliente envió un SYN incluso antes de confirmar FIN.

    – Felipe A.

    11 de julio de 2013 a las 15:28

  • En caso de que alguien más esté tratando de averiguar cómo getSO_ERROR() contribuye a resolver el problema: resulta que llamando getsockopt con SO_ERROR primero buscará el estado de error y luego lo restablecerá. Esta información no fue fácil de encontrar para mí, ni estoy seguro de que sea portátil. La siguiente página del manual documenta este comportamiento: linux.die.net/man/3/getsockopt Pero la misma página de manual (man 3 getsockopt) en mi distribución no (RHEL8).

    – pq

    30 de julio de 2021 a las 9:38

Gran respuesta de Joseph Quinsey. tengo comentarios sobre el haveInput función. Se pregunta qué tan probable es que select devuelva un fd que no incluyó en su conjunto. Este sería un error importante del sistema operativo en mi humilde opinión. Ese es el tipo de cosas que comprobaría si escribiera pruebas unitarias para el select función, no en una aplicación ordinaria.

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ías quedar atrapado en un bucle infinito si select siguió devolviendo EINTR, ya que este error permite que el ciclo comience de nuevo. Dado el tiempo de espera muy corto (0.01), parece muy poco probable que suceda. Sin embargo, creo que la forma adecuada de lidiar con esto sería devolver los errores a la persona que llama (flushSocketBeforeClose). La persona que llama puede seguir llamando haveInput tiene tiempo mientras su tiempo de espera no haya expirado, y declarar falla para otros errores.

ADICIÓN #1

flushSocketBeforeClose no saldrá rápidamente en caso de read devolviendo un error. Seguirá en bucle hasta que expire el tiempo de espera. No puedes confiar en el select en el interior 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; 

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

los Documentación de la biblioteca GNU C dice:

Cuando haya terminado de usar un socket, simplemente puede cerrar su descriptor de archivo con close

Nada sobre borrar cualquier indicador de error o esperar a que se vacíen los datos o algo por el estilo.

Tu código está bien; su sistema operativo tiene un error.

  • Me inclino por esta respuesta. Tomará algo de trabajo conseguir que otro sistema operativo se pruebe. Volveré a revisar esto una vez que lo haya probado. Quiero agregar este enlace de @Nemo ya que parece relevante para la pregunta. y la respuesta a la que estaba adjunto ha sido eliminada. sitios.google.com/site/michaelsafyan/software-engineering/…

    – DavidMFrey

    4 oct 2012 a las 16:42

  • Nothing about clearing any error flags or waiting for the data to be flushed or any such thing. Podría decirse que “esperar a que se vacíen los datos” se incluye en “cuando haya terminado de usar un socket”.

    – Carreras de ligereza en órbita

    8 de noviembre de 2012 a las 19:14


  • @DavidMFrey Eso significaría que hay una probabilidad cercana al 100% de que su código tenga/tenga un error lógico/error o condición de carrera en lugar de que sea un error del sistema operativo.

    – nos

    27 de diciembre de 2013 a las 23:23


  • Esta suposición es extremadamente improbable. Si close() no funcionó, nada funcionaría.

    – usuario207421

    27 mayo 2017 a las 22:49

  • @Nemo Eso es completamente incorrecto. Como solo un ejemplo de cómo es incorrecto, imagina si hay dos descriptores que hacen referencia al mismo socket. Vocación close en cualquiera de los descriptores no cerrar el enchufe.

    –David Schwartz

    19 de diciembre de 2019 a las 11:09

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad