Per quanto tempo un indirizzo socket locale TCP che è stato associato non disponibile dopo la chiusura?


13

Su Linux (i miei server live sono su RHEL 5.5 - i collegamenti LXR sotto sono alla versione del kernel in questo), man 7 ipdice:

Un indirizzo socket TCP locale che è stato associato non è disponibile per qualche tempo dopo la chiusura, a meno che non sia stato impostato il flag SO_REUSEADDR.

Non sto usando SO_REUSEADDR. Quanto dura "un po 'di tempo"? Come faccio a sapere quanto dura e come posso cambiarlo?

Ho cercato su Google e ho trovato alcune informazioni, nessuna delle quali lo spiega davvero dal punto di vista di un programmatore di applicazioni. Per dire:

  • TCP_TIMEWAIT_LEN in net/tcp.hè "quanto tempo attendere per distruggere lo stato TIME-WAIT" e viene fissato a "circa 60 secondi"
  • / proc / sys / net / ipv4 / tcp_fin_timeout è "Il tempo di tenere il socket nello stato FIN-WAIT-2, se fosse chiuso dalla nostra parte", e "Il valore predefinito è 60sec"

Il punto in cui inciampo è colmare il divario tra il modello del kernel del ciclo di vita TCP e il modello di porte del programmatore non disponibile, vale a dire nel comprendere come questi stati si relazionano al "qualche tempo".


@Caleb: per quanto riguarda i tag, anche il bind è una chiamata di sistema! Prova man 2 bindse non mi credi. Certo, probabilmente non è la prima cosa a cui pensano le persone unix quando qualcuno dice "legare", in modo abbastanza equo.
Tom Anderson,

Ero ben consapevole degli usi alternativi di bind, ma il tag qui è specificamente applicato al server DNS. Non abbiamo tag per ogni possibile chiamata di sistema.
Caleb,

Risposte:


14

Credo che l'idea che il socket non sia disponibile per un programma sia di consentire a tutti i segmenti di dati TCP ancora in transito di arrivare e di essere scartati dal kernel. Cioè, è possibile che un'applicazione chiami close(2)su un socket, ma i ritardi di instradamento o gli inconvenienti per controllare i pacchetti o ciò che è possibile consentire all'altro lato di una connessione TCP di inviare dati per un po '. L'applicazione ha indicato che non vuole più occuparsi dei segmenti di dati TCP, quindi il kernel dovrebbe semplicemente scartarli appena entrano.

Ho hackerato un piccolo programma in C che puoi compilare e usare per vedere quanto tempo è il timeout:

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

Ho provato questo programma su 3 macchine diverse e ho un tempo variabile, tra 55 e 59 secondi, quando il kernel rifiuta di consentire a un utente non root di riaprire un socket. Ho compilato il codice sopra in un eseguibile chiamato "apri", e l'ho eseguito in questo modo:

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

Ho aperto un'altra finestra e ho fatto questo:

telnet otherhost 7896

Ciò fa sì che la prima istanza di "apri" accetti una connessione, quindi la chiuda. La seconda istanza di "apri" tenta di bind(2)accedere alla porta TCP 7896 ogni secondo. "apri" riporta da 55 a 59 secondi di ritardo.

Cercando su Google, trovo che le persone raccomandino di fare questo:

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

per ridurre tale intervallo. Non ha funzionato per me. Delle 4 macchine Linux a cui ho avuto accesso, due avevano 30 e due avevano 60. Ho anche impostato quel valore a partire da 10. Nessuna differenza rispetto al programma "apri".

Facendo questo:

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

ha cambiato le cose. Il secondo "apri" ha impiegato solo circa 3 secondi per ottenere il suo nuovo socket.


3
Capisco (approssimativamente) qual è lo scopo del periodo di indisponibilità. Quello che vorrei sapere è esattamente quanto tempo è quel periodo su Linux e come può essere cambiato. Il problema con un numero di una pagina di Wikipedia su TCP è che è necessariamente un valore generalizzato e non qualcosa che è sicuramente vero per la mia piattaforma specifica.
Tom Anderson,

le tue speculazioni erano interessanti! basta contrassegnarli come quelli con un titolo invece di rimuoverli, dà modo di cercare il motivo per cui!
Philippe Gachoud,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.