web-dev-qa-db-fra.com

close () ne ferme pas correctement socket

J'ai un serveur multi-thread (pool de threads) qui gère un grand nombre de demandes (jusqu'à 500/s pour un nœud), en utilisant 20 threads. Un thread d'écoute accepte les connexions entrantes et les met en file d'attente pour être traitées par les threads gestionnaires. Une fois la réponse prête, les threads écrivent ensuite sur le client et ferment le socket. Tout semblait aller bien jusqu'à récemment, un programme client test a été suspendu de manière aléatoire après avoir lu la réponse. Après de nombreuses recherches, il semble que la méthode close () du serveur ne déconnecte pas réellement le socket. J'ai ajouté des impressions de débogage au code avec le numéro de descripteur de fichier et j'obtiens ce type de sortie.

Processing request for 21
Writing to 21
Closing 21

La valeur de retour de close () est 0, sinon une autre instruction de débogage sera imprimée. Après cette sortie avec un client qui se bloque, lsof montre une connexion établie.

SERVEUR 8160 racine 21u IPv4 32754237 TCP localhost: 9980-> localhost: 47530 (ESTABLISHED)

CLIENT 17747 racine 12u IPv4 32754228 TCP localhost: 47530-> localhost: 9980 (ESTABLISHED)

C'est comme si le serveur n'envoyait jamais la séquence d'arrêt au client, et cet état se bloque jusqu'à ce que le client soit tué, laissant le côté serveur dans un état d'attente proche.

SERVEUR 8160 racine 21u IPv4 32754237 TCP localhost: 9980-> localhost: 47530 (CLOSE_WAIT)

De plus, si le client a un délai d'expiration spécifié, il expirera au lieu de rester en attente. Je peux aussi exécuter manuellement

call close(21)

dans le serveur de gdb, et le client sera alors déconnecté. Cela se produit peut-être une fois sur 50 000, mais peut ne pas arriver pendant de longues périodes.

Version Linux: 2.6.21.7-2.fc8xen Version Centos: 5.4 (Final)

les actions de socket sont les suivantes

SERVEUR:

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  */
}

Ensuite, le thread prend le socket et construit la réponse.

/*  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 et 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);
  }

CLIENT:

Le côté client utilise 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);

Rien d'extraordinaire, juste une connexion de base curl. Le client se bloque dans tranfer.c (dans libcurl) car le socket n'est pas perçu comme étant fermé. Il attend plus de données du serveur.

Choses que j'ai essayées jusqu'à présent:

Fermeture avant fermeture

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

Paramétrage de SO_LINGER pour qu'il se ferme de force en 1 seconde

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

Ceux-ci n'ont fait aucune différence. Toutes les idées seraient grandement appréciées.

EDIT - Cela a fini par être un problème de sécurité des threads à l'intérieur d'une bibliothèque de files d'attente, ce qui a entraîné une gestion incorrecte du socket par plusieurs threads.

22
DavidMFrey

Voici du code que j'ai utilisé sur de nombreux systèmes de type Unix (SunOS 4, SGI IRIX, HPUX 10.20, CentOS 5, Cygwin, par exemple) pour fermer 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");
   }
}

Mais ce qui précède ne garantit pas que les écritures mises en tampon sont envoyées.

Gracieux proche: Il m'a fallu environ 10 ans pour comprendre comment fermer une prise. Mais pendant encore 10 ans, j’ai simplement appelé paresseusement usleep(20000) pour un léger délai afin de «s’assurer» que le tampon d’écriture a été vidé avant la fermeture. Ce n'est évidemment pas très intelligent, parce que:

  • Le délai était trop long la plupart du temps.
  • Le délai était parfois trop court - peut-être!
  • Un signal de ce type SIGCHLD pourrait survenir pour mettre fin à usleep() (mais j’ai habituellement appelé usleep() deux fois pour traiter ce cas - un hack).
  • Il n'y avait aucune indication si cela fonctionne. Mais cela n’est peut-être pas important si a) les réinitialisations dures sont parfaitement correctes et/ou b) vous avez le contrôle des deux côtés du lien.

Mais faire une vraie couleur est étonnamment difficile. Utiliser SO_LINGER est apparemment pas le chemin à parcourir; voir par exemple:

Et SIOCOUTQ semble être spécifique à Linux.

Remarque shutdown(fd, SHUT_WR)ne} _ arrête d'écrire, contrairement à son nom et éventuellement à man 2 shutdown.

Ce code flushSocketBeforeClose() attend jusqu'à une lecture de zéro octet ou jusqu'à l'expiration du temporisateur. La fonction haveInput() est un simple wrapper pour select (2) et est configurée pour bloquer jusqu'à 1/100e de seconde.

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;
}

Exemple d'utilisation:

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

Dans ce qui précède, mon getWallTimeEpoch() est similaire à time(), et Perror() est un wrapper pour perror().

Edit: Quelques commentaires:

  • Mon premier aveu est un peu gênant. L'OP et Nemo ont contesté la nécessité d'effacer le so_error interne avant la fermeture, mais je ne peux maintenant trouver aucune référence à ce sujet. Le système en question était HPUX 10.20. Après l’échec de connect(), le simple fait d’appeler close() n’a pas libéré le descripteur de fichier, car le système souhaitait me livrer une erreur en suspens. Mais comme la plupart des gens, je n'ai jamais pris la peine de vérifier la valeur de retour de close.. J'ai donc fini par manquer de descripteurs de fichier (ulimit -n),, ce qui a finalement attiré mon attention.

  • (point très mineur) Un commentateur s’est opposé aux arguments numériques codés en dur pour shutdown(), plutôt que par exemple. SHUT_WR pour 1. La réponse la plus simple est que Windows utilise par exemple différents # define/enums. SD_SEND. Et de nombreux autres auteurs (par exemple, Beej) utilisent des constantes, comme le font de nombreux systèmes hérités.

  • De plus, je place toujours, toujours, FD_CLOEXEC sur tous mes sockets, car dans mes applications, je ne souhaite jamais qu’elles soient transmises à un enfant et, plus important encore, je ne souhaite pas qu’un enfant pendu me frappe.

Exemple de code pour définir 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");
   }
56
Joseph Quinsey

Grande réponse de Joseph Quinsey. J'ai des commentaires sur la fonction haveInput. Vous vous demandez quelle est la probabilité que select renvoie un fd que vous n'avez pas inclus dans votre ensemble. Ce serait un bug majeur du système d'exploitation IMHO. C'est le genre de chose que je vérifierais si j'avais écrit des tests unitaires pour la fonction select, pas dans une application ordinaire.

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

Mon autre commentaire concerne la gestion de EINTR. En théorie, vous pourriez rester coincé dans une boucle infinie si select renvoyait EINTR, car cette erreur laissait la boucle recommencer. Compte tenu du délai très court (0,01), il semble très peu probable que cela se produise. Cependant, je pense que la manière appropriée de traiter cela serait de renvoyer les erreurs à l'appelant (flushSocketBeforeClose). L'appelant peut continuer à appeler haveInput tant que son délai d'attente n'a pas expiré et déclarer l'échec d'autres erreurs.

ADDITION # 1

flushSocketBeforeClose ne quittera pas rapidement si read renvoie une erreur. Il continuera à boucler jusqu'à l'expiration du délai. Vous ne pouvez pas compter sur select dans haveInput pour anticiper toutes les erreurs. read a ses propres erreurs (ex: EIO).

     while (haveInput(fd, 0.01)) 
        if (!read(fd, discard, sizeof discard)) <-- -1 does not end loop
           return TRUE; 
2
Philippe A.

Cela me semble être un bogue dans votre distribution Linux.

La documentation de la bibliothèque GNU C dit:

Lorsque vous avez fini d'utiliser un socket, vous pouvez simplement fermer son fichier descripteur avec close

Rien ne supprime les indicateurs d'erreur ou attend que les données soient vidées ou quoi que ce soit.

Votre code est bon; Votre O/S a un bug.

0
Nemo