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.
Attachment:
signature.asc
Description: OpenPGP digital signature