it-swarm.com.de

close () schließt den Sockel nicht richtig

Ich habe einen Multithread-Server (Thread-Pool), der eine große Anzahl von Anforderungen verarbeitet (bis zu 500/s für einen Knoten), wobei 20 Threads verwendet werden. Es gibt einen Listener-Thread, der eingehende Verbindungen akzeptiert und in eine Warteschlange stellt, damit die Handler-Threads verarbeitet werden können. Sobald die Antwort fertig ist, schreiben die Threads zum Client und schließen den Socket. Bis vor kurzem schien alles in Ordnung zu sein. Nach dem Lesen der Antwort begann ein Test-Client-Programm zufällig zu hängen. Nach vielem Graben scheint es, als würde das close () vom Server den Socket nicht wirklich trennen. Ich habe dem Code einige Debugging-Drucke mit der Dateideskriptor-Nummer hinzugefügt, und ich bekomme diese Art von Ausgabe.

Processing request for 21
Writing to 21
Closing 21

Der Rückgabewert von close () ist 0, oder es wird eine andere Debug-Anweisung gedruckt. Nach dieser Ausgabe mit einem Client, der hängt, zeigt lsof eine hergestellte Verbindung an.

SERVER 8160 root 21u IPv4 32754237 TCP localhost: 9980-> localhost: 47530 (ESTABLISHED)

KUNDE 17747 root 12u IPv4 32754228 TCP localhost: 47530-> localhost: 9980 (ERSTELLT)

Es ist, als würde der Server niemals die Herunterfahren-Sequenz an den Client senden, und dieser Status hängt, bis der Client beendet wird, und der Server befindet sich in einem geschlossenen Wartezustand

SERVER 8160 root 21u IPv4 32754237 TCP localhost: 9980-> localhost: 47530 (CLOSE_WAIT)

Wenn für den Client ein Timeout festgelegt wurde, wird es statt hängen bleiben. Ich kann auch manuell laufen

call close(21)

auf dem Server von gdb, und der Client wird dann die Verbindung trennen. Dies geschieht möglicherweise einmal in 50.000 Anfragen, aber möglicherweise nicht für längere Zeiträume.

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

socket-Aktionen sind wie folgt

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

Dann nimmt der Thread den Socket auf und baut die Antwort auf.

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

KLIENT:

Clientseite verwendet 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);

Nichts Besonderes, nur eine grundlegende Curl-Verbindung. Client hängt in tranfer.c (in libcurl), da der Socket nicht als geschlossen erkannt wird. Es wartet auf weitere Daten vom Server.

Dinge, die ich bisher ausprobiert habe:

Vor dem Schließen herunterfahren

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

Einstellung, dass SO_LINGER in 1 Sekunde zwangsweise geschlossen wird

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

Diese haben keinen Unterschied gemacht. Alle Ideen wären sehr dankbar.

BEARBEITEN - Dies wurde zu einem Thread-Sicherheitsproblem innerhalb einer Warteschlangenbibliothek, was dazu führte, dass der Socket von mehreren Threads falsch behandelt wurde.

22
DavidMFrey

Hier ist ein Code, den ich auf vielen Unix-ähnlichen Systemen verwendet habe (z. B. SunOS 4, SGI IRIX, HPUX 10.20, CentOS 5, Cygwin), um einen Socket zu schließen:

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

Das oben Gesagte garantiert jedoch nicht, dass gepufferte Schreibvorgänge gesendet werden.

Graceful close: Ich habe ungefähr 10 Jahre gebraucht, um herauszufinden, wie man eine Steckdose schließt. Aber für weitere 10 Jahre rief ich faul nur usleep(20000) für eine kurze Verzögerung an, um 'sicherzustellen', dass der Schreibpuffer vor dem Schließen geleert wurde. Das ist offensichtlich nicht sehr klug, denn:

  • Die Verspätung war meistens zu lang.
  • Die Verspätung war manchmal zu kurz - vielleicht!
  • Ein Signal wie SIGCHLD könnte auftreten, um usleep() zu beenden (normalerweise habe ich jedoch usleep() zweimal aufgerufen, um diesen Fall zu bearbeiten - einen Hack).
  • Es gab keinen Hinweis darauf, ob dies funktioniert. Dies ist jedoch möglicherweise nicht wichtig, wenn a) harte Zurücksetzungen vollkommen in Ordnung sind und/oder b) Sie die Kontrolle über beide Seiten des Links haben.

Aber einen richtigen Flush zu machen ist überraschend schwer. Die Verwendung von SO_LINGER ist anscheinend nicht der Weg zu gehen; siehe zum Beispiel:

Und SIOCOUTQ scheint Linux-spezifisch zu sein.

Hinweis shutdown(fd, SHUT_WR)nicht stoppt das Schreiben, entgegen dem Namen und möglicherweise entgegen dem man 2 shutdown.

Dieser Code flushSocketBeforeClose() wartet, bis null Bytes gelesen werden oder der Timer abläuft. Die Funktion haveInput() ist ein einfacher Wrapper für select (2) und kann bis zu 1/100 Sekunde blockieren.

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

Anwendungsbeispiel:

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

In der obigen Beschreibung ähnelt mein getWallTimeEpoch()time(), und Perror() ist ein Wrapper für perror()..

Edit: Einige Kommentare:

  • Mein erstes Eingeständnis ist etwas peinlich. Das OP und Nemo forderten die Notwendigkeit heraus, den internen so_error vor dem Schließen zu löschen, aber ich kann jetzt keine Referenz dafür finden. Das fragliche System war HPUX 10.20. Nach einem fehlgeschlagenen connect() wurde durch das Aufrufen von close() der Dateideskriptor nicht freigegeben, da das System einen ausstehenden Fehler an mich senden wollte. Aber wie die meisten Leute habe ich mich nie darum gekümmert, den Rückgabewert von close. zu überprüfen. Daher waren mir schließlich die Dateideskriptoren (ulimit -n), erschöpft, die schließlich meine Aufmerksamkeit erregt haben.

  • (sehr nebensächlicher Punkt) Ein Kommentator beanstandete die hart codierten numerischen Argumente für shutdown() und nicht z. SHUT_WR für 1. Die einfachste Antwort lautet, dass Windows unterschiedliche # define/enums verwendet, z. SD_SEND. Und viele andere Autoren (z. B. Beej) verwenden Konstanten, wie auch viele Altsysteme.

  • Außerdem setze ich immer und immer FD_CLOEXEC auf alle meine Sockets, da ich in meinen Anwendungen niemals möchte, dass sie an ein Kind weitergegeben werden, und was noch wichtiger ist: Ich möchte nicht, dass ein aufgehängtes Kind mich beeinflusst.

Beispielcode zum Setzen von 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

Tolle Antwort von Joseph Quinsey. Ich habe Kommentare zur Funktion haveInput. Sie fragen sich, wie wahrscheinlich es ist, dass select ein fd zurückgibt, das Sie nicht in Ihr Set aufgenommen haben. Dies wäre ein schwerwiegender OS-Fehler. So würde ich prüfen, ob ich Unit-Tests für die select-Funktion geschrieben habe, nicht in einer gewöhnlichen App.

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

Mein anderer Kommentar bezieht sich auf den Umgang mit EINTR. Theoretisch könnten Sie in einer Endlosschleife stecken bleiben, wenn select EINTR zurückgibt, da dieser Fehler die Schleife von vorne beginnen lässt. Angesichts des sehr kurzen Timeouts (0,01) ist dies höchst unwahrscheinlich. Ich denke jedoch, der beste Weg, um damit umzugehen, wäre die Rückgabe von Fehlern an den Aufrufer (flushSocketBeforeClose). Der Aufrufer kann weiterhin aufrufen, dass haveInput solange sein Timeout noch nicht abgelaufen ist, und für andere Fehler einen Fehler melden.

ZUSATZ # 1

flushSocketBeforeClose wird nicht schnell beendet, wenn read einen Fehler zurückgibt. Es läuft weiter, bis das Zeitlimit abgelaufen ist. Sie können sich nicht auf die select in haveInput verlassen, um alle Fehler zu antizipieren. read hat eigene Fehler (zB: EIO).

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

Das klingt für mich wie ein Fehler in Ihrer Linux-Distribution.

Die GNU C-Bibliotheksdokumentation sagt:

Wenn Sie mit dem Socket fertig sind, können Sie einfach seine Datei schließen Deskriptor mit close

Nichts über das Löschen von Fehlerflags oder das Warten auf das Löschen der Daten oder ähnliches.

Ihr Code ist in Ordnung. Ihr O/S hat einen Fehler.

0
Nemo