Ejemplo de análisis de fugas de memoria alternativas en la programación de redes

Este artículo se comparte desde Huawei Cloud Community " [Serie de desarrollo de programación de redes] Una fuga de memoria alternativa en la programación de redes ", autor: arquitecto Li Ken.

1 escrito al frente

Recientemente, estuve investigando un problema de prueba de estrés de la comunicación de red y finalmente descubrí que estaba relacionado con una " pérdida de memoria ", pero esto es un poco diferente de la comprensión convencional de la pérdida de memoria. Este artículo lo llevará a comprender el principio y final del problema.

Ante un problema de pérdida de memoria de este tipo, este artículo también proporciona algunos métodos de análisis y soluciones convencionales, que son solo para su referencia, y puede corregir el problema.

2 Descripción del problema

Veamos directamente la descripción del problema proporcionada por la prueba:

En pocas palabras, después de que el dispositivo ejecuta [Desconectarse de Internet - "Volver a conectarse a Internet] varias veces, descubre que no puede volver a conectarse con éxito a Internet y no ha podido hacerlo hasta que el dispositivo se reinicia y vuelve a normal.

reproducción de 3 escenas

3.1 Cree un entorno de pruebas de estrés

Dado que el departamento de prueba tiene un entorno de prueba especial, pero no quiero arreglar su equipo, tengo que arreglar un teléfono de prueba.

Su método de prueba es usar el punto de acceso del teléfono móvil como un AP, luego el dispositivo se conecta al AP y luego ejecuta un script en el teléfono móvil para cambiar dinámicamente el punto de acceso Wi-Fi para lograr el propósito de prueba de dejar que el dispositivo se desconecte. la red y luego restaurar la red .

Después de tener esta idea, pensé que solo tengo un Wi-Fi móvil portátil en la mano, ¿no sería posible implementar un punto de acceso inalámbrico? Siempre que el interruptor de punto de acceso Wi-Fi 360 se pueda cambiar dinámicamente en la PC, ¿se puede lograr el mismo propósito de prueba?

Después de tener las condiciones físicas anteriores, comencé a buscar ese guión.

Decir que en Linux, no es difícil escribir un script de este tipo, pero si escribe un script BAT en Windows, debe buscarlo.

Después de un tiempo, encontré un script BAT bastante bueno en Internet. Después de modificarlo, se ve así. La función principal es cambiar el adaptador de red con regularidad.

@echo off

:: Config your interval time (seconds)
set disable_interval_time=5
set enable_interval_time=15

:: Config your loop times: enable->disable->enable->disable...
set loop_time=10000

:: Config your network adapter list
SET adapter_num=1
SET adapter[0].name=WLAN
::SET adapter[0].name=屑薪鈺犘も晲协
::SET adapter[1].name=屑薪鈺犘も晲协 2

:::::::::::::::::::::::::::::::::::::::::::::::::::::::

echo Loop to switch network adapter state with interval time %interval_time% seconds

set loop_index=0

:LoopStart

if %loop_index% EQU %loop_time% goto :LoopStop

:: Set enable or disable operation
set /A cnt=%loop_index% + 1
set /A result=cnt%%2
if %result% equ 0 (
set operation=enabled
set interval_time=%enable_interval_time%
) else (
set operation=disable
set interval_time=%disable_interval_time%
)
echo [%date:~0,10% %time:~0,2%:%time:~3,2%:%time:~6,2%] loop time ... %cnt% ... %operation%

set adapter_index=0
:AdapterStart
if %adapter_index% EQU %adapter_num% goto :AdapterStop
set adapter_cur.name=0

for /F "usebackq delims==. tokens=1-3" %%I in (`set adapter[%adapter_index%]`) do (
	set adapter_cur.%%J=%%K
)

:: swtich adapter state
call:adapter_switch "%adapter_cur.name%" %operation%

set /A adapter_index=%adapter_index% + 1

goto AdapterStart

:AdapterStop

set /A loop_index=%loop_index% + 1

echo [%date:~0,10% %time:~0,2%:%time:~3,2%:%time:~6,2%] sleep some time (%interval_time% seconds) ...
ping -n %interval_time% 127.0.0.1 > nul

goto LoopStart

:LoopStop

echo End of loop ...

pause
goto:eof

:: function definition
:adapter_switch
set cmd=netsh interface set interface %1 %2
echo %cmd%
%cmd%
goto:eof

Nota: este lugar se llena con el adaptador de red que transmite el punto de acceso AP, como el siguiente. Si es un nombre chino , también debe prestar atención a la codificación del script BAT; de lo contrario, no se reconocerá el nombre correcto del adaptador de red.

3.2 Descripción de los problemas de medición de presión

Al mismo tiempo, para ubicar con precisión el problema de la recuperación de la desconexión de la red, agregué tres variables en el lugar donde la red se desconectó y se volvió a conectar, registrando el número total de reconexiones, el número de reconexiones exitosas y el número de reconexiones fallidas. .

Por otro lado, como se describe en la descripción del problema, este es un problema que está fuertemente relacionado con un número fijo de veces, y también puede estar estrechamente relacionado con el tiempo de ejecución.Después de reiniciar, todo vuelve a la normalidad.Esta serie de características llevar el problema a un problema muy serio.Problemas comunes: pérdidas de memoria .

Por lo tanto, antes de la prueba de estrés, volví a imprimir el estado de la memoria del sistema (memoria restante total, memoria restante mínima histórica) después de cada reconexión (independientemente de si fue exitosa o no), para juzgar el estado de la memoria del nodo problemático. .

Al ajustar los parámetros disabled_interval_time y enable_interval_time en el script de prueba de estrés, el problema se reprodujo en un período de tiempo relativamente corto. De hecho, si el problema lo describía, después de más de 30 veces, la reconexión no podría tener éxito y podría ser recuperado después de reiniciar.

4 Análisis de problemas

La mayoría de los problemas, siempre que haya una forma de reproducirlos, son relativamente fáciles de verificar, pero se necesita un poco de tiempo e investigación.

4.1 Análisis sencillo

En primer lugar, debemos sospechar la información de pérdida de memoria más probable.A primera vista:

Dado que el punto de acceso Wi-Fi puede cerrarse en el punto de tiempo correspondiente durante la operación de desconexión y reconexión, la reconexión definitivamente fallará. Cuando aparece el punto de acceso Wi-Fi, puede tener éxito, por lo que vimos que el la memoria fluctuó en un rango y no vio una tendencia descendente constante.

Por otro lado, con este valor de evmin (memoria mínima libre), después de que ocurre el problema, tiene un valor fijo y ha continuado.Desde este punto de vista, sospecho que debe haber un problema con esta memoria, pero yo "Lo estoy analizando por primera vez. Esta conclusión no se extrajo en el momento de esta situación. Mirando hacia atrás ahora, esta es una señal de advertencia.

El punto que especulé en ese momento (el punto que quería verificar) era que cuando ocurría un problema, el sistema no tenía suficiente memoria libre debido a una fuga de memoria, por lo que las operaciones que consumen memoria, como nuevos puntos de conexión y conexiones de red, no podían ser completado.

Por lo tanto, a través de la tabla de memoria anterior, estoy básicamente seguro de mi conclusión: no hay ningún signo evidente de pérdida de memoria, no es por falta de memoria que no se puede volver a conectar .

En este punto del análisis del problema, no debemos detenernos, pero el SDK original, como la lógica del punto de acceso, es una caja negra para nosotros, y solo podemos consultar la fábrica original para ver si podemos obtener alguna. información efectiva.

Después de preguntar en un círculo, la información válida obtenida es básicamente 0, ¡así que debes confiar en ti mismo para tus propios problemas!

4.2 En busca de un gran avance

En el escenario del problema anterior, hemos descartado la posibilidad de memoria insuficiente , entonces debemos centrarnos en tres aspectos:

  • ¿El dispositivo se conectó con éxito al punto de acceso Wi-Fi al final? ¿Se puede asignar normalmente la dirección IP de la subred?
  • Después de que el dispositivo se haya conectado con éxito al punto de acceso Wi-Fi, ¿la red externa es normal?
  • La red externa del dispositivo es normal, ¿por qué no se puede conectar con éxito al servidor?

Estas tres preguntas son una relación progresiva, ¡un eslabón es otro!

Veamos primero el primer problema. Obviamente, cuando se reproduce el problema, podemos ver el dispositivo conectado desde el punto de acceso Wi-Fi de la PC y ver la dirección IP de la subred asignada.

A continuación, veamos la segunda pregunta, la prueba de esta pregunta también es muy simple, porque nuestra línea de comandos integra el comando ping, y cuando ingresamos el comando ping, encontramos una información importante:

# ping www.baidu.com
ping_Command
ping IP address:www.baidu.com
ping: create socket failed

Un registro de ping normal se ve así:

# ping www.baidu.com
ping_Command
ping IP address:www.baidu.com
60 bytes from 14.215.177.39 icmp_seq=0 ttl=53 time=40 ticks
60 bytes from 14.215.177.39 icmp_seq=1 ttl=53 time=118 ticks
60 bytes from 14.215.177.39 icmp_seq=2 ttl=53 time=68 ticks
60 bytes from 14.215.177.39 icmp_seq=3 ttl=53 time=56 ticks

¡WC! ping: falló la creación del zócalo  ¡Esto también falló al crear un zócalo! ! ! ?

Primero me pregunté si había algún problema con el componente lwip.

Segunda duda: ¿No hay suficientes mangos de enchufe? Por lo tanto, la mayoría de las operaciones para crear memoria son para solicitar recursos de memoria de socket y no se realizan otras operaciones avanzadas.

Pensándolo así, la segunda posibilidad es muy grande, combinada con los anteriores signos total y total, es un objeto que necesita ser investigado.

4.3 Completar puntos de conocimiento

Antes de ubicar con precisión el problema, primero complementamos los puntos de conocimiento relevantes, para facilitar la posterior expansión y explicación del conocimiento.

4.3.1 El mango del zócalo de lwip

  • La creación de enchufes.

La forma en que se llama a la función de socket es la siguiente:

socket -> lwip_socket -> alloc_socket

Implementación de la función alloc_socket:

/**
 * Allocate a new socket for a given netconn.
 *
 * @param newconn the netconn for which to allocate a socket
 * @param accepted 1 if socket has been created by accept(),
 *                 0 if socket has been created by socket()
 * @return the index of the new socket; -1 on error
 */
static int
alloc_socket(struct netconn *newconn, int accepted)
{
  int i;
  SYS_ARCH_DECL_PROTECT(lev);

  /* allocate a new socket identifier */
  for (i = 0; i < NUM_SOCKETS; ++i) {
    /* Protect socket array */
    SYS_ARCH_PROTECT(lev);
    if (!sockets[i].conn && (sockets[i].select_waiting == 0)) {
      sockets[i].conn       = newconn;
      /* The socket is not yet known to anyone, so no need to protect
         after having marked it as used. */
      SYS_ARCH_UNPROTECT(lev);
      sockets[i].lastdata   = NULL;
      sockets[i].lastoffset = 0;
      sockets[i].rcvevent   = 0;
      /* TCP sendbuf is empty, but the socket is not yet writable until connected
       * (unless it has been created by accept()). */
      sockets[i].sendevent  = (NETCONNTYPE_GROUP(newconn->type) == NETCONN_TCP ? (accepted != 0) : 1);
      sockets[i].errevent   = 0;
      sockets[i].err        = 0;
	  SOC_INIT_SYNC(&sockets[i]);
      return i + LWIP_SOCKET_OFFSET;
    }
    SYS_ARCH_UNPROTECT(lev);
  }
  return -1;
}

Todos notaron que el bucle for en la función anterior tiene una macro  NUM_SOCKETS . El valor específico de esta macro es adaptable. Diferentes plataformas pueden elegir un valor apropiado de acuerdo con su uso real y las condiciones de la memoria.

Veamos la implementación de esta definición de macro NUM_SOCKETS :

宏定义替换
#define NUM_SOCKETS MEMP_NUM_NETCONN

在lwipopts.h中找到了其最终的替换
/**
 * MEMP_NUM_NETCONN: the number of struct netconns.
 * (only needed if you use the sequential API, like api_lib.c)
 *
 * This number corresponds to the maximum number of active sockets at any
 * given point in time. This number must be sum of max. TCP sockets, max. TCP
 * sockets used for listening, and max. number of UDP sockets
 */
#define MEMP_NUM_NETCONN	(MAX_SOCKETS_TCP + \
	MAX_LISTENING_SOCKETS_TCP + MAX_SOCKETS_UDP)

Mirando esto, es un poco confuso. ¿Cuánto es este valor?

  • Destrucción del mango del zócalo

Con la destrucción, todos sabemos que se usa la interfaz de cierre, y su ruta de llamada de función es la siguiente:

cerrar -> lwip_close -> free_socket

La implementación de la función lwip_close es la siguiente:

int
lwip_close(int s)
{
  struct lwip_sock *sock;
  int is_tcp = 0;
  err_t err;

  LWIP_DEBUGF(SOCKETS_DEBUG, ("lwip_close(%d)\n", s));

  sock = get_socket(s);
  if (!sock) {
    return -1;
  }
  SOCK_DEINIT_SYNC(1, sock);

  if (sock->conn != NULL) {
    is_tcp = NETCONNTYPE_GROUP(netconn_type(sock->conn)) == NETCONN_TCP;
  } else {
    LWIP_ASSERT("sock->lastdata == NULL", sock->lastdata == NULL);
  }

#if LWIP_IGMP
  /* drop all possibly joined IGMP memberships */
  lwip_socket_drop_registered_memberships(s);
#endif /* LWIP_IGMP */

  err = netconn_delete(sock->conn);
  if (err != ERR_OK) {
    sock_set_errno(sock, err_to_errno(err));
    return -1;
  }

  free_socket(sock, is_tcp);
  set_errno(0);
  return 0;
}

Free_socket se llama aquí:

/** Free a socket. The socket's netconn must have been
 * delete before!
 *
 * @param sock the socket to free
 * @param is_tcp != 0 for TCP sockets, used to free lastdata
 */
static void
free_socket(struct lwip_sock *sock, int is_tcp)
{
  void *lastdata;

  lastdata         = sock->lastdata;
  sock->lastdata   = NULL;
  sock->lastoffset = 0;
  sock->err        = 0;

  /* Protect socket array */
  SYS_ARCH_SET(sock->conn, NULL);
  /* don't use 'sock' after this line, as another task might have allocated it */

  if (lastdata != NULL) {
    if (is_tcp) {
      pbuf_free((struct pbuf *)lastdata);
    } else {
      netbuf_delete((struct netbuf *)lastdata);
    }
  }
}

Este SYS_ARCH_SET(sock->conn, NULL); liberará el identificador de socket correspondiente, asegurando así que el identificador de socket se pueda usar cíclicamente.

4.3.2 cerrar y apagar en la programación de red TCP

La razón por la que se discute aquí este punto de conocimiento es porque este punto de conocimiento es la clave para resolver todo el problema.

Aquí está la conclusión directa:

  • close reduce el conteo de referencia del descriptor en 1 y cierra el socket solo cuando el conteo llega a 0. shutdown puede desencadenar la secuencia de finalización de conexión normal de TCP, independientemente de los recuentos de referencia.
  • close finaliza la transmisión de datos en ambas direcciones de lectura y escritura. TCP es full-duplex y, a veces, es necesario informar a la otra parte que la transferencia de datos se ha completado, incluso si la otra parte todavía tiene datos para enviarnos.
  • Shutdown no tiene nada que ver con los descriptores de socket.Incluso si se llama a shutdown(fd, SHUT_RDWR), fd no se cerrará y se requerirá close(fd) al final.

4.4 Análisis en profundidad

Después de comprender la creación y el cierre de identificadores de socket en el componente lwip, volvamos al problema de reproducción en sí.

Desde el registro más sutil, sabemos que el problema radica en la incapacidad de asignar nuevos sockets. Veamos la lógica de asignación de sockets. Hay una condición de juicio:

if (!sockets[i].conn && (sockets[i].select_waiting == 0)) {
      //分配新的句柄编号
      sockets[i].conn       = newconn;
      。。。
}

Al aumentar el registro, sabemos que el valor de select_waiting es 0, por lo que el problema es que conn no es NULL.

En lwip_close, .conn se asigna NULL, por lo que me pregunto si no se llama a lwip_close. ¿El proceso hace que el mango no se suelte por completo?

Para responder a esta pregunta, debemos volver a nuestra arquitectura de software. En la implementación de la arquitectura, nuestras diferentes plataformas de chips usan diferentes versiones de componentes lwip, y el protocolo MQTT que se ejecuta en la capa superior es público, es decir, si está en la lógica de la capa superior Si la lógica de cierre no se maneja correctamente, entonces este problema debería ocurrir en todas las plataformas, pero ¿por qué solo esta plataforma tiene problemas?

Solo hay una respuesta, el problema puede estar en la capa de implementación de lwip.

Dado que lwip fue adaptado por la fábrica original, inmediatamente encontré la versión nativa de lwip-2.0.2 para comparar. Principalmente quería saber qué optimizaciones y ajustes se hicieron cuando se adaptó la fábrica original.

Después de comparar los resultados, se encontró el problema.

Tomemos el problema sockets.c como ejemplo, nos enfocamos en la aplicación y liberación de sockets:

Para describir mejor la optimización realizada por la fábrica original, realicé algunas modificaciones en el código agregado y agregué aproximadamente algunas definiciones de macro. Los comentarios de estas definiciones de macro deben usarse para tratar con sockets nuevos y cerrados bajo múltiples Problemas de sincronización.

#define SOC_INIT_SYNC(sock) do { something ... } while(0)#define SOC_DEINIT_SYNC(sock) do { SOCK_CHECK_NOT_CLOSING(sock); something ... } while(0)#define SOCK_CHECK_NOT_CLOSING(sock) do { \		if ((sock)->closing) { \			SOCK_DEBUG(1, "SOCK_CHECK_NOT_CLOSING:[%d]\n", (sock)->closing); \			return -1; \		} \	} while (0)

Simplemente siga su lógica.Cuando la capa superior llame a lwip_close, llamará a SOC_DEINIT_SYNC, y llamará a SOCK_CHECK_NOT_CLOSING, finalizando así todo el proceso de liberación del socket.

Pero cuando la capa superior de MQTT que hicimos llama al enlace TCP para colgar, se reproduce así:

/* * Gracefully close the connection */void mbedtls_net_free( mbedtls_net_context *ctx ){    if( ctx->fd == -1 )        return;    shutdown( ctx->fd, 2 );    close( ctx->fd );    ctx->fd = -1;}

Cierre correctamente el enlace TCP, en este momento debe recordar los puntos de conocimiento en el capítulo 4.3.2 .

¿Esta llamada afectará a esas macros?

La respuesta es sí.

Resulta que lwip_shutdown también llama a SOC_DEINIT_SYNC durante la adaptación original de fábrica, lo que lleva al hecho de que si la capa superior cierra el enlace y llama tanto a shutdown como a close, habrá un problema con su lógica, lo que hará que el proceso de cierre estar incompleto.

Para simplificar este problema, escribí aproximadamente su lógica:

1) Cuando se llame a la función de apagado, inicie el proceso de apagado SOC_DEINIT_SYNC, ingrese esas macros, habrá un paso: (calcetín) -> cierre = 1, luego regrese a 0 normalmente;

2) Cuando se llama a la función de cierre, ingrese nuevamente el proceso de cierre SOC_DEINIT_SYNC Tan pronto como se juzgue que (sock)->closing ya es 1, se informa un error y se devuelve -1, por lo que el retorno de close es anormal ;

3) Mira la lógica de la función lwip_close:

Entonces, existe el problema anterior: el índice del identificador del zócalo sigue aumentando, y el identificador del zócalo antiguo debe estar ocupado todo el tiempo hasta que se agote el número de identificadores.

¿Cuál es el número máximo de identificadores NUM_SOCKETS? Puede consultar mi artículo anterior sobre cómo mirar el código precompilado. Podemos ver claramente que su valor es 38 .

Se abren todas las dudas, por lo que el problema debe ser después de más de 30 veces, ¡aquí se da la respuesta!

Aquí supuse audazmente que cuando la fábrica original estaba adaptando esta lógica de operación síncrona , no consideró que la capa superior también puede apagarse primero y luego cerrarse , lo que causó este problema.

5 correcciones de errores

En el análisis anterior, el código del problema se localizó inicialmente y el siguiente paso es reparar el problema.

La causa raíz del problema es ajustar el apagado primero y luego cerrar.Dado que es un código de nivel superior, también se comparten otras plataformas, y no hay problema con otras plataformas, por lo que la operación de cerrar el enlace TCP con gracia por parte del No se debe quitar la capa superior, solo se puede usar la capa inferior.El componente lwip se optimiza y resuelve solo. El llamado es: ¡cualquiera que sea el culpable, quién limpiará el culo!

La clave para resolver el problema es asegurarse de que después de ajustar el apagado, la operación de cierre debe pasar por un proceso completo, de modo que la manija del enchufe ocupado pueda liberarse.

Por lo tanto, al ejecutar apagado y cierre, SOC_DEINIT_SYNC necesita tomar un parámetro para informar si se trata de una operación de cierre, si no es así, se seguirá un proceso simple para garantizar que el proceso de cierre se complete.

Cuando la capa superior solo llama al cierre, también puede garantizar que el proceso de cierre esté completo.

Sin embargo, si la participación accionaria de nivel superior se cierra primero y luego se cierra, el proceso no funcionará.

Por supuesto, las capas superiores no pueden jugar así. Para más detalles, consulte los puntos de conocimiento en 4.3.2.

6 Verificación de preguntas

Una vez que se soluciona el problema, se debe volver a probar el mismo proceso para garantizar que el problema se haya solucionado.

La verificación del problema también es muy simple. Modifique el NUM_SOCKETS en sockets.c y cámbielo a un valor pequeño, como 3 o 5, para acelerar la recurrencia del problema. Al mismo tiempo, escriba el identificador de identificador obtenido en alloc_socket y observe si sube. , en pruebas normales, en ausencia de otros enlaces de comunicación de red, debería estabilizarse en 0.

Se verificará pronto y el problema no se volverá a reproducir.

A continuación, debe restaurar el valor de NUM_SOCKETS al valor del principio y probar la escena original reproducida para asegurarse de que solo este lugar causó el problema y que otros códigos no interfirieron.

Afortunadamente, la prueba después de la restauración también pasó, lo que demuestra que el problema se solucionó por completo sin efectos secundarios, lo que es una corrección de errores exitosa.

7 Resumen de experiencia

  • Hay muchos tipos de fugas de memoria, pero debemos prestar atención a sus características esenciales;
  • La fuga de identificador de socket también es un tipo de fuga de memoria;
  • Cada optimización tiene su escenario específico y debe reconsiderar la universalidad de esta optimización sin este escenario específico;
  • Mejore la sensibilidad a la información de registro clave, lo que conduce a encontrar la luz de dirección para la resolución de problemas en el gran problema;
  • La comprensión precisa de la función de cierre y la función de apagado en la interfaz de programación TCP puede ayudar a resolver el problema de la caída de la red;
  • Una prueba de estrés antes de conectarse es esencial.

8 enlaces de referencia

Haga clic en Seguir para conocer las nuevas tecnologías de HUAWEI CLOUD por primera vez~

Supongo que te gusta

Origin blog.csdn.net/devcloud/article/details/124018572
Recomendado
Clasificación