close() schließt Socket nicht richtig

Lesezeit: 11 Minuten

Benutzer-Avatar
DavidMFrey

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

Processing request for 21
Writing to 21
Closing 21

Der Rückgabewert von close() ist 0, oder es würde eine weitere Debug-Anweisung ausgegeben werden. Nach dieser Ausgabe mit einem hängenden Client zeigt lsof eine aufgebaute Verbindung an.

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

CLIENT 17747 root 12u IPv4 32754228 TCP localhost:47530->localhost:9980 (EINRICHTET)

Es ist, als ob der Server niemals die Shutdown-Sequenz an den Client sendet und dieser Zustand hängen bleibt, bis der Client beendet wird, wodurch die Serverseite in einem engen Wartezustand verbleibt

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

Auch wenn für den Client ein Timeout angegeben ist, kommt es zu einem Timeout, anstatt zu hängen. Ich kann auch manuell ausführen

call close(21)

im Server von gdb, und der Client wird dann die Verbindung trennen. Dies passiert vielleicht 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 (Endgültig)

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:

Die 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 einfache Lockenverbindung. Der Client hängt in transfer.c (in libcurl), weil der Socket nicht als geschlossen wahrgenommen wird. Es wartet auf weitere Daten vom Server.

Dinge, die ich bisher ausprobiert habe:

Abschaltung vor Schließung

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

Festlegen von SO_LINGER zum zwangsweisen Schließen in 1 Sekunde

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. Irgendwelche Ideen würden sehr geschätzt.

BEARBEITEN – Dies stellte sich als Thread-Sicherheitsproblem in einer Warteschlangenbibliothek heraus, das dazu führte, dass der Socket von mehreren Threads unangemessen behandelt wurde.

  • Sind Sie sich zu 100 % sicher, dass kein anderer Thread möglicherweise den Socket verwendet, wenn Sie anrufen? close drauf? Wie führen Sie Ihre nicht blockierenden Lesevorgänge durch?

    – David Schwartz

    23. Dezember 2014 um 16:34 Uhr

  • Ich fürchte, ich habe mich gerade hier angemeldet und mich an dieses Problem erinnert. Ich fand später heraus, dass es ein Thread-Sicherheitsproblem in einer Warteschlange gab, die verwendet wurde, um die Verbindungen herumzureichen. Hier war kein Fehler. Sorry für die Fehlinformation.

    – DavidMFrey

    15. März 2016 um 15:46 Uhr

Benutzer-Avatar
Josef Quinsey

Hier ist ein Code, den ich auf vielen Unix-ähnlichen Systemen (z. B. SunOS 4, SGI IRIX, HPUX 10.20, CentOS 5, Cygwin) verwendet habe, 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 Obige garantiert jedoch nicht, dass gepufferte Schreibvorgänge gesendet werden.

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

  • Die Verzögerung war meistens zu lang.
  • Die Verzögerung war manchmal zu kurz – vielleicht!
  • Ein Signal wie SIGCHLD könnte zu Ende auftreten usleep() (Aber meistens rufe ich an usleep() zweimal, um diesen Fall zu behandeln – ein Hack).
  • Es gab keinen Hinweis darauf, ob dies funktioniert. Aber das ist vielleicht nicht wichtig, wenn a) Hard-Resets vollkommen in Ordnung sind und/oder b) Sie die Kontrolle über beide Seiten des Links haben.

Aber eine richtige Spülung durchzuführen ist überraschend schwer. Verwenden SO_LINGER ist anscheinend nicht der Weg, den man gehen sollte; siehe zum beispiel:

Und SIOCOUTQ scheint Linux-spezifisch zu sein.

Notiz shutdown(fd, SHUT_WR) nicht hör auf zu schreiben, im Gegensatz zu seinem Namen und vielleicht im Gegensatz zu man 2 shutdown.

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

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, meine getWallTimeEpoch() ist ähnlich wie time(), und Perror() ist eine Hülle für perror().

Bearbeiten: Einige Kommentare:

  • Mein erstes Eingeständnis ist etwas peinlich. Das OP und Nemo bestritten die Notwendigkeit, das Interne zu löschen so_error vor dem Schließen, aber ich kann jetzt keine Referenz dafür finden. Das fragliche System war HPUX 10.20. Nach einem Fehlschlag connect()rufe nur an close() hat den Dateideskriptor nicht freigegeben, weil das System mir einen ausstehenden Fehler liefern wollte. Aber ich habe mir, wie die meisten Leute, nie die Mühe gemacht, den Rückgabewert von zu überprüfen close. Also gingen mir irgendwann die Dateideskriptoren aus (ulimit -n), was endlich meine Aufmerksamkeit erregte.

  • (sehr kleiner Punkt) Ein Kommentator wandte sich gegen die fest codierten numerischen Argumente shutdown()anstatt zB SHUT_WR für 1. Die einfachste Antwort ist, dass Windows verschiedene #defines/enums verwendet, zB SD_SEND. Und viele andere Schreiber (z. B. Beej) verwenden Konstanten, ebenso wie viele Legacy-Systeme.

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

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

  • Ich wünschte, ich könnte zweimal abstimmen. Dies ist erst das zweite Beispiel einer korrekt verschlossenen Steckdose, die ich in freier Wildbahn gesehen habe.

    – trauern

    4. Oktober 2012 um 15:44 Uhr

  • Ich finde shutdown sollten mit den entsprechenden Makros betrieben werden SHUT_RD etc

    – Jens Gustedt

    4. Oktober 2012 um 16:15 Uhr

  • Lesen Sie die glorreichen FINWAIT Funktion von TCP.

    – Steve-o

    4. Oktober 2012 um 18:15 Uhr


  • Ihr Code hat ein Problem in meinem Client behoben, bei dem die Verbindung nicht sofort wiederhergestellt werden konnte, nachdem die Verbindung vom Server getrennt wurde, da der Client ein SYN gesendet hat, bevor er FIN überhaupt bestätigt hat.

    – Philippe A.

    11. Juli 2013 um 15:28 Uhr

  • Nur für den Fall, dass jemand anderes versucht, herauszufinden, wie getSO_ERROR() trägt zur Lösung des Problems bei: es stellt sich heraus, dass Berufung getsockopt mit SO_ERROR wird zuerst den Fehlerstatus abrufen und dann zurücksetzen. Diese Informationen waren für mich nicht leicht zu finden, und ich bin mir auch nicht sicher, ob sie übertragbar sind. Die folgende Manpage dokumentiert dieses Verhalten: linux.die.net/man/3/getsockopt Aber dieselbe Manpage (man 3 getsockopt) auf meiner Distribution nicht (RHEL8).

    – Psq

    30. Juli 2021 um 9:38 Uhr

Tolle Antwort von Joseph Quinsey. Ich habe Anmerkungen zu den haveInput Funktion. Sie fragen sich, wie wahrscheinlich es ist, dass select ein fd zurückgibt, das Sie nicht in Ihr Set aufgenommen haben. Dies wäre IMHO ein großer Betriebssystemfehler. Das ist die Art von Dingen, die ich überprüfen würde, wenn ich Komponententests für die schreiben würde select Funktion, 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 die Handhabung von EINTR. Theoretisch könnten Sie in einer Endlosschleife stecken bleiben, wenn select gab weiterhin EINTR zurück, da dieser Fehler die Schleife von vorne beginnen lässt. Angesichts des sehr kurzen Timeouts (0,01) scheint es sehr unwahrscheinlich, dass dies geschieht. Ich denke jedoch, dass der angemessene Weg, damit umzugehen, darin besteht, Fehler an den Aufrufer zurückzugeben (flushSocketBeforeClose). Der Anrufer kann weiter anrufen haveInput hat, solange das Timeout noch nicht abgelaufen ist, und deklarieren Sie bei anderen Fehlern einen Fehler.

ERGÄNZUNG #1

flushSocketBeforeClose wird im Falle von nicht schnell beendet read Rückgabe eines Fehlers. Es wird so lange wiederholt, bis das Timeout abgelaufen ist. Auf die kann man sich nicht verlassen select Innerhalb haveInput 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; 

Das klingt für mich nach einem Fehler in Ihrer Linux-Distribution.

Das Dokumentation der GNU C-Bibliothek sagt:

Wenn Sie mit der Verwendung eines Sockets fertig sind, können Sie seinen Dateideskriptor einfach mit schließen 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 Betriebssystem hat einen Fehler.

  • Neige zu dieser Antwort. Es wird einige Arbeit erfordern, ein anderes Betriebssystem zum Testen bereitzustellen. Ich werde das nachholen, sobald ich es getestet habe. Ich möchte diesen Link von @Nemo hinzufügen, da er für die Frage relevant erscheint. und die Antwort, an die es angehängt war, wurde gelöscht. sites.google.com/site/michaelsafyan/software-engineering/…

    – DavidMFrey

    4. Oktober 2012 um 16:42 Uhr

  • Nothing about clearing any error flags or waiting for the data to be flushed or any such thing. “Warten auf das Löschen der Daten” fällt wohl unter “wenn Sie mit der Verwendung eines Sockets fertig sind”.

    – Leichtigkeitsrennen im Orbit

    8. November 2012 um 19:14 Uhr


  • @DavidMFrey Das würde bedeuten, dass es eine nahezu 100% ige Chance gibt, dass Ihr Code einen Logikfehler/Bug oder eine Racebedingung hatte/hat, anstatt dass es sich um einen Betriebssystemfehler handelt.

    – Nr

    27. Dezember 2013 um 23:23 Uhr


  • Diese Annahme ist verschwindend unwahrscheinlich. Wenn close() funktionierte nicht, nichts würde funktionieren.

    – Benutzer207421

    27. Mai 2017 um 22:49 Uhr

  • @Nemo Das ist völlig falsch. Stellen Sie sich als nur ein Beispiel dafür vor, dass es zwei Deskriptoren gibt, die auf denselben Socket verweisen. Berufung close auf beiden Deskriptoren wird nicht Schließen Sie die Steckdose.

    – David Schwartz

    19. Dezember 2019 um 11:09 Uhr

Benutzer-Avatar
Sammy

include: #include

dies sollte helfen, das close(); Problem

1372680cookie-checkclose() schließt Socket nicht richtig

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy