Todas las CPU modernas tienen la capacidad de interrumpir las instrucciones de la máquina que se está ejecutando actualmente. Guardan suficiente estado (generalmente, pero no siempre, en la pila) para que sea posible reanudar la ejecución más tarde, como si nada hubiera sucedido (la instrucción interrumpida se reiniciará desde cero, generalmente). Luego comienzan a ejecutar un controlador de interrupciones , que es solo más código de máquina, pero colocado en una ubicación especial para que la CPU sepa dónde está por adelantado. Los controladores de interrupción son siempre parte del núcleo del sistema operativo: el componente que se ejecuta con el mayor privilegio y es responsable de supervisar la ejecución de todos los demás componentes. 1,2
Las interrupciones pueden ser síncronas , lo que significa que son activadas por la CPU como respuesta directa a algo que hizo la instrucción que se está ejecutando actualmente, o asíncronas , lo que significa que suceden en un momento impredecible debido a un evento externo, como los datos que llegan a la red. Puerto. Algunas personas reservan el término "interrupción" para las interrupciones asíncronas, y llaman a las interrupciones sincrónicas "trampas", "fallas" o "excepciones", pero todas esas palabras tienen otros significados, así que me voy a quedar con "interrupción sincrónica".
Ahora, la mayoría de los sistemas operativos modernos tienen una noción de procesos . En su forma más básica, este es un mecanismo por el cual la computadora puede ejecutar más de un programa al mismo tiempo, pero también es un aspecto clave de cómo los sistemas operativos configuran la protección de memoria , que es una característica de la mayoría (pero, por desgracia, todavía no todas ) CPU modernas. Va junto con la memoria virtual, que es la capacidad de alterar la asignación entre direcciones de memoria y ubicaciones reales en RAM. La protección de memoria permite que el sistema operativo otorgue a cada proceso su propia porción de RAM privada, a la que solo él puede acceder. También permite que el sistema operativo (que actúa en nombre de algún proceso) designe regiones de RAM como de solo lectura, ejecutables, compartidas entre un grupo de procesos cooperantes, etc. También habrá una porción de memoria a la que solo puede acceder el núcleo. 3
Siempre que cada proceso acceda a la memoria solo de la forma en que la CPU está configurada para permitir, la protección de la memoria es invisible. Cuando un proceso rompe las reglas, la CPU generará una interrupción síncrona, pidiéndole al núcleo que resuelva las cosas. Regularmente sucede que el proceso realmente no rompió las reglas, solo el núcleo necesita hacer un trabajo antes de que el proceso pueda continuar. Por ejemplo, si una página de la memoria de un proceso necesita ser "expulsada" al archivo de intercambio para liberar espacio en la RAM para otra cosa, el núcleo marcará esa página como inaccesible. La próxima vez que el proceso intente usarlo, la CPU generará una interrupción de protección de memoria; el núcleo recuperará la página del intercambio, la volverá a colocar donde estaba, la marcará como accesible nuevamente y reanudará la ejecución.
Pero supongamos que el proceso realmente rompió las reglas. Trató de acceder a una página que nunca tuvo RAM asignada, o intentó ejecutar una página que está marcada como que no contiene código de máquina, o lo que sea. La familia de sistemas operativos generalmente conocida como "Unix" usa señales para hacer frente a esta situación. 4 Las señales son similares a las interrupciones, pero son generadas por el kernel y enviadas por procesos, en lugar de ser generadas por el hardware y enviadas por el kernel. Los procesos pueden definir manejadores de señalen su propio código, y decirle al kernel dónde están. Esos manejadores de señal se ejecutarán, interrumpiendo el flujo normal de control, cuando sea necesario. Todas las señales tienen un número y dos nombres, uno de los cuales es un acrónimo críptico y el otro una frase un poco menos críptica. La señal que se genera cuando un proceso rompe las reglas de protección de memoria es (por convención) el número 11, y sus nombres son SIGSEGV
"Fallo de segmentación". 5,6
Una diferencia importante entre señales e interrupciones es que existe un comportamiento predeterminado para cada señal. Si el sistema operativo no puede definir controladores para todas las interrupciones, eso es un error en el sistema operativo, y toda la computadora se bloqueará cuando la CPU intente invocar un controlador perdido. Pero los procesos no tienen la obligación de definir manejadores de señal para todas las señales. Si el kernel genera una señal para un proceso, y esa señal se ha dejado en su comportamiento predeterminado, el kernel simplemente seguirá adelante y hará lo que sea el predeterminado y no molestará al proceso. Los comportamientos predeterminados de la mayoría de las señales son "no hacer nada" o "terminar este proceso y tal vez también producir un volcado de núcleo". SIGSEGV
Es uno de los últimos.
Entonces, para recapitular, tenemos un proceso que rompió las reglas de protección de memoria. La CPU suspendió el proceso y generó una interrupción síncrona. El núcleo envió esa interrupción y generó una SIGSEGV
señal para el proceso. Supongamos que el proceso no configuró un controlador de señal SIGSEGV
, por lo que el núcleo lleva a cabo el comportamiento predeterminado, que es finalizar el proceso. Esto tiene los mismos efectos que la _exit
llamada al sistema: los archivos abiertos están cerrados, la memoria está desasignada, etc.
Hasta este momento, nada ha impreso ningún mensaje que un humano pueda ver, y el shell (o, más en general, el proceso padre del proceso que acaba de terminar) no ha estado involucrado en absoluto. SIGSEGV
va al proceso que rompió las reglas, no a su padre. Sin embargo, el siguiente paso en la secuencia es notificar al proceso padre que su hijo ha finalizado. Esto puede suceder de varias maneras diferentes, de los cuales el más simple es cuando el padre ya está a la espera de esta notificación, usando una de las wait
llamadas al sistema ( wait
, waitpid
, wait4
, etc.). En ese caso, el kernel solo hará que la llamada del sistema regrese y proporcionará al proceso padre un número de código llamado estado de salida. 7 El estado de salida informa al padre por qué se terminó el proceso hijo; en este caso, aprenderá que el niño fue terminado debido al comportamiento predeterminado de una SIGSEGV
señal.
El proceso principal puede luego informar el evento a un humano imprimiendo un mensaje; Los programas de shell casi siempre hacen esto. Su crsh
no incluye código para hacer eso, pero sucede de todos modos, debido a que la rutina de biblioteca C system
se ejecuta una concha con todas las funciones, /bin/sh
"bajo el capó". crsh
es el abuelo en este escenario; se envía la notificación del proceso principal /bin/sh
, que imprime su mensaje habitual. Luego /bin/sh
se cierra, ya que no tiene nada más que hacer, y la implementación de la biblioteca C system
recibe esa notificación de salida. Puede ver esa notificación de salida en su código inspeccionando el valor de retorno desystem
; pero no le dirá que el proceso de nieto murió por una falla de seguridad, porque eso fue consumido por el proceso de shell intermedio.
Notas al pie
Algunos sistemas operativos no implementan controladores de dispositivos como parte del núcleo; sin embargo, todos los manejadores de interrupciones aún tienen que ser parte del núcleo, y también el código que configura la protección de la memoria, porque el hardware no permite nada más que el núcleo para hacer estas cosas.
Puede haber un programa llamado "hipervisor" o "administrador de máquina virtual" que sea aún más privilegiado que el kernel, pero a los fines de esta respuesta puede considerarse parte del hardware .
El núcleo es un programa , pero es no un proceso; Es más como una biblioteca. Todos los procesos ejecutan partes del código del núcleo, de vez en cuando, además de su propio código. Puede haber una serie de "hilos de kernel" que solo ejecutan código de kernel, pero no nos conciernen aquí.
El único sistema operativo con el que probablemente tenga que lidiar y que no pueda considerarse una implementación de Unix es, por supuesto, Windows. No utiliza señales en esta situación. (De hecho, no tiene señales; en Windows, la <signal.h>
interfaz está completamente falsificada por la biblioteca C). En su lugar, utiliza algo llamado " manejo de excepciones estructuradas ".
Algunas violaciones de protección de memoria generan SIGBUS
("Error de bus") en lugar de SIGSEGV
. La línea entre los dos está subespecificada y varía de un sistema a otro. Si ha escrito un programa que define un controlador para SIGSEGV
, probablemente sea una buena idea definir el mismo controlador SIGBUS
.
"Falla de segmentación" era el nombre de la interrupción generada por violaciones de protección de memoria por una de las computadoras que ejecutaban el Unix original , probablemente el PDP-11 . " Segmentación " es un tipo de protección de memoria, pero hoy en día el término " falla de segmentación " se refiere genéricamente a cualquier tipo de violación de la protección de memoria.
Todas las otras formas en que el proceso principal puede ser notificado de que un niño ha finalizado, terminan con el padre llamando wait
y recibiendo un estado de salida. Es solo que algo más sucede primero.
crsh
Es una gran idea para este tipo de experimentación. Gracias por informarnos a todos sobre la idea que hay detrás.