Combien de temps une adresse de socket local TCP qui a été liée n'est-elle pas disponible après la fermeture?

13

Sous Linux (mes serveurs en direct sont sur RHEL 5.5 - les liens LXR ci-dessous sont vers la version du noyau), man 7 ipdit:

Une adresse de socket local TCP qui a été liée n'est pas disponible pendant un certain temps après la fermeture, sauf si l'indicateur SO_REUSEADDR a été défini.

Je n'utilise pas SO_REUSEADDR. Combien de temps dure "un certain temps"? Comment savoir combien de temps il dure et comment le changer?

J'ai fait des recherches sur ce sujet et j'ai trouvé quelques morceaux d'informations, dont aucun n'explique vraiment cela du point de vue d'un programmeur d'applications. En être témoin:

Là où je trébuche, c'est pour combler le fossé entre le modèle du noyau du cycle de vie TCP et le modèle de programmeur des ports qui ne sont pas disponibles, c'est-à-dire pour comprendre comment ces états sont liés au "quelque temps".

Tom Anderson
la source
@Caleb: Concernant les tags, bind est aussi un appel système! Essayez man 2 bindsi vous ne me croyez pas. Certes, ce n'est probablement pas la première chose à laquelle les gens d'Unix pensent quand quelqu'un dit "lier", donc assez juste.
Tom Anderson
J'étais bien conscient des utilisations alternatives de bind, mais la balise ici est spécifiquement appliquée au serveur DNS. Nous n'avons pas de balises pour chaque appel système possible.
Caleb

Réponses:

14

Je crois que l'idée que le socket n'est pas disponible pour un programme est de permettre à tous les segments de données TCP encore en transit d'arriver et d'être rejetés par le noyau. Autrement dit, il est possible pour une application d'appeler close(2)sur un socket, mais les retards ou les incidents de routage pour contrôler les paquets ou ce que vous avez peuvent permettre à l'autre côté d'une connexion TCP d'envoyer des données pendant un certain temps. L'application a indiqué qu'elle ne souhaitait plus traiter les segments de données TCP, le noyau devrait donc simplement les supprimer à mesure qu'ils entrent.

J'ai piraté un petit programme en C que vous pouvez compiler et utiliser pour voir combien de temps le timeout est:

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

J'ai essayé ce programme sur 3 machines différentes, et j'obtiens un temps variable, entre 55 et 59 secondes, lorsque le noyau refuse d'autoriser un utilisateur non root à rouvrir un socket. J'ai compilé le code ci-dessus dans un exécutable nommé "opener" et l'ai exécuté comme ceci:

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

J'ai ouvert une autre fenêtre et j'ai fait ceci:

telnet otherhost 7896

Cela oblige la première instance de "opener" à accepter une connexion, puis à la fermer. La seconde instance de "opener" essaie de bind(2)se connecter au port TCP 7896 chaque seconde. "ouvreur" signale 55 à 59 secondes de retard.

Googler autour, je trouve que les gens recommandent de faire ceci:

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

pour réduire cet intervalle. Ça n'a pas marché pour moi. Sur les 4 machines Linux auxquelles j'avais accès, deux en avaient 30 et deux en avaient 60. J'ai également réglé cette valeur à 10. Aucune différence avec le programme "opener".

Ce faisant:

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

a changé les choses. Le deuxième "ouvreur" n'a pris que 3 secondes environ pour obtenir sa nouvelle prise.

Bruce Ediger
la source
3
Je comprends (approximativement) quel est le but de la période d'indisponibilité. Ce que je voudrais savoir, c'est exactement combien de temps cette période est sous Linux et comment elle peut être modifiée. Le problème avec un numéro d'une page Wikipédia sur TCP est qu'il s'agit nécessairement d'une valeur généralisée, et pas quelque chose qui est définitivement vrai de ma plate-forme spécifique.
Tom Anderson
vos spéculations étaient intéressantes! il suffit de les marquer comme ayant un titre au lieu de les supprimer, cela permet de rechercher la raison!
Philippe Gachoud