• [Un peu HS] Truc bizarre avec des sockets Linux

    From =?UTF-8?Q?BERTRAND_Jo=c3=abl?=@21:1/5 to All on Thu Nov 23 20:00:01 2023
    This is an OpenPGP/MIME signed message (RFC 4880 and 3156) --nuUW8Qnr74ZFM8lvMwH0l0zZA5Q6vsrle
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: quoted-printable

    Bonjour à tous,

    Je développe le logiciel suivant http://www.rpl2.fr et un utilisateur du bout du monde vient de me remonter un bug bizarre sur la gestion des
    sockets réseau TCP. Je viens de passer la journée dessus et je ne
    comprends pas.

    Je précise que la fonction a été méchamment testée et qu'il me semble
    qu'elle fonctionnait parfaitement lorsqu'elle a été publiée il y a de
    cela plusieurs années.

    La séquence de commandes C effectuée est la suivante :
    - création d'une socket avec socket() et bind() ;
    - attente d'une connexion entrante avec accept() ;
    - fork() et traitement du client dans le processus fils (y compris la libération de la socket créée par accept()).

    C'est ni plus ni moins que ceci (fonction serve_forever()): https://gist.github.com/laobubu/d6d0e9beb934b60b2e552c2d03e1409e#file-httpd-c

    Dans le langage en question, ça se traduit comme ceci :

    SERVEUR
    <<
    // Création d'une socket TCP écoutant
    // sur le port 87 avec un maximum de 16
    // sockets clientes.

    {
    "local" "stream" "flow"
    { "protocol" "ipv4" }
    { "listen" 16 }
    { "port" 87 }
    { "option" "reuse address" }
    } open

    // Format binaire
    { "length*(*)" } swap format
    -> SOCKET
    <<
    do
    // On attend une connexion
    SOCKET wfsock

    // Si connexion, on lance un
    // traitement dans un processus
    // fils.
    'CIRCUIT_CONNECTE' detach
    // On efface le PID de la pile
    drop
    // On efface les deux sockets de la pile
    drop2
    until
    false
    end
    >>


    CIRCUIT_CONNECTE
    <<
    "Socket connectée" disp
    'SOCKET' sto
    'FERMETURE_SOCKET' atexit

    do
    SOCKET "POLLIN" 2 ->list 1 ->list TIMEOUT_CONNEXION poll
    ...
    until
    ...
    end


    FERMETURE_SOCKET
    <<
    SOCKET close


    Si je remplace 'detach' (fork()) par 'spawn' (thread_create()), ça fonctionne parfaitement bien. Ça peut tourner des heures (j'ai laissé le processus fonctionner durant plusieurs heures, ce qui correspond à
    plusieurs centaines de milliers de connexion sur la socket). Avec
    'detach', le programme finit par planter faute de descripteur de socket disponible (trop de fichiers ouverts).

    Je viens de passer le code dans valgrind et je ne comprends pas bien :

    Root rayleigh:[~/exemple] > valgrind --track-fds=yes ./connecteur.rpl

    +++RPL/2 (R) version 4.1.35 (Jeudi 23/11/2023, 16:42:36 CET)
    +++Copyright (C) 1989 à 2022, 2023 BERTRAND Joël
    ...
    socket: 7 <- renvoyée par accept() dans WFSOCK (la socket créée par
    socket est la 6)
    Socket connectée
    close 7 <- fermeture de la socket 7 (par un shutdown() puis close())
    close 7 OK
    ...
    socket: 9
    Socket connectée
    close 9
    close 9 OK
    ==20196== FILE DESCRIPTORS: 7 open (3 std) at exit.
    ==20196== Open AF_INET socket 7: 127.0.0.1:87 <-> unbound
    ==20196== at 0x546950F: accept (accept.c:26)
    ==20196== by 0x5C0661: librpl_instruction_wfsock (instructions_w1-conv.c:3410)
    ==20196== by 0x4CC36D: librpl_analyse (analyse-conv.c:1076)
    ==20196== by 0x4D9C58: librpl_evaluation (evaluation-conv.c:764)
    ==20196== by 0x5CF16D: librpl_sequenceur_optimise
    (optimisation-conv.c:399)
    ==20196== by 0x5D5A46: librpl_rplinit (rpl-conv.c:5198)
    ==20196== by 0x466F9D: main (init-conv.c:29)
    ...

    Comment se fait-il que la socket soit toujours ouverte dans le père (elle a été explicitement fermée dans le processus fils) ? Les
    ressources système ne sont pas libérées. Pire, chaque socket cliente
    reste ouverte pour le système et le processus parent.

    Je résous une partie du problème en rajoutant un close de la socket cliente après un timeout dans le processus serveur (mais ce n'est pas satisfaisant).

    Je constate aussi que si je ferme dans le processus fils la socket initiale (celle créée par socket()), accept() râle. Le processus fils
    peut donc fermer la socket en attente sur accept() mais s'il ferme la
    socket() renvoyée par accept(), il ne la ferme que pour lui-même (et pas
    pour le processus père).

    La question est donc : suis-je passé à côté de quelque chose ?

    J'en reviens donc à ce bout de code : https://gist.github.com/laobubu/d6d0e9beb934b60b2e552c2d03e1409e#file-httpd-c

    Sauf erreur de ma part, dans la fonction serve_forever(), je trouve bien un accept(), mais jamais de close() sur la socket créée par
    accept() (plus exactement, je trouve le close dans le processus détaché
    par fork()). Comment ce bout de code peut-il fonctionner sans qu'il ne
    finisse par planter par un dépassement du nombre de fichiers ouverts ?

    Merci de votre attention,

    JB

    PS: j'essaie de compiler sur un NetBSD, mais j'ai un problème de
    symboles entre ncurses et readline :

    ltiple definition of `UP'; ../tools/readline-8.2/libreadline.a(terminal.o):(.bss+0xa8): first
    defined here
    /usr/bin/ld:
    ../tools/ncurses-6.4/lib/libncurses.a(lib_termcap.o):(.bss+0x0):
    multiple definition of `BC'; ../tools/readline-8.2/libreadline.a(terminal.o):(.bss+0xb0): first
    defined here
    /usr/bin/ld:
    ../tools/ncurses-6.4/lib/libncurses.a(lib_tputs.o):(.bss+0x6): multiple definition of `PC'; ../tools/readline-8.2/libreadline.a(terminal.o):(.bss+0xb8): first
    defined here
    collect2: erreur: ld a retourné le statut de sortie 1

    À vue de nez, vu le méchant define dans readline, je vais avoir le même
    avec FreeBSD.


    --nuUW8Qnr74ZFM8lvMwH0l0zZA5Q6vsrle--

    -----BEGIN PGP SIGNATURE-----

    iHUEABYIAB0WIQQj8MW8iOsC2RXEznnFW/s/mMLXCAUCZV+eIQAKCRDFW/s/mMLX CDczAQCh0E7Cd7LUnnpeNI0yN4dyhBHlNHZDNuLRnxeKXXMsewD/aTmMH2SHUNVb rVMLnTPI/Zk7EwxdE0XPMNsgNaoOTQw=
    =jaMp
    -----END PGP SIGNATURE-----

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From =?UTF-8?Q?BERTRAND_Jo=c3=abl?=@21:1/5 to All on Fri Nov 24 09:00:01 2023
    This is an OpenPGP/MIME signed message (RFC 4880 and 3156) --vbPphJYBnLKuWNRHxtGRCZwP0faizGm4a
    Content-Type: multipart/mixed;
    boundary="------------D10F031C90B46D4B88AF78CD"

    This is a multi-part message in MIME format. --------------D10F031C90B46D4B88AF78CD
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: quoted-printable

    Désolé, j'ai oublié le code serveur.

    JB

    --------------D10F031C90B46D4B88AF78CD
    Content-Type: text/x-csrc;
    name="tcpServer.c"
    Content-Transfer-Encoding: quoted-printable
    Content-Disposition: attachment;
    filename="tcpServer.c"

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/socket.h>
    #include <sys/types.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>

    #define PORT 4444

    int main(){

    int sockfd, ret;
    struct sockaddr_in serverAddr;

    int newSocket;
    struct sockaddr_in newAddr;

    socklen_t addr_size;

    char buffer[1024];
    pid_t childpid;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd < 0){
    printf("[-]Error in connection.\n");
    exit(1);
    }
    printf("[+]Server Socket is created.\n");

    memset(&serverAddr, '\0', sizeof(serverAddr));
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(PORT);
    serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    ret = bind(sockfd, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
    if(ret < 0){
    printf("[-]Error in binding.\n");
    exit(1);
    }
    printf("[+]Bind to port %d\n", 4444);

    if(listen(sockfd, 10) == 0){
    printf("[+]Listening....\n");
    }else{
    printf("[-]Error in binding.\n");
    }


    while(1){
    newSocket = accept(sockfd, (struct sockaddr*)&newAddr, &addr_size);
    if(newSocket < 0){
    exit(1);
    }
    printf("accept(%d, %d)\n", sockfd, newSocket);
    printf("Connection accepted from %s:%d\n", inet_ntoa(newAddr.sin_addr), ntohs(newAddr.sin_port));

    if((childpid = fork()) == 0){
    close(sockfd);

    while(1){
    recv(newSocket, buffer, 1024, 0);
    if(strcmp(buffer, ":exit") == 0){
    printf("Disconnected from %s:%d\n", inet_ntoa(newAddr.sin_addr), ntohs(newAddr.sin_port));
    printf("shutdown: %d\n", shutdown(newSocket, SHUT_RDWR));
    printf("close: %d\n", close(newSocket));
    break;
    }else{
    printf("Client: %s\n", buffer);
    send(newSocket, buffer, strlen(buffer), 0);
    bzero(buffer, sizeof(buffer));
    }
    }
    }

    }

    close(newSocket);


    return 0;
    }

    --------------D10F031C90B46D4B88AF78CD--

    --vbPphJYBnLKuWNRHxtGRCZwP0faizGm4a--

    -----BEGIN PGP SIGNATURE-----

    iHUEABYIAB0WIQQj8MW8iOsC2RXEznnFW/s/mMLXCAUCZWBXVQAKCRDFW/s/mMLX CNisAQCU+7AZhuPFTq9hA2HynQ5H7j4lN/Z+g+5IpoHd6WJBsQD+NbigxzzB8RPg QgJaqhKJ7eHZF5T6lEC3TCySrFISgwI=
    =/Tcl
    -----END PGP SIGNATURE-----

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From =?UTF-8?Q?BERTRAND_Jo=c3=abl?=@21:1/5 to All on Fri Nov 24 08:50:01 2023
    This is an OpenPGP/MIME signed message (RFC 4880 and 3156) --HFTmE2ODBAjpsH3izab3kXpdlbsvNKNgQ
    Content-Type: multipart/mixed;
    boundary="------------C0A8CF03A656648C558CFE9D"

    This is a multi-part message in MIME format. --------------C0A8CF03A656648C558CFE9D
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: quoted-printable

    Je viens de reproduire la chose avec deux bouts de programmes écrits en
    C (voir les deux pièces jointes).

    hilbert:[~/rpl-test] > ./server
    [+]Server Socket is created.
    [+]Bind to port 4444
    [+]Listening....
    accept(3, 4)
    Connection accepted from 127.0.0.1:43388
    Client: aze
    Disconnected from 127.0.0.1:43388
    shutdown: 0
    close: 0
    accept(3, 6)
    Connection accepted from 127.0.0.1:49504
    Client: aze
    Disconnected from 127.0.0.1:49504
    shutdown: 0
    close: 0

    hilbert:[~/rpl-test] > ./client
    [+]Client Socket is created.
    [+]Connected to Server.
    Client: aze
    Server: aze
    Client: :exit
    [-]Disconnected from server.
    hilbert:[~/rpl-test] > ./client
    [+]Client Socket is created.
    [+]Connected to Server.
    Client: aze
    Server: aze
    Client: :exit
    [-]Disconnected from server.

    Un coup de valgrind montre exactement la même chose lorsque le processus serveur racine est tué par un SIGINT :

    ==10415== Process terminating with default action of signal 2 (SIGINT) ==10415== at 0x49A04D0: accept (accept.c:26)
    ==10415== by 0x109233: main (tcpServer.c:52)
    ==10415==
    ==10415== FILE DESCRIPTORS: 7 open (3 std) at exit.
    ==10415== Open AF_INET socket 6: 127.0.0.1:4444 <-> 127.0.0.1:57256
    ==10415== at 0x49A04D0: accept (accept.c:26)
    ==10415== by 0x109233: main (tcpServer.c:52)
    ==10415==
    ==10415== Open AF_INET socket 4: 127.0.0.1:4444 <-> 127.0.0.1:46402
    ==10415== at 0x49A04D0: accept (accept.c:26)
    ==10415== by 0x109233: main (tcpServer.c:52)
    ==10415==
    ==10415== Open AF_INET socket 3: 127.0.0.1:4444 <-> unbound
    ==10415== at 0x49A0BA7: socket (syscall-template.S:120)
    ==10415== by 0x109141: main (tcpServer.c:25)
    ==10415==
    ==10415== Open file descriptor 5:
    ==10415== <inherited from parent>

    Les sockets créées par accept() [ici fd=4 et fd=6] restent ouvertes dans
    le processus racine du serveur et au bout d'un certain nombre de
    connexions (dépendant de la valeur max open files), accept() retourne
    une erreur.

    À noter : la socket est bien fermée dans le processus créé par fork()
    [shutdown + close] et est fermée dans le client.

    Merci de vos lumières,

    JB

    --------------C0A8CF03A656648C558CFE9D
    Content-Type: text/x-csrc;
    name="tcpClient.c"
    Content-Transfer-Encoding: quoted-printable
    Content-Disposition: attachment;
    filename="tcpClient.c"

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/socket.h>
    #include <sys/types.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>

    #define PORT 4444

    int main(){

    int clientSocket, ret;
    struct sockaddr_in serverAddr;
    char buffer[1024];

    clientSocket = socket(AF_INET, SOCK_STREAM, 0);
    if(clientSocket < 0){
    printf("[-]Error in connection.\n");
    exit(1);
    }
    printf("[+]Client Socket is created.\n");

    memset(&serverAddr, '\0', sizeof(serverAddr));
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(PORT);
    serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    ret = connect(clientSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
    if(ret < 0){
    printf("[-]Error in connection.\n");
    exit(1);
    }
    printf("[+]Connected to Server.\n");

    while(1){
    printf("Client: \t");
    scanf("%s", &buffer[0]);
    send(clientSocket, buffer, strlen(buffer), 0);

    if(strcmp(buffer, ":exit") == 0){
    printf("shutdown: %d\n", shutdown(clientSocket, SHUT_RDWR));
    printf("close: %d\n", close(clientSocket));
    printf("[-]Disconnected from server.\n");
    exit(1);
    }

    if(recv(clientSocket, buffer, 1024, 0) < 0){
    printf("[-]Error in receiving data.\n");
    }else{
    printf("Server: \t%s\n", buffer);
    }
    }

    return 0;
    }

    --------------C0A8CF03A656648C558CFE9D--

    --HFTmE2ODBAjpsH3izab3kXpdlbsvNKNgQ--

    -----BEGIN PGP SIGNATURE-----

    iHUEABYIAB0WIQQj8MW8iOsC2RXEznnFW/s/mMLXCAUCZWBT2wAKCRDFW/s/mMLX CIoDAP4q9L9QnMtmi+QrDuPMII/UHyPrPKu94ABBbUvIwWuNBwD/UCJv28ZqJoVO Hg6dI59IWfPwNG5mgQNQjgtsr8lyKgM=
    =8KnR
    -----END PGP SIGNATURE-----

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From =?UTF-8?Q?BERTRAND_Jo=c3=abl?=@21:1/5 to All on Fri Nov 24 10:00:01 2023
    This is an OpenPGP/MIME signed message (RFC 4880 and 3156) --ahlLr8YMNTo4UaDlnzP3SL1UmyO24WBld
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: quoted-printable

    Bon, trouvé.

    Dans tcpServer.c, il faut remonter d'une ligne close(newSocket).

    Désolé pour le bruit.


    --ahlLr8YMNTo4UaDlnzP3SL1UmyO24WBld--

    -----BEGIN PGP SIGNATURE-----

    iHUEABYIAB0WIQQj8MW8iOsC2RXEznnFW/s/mMLXCAUCZWBj6AAKCRDFW/s/mMLX CBfjAQC+fvFoPMEC5PoSwpFMVap6xY/836f1Waaadt2mdiOq4wEA/hy1rZqMuevd 8a1scMMvGgL6G1MC+3kKam80Ej8Q6w0=
    =A+2y
    -----END PGP SIGNATURE-----

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)