Funcionamiento interno del interruptor de contexto


Quiero aprender y llenar lagunas en mi conocimiento con la ayuda de esta pregunta

Entonces, un usuario está ejecutando un subproceso (a nivel del núcleo) y ahora llama a yield (una llamada al sistema, supongo) El scheduler ahora debe guardar el contexto del subproceso actual en el TCB (que está almacenado en el núcleo en algún lugar) y elegir otro subproceso para ejecutar y cargar su contexto y saltar a su CS:EIP. Para reducir las cosas, estoy trabajando en Linux que se ejecuta sobre la arquitectura x86. Ahora, quiero entrar en el detalles:

Entonces, primero tenemos una llamada al sistema:

1) La función wrapper para yield empujará los argumentos de la llamada al sistema a la pila. Empuje la dirección de retorno y levante una interrupción con el número de llamada del sistema empujado en algún registro (por ejemplo, EAX).

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

3) supongo que el programador se llama ahora, y ahora debe guarde el estado actual en el TCB. Aquí está mi dilema. Dado que el scheduler usará la pila del kernel y no la pila del usuario para realizar su operación (lo que significa que el SS y el SP tienen que ser cambiados) cómo almacena el estado del usuario sin modificar ningún registro en el proceso. He leído en los foros que hay instrucciones especiales de hardware para guardar el estado, pero entonces ¿cómo obtiene el programador acceso a ellos y quién ejecuta estas instrucciones y cuándo?

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

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

Preguntas secundarias: ¿El scheduler se ejecuta como un subproceso solo del núcleo (es decir, un subproceso que solo puede ejecutar código del núcleo)? ¿Hay una pila de kernel separada para cada kernel-thread o cada proceso?

Author: Bruce, 2012-09-28

3 answers

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 de ejecutar código usermode a ejecutar código del kernel en el contexto de ese hilo, y viceversa. El segundo es el mecanismo de cambio de contexto en sí, que cambia en modo kernel de ejecutarse en el contexto de un subproceso a otro.

Entonces, cuando el hilo A llama a sched_yield() y es reemplazado por el hilo B, ¿qué sucede is:

  1. El hilo A entra en el núcleo, cambiando de modo de usuario a modo de núcleo;
  2. Hilo A en el contexto del núcleo-cambia al hilo B en el núcleo;
  3. El hilo B sale del kernel, cambiando de modo kernel a modo usuario.

Cada hilo de usuario tiene una pila de modo de usuario y una pila de modo de kernel. Cuando un subproceso 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 el modo kernel del subproceso pila, y la CPU cambia a la pila de modo kernel-con el mecanismo syscall int $80, esto es hecho por la propia CPU. Los valores y banderas restantes del registro también se guardan en la pila del núcleo.

Cuando un hilo regresa del kernel al modo usuario, los valores de registro y las banderas se sacan de la pila kernel-mode, luego los valores de la pila user-mode y del puntero de instrucción se restauran a partir de los valores guardados en la pila kernel-mode.

Cuando un hilo cambia de contexto, se llama al scheduler (el scheduler no se ejecuta como un subproceso separado - siempre se ejecuta en el contexto del subproceso actual). El código scheduler selecciona un proceso a ejecutar a continuación, y llama a la función switch_to(). Esta función esencialmente solo cambia las pilas del núcleo-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 desde el TCB para el siguiente hilo. En este punto también guarda y restaura algún otro estado de hilo que no es usualmente usado por el núcleo - cosas como los registros de punto flotante/SSE. Si los hilos que se están conmutando no comparten el mismo espacio de memoria virtual (es decir. están en diferentes procesos), las tablas de página también se cambian.

Así que puedes ver que el estado de modo de usuario del núcleo 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 entras y sales del núcleo. El código de cambio de contexto no tiene que preocuparse por clobbering los valores de registro de modo de usuario-que ya están guardados de forma segura en la pila del kernel en ese momento.

 93
Author: caf,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-06-29 02:23:59

Lo que te perdiste durante el paso 2 es que la pila cambia de la pila de nivel de usuario de un hilo (donde pusiste args) a la pila de nivel protegido de un hilo. El contexto actual del subproceso interrumpido por la llamada de sistema se guarda realmente en esta pila protegida. Dentro del ISR y justo antes de entrar en el kernel, esta pila protegida se cambia de nuevo a la pila del kernel de la que estás hablando. Una vez dentro del núcleo, las funciones del núcleo como las funciones del programador eventualmente usan la pila del núcleo. Más tarde, un subproceso es elegido por el planificador y el sistema regresa al ISR, cambia de nuevo de la pila del núcleo a la pila de nivel protegido del subproceso recién elegido (o el primero si no hay un subproceso de mayor prioridad activo), que eventualmente contiene el nuevo contexto del subproceso. Por lo tanto, el contexto se restaura desde esta pila por código automáticamente (dependiendo de la arquitectura subyacente). Finalmente, una instrucción especial restaura los últimos resgisters delicados como el puntero de pila y el puntero de instrucciones. De vuelta en la tierra de los usuarios...

Para resumir, un subproceso tiene (generalmente) dos pilas, y el propio núcleo tiene una. La pila del núcleo se borra al final de cada entrada del núcleo. Es interesante señalar que desde la versión 2.6, el kernel en sí mismo se enhebra para algún procesamiento, por lo tanto un kernel-thread tiene su propia pila de nivel protegido al lado de la pila general del kernel.

Algunos recursos:

  • 3.3.3 Realización del Proceso Switch of Understanding the Linux Kernel, O'Reilly
  • 5.12.1 Procedimientos de Manejo de Excepciones o Interrupciones del manual 3A (sysprogramming) de Intel. El número de capítulo puede variar de una edición a otra, por lo que una búsqueda en "Uso de Pila en Transferencias para Interrumpir y Rutinas de Manejo de Excepciones" debería llevarlo a la buena.

Espero que esta ayuda!

 10
Author: Benoit,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-10-03 08:40:04

El núcleo en sí no tiene ninguna pila. Lo mismo es cierto para el proceso. También no tienen pila. Los subprocesos son solo ciudadanos del sistema que se consideran como unidades de ejecución. Debido a esto, solo los hilos se pueden programar y solo los hilos tienen pilas. Pero hay un punto en el que el código de modo kernel explota fuertemente: cada momento del sistema de tiempo funciona en el contexto del subproceso actualmente activo. Debido a esto el propio kernel 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 el núcleo, 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 manejadores de señales.

A su vez, la pila de subprocesos se divide en dos partes aisladas, una de las cuales se llama pila de usuarios (porque se usa cuando el subproceso se ejecuta en modo usuario), y el segundo se llama kernel stack (porque se usa cuando se ejecuta thread en modo kernel). Una vez que el subproceso cruza la frontera entre el modo usuario y el kernel, la CPU lo cambia automáticamente de una pila a otra. Ambas pilas son rastreadas por el núcleo y la CPU de manera diferente. Para la pila del núcleo, CPU mantiene permanentemente en mente el puntero a la parte superior de la pila del núcleo del hilo. Es fácil, porque esta dirección es constante para el hilo. Cada vez que el hilo entra en el núcleo, se encuentra el núcleo vacío pila 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 de usuarios, cuando el subproceso se ejecuta en el modo kernel. En su lugar, al ingresar al núcleo, CPU crea un marco de pila especial de "interrupción" en la parte superior de la pila del núcleo y almacena el valor del puntero de pila de modo de usuario en ese marco. Cuando el subproceso sale del núcleo, la CPU restaura el valor de ESP del marco de pila "interrumpir" creado previamente, inmediatamente antes de su limpieza. (en x86 heredado el par de instrucciones int/iret manejar entrar y salir del modo kernel)

Durante la entrada 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 registros de la CPU a la pila del kernel. Tenga en cuenta que solo guarda valores para esos registros, que pueden ser utilizados por el código del núcleo. Por ejemplo, el kernel no guarda el contenido de los registros SSE solo porque nunca los tocará. Del mismo modo justo antes al pedir a la CPU que devuelva el control al modo de usuario, el kernel devuelve el contenido previamente guardado a los registros.

Tenga en cuenta que en sistemas como Windows y Linux hay una noción de hilo de sistema (frecuentemente llamado hilo de kernel, sé que es confuso). Subprocesos de sistema una especie de subprocesos especiales, porque se ejecutan solo en modo kernel y debido a esto no tienen parte de usuario de la pila. Kernel los emplea para tareas auxiliares de limpieza.

El interruptor de rosca se realiza solo en modo kernel. Eso significa que tanto los hilos salientes como los entrantes se ejecutan en modo kernel, ambos usan sus propias pilas de kernel, y ambos tienen pilas de kernel que tienen marcos de "interrupción" con punteros a la parte superior de las pilas de usuario. El punto clave del interruptor de subproceso es un interruptor entre pilas de subprocesos del núcleo, 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 núcleo que realiza el cambio de hilo. Debido a esto, cada vez que el kernel tiene pilas cambiadas, puede encontrar un contexto de subproceso entrante en la parte superior de la pila. Solo porque cada vez que antes de stack switch el kernel empuja el contexto del hilo saliente a su pila.

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

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

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

 4
Author: ZarathustrA,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-10-25 11:59:26