Jak długo adres lokalnego gniazda TCP, który został powiązany, jest niedostępny po zamknięciu?


13

W systemie Linux (moje serwery na żywo są w wersji RHEL 5.5 - poniższe łącza LXR dotyczą wersji jądra), man 7 ipmówi:

Powiązany adres lokalnego gniazda TCP jest niedostępny przez pewien czas po zamknięciu, chyba że ustawiono flagę SO_REUSEADDR.

Nie używam SO_REUSEADDR. Jak długo trwa „jakiś czas”? Jak mogę dowiedzieć się, jak długo to trwa i jak mogę to zmienić?

Rozglądam się po tym i znalazłem kilka drobiazgów informacji, z których żadne tak naprawdę nie wyjaśnia tego z perspektywy programisty aplikacji. To znaczy:

Tam, gdzie potykam się, jest wypełnianie luki między modelem jądra cyklu życia TCP a modelem portów programisty niedostępnym, to znaczy w zrozumieniu, w jaki sposób te stany odnoszą się do „pewnego czasu”.


@Caleb: Jeśli chodzi o tagi, bind jest również wywołaniem systemowym! Spróbuj, man 2 bindjeśli mi nie wierzysz. Trzeba przyznać, że prawdopodobnie nie jest to pierwsza rzecz, o której ludzie uniksowi myślą, kiedy ktoś mówi „wiąż”, co jest dość sprawiedliwe.
Tom Anderson

Byłem świadomy alternatywnych zastosowań bind, ale tutaj znacznik jest specjalnie stosowany na serwerze DNS. Nie mamy tagów dla każdego możliwego wywołania systemowego.
Caleb

Odpowiedzi:


14

Uważam, że idea, że ​​gniazdo jest niedostępne dla programu, polega na umożliwieniu dotarcia segmentów danych TCP wciąż będących w tranzycie i odrzucenia ich przez jądro. Oznacza to, że aplikacja może wywoływać close(2)na gnieździe, ale opóźnienia routingu lub nieszczęścia w celu kontrolowania pakietów lub co możesz pozwolić drugiej stronie połączenia TCP na wysyłanie danych przez pewien czas. Aplikacja wskazała, że ​​nie chce już zajmować się segmentami danych TCP, dlatego jądro powinno je po prostu odrzucić, gdy tylko się pojawią.

Zhakowałem mały program w C, który możesz skompilować i użyć, aby sprawdzić, jak długi jest limit czasu:

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

Wypróbowałem ten program na 3 różnych komputerach i otrzymuję zmienny czas, od 55 do 59 sekund, kiedy jądro odmawia zezwolenia użytkownikowi innemu niż root na ponowne otwarcie gniazda. Skompilowałem powyższy kod do pliku wykonywalnego o nazwie „otwieracz” i uruchomiłem go w następujący sposób:

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

Otworzyłem kolejne okno i zrobiłem to:

telnet otherhost 7896

To powoduje, że pierwsza instancja „otwieracza” akceptuje połączenie, a następnie je zamyka. Druga instancja „otwieracza” bind(2)co sekundę próbuje połączyć się z portem TCP 7896. „otwieracz” zgłasza od 55 do 59 sekund opóźnienia.

Googlując się po okolicy, stwierdzam, że ludzie zalecają to:

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

aby skrócić ten interwał. Nie działało to dla mnie. Z 4 maszyn linuksowych, do których miałem dostęp, dwie miały 30, a dwie 60. Ustawiłem też tę wartość na 10. Nie ma różnicy w programie „otwieracza”.

Robiąc to:

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

zmienił rzeczy. Drugi „otwieracz” potrzebował tylko około 3 sekund, aby uzyskać nowe gniazdo.


3
Rozumiem (z grubsza) jaki jest cel okresu niedostępności. Chciałbym wiedzieć dokładnie, jak długo ten okres trwa w Linuksie i jak można go zmienić. Problem z numerem ze strony Wikipedii na temat TCP polega na tym, że jest to z konieczności wartość uogólniona, a nie coś, co zdecydowanie jest zgodne z moją konkretną platformą.
Tom Anderson

twoje spekulacje były interesujące! wystarczy je oznaczyć tytułem zamiast usuwać, daje to możliwość wyszukiwania przyczyny!
Philippe Gachoud
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.