Wie lange ist eine lokale TCP-Socket-Adresse, die nach dem Schließen gebunden wurde, nicht verfügbar?

13

Unter Linux (meine Live-Server sind auf RHEL 5.5 - die folgenden LXR-Links verweisen auf die Kernel-Version), man 7 ipheißt es:

Eine gebundene lokale TCP-Socket-Adresse ist nach dem Schließen einige Zeit lang nicht verfügbar, es sei denn, das Flag SO_REUSEADDR wurde gesetzt.

Ich benutze nicht SO_REUSEADDR. Wie lang ist "einige Zeit"? Wie kann ich herausfinden, wie lange es dauert und wie kann ich es ändern?

Ich habe darüber gegoogelt und ein paar Informationen gefunden, von denen keine das wirklich aus der Sicht eines Anwendungsprogrammierers erklärt. Nämlich:

  • TCP_TIMEWAIT_LEN in net/tcp.hgibt an, "wie lange gewartet werden muss, um den Status TIME-WAIT zu zerstören", und ist auf "ungefähr 60 Sekunden" festgelegt.
  • / proc / sys / net / ipv4 / tcp_fin_timeout ist "Zeit, den Socket im Zustand FIN-WAIT-2 zu halten, falls er von unserer Seite geschlossen wurde" und "Standardwert ist 60sec".

Ich stolpere über die Lücke zwischen dem Kernel-Modell des TCP-Lebenszyklus und dem Modell des Programmierers, dass Ports nicht verfügbar sind.

Tom Anderson
quelle
@Caleb: In Bezug auf die Tags ist bind auch ein Systemaufruf! Versuch es, man 2 bindwenn du mir nicht glaubst. Zugegeben, es ist wahrscheinlich nicht das erste, woran Unix-Leute denken, wenn jemand "bind" sagt, also fair genug.
Tom Anderson
Ich war mir der alternativen Verwendung von wohl bewusst bind, aber das Tag hier wird speziell auf den DNS-Server angewendet. Wir haben nicht für jeden möglichen Systemaufruf Tags.
Caleb

Antworten:

14

Ich glaube, dass die Idee, dass der Socket für ein Programm nicht verfügbar ist, darin besteht, dass alle TCP-Datensegmente, die sich noch auf dem Weg befinden, eintreffen und vom Kernel verworfen werden. Das heißt, es ist möglich, dass eine Anwendung close(2)einen Socket aufruft, aber Verzögerungen oder Pannen weiterleitet, um Pakete zu steuern, oder was Sie der anderen Seite einer TCP-Verbindung erlauben können, für eine Weile Daten zu senden. Die Anwendung hat angegeben, dass sie keine TCP-Datensegmente mehr verarbeiten möchte, daher sollte der Kernel sie einfach verwerfen, sobald sie eingehen.

Ich habe ein kleines Programm in C gehackt, das Sie kompilieren und verwenden können, um zu sehen, wie lange das Timeout dauert:

#include <stdio.h>        /* fprintf() */
#include <string.h>       /* strerror() */
#include <errno.h>        /* errno */
#include <stdlib.h>       /* strtol() */
#include <signal.h>       /* signal() */
#include <sys/time.h>     /* struct timeval */
#include <unistd.h>       /* read(), write(), close(), gettimeofday() */
#include <sys/types.h>    /* socket() */
#include <sys/socket.h>   /* socket-related stuff */
#include <netinet/in.h>
#include <arpa/inet.h>    /* inet_ntoa() */
float elapsed_time(struct timeval before, struct timeval after);
int
main(int ac, char **av)
{
        int opt;
        int listen_fd = -1;
        unsigned short port = 0;
        struct sockaddr_in  serv_addr;
        struct timeval before_bind;
        struct timeval after_bind;

        while (-1 != (opt = getopt(ac, av, "p:"))) {
                switch (opt) {
                case 'p':
                        port = (unsigned short)atoi(optarg);
                        break;
                }
        }

        if (0 == port) {
                fprintf(stderr, "Need a port to listen on\n");
                return 2;
        }

        if (0 > (listen_fd = socket(AF_INET, SOCK_STREAM, 0))) {
                fprintf(stderr, "Opening socket: %s\n", strerror(errno));
                return 1;
        }

        memset(&serv_addr, '\0', sizeof(serv_addr));
        serv_addr.sin_family      = AF_INET;
        serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
        serv_addr.sin_port        = htons(port);

        gettimeofday(&before_bind, NULL);
        while (0 > bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr))) {
                fprintf(stderr, "binding socket to port %d: %s\n",
                        ntohs(serv_addr.sin_port),
                        strerror(errno));

                sleep(1);
        }
        gettimeofday(&after_bind, NULL);
        printf("bind took %.5f seconds\n", elapsed_time(before_bind, after_bind));

        printf("# Listening on port %d\n", ntohs(serv_addr.sin_port));
        if (0 > listen(listen_fd, 100)) {
                fprintf(stderr, "listen() on fd %d: %s\n",
                        listen_fd,
                        strerror(errno));
                return 1;
        }

        {
                struct sockaddr_in  cli_addr;
                struct timeval before;
                int newfd;
                socklen_t clilen;

                clilen = sizeof(cli_addr);

                if (0 > (newfd = accept(listen_fd, (struct sockaddr *)&cli_addr, &clilen))) {
                        fprintf(stderr, "accept() on fd %d: %s\n", listen_fd, strerror(errno));
                        exit(2);
                }
                gettimeofday(&before, NULL);
                printf("At %ld.%06ld\tconnected to: %s\n",
                        before.tv_sec, before.tv_usec,
                        inet_ntoa(cli_addr.sin_addr)
                );
                fflush(stdout);

                while (close(newfd) == EINTR) ;
        }

        if (0 > close(listen_fd))
                fprintf(stderr, "Closing socket: %s\n", strerror(errno));

        return 0;
}
float
elapsed_time(struct timeval before, struct timeval after)
{
        float r = 0.0;

        if (before.tv_usec > after.tv_usec) {
                after.tv_usec += 1000000;
                --after.tv_sec;
        }

        r = (float)(after.tv_sec - before.tv_sec)
                + (1.0E-6)*(float)(after.tv_usec - before.tv_usec);

        return r;
}

Ich habe dieses Programm auf 3 verschiedenen Computern ausprobiert und erhalte eine variable Zeit zwischen 55 und 59 Sekunden, wenn der Kernel einem Benutzer ohne Rootberechtigung das erneute Öffnen eines Sockets verweigert. Ich habe den obigen Code zu einer ausführbaren Datei mit dem Namen "opener" kompiliert und sie folgendermaßen ausgeführt:

./opener -p 7896; ./opener -p 7896

Ich öffnete ein anderes Fenster und tat dies:

telnet otherhost 7896

Das bewirkt, dass die erste Instanz von "opener" eine Verbindung akzeptiert und dann schließt. Die zweite Instanz von "opener" versucht bind(2)jede Sekunde , auf den TCP-Port 7896 zuzugreifen. "opener" meldet eine Verzögerung von 55 bis 59 Sekunden.

Beim googeln finde ich, dass die Leute dies empfehlen:

echo 30 > /proc/sys/net/ipv4/tcp_fin_timeout

um dieses Intervall zu reduzieren. Es hat bei mir nicht funktioniert. Von den 4 Linux-Rechnern, auf die ich Zugriff hatte, hatten zwei 30 und zwei 60. Ich habe diesen Wert auch auf 10 gesetzt. Kein Unterschied zum "opener" -Programm.

Dies tun:

echo 1 > /proc/sys/net/ipv4/tcp_tw_recycle

Dinge geändert haben. Der zweite "Öffner" brauchte nur ca. 3 Sekunden, um seinen neuen Sockel zu bekommen.

Bruce Ediger
quelle
3
Ich verstehe (ungefähr), was der Zweck des Zeitraums der Nichtverfügbarkeit ist. Was ich genau wissen möchte, ist, wie lange dieser Zeitraum unter Linux ist und wie er geändert werden kann. Das Problem mit einer Nummer von einer Wikipedia-Seite über TCP ist, dass es sich notwendigerweise um einen verallgemeinerten Wert handelt und nicht um etwas, das für meine spezielle Plattform definitiv zutrifft.
Tom Anderson
Ihre Spekulationen waren interessant! Kennzeichnen Sie sie einfach als Titel, anstatt sie zu entfernen. Auf diese Weise können Sie nach dem Grund suchen, warum!
Philippe Gachoud