Desprès d'haver acabat amb IPv6, he dedicat la darrera setmana a arreglar errors, principalment els relacionats amb la gestió multifil. Era un tema en què no havia reparat fins ara i després d'haver-li dedicat unes hores vaig descobrir que era un autèntic desastre. Us en faré un resum.
En primer lloc, a LwIP hi ha in fil anomenat "tcpip_thread" que és on realment s'executa la pila, per tant la pila és completament monofil i la seua gestió multifil consisteix a garantir que només el fil tcpip té accés als recursos de la pila mentre que els altres fils es comuniquen amb aquest mitjançant el pas de missatges. Un semàfor global garanteix que les peticions que rep aquest fil s'atenen seqüencialment. En podeu trobar més informació a la documentació d'LwIP. Doncs bé, el fil tcpip s'inicia quan s'inicialitza el translator, però en el nostre cas, també es reinicia cada cop que l'usuari crida a fsysopts. Això provocava que hi haguera tants fils "tcpip_thread" en execució com vegades s'havia cridat a fsysopts + 1. D'alguna manera la pila continuava funcionant correctament, però s'estaven malbaratant fils d'execució. Ara aquest error està arreglat perquè ja no s'inicialitza la pila quan es crida a fsysopts, ara s'esborren i creen noves interfícies sobre el mateix fil tcpip. Tanmateix, al fil no li agrada que li afegisquen i eliminen interfícies mentre està en funcionament i això ha generat nous errors de què m'hauré d'ocupar més endavant.
Un altre problema era degut a l'arquitectura del Hurd. El servidor té un component que hem anomenat "mòdul Ethernet" que s'encarrega d'agafar les dades que genera la pila i enviar-les al controlador, i a l'inrevés. En el Hurd, la comunicació amb el controlador es fa mitjançant el pas de missatges a través de la interfície device, i cal crear un fil per a escoltar els missatges entrants i executar el demuxer apropiat. En el nostre cas, hi havia un error de disseny i s'estava creant un fil d'escolta per cada interfície de xarxa que s'afegia a la pila. És més, cada cop que es cridava a fsysopts i es reiniciava la pila, es tornaven a crear fils d'espera per a les noves interfícies especificades, sense eliminar els anteriors. Aquest problema l'he arreglat iniciant el fil d'escolta una sola vegada des de la funció main()
quan s'inicia el translator.
El darrer dels errors el vaig descobrir quan vaig posar el servidor SSH a treballar sobre LwIP. Als pocs segons de funcionament, el servidor moria avortat perquè es produïa el desbordament d'una variable que s'encarrega de comptar la quantitat de fils que hi ha en espera a la funció lwip_select()
. La variable és de tipus uint8_t
, per tant, hi havien... 255 fils en espera!. Vaig perdre uns tres dies intentant treure l'entrellat d'aquest problema, però finalment vaig trobar l'error i el vaig poder solucionar. Paga la pena parar un moment a explicar amb detall l'error, perquè és realment molt il·lustratiu si es vol entendre com funciona el Hurd.
Fem una ullada al fitxer hurdselect.c
de la Glibc, concretament a dues seccions: la que comença a la línia 280 i les dues declaracions if de les línies 494 i 498. A partir de la línia 280, veiem com la crida de l'usuari a select()
pot acabar derivant en múltiples crides RPC a l'operació io_select()
. Cada crida RPC s'encarrega d'un únic socket, de manera que si l'usuari ha ficat, per exemple, tres sockets distints entre tots els FD_SET passats com a paràmetre, es realitzaran tres crides paral·leles a l'operació io_select()
, cadascuna en un fil d'execució propi. Quan hi ha un esdeveniment en un dels sockets, la seua operació io_select()
retorna correctament, però les altres dues continuen bloquejades fins que caba el temps d'espera. Si no s'especifica temps d'espera, els seus fils es bloquegen per sempre i mai no retornen. El servidor SSH crida a select()
sobre tres sockets cada cop que ha de rebre o enviar un caràcter, i ho fa sense temps límit, de manera que pot generar centenars de fils bloquejats en qüestió de segons.
En realitat, aquest disseny és força intel·ligent perquè permet treballar de manera transparent amb múltiples piles a la vegada. Ho podem il·lustrar amb el servidor SSH. Com es veu a l'exemple, el servidor no dóna per fet que tindrà una pila dual-stack, si fóra així, n'hi hauria prou amb obrir un socket IPv6 per a rebre també les peticions adreçades a adreces IPv4. En comptes d'això, obri explícitament els sockets IPv4 i IPv6 per separat, on al segon li estableix l'opció IPV6_V6ONLY
per evitar que aquest escolte adreces IPv4 si és que es troba en una pila dual-stack. En el Hurd, la crida per obtindre el socket IPv4 s'adreçaria a /servers/socket/2
mentre que la del socket IPv6 aniria a /servers/socket/26
. Per tant, si l'usuari crida a select()
i inclou els dos sockets com ho fa SSHD, cal cridar a una operació io_select()
en /servers/socket/2
i a una altra en /servers/socket/26
.
Ara bé, com ho podem fer per cancel·lar els fils io_select()
sense límit de temps que queden penjats? i el més important, quan hi ha un esdeveniment sobre un socket que fa retornar el seu fil, com podem identificar quins són exactament els altres fils que s'havien creat a la vegada i que ja no serveixen? La resposta està a les línies 494 i 498 del fitxer hurdselect.c
. Cada fil té assignat un port reply que és destruït quan el fil ja no serveix, i el fil en qüestió rep una còpia del port, de manera que el pot emprar per rebre notificacions. La llibreria libports té una funció específica per a això. Si cridem a ports_interrupt_self_on_notification()
, podem cancel·lar el nostre fil quan ocorre alguna cosa en el port donat, per exemple, quan és destruït.
Tanmateix, després de tot els fils seguien sense cancel·lar-se. El problema aquí és que la funció estàndard on es bloquegen els fils, pthread_cond_wait()
, no responia a les peticions de cancel·lació de la funció hurd_thread_cancel()
. Em semblava estrany, atès que pthread_cond_wait()
és un punt de cancel·lació vàlid. Però als servidors del Hurd és necessari emprar una altra versió no estàndard, pthread_hurd_cond_wait_np(), que aquesta sí reacciona a les peticions de hurd_thread_cancel()
i desbloqueja el fil.