linux-kernel kernel context-switch

linux kernel - Contexto switch internal



linux-kernel context-switch (3)

En un nivel alto, hay dos mecanismos separados para entender. El primero es el mecanismo de entrada / salida del kernel: esto cambia un único hilo en ejecución desde el código del modo de usuario en ejecución al código kernel en ejecución en el contexto de ese hilo, y viceversa. El segundo es el mecanismo de cambio de contexto en sí mismo, que cambia en modo kernel para que no se ejecute en el contexto de un hilo a otro.

Entonces, cuando el Subproceso A llama a sched_yield() y es reemplazado por el Subproceso B, lo que sucede es:

  1. El subproceso A ingresa al kernel, cambiando del modo de usuario al modo kernel;
  2. El hilo A en el contexto del kernel-cambia al hilo B en el kernel;
  3. El hilo B sale del kernel, cambiando del modo Kernel al modo usuario.

Cada subproceso de usuario tiene una pila de modo de usuario y una pila de modo de kernel. Cuando un hilo entra en el kernel, el valor actual de la pila de modo de usuario ( SS:ESP ) y el puntero de instrucción ( CS:EIP ) se guardan en la pila del modo kernel del subproceso y la CPU pasa a la pila en modo kernel. con el mecanismo de llamada de teclado int $80 , esto es hecho por la CPU misma. Los valores de registro restantes y los indicadores también se guardan en la pila del kernel.

Cuando un hilo retorna del kernel al modo de usuario, los valores de registro y los indicadores se extraen de la pila del modo kernel, luego los valores del puntero de la pila y la instrucción del modo usuario se restauran a partir de los valores guardados en la pila kernel-mode.

Cuando un subproceso cambia de contexto, llama al planificador (el programador no se ejecuta como un subproceso separado, siempre se ejecuta en el contexto del subproceso actual). El código del programador selecciona un proceso para ejecutarlo a continuación y llama a la función switch_to() . Esta función básicamente solo cambia las pilas del kernel: guarda el valor actual del puntero de pila en el TCB para el hilo actual (llamado struct task_struct en Linux) y carga un puntero de pila previamente guardado del TCB para el siguiente hilo. En este punto, también guarda y restaura algún otro estado de subproceso que generalmente no utiliza el kernel, como registros de punto flotante / SSE. Si los hilos que se intercambian no comparten el mismo espacio de memoria virtual (es decir, están en procesos diferentes), las tablas de página también se cambian.

Por lo tanto, puede ver que el estado del modo de usuario central de un hilo no se guarda y restaura en el momento del cambio de contexto; se guarda y restaura en la pila del núcleo del hilo cuando ingresa y abandona el kernel. El código de cambio de contexto no tiene que preocuparse por aplastar los valores de registro en modo usuario, que ya están guardados de forma segura en la pila del kernel en ese punto.

Quiero aprender y llenar vacíos en mi conocimiento con la ayuda de esta pregunta

Entonces, un usuario está ejecutando un hilo (kernel-level) y ahora llama a yield (una llamada al sistema, supongo) El planificador ahora debe guardar el contexto del hilo actual en el TCB (que está almacenado en el núcleo en alguna parte) y elegir otro subproceso para ejecutar y carga su contexto y salta a su CS: EIP. Para reducir las cosas, estoy trabajando en Linux ejecutándose sobre la arquitectura x86. Ahora, quiero entrar en los detalles:

Entonces, primero tenemos una llamada al sistema:

1) La función de envoltura para rendimiento empujará los argumentos de llamadas del sistema a la pila. Presione la dirección de retorno y active una interrupción con el número de llamada del sistema insertado en algún registro (por ejemplo, EAX).

2) La interrupción cambia el modo de CPU de usuario a núcleo y salta a la tabla de vectores de interrupción y de allí a la llamada al sistema real en el kernel.

3) Supongo que se llama al programador ahora y ahora debe guardar el estado actual en el TCB. Aquí está mi dilema Dado que, el planificador usará la pila del kernel y no la pila del usuario para realizar su operación (lo que significa que deben cambiarse la SS y la SP), cómo almacena el estado del usuario sin modificar ningún registro en el proceso. He leído en los foros que hay instrucciones de hardware especiales para guardar el estado, pero ¿cómo accede el programador a ellas y quién las ejecuta y cuándo?

4) El programador ahora almacena el estado en el TCB y carga otro TCB

5) Cuando el programador ejecuta el hilo original, el control vuelve a la función de contenedor que borra la pila y el hilo se reanuda

Preguntas secundarias: ¿se ejecuta el programador como un hilo de solo kernel (es decir, un hilo que solo puede ejecutar código de kernel)? ¿Hay una pila de kernel por separado para cada kernel-thread o cada proceso?


Kernel en sí no tiene pila en absoluto. Lo mismo es cierto para el proceso. Tampoco tiene pila. Los hilos son solo ciudadanos del sistema que se consideran unidades de ejecución. Debido a esto, solo se pueden programar subprocesos y solo los subprocesos tienen acumulaciones. Pero hay un punto que el código del modo kernel explota en gran medida: cada momento del sistema funciona en el contexto del hilo actualmente activo. Debido a este núcleo, puede reutilizar la pila de la pila actualmente activa. Tenga en cuenta que solo uno de ellos puede ejecutar en el mismo momento el código del kernel o el código de usuario. Debido a esto, cuando se invoca al kernel, simplemente reutiliza la pila de subprocesos y realiza una limpieza antes de devolver el control a las actividades interrumpidas en el subproceso. El mismo mecanismo funciona para los manejadores de interrupciones. El mismo mecanismo es explotado por los controladores de señal.

A su vez, la pila de subprocesos se divide en dos partes aisladas, una llamada pila de usuario (porque se utiliza cuando la secuencia se ejecuta en modo usuario) y la segunda se llama pila de kernel (porque se usa cuando la secuencia se ejecuta en modo kernel) . Una vez que el hilo cruza el límite entre el usuario y el modo kernel, la CPU lo cambia automáticamente de una pila a otra. Ambas pilas son rastreadas por kernel y CPU de forma diferente. Para la pila del kernel, la CPU tiene en cuenta de forma permanente el puntero a la parte superior de la pila del kernel del subproceso. Es fácil, porque esta dirección es constante para el hilo. Cada vez que el hilo entra al kernel, se encuentra la pila de kernel vacía y cada vez que vuelve al modo de usuario, limpia la pila del kernel. Al mismo tiempo, la CPU no tiene en cuenta el puntero a la parte superior de la pila del usuario, cuando la secuencia se ejecuta en modo kernel. En cambio, al ingresar al kernel, la CPU crea un marco de pila de "interrupción" especial en la parte superior de la pila del kernel y almacena el valor del puntero de pila del modo de usuario en ese marco. Cuando el hilo sale del kernel, la CPU restaura el valor de ESP del marco de pila de "interrupción" creado previamente, inmediatamente antes de su limpieza. (en legacy x86 el par de instrucciones int / iret manejan entrar y salir del modo kernel)

Al ingresar al modo kernel, inmediatamente después de que la CPU haya creado un marco de pila de "interrupción", el kernel envía el contenido del resto de los registros de la CPU a la pila del kernel. Tenga en cuenta que guarda valores solo para esos registros, que pueden ser utilizados por el código kernel. Por ejemplo kernel no guarda el contenido de los registros SSE simplemente porque nunca los tocará. De manera similar, justo antes de pedirle a la CPU que devuelva el control al modo de usuario, Kernel vuelve a mostrar el contenido previamente guardado en los registros.

Tenga en cuenta que en sistemas como Windows y Linux existe una noción de subproceso del sistema (a menudo llamado subproceso kernel, sé que es confuso). El sistema enhebra un tipo de subprocesos especiales, ya que se ejecutan solo en modo núcleo y debido a esto no tienen parte de usuario de la pila. Kernel los emplea para tareas domésticas auxiliares.

El interruptor de hilo se realiza solo en modo kernel. Eso significa que ambos subprocesos entrantes y salientes se ejecutan en modo núcleo, ambos usan sus propias pilas de kernel, y ambos tienen pilas de kernel que tienen marcos de "interrupción" con punteros en la parte superior de las pilas de usuarios. El punto clave del cambio de hilo es un cambio entre pilas de hilos del kernel, tan simple como:

pushad; // save context of outgoing thread on the top of the kernel stack of outgoing thread ; here kernel uses kernel stack of outgoing thread mov [TCB_of_outgoing_thread], ESP; mov ESP , [TCB_of_incoming_thread] ; here kernel uses kernel stack of incoming thread popad; // save context of incoming thread from the top of the kernel stack of incoming thread

Tenga en cuenta que solo hay una función en el kernel que realiza el cambio de subprocesos. Debido a esto, cada vez que kernel tiene pilas conmutadas, puede encontrar un contexto de subprocesos entrantes en la parte superior de la pila. Solo porque cada vez antes del cambio de pila, el kernel empuja el contexto del hilo saliente a su pila.

Tenga en cuenta también que cada vez después del cambio de pila y antes de volver al modo de usuario, kernel vuelve a cargar la mente de la CPU por el nuevo valor de la parte superior de la pila del núcleo. Al hacer esto, se asegura que cuando un nuevo hilo activo intente ingresar kernel en el futuro, la CPU lo conmutará a su propia pila de kernel.

Tenga en cuenta también que no todos los registros se guardan en la pila durante el cambio de hilo, algunos registros como FPU / MMX / SSE se guardan en un área especialmente dedicada en el TCB del hilo de salida. Kernel emplea una estrategia diferente aquí por dos razones. En primer lugar, no todos los hilos del sistema los utilizan. Empujar su contenido hacia y desde la pila para cada hilo es ineficiente. Y el segundo hay instrucciones especiales para guardar y cargar su contenido "rápido". Y estas instrucciones no usan pila.

Tenga en cuenta también que, de hecho, la parte del kernel de la pila de subprocesos tiene un tamaño fijo y se asigna como parte de TCB. (cierto para Linux y creo para Windows también)


Lo que te perdiste durante el paso 2 es que la pila se cambia de la pila de nivel de usuario de un subproceso (donde empujaste args) a la pila de nivel protegido de un subproceso. El contexto actual del hilo interrumpido por syscall se guarda en esta pila protegida. Dentro del ISR y justo antes de ingresar al kernel, esta pila protegida se vuelve a cambiar a la pila del kernel de la que está hablando. Una vez dentro del kernel, las funciones del kernel, como las funciones del planificador, eventualmente usan kernel-stack. Más adelante, un subproceso es elegido por el planificador y el sistema vuelve al ISR, cambia de la pila del kernel a la pila del nivel protegido recién elegida (o la anterior si no hay subproceso de mayor prioridad activa), que finalmente contiene el nuevo contexto de hilo. Por lo tanto, el contexto se restablece desde esta pila por código automáticamente (dependiendo de la arquitectura subyacente). Finalmente, una instrucción especial restaura los últimos resgistros sensibles como el puntero de pila y el puntero de instrucción. De vuelta en el sitio de usuario ...

En resumen, un hilo tiene (generalmente) dos pilas, y el núcleo en sí tiene una. La pila del kernel se borra al final de cada núcleo que ingresa. Es interesante señalar que desde 2.6, el kernel en sí se enhebra para algún procesamiento, por lo tanto, un hilo del kernel tiene su propia pila de nivel protegido al lado de la pila general del kernel.

Algunos recursos:

  • 3.3.3 Ejecución del cambio de proceso de comprensión del kernel de Linux , O''Reilly
  • 5.12.1 Procedimientos de excepción o manejo de interrupción del manual de Intel 3A (sysprogramming) . El número de capítulo puede variar de una edición a otra, por lo tanto, una búsqueda en "Uso de pila en transferencias a rutinas de interrupción y manejo de excepciones" debería llevarlo al bueno.

¡Espero que esto ayude!