¡La entrevista más agotadora fue con Tencent!

El siguiente artículo proviene de Xiaolin coding, autor Xiaolin coding

El estilo de entrevista de Tencent presta más atención a los conceptos básicos de la informática y hace más preguntas sobre sistemas operativos y redes, por lo que al entrevistar a diferentes empresas, debe tener un enfoque de preparación.

Hoy analicé la experiencia de la entrevista de desarrollo back-end de un compañero de clase en el campus de Tencent. La entrevista duró 2 horas, 1 hora de algoritmo de desgarro manual y 1 hora de tortura básica. La entrevista fue la más agotadora. Aunque la entrevista fue muy agotador, el compañero le dio su opinión al entrevistador. Es una persona muy agradable y siempre me guiará.

Permítanme enumerar brevemente los puntos de prueba de esta entrevista:

  • Java: hashmap, algoritmo de recolección de basura, modelo de subprocesos

  • Sistema operativo: modo de usuario y modo kernel, programación de procesos, comunicación entre procesos, memoria virtual, proceso, subproceso y rutina

  • Red: protocolo de enlace tcp de tres vías, control de congestión, https

  • redis: 5 estructuras de datos

  • mysql:b+árbol

  • Algoritmo: 4 preguntas de algoritmo

Java

¿Son los subprocesos en Java lo mismo que los subprocesos en el sistema operativo?

La capa inferior de Java llamará a pthread_create para crear subprocesos, por lo que esencialmente los subprocesos creados por el programa Java son los mismos que los subprocesos del sistema operativo y son un modelo de subproceso 1 a 1.

imagen

Cara a cara

¿El principio subyacente de HashMap?

imagen

imagen

Al almacenar objetos, cuando pasamos K/V al método put, llama a hashCode para calcular el hash para obtener la ubicación del depósito. Para un mayor almacenamiento, HashMap ajustará automáticamente la capacidad de acuerdo con la ocupación actual del depósito (si el Load Facotr es Si se excede, la capacidad se redimensionará a 2 veces la original).

Al obtener el objeto, pasamos K para obtener, que llama a hashCode para calcular el hash para obtener la posición del depósito y luego llama al método equals() para determinar el par clave-valor. Si ocurre una colisión, Hashmap organiza los elementos en conflicto a través de una lista vinculada. En Java 8, si los elementos en conflicto en un depósito exceden un cierto límite (el valor predeterminado es 8), se usa un árbol rojo-negro para reemplazar la lista vinculada. aumentando así la velocidad.

¿Cómo determinar si un objeto es inútil?

Determinar si un objeto es inútil (es decir, si ya no se hace referencia a él y puede ser recolectado por el recolector de basura) generalmente depende del trabajo del recolector de basura. El recolector de basura de Java utiliza un algoritmo de análisis de accesibilidad para determinar si un objeto es accesible. Si un objeto es inalcanzable, se puede determinar que es un objeto inútil.

La forma específica de determinar si un objeto es inútil es la siguiente:

  1. Conteo de referencias: este método mantiene un contador de referencia en el objeto. Siempre que hay una referencia que apunta al objeto, el contador se incrementa en 1. Cuando se libera la referencia que apunta al objeto, el contador se reduce en 1. Cuando el contador es 0, se puede determinar que el objeto es inútil. Sin embargo, el recolector de basura de Java no utiliza el recuento de referencias porque no puede resolver el problema de las referencias circulares.

  2. Análisis de accesibilidad: el recolector de basura de Java utiliza principalmente el análisis de accesibilidad para determinar la accesibilidad de los objetos. Este método comienza desde un conjunto de objetos raíz llamado "GC Roots" y atraviesa el gráfico de objetos, marcando todos los objetos accesibles desde el objeto raíz. Aquellos objetos que no están marcados se consideran objetos inútiles.

Suponiendo que hay 1 millón de objetos almacenados en HashMap, ¿qué problemas pueden ocurrir con gc?

Hay demasiados objetos y llevará mucho tiempo escanear estos objetos inútiles utilizando el método de análisis de accesibilidad, y el tiempo empleado por gc será mayor.

Sistema operativo

¿Cuál es la diferencia entre el modo de usuario y el modo kernel?

El modo de usuario y el modo kernel son dos niveles de privilegios en el sistema operativo. Tienen las siguientes diferencias:

  1. Permisos de acceso: en el modo de usuario, las aplicaciones solo pueden acceder a recursos restringidos y realizar operaciones restringidas, como memoria, archivos y dispositivos del espacio del usuario. En modo kernel, el sistema operativo tiene derechos de acceso total y puede acceder a todos los recursos del sistema y realizar todas las operaciones.

  2. Conjunto de instrucciones de la CPU: en modo usuario, la CPU solo puede ejecutar instrucciones sin privilegios, como operaciones aritméticas, operaciones lógicas, etc. En modo kernel, la CPU puede ejecutar instrucciones privilegiadas, como acceder a dispositivos, modificar el estado del sistema, etc.

  3. Manejo de interrupciones y excepciones: en el modo de usuario, cuando ocurre una interrupción o excepción, el sistema operativo manejará la interrupción y transferirá el control al controlador de interrupciones en el modo kernel. En el estado del kernel, el sistema operativo puede manejar directamente interrupciones y excepciones y realizar las operaciones de procesamiento correspondientes.

  4. Protección de la memoria: en el modo de usuario, las aplicaciones solo pueden acceder a su propio espacio de memoria y no pueden acceder al espacio de memoria de otras aplicaciones ni al espacio de memoria del sistema operativo. En modo kernel, el sistema operativo puede acceder a todos los espacios de memoria, incluidos los espacios de memoria de las aplicaciones.

  5. Seguridad: dado que las aplicaciones en modo de usuario están restringidas, el sistema operativo puede aislarlas y protegerlas para evitar que códigos maliciosos causen daños al sistema. El sistema operativo en modo kernel tiene permisos más altos y requiere una gestión de seguridad estricta para evitar accesos ilegales y operaciones maliciosas.

Hablemos del algoritmo de programación de procesos.

01 Algoritmo de programación por orden de llegada

El algoritmo de programación más simple es el algoritmo no preventivo por orden de llegada (*First Come First Serve, FCFS*) .

imagen

Algoritmo de programación FCFS

Como sugiere el nombre, por orden de llegada, cada vez que el proceso que ingresa primero a la cola se selecciona de la cola lista y luego se ejecuta hasta que el proceso sale o se bloquea, entonces se seleccionará y ejecutará el primer proceso de la cola.

Esto parece justo, pero cuando se ejecuta primero un trabajo largo, el tiempo de espera de los trabajos cortos posteriores será muy largo, lo que no favorece los trabajos cortos.

FCFS es bueno para trabajos largos y adecuado para sistemas con trabajos de CPU ocupados, pero no para sistemas con trabajos de E/S ocupados.

02 Algoritmo de programación del primer trabajo más corto

El algoritmo de programación *Shortest Job First (SJF*) también, como su nombre indica, dará prioridad al proceso con el menor tiempo de ejecución , lo que ayuda a mejorar el rendimiento del sistema.

imagen

Algoritmo de programación SJF

Evidentemente, esto no es bueno para operaciones prolongadas y puede conducir fácilmente a un fenómeno extremo.

Por ejemplo, si un trabajo largo está esperando ser ejecutado en la cola de listos y hay muchos trabajos cortos en la cola de listos, el trabajo largo continuará retrasándose y el tiempo de respuesta será más largo, lo que resultará en un trabajo largo. El trabajo no se ejecuta durante mucho tiempo.

03 Algoritmo de programación de prioridad de alta tasa de respuesta

El anterior "algoritmo de programación por orden de llegada" y el "algoritmo de programación del trabajo más corto primero" no tienen un buen equilibrio entre trabajos cortos y trabajos largos.

Luego, el algoritmo de programación del siguiente índice de respuesta más alto (HRRN*) sopesa principalmente los trabajos cortos y los trabajos largos.

Cada vez que se realiza la programación del proceso, primero se calcula la "prioridad del índice de respuesta" y luego se pone en funcionamiento el proceso con la "prioridad del índice de respuesta" más alta. La fórmula de cálculo de la "prioridad del índice de respuesta" es:

imagen

imagen

De la fórmula anterior, podemos encontrar:

  • Si el "tiempo de espera" de dos procesos es el mismo, cuanto más corto sea el "tiempo de servicio requerido", mayor será la "proporción de respuesta", de modo que el proceso con un trabajo corto se seleccione fácilmente para ejecutarse;

  • Si el "tiempo de servicio requerido" de dos procesos es el mismo, cuanto mayor sea el "tiempo de espera", mayor será el "índice de respuesta", que tiene en cuenta los procesos de trabajo largos, porque el índice de respuesta del proceso puede aumentar a medida que avanza la espera. el tiempo aumenta. Cuando espera lo suficiente, su índice de respuesta puede aumentar a un nivel alto, dándole la oportunidad de ejecutarse;

04 Algoritmo de programación de rotación de intervalos de tiempo

El algoritmo más antiguo, más simple, más justo y más utilizado es el algoritmo de programación round robin (*Round Robin, RR*) .

imagen

Algoritmo de programación RR

A cada proceso se le asigna un período de tiempo, llamado segmento de tiempo (*Quantum*), es decir, el proceso puede ejecutarse durante este período de tiempo.

  • Si el intervalo de tiempo se agota y el proceso aún se está ejecutando, el proceso se liberará de la CPU y la CPU se asignará a otro proceso;

  • Si el proceso se bloquea o finaliza antes del final del intervalo de tiempo, la CPU cambia inmediatamente;

Además, la duración del intervalo de tiempo es un punto muy crítico:

  • Si el intervalo de tiempo se establece demasiado corto, se producirán demasiados cambios de contexto de proceso y se reducirá la eficiencia de la CPU;

  • Si se establece demasiado largo, puede provocar que el tiempo de respuesta a procesos de trabajo cortos se alargue. Voluntad

En términos generales, establecer el intervalo de tiempo  20ms~50ms suele ser un valor de compromiso razonable.

05 Algoritmo de programación de máxima prioridad

El "algoritmo de rotación de intervalos de tiempo" anterior supone que todos los procesos son igualmente importantes y que nadie está sesgado. El tiempo de ejecución de todos es el mismo.

Sin embargo, existen diferentes puntos de vista sobre los sistemas informáticos multiusuario. Esperan que la programación tenga prioridad, es decir, esperan que el programador pueda el proceso de mayor prioridad de la cola lista para ejecutar. Esto se denomina prioridad más alta (*Más alta Prioridad primero, algoritmo de programación HPF*) .

La prioridad de un proceso se puede dividir en prioridad estática y prioridad dinámica:

  • Prioridad estática: la prioridad ya está determinada cuando se crea el proceso y no cambiará durante todo el tiempo de ejecución;

  • Prioridad dinámica: ajuste la prioridad de acuerdo con los cambios dinámicos del proceso. Por ejemplo, si el tiempo de ejecución del proceso aumenta, su prioridad se reduce. Si el tiempo de espera del proceso (tiempo de espera de la cola lista) aumenta, su prioridad es aumenta, es decir, a medida que aumenta el tiempo de ejecución del proceso, aumenta su prioridad.El paso del tiempo aumenta la prioridad del proceso de espera .

Este algoritmo también tiene dos métodos para procesar alta prioridad, no preventivo y preventivo:

  • No preventivo: cuando un proceso con una prioridad más alta aparece en la cola listo, ejecute el proceso actual y luego seleccione el proceso con una prioridad más alta.

  • Preventivo: cuando un proceso con alta prioridad aparece en la cola listo, el proceso actual se suspende y se programa la ejecución del proceso con alta prioridad.

Pero todavía existen desventajas que pueden provocar que los procesos de baja prioridad nunca se ejecuten.

06 Algoritmo de programación de cola de retroalimentación multinivel

El algoritmo de programación de la cola de retroalimentación multinivel (*Cola de retroalimentación multinivel*) es la síntesis y el desarrollo del "algoritmo de rotación de intervalos de tiempo" y el "algoritmo de mayor prioridad".

Como el nombre sugiere:

  • "Multinivel" significa que hay varias colas y la prioridad de cada cola es de mayor a menor. Al mismo tiempo, cuanto mayor sea la prioridad, más corto será el intervalo de tiempo.

  • "Retroalimentación" significa que si un nuevo proceso se une a la cola con una prioridad más alta, el proceso que se está ejecutando actualmente se detendrá inmediatamente y en su lugar se ejecutará la cola con una prioridad más alta;

imagen

Cola de comentarios de varios niveles

Vamos a ver cómo funciona:

  • Se configuran varias colas y a cada cola se le asigna una prioridad diferente. La prioridad de cada cola es de mayor a menor . Al mismo tiempo, cuanto mayor sea la prioridad, más corto será el intervalo de tiempo ;

  • El nuevo proceso se colocará al final de la cola de primer nivel y se pondrá en cola para esperar la programación por orden de llegada. Si el proceso no se completa dentro del intervalo de tiempo especificado por la cola de primer nivel , se transferirá a la cola de segundo nivel, al final de la cola, y así sucesivamente hasta que se complete;

  • Cuando la cola de mayor prioridad está vacía, se programa la ejecución de los procesos de la cola de menor prioridad. Si un nuevo proceso ingresa a una cola de mayor prioridad mientras se está ejecutando, el proceso que se está ejecutando actualmente se detiene y se mueve al final de la cola original, y luego se permite que se ejecute el proceso de mayor prioridad;

Se puede encontrar que los trabajos cortos se pueden procesar rápidamente en la cola de primer nivel. Para trabajos largos, si no se pueden procesar en la cola de primer nivel, se pueden mover a la siguiente cola para esperar la ejecución. Aunque el tiempo de espera aumenta, el tiempo de ejecución también aumenta, por lo que este algoritmo tiene en cuenta ambos. Trabajos largos y cortos . Al mismo tiempo, tiene un mejor tiempo de respuesta.

¿Mecanismo de comunicación entre procesos?

imagen

imagen

Dado que el espacio de usuario de cada proceso es independiente y no pueden acceder entre sí, es necesario utilizar el espacio del kernel para lograr la comunicación entre procesos, la razón es muy simple: cada proceso comparte un espacio del kernel.

El kernel de Linux proporciona muchos métodos de comunicación entre procesos, el más simple de los cuales son las canalizaciones, que se dividen en "canalizaciones anónimas" y "canalizaciones con nombre".

Como sugiere el nombre, las canalizaciones anónimas no tienen identificador de nombre. Las canalizaciones anónimas son archivos especiales que sólo existen en la memoria y no existen en el sistema de archivos. La " " |barra vertical en el comando Shell es una canalización anónima. Los datos de comunicación son un flujo sin formato y tiene un tamaño limitado. , el método de comunicación es unidireccional y los datos solo pueden fluir en una dirección. Si desea una comunicación bidireccional, necesita crear dos canales. Además, los canales anónimos solo se pueden usar para comunicación entre procesos con una relación padre-hijo.El ciclo de vida de las tuberías anónimas se establece cuando se crea el proceso y desaparece cuando finaliza el proceso.

Las canalizaciones con nombre rompen la limitación de que las canalizaciones anónimas solo pueden comunicarse entre procesos relacionados. Debido a que la premisa de utilizar canalizaciones con nombre es crear un archivo de dispositivo de tipo p en el sistema de archivos, los procesos no relacionados pueden utilizar este archivo de dispositivo. Además, ya sea una canalización anónima o una canalización con nombre, los datos escritos por el proceso se almacenan en caché en el kernel . Cuando otro proceso lee los datos, los obtendrá naturalmente del kernel. Al mismo tiempo, los datos de comunicación Sigue el principio de primero en entrar, primero en salir y no admite operaciones de ubicación de archivos de clase lseek.

La cola de mensajes supera el problema de que los datos en la comunicación de canalización son un flujo de bytes sin formato. La cola de mensajes es en realidad una "lista vinculada de mensajes" almacenada en el núcleo. El cuerpo del mensaje de la cola de mensajes es un tipo de datos definido por el usuario. Al enviar datos, se dividirá en cuerpos de mensajes independientes y, por supuesto, al recibir datos, también debe ser coherente con el tipo de datos del cuerpo del mensaje enviado por el remitente, para garantizar que los datos leídos sean correctos. La velocidad de comunicación de la cola de mensajes no es la más oportuna, después de todo, cada escritura y lectura de datos requiere un proceso de copia entre el modo de usuario y el modo kernel.

La memoria compartida puede resolver la sobrecarga causada por el proceso de copia de datos entre el modo de usuario y el modo kernel en la comunicación de la cola de mensajes. Asigna directamente un espacio compartido y cada proceso puede acceder directamente a él . Es tan rápido y conveniente como acceder al propio espacio del proceso. Debe caer en el estado del núcleo o llamada al sistema, lo que mejora en gran medida la velocidad de la comunicación y goza de la reputación de ser el método de comunicación entre procesos más rápido. Sin embargo, la comunicación conveniente y eficiente en la memoria compartida trae nuevos problemas: múltiples procesos que compiten por el mismo recurso compartido causarán confusión en los datos.

Luego, se necesita un semáforo para proteger los recursos compartidos y garantizar que solo un proceso pueda acceder a los recursos compartidos en cualquier momento. Este método es de acceso mutuamente excluyente. El semáforo no solo puede lograr la exclusión mutua del acceso, sino también lograr la sincronización entre procesos . El semáforo es en realidad un contador, que representa la cantidad de recursos, y su valor se puede controlar mediante dos operaciones atómicas, a saber,  operación P y operación V.

Aquel que tiene un nombre muy parecido a un semáforo se llama señal , aunque sus nombres son similares, sus funciones son completamente diferentes. Las señales son un mecanismo de comunicación asincrónico . Las señales pueden interactuar directamente entre los procesos de la aplicación y el kernel. El kernel también puede usar señales para notificar a los procesos del espacio del usuario qué eventos del sistema han ocurrido. Las principales fuentes de eventos de señales son fuentes de hardware (como el teclado Cltr +C) Y las fuentes de software (como el comando kill), una vez que ocurre una señal, el proceso tiene tres formas de responder a la señal: 1. Realizar la operación predeterminada, 2. Capturar la señal, 3. Ignorar la señal . Hay dos señales que el proceso de solicitud no puede captar ni ignorar, a saber  SIGKILL y  SIGSTOPEsto es para nuestra conveniencia de finalizar o detener un proceso en cualquier momento.

Todos los mecanismos de comunicación mencionados anteriormente funcionan en el mismo host. Si desea comunicarse con procesos en diferentes hosts, se requiere comunicación Socket . En realidad, Socket no solo se usa para la comunicación entre diferentes procesos de host, sino también para la comunicación entre procesos de host local. Se puede dividir en tres métodos de comunicación comunes según el tipo de Socket creado, uno se basa en el protocolo TCP y el otro. Uno es un método de comunicación basado en el protocolo UDP y el otro es un método de comunicación local entre procesos.

Multiplexación IO, ¿cuál es la diferencia entre seleccionar, sondear y epoll?

La forma en que select implementa la multiplexación es colocar todos los Sockets conectados en un conjunto de descriptores de archivos y luego llamar a la función de selección para copiar el conjunto de descriptores de archivos en el kernel, de modo que el kernel pueda verificar si hay eventos de red. , Es decir, al atravesar el conjunto de descriptores de archivos, cuando se detecta un evento, el Socket se marca como legible o escribible, y luego todo el conjunto de descriptores de archivos se copia nuevamente al modo de usuario, y luego al modo de usuario También es necesario para encontrar el Socket legible o escribible mediante el método transversal y luego procesarlo.

Por lo tanto, para el método de selección, es necesario  "recorrer" el conjunto de descriptores de archivos dos veces , una vez en modo kernel y otra en modo usuario, y  "copiar" dos veces el conjunto de descriptores de archivos . Primero, se pasa desde el espacio de usuario al espacio del kernel, modificado por el kernel y luego pasado al espacio del usuario.

select utiliza un BitsMap de longitud fija para representar un conjunto de descriptores de archivos, y el número de descriptores de archivos admitidos es limitado. En los sistemas Linux, está limitado por FD_SETSIZE en el kernel. El valor máximo predeterminado es y solo puede escuchar 0  1024~ 1023 descriptor de archivo.

Poll ya no utiliza BitsMap para almacenar los descriptores de archivos de interés, sino que utiliza una matriz dinámica organizada en forma de lista enlazada, que supera el límite del número de descriptores de archivos seleccionados. Por supuesto, también está sujeto hasta el límite de los descriptores de archivos del sistema.

Sin embargo, no hay mucha diferencia esencial entre sondear y seleccionar. Ambos utilizan una "estructura lineal" para almacenar la colección de Sockets que interesa al proceso. Por lo tanto, ambos necesitan atravesar la colección de descriptores de archivos para encontrar el Socket legible o escribible. La complejidad del tiempo es O (n), y también es necesario copiar el descriptor de archivo configurado entre el modo de usuario y el modo kernel . De esta manera, a medida que aumenta el número de concurrencia, la pérdida de rendimiento aumentará exponencialmente.

epoll resuelve muy bien el problema de selección/encuesta a través de dos aspectos.

  • El primer punto es que epoll utiliza un árbol rojo-negro en el kernel para rastrear todos los descriptores de archivos que se detectarán en el proceso y agrega los sockets que deben monitorearse  epoll_ctl() al árbol rojo-negro en el kernel a través de funciones. El árbol rojo-negro es una estructura de datos eficiente, que agrega, elimina y modifica la complejidad del tiempo general  O(logn). El kernel select/poll no tiene una estructura de datos similar al árbol rojo-negro de epoll que guarda todos los sockets para ser detectados, por lo que select/poll pasa todo el conjunto de sockets al kernel cada vez que opera, y epoll mantiene el kernel rojo-negro. árbol negro en el kernel El árbol puede guardar todos los sockets para detectar, por lo que solo es necesario pasar un socket para detectar, lo que reduce una gran cantidad de copia de datos y asignación de memoria en el kernel y el espacio del usuario.

  • El segundo punto es que epoll utiliza un mecanismo controlado por eventos . El kernel mantiene una lista vinculada para registrar eventos listos . Cuando ocurre un evento en un socket, el kernel lo agregará a la lista de eventos listos  a través de la función de devolución de llamadaepoll_wait() . Cuando el usuario  llama a la función En este momento, solo se devolverá el número de descriptores de archivos con ocurrencia de eventos. No es necesario sondear y escanear toda la colección de sockets como select/poll, lo que mejora en gran medida la eficiencia de detección.

En la siguiente figura puede ver las funciones de la interfaz relacionadas con epoll:

imagen

imagen

Incluso si el método epoll aumenta el número de Sockets monitoreados, la eficiencia no se reducirá significativamente. El número de Sockets que se pueden monitorear al mismo tiempo también es muy grande. El límite superior es el número máximo de descriptores de archivos abiertos por el proceso definido por el sistema. Por lo tanto, se considera que epoll es una herramienta poderosa para resolver el problema C10K .

¿Por qué el sistema operativo diseña memoria virtual?

Si no hay memoria virtual, el programa opera directamente la memoria física.

imagen

imagen

En este caso, es imposible ejecutar dos programas en memoria al mismo tiempo. Si el primer programa escribe un nuevo valor en la ubicación 2000, borrará todo lo que el segundo programa haya almacenado en la misma ubicación, por lo que ejecutar dos programas al mismo tiempo simplemente no es factible. Los dos programas colapsarán inmediatamente.

La cuestión clave aquí es que ambos programas se refieren a direcciones físicas absolutas, que es lo que más debemos evitar.

imagen

capa intermedia del proceso

Podemos "aislar" las direcciones utilizadas por el proceso, es decir, dejar que el sistema operativo asigne un conjunto independiente de " direcciones virtuales " a cada proceso, todos lo tienen y todos pueden jugar con sus propias direcciones sin interferir entre sí. Pero existe la premisa de que cada proceso no puede acceder a la dirección física. En cuanto a cómo la dirección virtual finalmente cae en la memoria física, es transparente para el proceso, el sistema operativo las ha organizado claramente.

El sistema operativo proporcionará un mecanismo para mapear las direcciones virtuales de diferentes procesos y las direcciones físicas de diferentes memorias. Si un programa quiere acceder a una dirección virtual, el sistema operativo la convertirá en una dirección física diferente, de modo que cuando se ejecuten diferentes procesos, escribirán en diferentes direcciones físicas, por lo que no habrá conflicto.

¿Cuál es la diferencia entre proceso e hilo?

  • Asignación de recursos: un proceso es una entidad de ejecución en el sistema operativo y tiene espacio de direcciones, descriptores de archivos, archivos abiertos y otros recursos independientes. A cada proceso se le asignan recursos del sistema independientes. Un subproceso es una unidad de ejecución en un proceso. Varios subprocesos comparten el espacio de direcciones y otros recursos del mismo proceso, incluidos descriptores de archivos, archivos abiertos, etc.

  • Programación y cambio: la programación de procesos la realiza el kernel del sistema operativo. El cambio de procesos requiere un cambio de contexto, lo que implica cambiar entre el modo de usuario y el modo kernel, y la sobrecarga es relativamente grande. La programación de subprocesos se completa en el programa de usuario y el cambio de subprocesos se puede cambiar rápidamente en modo de usuario, lo que reduce la sobrecarga de las llamadas al sistema.

  • Concurrencia: los procesos son entidades de ejecución independientes y diferentes procesos intercambian y comparten datos a través de la comunicación entre procesos (IPC). Los métodos de comunicación entre procesos incluyen canalizaciones, semáforos, memoria compartida, etc. Los subprocesos se ejecutan en el mismo proceso, varios subprocesos comparten los recursos del mismo proceso y los datos se pueden intercambiar y compartir a través de la memoria compartida.

¿Qué hay en el espacio de direcciones de un proceso?

imagen

División del espacio de memoria virtual

Puede ver en esta imagen que la memoria del espacio de usuario tiene 6 segmentos de memoria diferentes, de menor a mayor :

  • Segmentos de código, incluido el código binario ejecutable;

  • Segmento de datos, incluidas constantes estáticas inicializadas y variables globales;

  • Sección BSS, que incluye variables estáticas no inicializadas y variables globales;

  • El segmento del montón, incluida la memoria asignada dinámicamente, crece hacia arriba a partir de direcciones bajas;

  • Segmentos de mapeo de archivos, incluidas bibliotecas dinámicas, memoria compartida, etc.;

  • Segmento de pila, incluidas variables locales y contexto de llamada de función, etc. El tamaño de la pila es fijo, generalmente  8 MB. Por supuesto, el sistema también proporciona parámetros para que podamos personalizar el tamaño;

Como puede ver en el diseño de la memoria en la imagen de arriba, hay una sección de espacio de memoria (parte gris) debajo del segmento de código. Esta área es el "área reservada". La razón por la cual hay un área reservada es porque en la mayoría sistemas, consideramos que una dirección con un valor relativamente pequeño no es una dirección legal, por ejemplo, generalmente asignamos punteros no válidos a NULL en el código C. Por lo tanto, aquí habrá un área reservada de memoria inaccesible para evitar que el programa lea o escriba datos en direcciones de memoria pequeñas debido a errores, lo que provocará que el programa se ejecute.

Entre estos 7 segmentos de memoria, la memoria de los segmentos de mapeo de archivos y montón se asigna dinámicamente. malloc() Por ejemplo  , utilizando la biblioteca estándar C  mmap() , puede asignar memoria dinámicamente en los segmentos de mapeo de archivos y montón, respectivamente.

¿Qué contexto se debe guardar al cambiar de hilo?

  • Contexto de registro: al cambiar de subproceso, el valor del registro utilizado por el subproceso debe guardarse y restaurarse para que la ejecución pueda continuar después del cambio. Los registros comunes incluyen registros de propósito general (como EAX, EBX, ECX, etc.), registros de puntero de instrucciones (como EIP), registros de puntero de pila (como ESP), etc.

  • Pila: la pila del hilo se utiliza para guardar variables locales, información de llamadas a funciones, etc. Al cambiar de subproceso, es necesario guardar y restaurar el puntero de pila del subproceso actual y la información del marco de pila correspondiente.

  • Información de contexto del hilo: al cambiar de hilo, es necesario guardar y restaurar otra información de contexto relacionada con el hilo, como el estado del hilo, la prioridad de programación, etc.

¿Cuál es la diferencia entre corrutinas y subprocesos?

  • Método de programación: la programación de subprocesos la realiza el núcleo del sistema operativo, mientras que la programación de rutinas la realizan los programadores o un programador de rutinas específico. La programación de subprocesos está controlada por el kernel del sistema operativo. El cambio de subprocesos requiere llamadas al sistema, lo que implica cambiar entre el modo de usuario y el modo kernel, lo que lleva relativamente tiempo. La programación de corrutinas se completa en el programa de usuario y el cambio de rutinas se puede cambiar rápidamente en modo de usuario, lo que reduce la sobrecarga de las llamadas al sistema.

  • Concurrencia: los subprocesos son procesos livianos proporcionados por el sistema operativo. Se pueden ejecutar varios subprocesos al mismo tiempo. Sin embargo, en los procesadores de múltiples núcleos, la concurrencia de subprocesos se logra mediante la programación de subprocesos del sistema operativo. La corrutina se ejecuta en un solo subproceso y se cambian varias corrutinas a través del programador de corrutinas para lograr una concurrencia más detallada.

  • Consumo de memoria: la creación y destrucción de subprocesos requiere que el sistema operativo realice una serie de asignaciones y reciclaje de recursos, incluido el espacio de pila de subprocesos y los bloques de control de subprocesos. La corrutina se ejecuta en un solo subproceso y no requiere asignación adicional de recursos del sistema, solo requiere que el programador de corrutina guarde y restaure el contexto de la corrutina.

  • Método de sincronización: la comunicación y sincronización entre subprocesos requiere el uso de mecanismos como bloqueos y variables de condición, que requieren operaciones de bloqueo y desbloqueo, lo que puede provocar fácilmente problemas como interbloqueos y condiciones de carrera. Las corrutinas pueden utilizar métodos más ligeros para comunicarse y sincronizarse, como el uso de canales para implementar el paso de mensajes entre corrutinas.

red

Hablemos del proceso del protocolo de enlace de tres vías TCP

  • TCP es un protocolo orientado a la conexión, por lo que se debe establecer una conexión antes de usar TCP, y la conexión se establece mediante un protocolo de enlace de tres vías . El proceso de protocolo de enlace de tres vías es el siguiente:

    imagen

    Protocolo de enlace de tres vías TCP

    imagen

    El primer mensaje: mensaje SYN

    imagen

    El segundo mensaje: mensaje SYN + ACK

    imagen

    El tercer mensaje: mensaje ACK

    Una vez que se completa el protocolo de enlace de tres vías y ambas partes se encuentran en  ESTABLISHED el estado, se establece la conexión y el cliente y el servidor pueden enviarse datos entre sí.

    • ACK Después de recibir el mensaje del servidor, el cliente debe responder al servidor con el último mensaje de respuesta. Primero, la posición del indicador del encabezado TCP del  mensaje de respuesta es, luego  1 se completa el campo "Número de respuesta de confirmación"  server_isn + 1 y finalmente se envía el mensaje. al servicio.final, esta vez el mensaje puede transportar los datos del cliente al servidor, y luego el cliente está en el  ESTABLISHED estado.

    • Después de recibir el mensaje de respuesta del cliente, el servidor también ingresa  ESTABLISHED al estado.

    • Después de que el servidor recibe el  SYN mensaje del cliente, primero inicializa aleatoriamente su propio número de secuencia ( server_isn), completa este número de secuencia en el campo "Número de serie" del encabezado TCP y luego completa el campo "Número de respuesta de confirmación" del encabezado TCP.  client_isn + 1Luego coloque  SYN la  ACK marca y en su posición  1. Finalmente, el mensaje se envía al cliente. El mensaje no contiene datos de la capa de aplicación. Después de eso, el servidor está en  SYN-RCVD estado.

    • El cliente inicializará aleatoriamente el número de secuencia ( client_isn), colocará este número de secuencia en el campo "número de secuencia" del encabezado TCP y establecerá  SYN la posición de la bandera para  1indicar  SYN el mensaje. Luego, el primer mensaje SYN se envía al servidor, lo que indica que se inició una conexión con el servidor. El mensaje no contiene datos de la capa de aplicación y el cliente se encuentra en el estado posterior  SYN-SENT .

    • Inicialmente, tanto el cliente como el servidor están en  CLOSE estado. Primero, el servidor escucha activamente un determinado puerto y está en el  LISTEN estado

¿Cuál es el proceso específico de control de congestión Tcp?

El control de la congestión consta principalmente de cuatro algoritmos:

  • comienzo lento

  • evitación de congestión

  • se produce congestión

  • Rápida recuperación

1. Inicio lento

El algoritmo de inicio lento sólo necesita recordar una regla: cada vez que el remitente recibe un ACK, el tamaño de la ventana de congestión cwnd aumentará en 1.

Aquí se supone que la ventana de congestión  cwnd y la ventana de envío  swnd son iguales. A continuación se muestra un ejemplo:

  • Una vez establecida la conexión, se inicializa inicialmente  , lo que significa que cwnd = 1se pueden transferir  MSS datos de un tamaño.

  • Cuando se recibe una respuesta de confirmación ACK, cwnd aumenta en 1, por lo que se pueden enviar 2 a la vez

  • Después de recibir 2 respuestas de confirmación ACK, cwnd aumenta en 2, por lo que se pueden enviar 2 ACK más que antes, por lo que esta vez se pueden enviar 4

  • Cuando llegan estas 4 confirmaciones ACK, el cwnd de cada confirmación aumenta en 1 y el cwnd de las 4 confirmaciones aumenta en 4, por lo que se pueden enviar 4 más que antes, por lo que esta vez se pueden enviar 8.

El proceso de cambio del algoritmo de inicio lento es el siguiente:

imagen

algoritmo de inicio lento

Se puede observar que con el algoritmo de inicio lento, la cantidad de paquetes enviados aumenta exponencialmente .

¿Cuándo terminará el lento comienzo?

Hay una  ssthresh variable de estado llamada umbral de inicio lento.

  • Cuando  cwnd <  ssthresh , se utiliza el algoritmo de inicio lento.

  • Cuando  cwnd >=  ssthresh , se utilizará el "algoritmo para evitar la congestión".

2. Algoritmo para evitar la congestión

Como se mencionó anteriormente, cuando la ventana de congestión  cwnd "excede" el umbral de inicio lento,  ssthresh se ingresará el algoritmo para evitar la congestión.

Generalmente  ssthresh el tamaño está  65535 en bytes. Luego, después de ingresar al algoritmo para evitar la congestión, sus reglas son: ** Siempre que se recibe un ACK, cwnd aumenta en 1/cwnd. *Continuando con la castaña de inicio lento anterior, ahora se supone  ssthresh que es  8:

  • Cuando llegan 8 confirmaciones de respuesta ACK, cada confirmación aumenta en 1/8, y el cwnd de las 8 confirmaciones de ACK aumenta en 1 en total, por lo que  MSS esta vez se pueden enviar 9 datos, lo que se convierte en un aumento lineal.

El proceso de cambio del algoritmo para evitar la congestión es el siguiente:

imagen

evitación de congestión

Por lo tanto, podemos encontrar que el algoritmo para evitar la congestión cambia el crecimiento exponencial del algoritmo de inicio lento original a un crecimiento lineal, que todavía está en la etapa de crecimiento, pero la tasa de crecimiento es más lenta.

Después de que continúe creciendo así, la red entrará lentamente en una situación de congestión, por lo que se producirá la pérdida de paquetes, en este momento los paquetes de datos perdidos deberán retransmitirse.

Cuando se activa el mecanismo de retransmisión, se ingresa el "algoritmo de aparición de congestión".

3. Se produce congestión

Cuando se produce una congestión en la red, los paquetes de datos se retransmitirán. Existen dos mecanismos principales de retransmisión:

  • Retransmisión de tiempo de espera

  • Retransmisión rápida

Los dos algoritmos de envío de congestión utilizados son diferentes y se analizarán por separado a continuación.

3.1 Cuando se produzca una "retransmisión por tiempo de espera", se utilizará el algoritmo de generación de congestión.

En este momento, los valores de ssthresh y cwnd cambiarán:

  • ssthresh Establecer en  cwnd/2,

  • cwnd Restablecer a  1 (restaurar al valor de inicialización de cwnd, asumo que el valor de inicialización de cwnd es 1 aquí)

Los cambios en el algoritmo de generación de congestión son los siguientes:

imagen

Envío congestionado: retransmisión por tiempo de espera

Luego, se reinicia el inicio lento, lo que reducirá repentinamente el flujo de datos. Este es realmente un caso de "retransmisión de tiempo de espera" y volverá inmediatamente a antes de la liberación. Pero este método es demasiado radical y la reacción es muy fuerte, lo que provocará un retraso en la red. Es como derrapar a gran velocidad en la montaña Akina y de repente hay un freno de emergencia, ¿podrán los neumáticos soportarlo? . .

3.1 Algoritmo de ocurrencia de congestión con retransmisión rápida

Hay una manera mejor: hablamos antes sobre el "algoritmo de retransmisión rápida". Cuando el receptor descubre que se ha perdido un paquete intermedio, envía el ACK del paquete anterior tres veces, por lo que el remitente retransmitirá rápidamente sin esperar el tiempo de espera antes de retransmitir.

TCP cree que esta situación no es grave, porque la mayor parte no se pierde, solo una pequeña parte se pierde y la  ssthresh suma  cwnd cambia de la siguiente manera:

  • cwnd = cwnd/2 , es decir, establecido en la mitad del valor original;

  • ssthresh = cwnd;

  • Ingrese al algoritmo de recuperación rápida

4. Recuperación rápida

Los algoritmos de retransmisión rápida y recuperación rápida generalmente se usan al mismo tiempo. El algoritmo de recuperación rápida cree que si aún puede recibir 3 ACK duplicados, significa que la red no es tan mala, por lo que no es necesario ser tan fuerte como un tiempo de espera  RTO .

Como se mencionó anteriormente, antes de ingresar a la recuperación rápida, cwnd se  ssthresh han actualizado:

  • cwnd = cwnd/2 , es decir, establecido en la mitad del valor original;

  • ssthresh = cwnd;

Luego, ingrese el algoritmo de recuperación rápida de la siguiente manera:

  • Ventana de congestión  cwnd = ssthresh + 3 (3 significa confirmación de que se recibieron 3 paquetes);

  • Retransmitir paquetes perdidos;

  • Si se vuelve a recibir un ACK duplicado, cwnd aumenta en 1;

  • Si después de recibir el ACK de datos nuevos, establece cwnd en el valor de ssthresh en el primer paso, el motivo es que el ACK confirma los nuevos datos, lo que indica que se han recibido los datos del ACK duplicado y que el proceso de recuperación ha finalizado. Puede volver al estado anterior a la recuperación, es decir, volver a entrar en el estado para evitar la congestión;

El proceso de cambio del algoritmo de recuperación rápida es el siguiente:

imagen

Retransmisión rápida y recuperación rápida

Es decir, no vuelve al estado anterior a la liberación de la noche a la mañana como la "retransmisión de tiempo de espera", pero todavía tiene un valor relativamente alto y luego aumenta linealmente.

¿Cuál es la diferencia entre http y https?

  • HTTP es el protocolo de transferencia de hipertexto y la información se transmite en texto claro, lo que plantea riesgos de seguridad. HTTPS resuelve las deficiencias de inseguridad de HTTP y agrega el protocolo de seguridad SSL/TLS entre las capas de red TCP y HTTP para que los mensajes puedan cifrarse y transmitirse.

  • El establecimiento de una conexión HTTP es relativamente simple y la transmisión de mensajes HTTP se puede realizar después del protocolo de enlace de tres vías TCP. Después del protocolo de enlace de tres vías de TCP, HTTPS aún necesita llevar a cabo el proceso de protocolo de enlace SSL/TLS antes de poder ingresar a la transmisión de mensajes cifrados.

  • Los puertos predeterminados de los dos son diferentes: el número de puerto predeterminado de HTTP es 80 y el número de puerto predeterminado de HTTPS es 443.

  • El protocolo HTTPS requiere solicitar un certificado digital de una CA (autoridad certificadora) para garantizar que la identidad del servidor sea confiable.

¿Cuál es el proceso de cifrado https?

Proceso básico del protocolo SSL/TLS:

  • El cliente solicita al servidor y verifica la clave pública del servidor.

  • Las dos partes negocian para producir una "clave de sesión".

  • Ambas partes utilizan una "clave de sesión" para la comunicación cifrada.

Los primeros dos pasos son el proceso de establecimiento de SSL/TLS, que también es la fase de protocolo de enlace de TLS.

El proceso de protocolo de enlace TLS basado en el algoritmo RSA es relativamente fácil de entender, por lo que aquí usamos esto para mostrarle el proceso de protocolo de enlace TLS, como se muestra a continuación:

imagen

Proceso de establecimiento de conexión HTTPS

Proceso detallado para establecer el protocolo TLS:

1. ClienteHola

Primero, el cliente inicia una solicitud de comunicación cifrada al servidor, es decir,  ClientHello una solicitud.

En este paso, el cliente envía principalmente la siguiente información al servidor:

(1) Versión del protocolo TLS admitida por el cliente, como TLS versión 1.2.

(2) El número aleatorio ( ) generado por el cliente Client Randomse utiliza posteriormente para generar una de las condiciones de "clave de sesión".

(3) Lista de conjuntos de cifrado admitidos por el cliente, como el algoritmo de cifrado RSA.

2. SeverHola

Después de recibir la solicitud del cliente, el servidor envía una respuesta al cliente, es decir  SeverHello. El contenido de la respuesta del servidor es el siguiente:

(1) Confirme la versión del protocolo TLS. Si el navegador no lo admite, desactive la comunicación cifrada.

(2) El número aleatorio ( ) generado por el servidor Server Randomes también una de las condiciones utilizadas posteriormente para producir la "clave de sesión".

(3) Lista de conjuntos de cifrado confirmados, como el algoritmo de cifrado RSA.

(4) Certificado digital del servidor.

3. Respuesta del cliente

Después de recibir la respuesta del servidor, el cliente primero confirma la autenticidad del certificado digital del servidor a través de la clave pública de CA en el navegador o sistema operativo.

Si no hay ningún problema con el certificado, el cliente extraerá la clave pública del servidor del certificado digital , luego la usará para cifrar el mensaje y enviará la siguiente información al servidor:

(1) Un número aleatorio ( pre-master key). El número aleatorio será cifrado por la clave pública del servidor.

(2) Notificación de cambio del algoritmo de comunicación de cifrado, indicando que los mensajes posteriores se cifrarán con la "clave de sesión".

(3) La notificación de finalización del protocolo de enlace del cliente indica que la fase de protocolo de enlace del cliente ha finalizado. Este elemento también resume los datos de aparición de todos los contenidos anteriores para la verificación del lado del servidor.

El número aleatorio en el primer elemento anterior es el tercer número aleatorio en toda la fase de protocolo de enlace y se enviará al servidor, por lo que este número aleatorio es el mismo tanto en el cliente como en el servidor.

El servidor y el cliente tienen estos tres números aleatorios (Cliente aleatorio, Servidor aleatorio, clave maestra previa) y luego utilizan el algoritmo de cifrado negociado por ambas partes para generar cada uno una "clave de sesión" para esta comunicación .

4. La respuesta final del servidor

Después de que el servidor recibe el tercer número aleatorio () del cliente pre-master key, calcula la "clave de sesión" para esta comunicación a través del algoritmo de cifrado negociado.

Luego, envía el mensaje final al cliente:

(1) Notificación de cambio del algoritmo de comunicación de cifrado, que indica que los mensajes posteriores se cifrarán con la "clave de sesión".

(2) La notificación de finalización del protocolo de enlace del servidor indica que la fase de protocolo de enlace del servidor ha finalizado. Este elemento también resume los datos de aparición de todo el contenido anterior para la verificación del cliente.

En este punto, toda la fase de protocolo de enlace TLS ha finalizado. A continuación, el cliente y el servidor inician una comunicación cifrada, utilizando completamente el protocolo HTTP normal, pero utilizando la "clave de sesión" para cifrar el contenido.

Redistribuir

¿Estructuras de datos de uso común en Redis?

Redis proporciona una gran cantidad de tipos de datos y hay cinco tipos de datos comunes: String, Hash, List, Set y Zset .

imagen

imagen

Con la actualización de la versión de Redis, se admiten cuatro tipos de datos más: BitMap (nuevo en la versión 2.2), HyperLogLog (nuevo en la versión 2.8), GEO (nuevo en la versión 3.2) y Stream (nuevo en la versión 5.0) . Escenarios de aplicación de los cinco tipos de datos de Redis:

  • Escenarios de aplicación de tipo cadena: objetos de caché, recuento regular, bloqueos distribuidos, información de sesión compartida, etc.

  • Escenarios de aplicación de tipo lista: cola de mensajes (pero hay dos problemas: 1. El productor necesita implementar una ID global única por sí mismo; 2. Los datos no se pueden consumir en forma de un grupo de consumidores), etc.

  • Tipo de hash: objeto de caché, carrito de compras, etc.

  • Tipo de conjunto: escenarios de cálculo de agregación (unión, intersección, conjunto de diferencias), como me gusta, seguidores comunes, actividades de lotería, etc.

  • Tipo Zset: escenarios de clasificación, como clasificaciones, clasificación de teléfonos y nombres, etc.

Las versiones posteriores de Redis admiten cuatro tipos de datos más y sus escenarios de aplicación son los siguientes:

  • BitMap (nuevo en la versión 2.2): escenarios de estadísticas de estado binario, como registro, determinación del estado de inicio de sesión del usuario, número total de usuarios de registro consecutivos, etc.;

  • HyperLogLog (nuevo en la versión 2.8): escenarios para estadísticas de bases de datos masivas, como recuento UV de millones de páginas web, etc.;

  • GEO (nuevo en la versión 3.2): escenarios para almacenar información de ubicación geográfica, como el transporte compartido de Didi;

  • Stream (nuevo en la versión 5.0): la cola de mensajes, en comparación con la cola de mensajes implementada según el tipo de Lista, tiene estas dos características únicas: genera automáticamente una ID de mensaje única a nivel mundial y admite el consumo de datos en forma de grupos de consumidores.

zset estructura de datos subyacente?

La estructura de datos subyacente del tipo Zset se implementa mediante una lista comprimida o una lista de omisión :

  • Si el número de elementos en el conjunto ordenado es inferior a 128 y el valor de cada elemento es inferior a 64 bytes, Redis utilizará una lista comprimida como estructura de datos subyacente del tipo Zset;

  • Si los elementos del conjunto ordenado no cumplen con las condiciones anteriores, Redis utilizará una lista de salto como estructura de datos subyacente del tipo Zset;

En Redis 7.0, la estructura de datos de lista comprimida se abandonó y se implementa mediante la estructura de datos del paquete de lista.

MySQL

¿Cuáles son las características de los árboles B+?

El árbol B+ es una actualización del árbol B. La estructura de datos del índice en MySQL utiliza el árbol B+. La estructura del árbol B+ es la siguiente:

imagen

imagen

Las principales diferencias entre árboles B+ y árboles B son los siguientes puntos:

  • Solo los nodos hoja (los nodos inferiores) almacenarán datos reales (índice + registro), y los nodos que no son hoja solo almacenarán índices;

  • Todos los índices aparecerán en los nodos hoja y se forma una lista vinculada ordenada entre los nodos hoja;

  • El índice de un nodo no hoja también existirá en los nodos secundarios y es el máximo (o mínimo) de todos los índices en los nodos secundarios.

  • Hay tantos índices como nodos secundarios en nodos que no son hoja;

El motor de almacenamiento predeterminado de MySQL, InnoDB, utiliza B+ como estructura de datos de índice por las siguientes razones:

  • Los nodos que no son hoja del árbol B + no almacenan datos de registros reales, solo almacenan índices. Por lo tanto, cuando la cantidad de datos es la misma, en comparación con el árbol B que almacena índices y registros, los nodos que no son hoja del árbol B+ puede almacenar más índices, por lo que el árbol B+ puede ser "más fragmentado" que el árbol B, y la cantidad de E/S de disco necesarias para consultar los nodos subyacentes será menor.

  • El árbol B + tiene una gran cantidad de nodos redundantes (todos los nodos que no son hoja son índices redundantes). Estos índices redundantes hacen que el árbol B + sea más eficiente al insertar y eliminar. Por ejemplo, al eliminar el nodo raíz, no será como el Árbol B. Se producen cambios complejos en el árbol;

  • Los nodos de hoja del árbol B + están conectados con listas vinculadas, lo que favorece consultas de rango, pero el árbol B necesita implementar consultas de rango, por lo que las consultas de rango solo se pueden completar a través del recorrido del árbol, lo que implicará operaciones de E / S de disco. en múltiples nodos La eficiencia de la consulta de rango no es tan buena como la del árbol B +.

¿Qué se puede hacer para que la consulta no requiera 3 ios?

Almacene en caché el nodo en la memoria, de modo que la próxima vez que consulte los datos, pueda realizar una búsqueda de índice directamente en la memoria.

Preguntas de inteligencia

  • Sólo 1 botella de cada 1000 botellas de veneno es venenosa. ¿Cuántos ratones se necesitan para probar qué botella es venenosa después de 24 horas?

Referencia: Este es un problema clásico de dicotomía o binario. Para 1000 frascos de medicamento, se puede representar en binario como un rango de 0000000000 (0) a 1111101000 (1000), lo que requiere un total de 10 bits. Por lo tanto, se necesitan 10 ratones para comprobar qué botella es venenosa. El plan de implementación específico es asignar cada frasco de medicamento a un número binario y luego dejar que el mouse correspondiente a la posición 1 pruebe ese frasco de medicamento. De esta forma, después de 24 horas, sólo hace falta ver qué ratones están muertos para determinar qué frasco de medicamento es venenoso.

Supongo que te gusta

Origin blog.csdn.net/Blue92120/article/details/133317868
Recomendado
Clasificación