Estoy estudiando CPU y sé cómo lee un programa de la memoria y ejecuto sus instrucciones. También entiendo que un sistema operativo separa los programas en los procesos, y luego alterna entre cada uno tan rápido que cree que se están ejecutando al mismo tiempo, pero de hecho cada programa se ejecuta solo en la CPU. Pero, si el sistema operativo también es un montón de código que se ejecuta en la CPU, ¿cómo puede administrar los procesos?
He estado pensando y la única explicación que podría pensar es: cuando el sistema operativo carga un programa desde la memoria externa a la RAM, agrega sus propias instrucciones en el medio de las instrucciones originales del programa, de modo que el programa se ejecuta, el programa puede llamar al sistema operativo y hacer algunas cosas. Creo que hay una instrucción que el sistema operativo agregará al programa, que permitirá que la CPU regrese al código del sistema operativo en algún momento. Y también, creo que cuando el sistema operativo carga un programa, comprueba si hay algunas instrucciones prohibidas (que saltarían a direcciones prohibidas en la memoria) y las elimina.
¿Estoy pensando bien? No soy un estudiante de CS, pero de hecho, un estudiante de matemáticas. Si es posible, me gustaría un buen libro sobre esto, porque no encontré a nadie que explique cómo el sistema operativo puede administrar un proceso si el sistema operativo también es un montón de código que se ejecuta en la CPU, y no puede ejecutarse al mismo tiempo del programa. Los libros solo dicen que el sistema operativo puede administrar las cosas, pero ahora cómo.
fuente
Respuestas:
No. El sistema operativo no pierde el tiempo con el código del programa inyectando código nuevo en él. Eso tendría una serie de desventajas.
Llevaría mucho tiempo, ya que el sistema operativo tendría que escanear todo el ejecutable haciendo sus cambios. Normalmente, parte del ejecutable solo se carga según sea necesario. Además, la inserción es costosa ya que tiene que mover un montón de cosas fuera del camino.
Debido a la indecidibilidad del problema de detención, es imposible saber dónde insertar las instrucciones "Volver al SO". Por ejemplo, si el código incluye algo como
while (true) {i++;}
, definitivamente necesita insertar un gancho dentro de ese bucle, pero la condición en el bucle (true
aquí) podría ser arbitrariamente complicada, por lo que no puede decidir por cuánto tiempo se repite. Por otro lado, sería muy ineficiente insertar ganchos en cada bucle: por ejemplo, volver al SO durante muchofor (i=0; i<3; i++) {j=j+i;}
tiempo ralentizaría mucho el proceso. Y, por la misma razón, no puede detectar bucles cortos para dejarlos solos.Debido a la indecidibilidad del problema de detención, es imposible saber si las inyecciones de código cambiaron el significado del programa. Por ejemplo, suponga que usa punteros de función en su programa en C. La inyección de un nuevo código movería las ubicaciones de las funciones, por lo que, cuando llama a uno a través del puntero, salta al lugar equivocado. Si el programador estuviera lo suficientemente enfermo como para usar saltos calculados, también fallarían.
Sería un infierno con cualquier sistema antivirus, ya que también cambiaría el código del virus y acumularía todas sus sumas de verificación.
Puede solucionar el problema del problema de detención simulando el código e insertando ganchos en cualquier bucle que se ejecute más de un cierto número fijo de veces. Sin embargo, eso requeriría una simulación extremadamente costosa de todo el programa antes de poder ejecutarlo.
En realidad, si quisieras inyectar código, el compilador sería el lugar natural para hacerlo. De esa manera, solo tendría que hacerlo una vez, pero aún así no funcionaría por la segunda y la tercera razón mencionadas anteriormente. (Y alguien podría escribir un compilador que no funcionara).
Hay tres formas principales en que el sistema operativo recupera el control de los procesos.
En los sistemas cooperativos (o no preventivos), hay una
yield
función que un proceso puede llamar para devolver el control al sistema operativo. Por supuesto, si ese es su único mecanismo, depende de que los procesos se comporten bien y un proceso que no rinda acaparará la CPU hasta que finalice.Para evitar ese problema, se utiliza una interrupción del temporizador. Las CPU permiten al sistema operativo registrar devoluciones de llamada para todos los diferentes tipos de interrupciones que implementa la CPU. El sistema operativo utiliza este mecanismo para registrar una devolución de llamada para una interrupción del temporizador que se activa periódicamente, lo que le permite ejecutar su propio código.
Cada vez que un proceso intenta leer un archivo o interactuar con el hardware de cualquier otra forma, le está pidiendo al sistema operativo que trabaje por él. Cuando un proceso le pide al sistema operativo que haga algo, puede decidir poner ese proceso en espera y comenzar a ejecutar uno diferente. Esto puede sonar un poco maquiavélico, pero es lo correcto: la E / S del disco es lenta, por lo que también puede dejar que el proceso B se ejecute mientras el proceso A está esperando que los trozos de metal que giran se muevan al lugar correcto. La E / S de red es aún más lenta. La E / S del teclado es glacial porque las personas no son seres de gigahercios.
fuente
1
, la CPU detiene lo que sea que esté haciendo y comienza a procesar la interrupción (que básicamente significa "preservar el estado y saltar a una dirección en la memoria"). El manejo de la interrupción en sí mismo no esx86
o el código que sea, literalmente está cableado. Después de saltar, vuelve a ejecutar (cualquier)x86
código. Los hilos son una forma de abstracción más alta.Si bien la respuesta de David Richerby es buena, es un poco confuso sobre cómo los sistemas operativos modernos detienen los programas existentes. Mi respuesta debe ser precisa para la arquitectura x86 o x86_64, que es la única que se usa comúnmente para computadoras de escritorio y portátiles. Otras arquitecturas deberían tener métodos similares para lograr esto.
Cuando el sistema operativo se está iniciando, configura una tabla de interrupciones. Cada entrada de la tabla apunta a un bit de código dentro del sistema operativo. Cuando ocurren interrupciones, que es controlada por la CPU, mira esta tabla y llama al código. Hay varias interrupciones, como dividir por cero, código no válido y algunos definidos por el sistema operativo.
Así es como el proceso del usuario habla con el núcleo, por ejemplo, si quiere leer / escribir en el disco o algo más que controle el núcleo del sistema operativo. Un sistema operativo también configurará un temporizador que llama a una interrupción cuando finaliza, por lo que el código en ejecución se cambia por la fuerza del programa de usuario al kernel del sistema operativo, y el kernel puede hacer otras cosas, como poner en cola otros programas para ejecutar.
Desde la memoria, cuando esto sucede, el núcleo del sistema operativo tiene que guardar donde estaba el código, y cuando el núcleo ha terminado de hacer lo que debe hacer, restaura el estado anterior del programa. Por lo tanto, el programa ni siquiera sabe que fue interrumpido.
El proceso no puede cambiar la tabla de interrupciones por dos razones, la primera es que se está ejecutando en un entorno protegido, por lo que si intenta llamar a cierto código de ensamblaje protegido, la CPU activará otra interrupción. La segunda razón es la memoria virtual. La ubicación de la tabla de interrupciones es de 0x0 a 0x3FF en la memoria real, pero con los procesos del usuario esa ubicación generalmente no está asignada, e intentar leer la memoria no asignada desencadenará otra interrupción, por lo que sin la función protegida y la capacidad de escribir en RAM real , el proceso del usuario no puede cambiarlo.
fuente
El núcleo del sistema operativo recupera el control del proceso en ejecución debido al controlador de interrupción del reloj de la CPU, no inyectando código en el proceso.
Debe leer sobre las interrupciones para obtener más aclaraciones sobre cómo funcionan y cómo los manejan los núcleos del SO e implementan diferentes características.
fuente
No es un método similar a lo que usted describe: la multitarea cooperativa . El sistema operativo no inserta instrucciones, pero cada programa debe escribirse para llamar a las funciones del sistema operativo que pueden elegir ejecutar otro de los procesos cooperativos. Esto tiene las desventajas que usted describe: el bloqueo de un programa elimina todo el sistema. Windows hasta 3.0 incluido e funcionó así; 3.0 en "modo protegido" y superior no.
La multitarea preventiva (el tipo normal en estos días) se basa en una fuente externa de interrupciones. Las interrupciones anulan el flujo normal de control y generalmente guardan los registros en algún lugar, por lo que la CPU puede hacer otra cosa y luego reanudar el programa de manera transparente. Por supuesto, el sistema operativo puede cambiar el registro "cuando deja las interrupciones reanudar aquí", por lo que se reanuda dentro de un proceso diferente.
(Algunos sistemas hacen las instrucciones de reescritura de una forma limitada de la carga del programa, llamado "thunk", y el procesador Transmeta dinámicamente recompilan a su propio conjunto de instrucciones)
fuente
La multitarea no requiere nada como la inyección de código. En un sistema operativo como Windows, hay un componente del código del sistema operativo llamado planificador que se basa en una interrupción de hardware activada por un temporizador de hardware. El sistema operativo lo utiliza para cambiar entre los diferentes programas y para sí mismo, haciendo que nuestra percepción humana suceda simultáneamente.
Básicamente, el sistema operativo programa el temporizador de hardware para que se active de vez en cuando ... tal vez 100 veces por segundo. Cuando el temporizador se apaga, genera una interrupción de hardware, una señal que le dice a la CPU que pare lo que está haciendo, guarde su estado en la pila, cambie su modo a algo más privilegiado y ejecute el código que encontrará en un lugar especialmente designado colocar en la memoria. Ese código es parte del planificador, que decide qué se debe hacer a continuación. Podría ser reanudar algún otro proceso, en cuyo caso tendrá que realizar lo que se conoce como un "cambio de contexto": reemplazar la totalidad de su estado actual (incluidas las tablas de memoria virtual) con el del otro proceso. Al volver a un proceso, tiene que restaurar todo el contexto de ese proceso,
El lugar "especialmente designado" en la memoria no tiene que ser conocido por nada más que el sistema operativo. Las implementaciones varían, pero lo esencial es que la CPU responderá a varias interrupciones realizando una búsqueda en la tabla; la ubicación de la tabla está en un lugar específico de la memoria (determinado por el diseño del hardware de la CPU), el contenido de la tabla lo establece el sistema operativo (generalmente en el momento del arranque) y el "tipo" de interrupción determinará qué entrada en la tabla debe usarse como "rutina de interrupción del servicio".
Nada de esto implica "inyección de código" ... se basa en el código contenido en el sistema operativo en cooperación con las características de hardware de la CPU y sus circuitos de soporte.
fuente
Creo que el ejemplo del mundo real más cercano a lo que describe es una de las técnicas utilizadas por VMware , la virtualización completa mediante traducción binaria .
VMware actúa como una capa debajo de uno o más sistemas operativos que se ejecutan simultáneamente en el mismo hardware.
La mayoría de las instrucciones que se ejecutan (por ejemplo, en aplicaciones ordinarias) se pueden virtualizar utilizando el hardware, pero el núcleo del sistema operativo hace uso de instrucciones que no se pueden virtualizar, porque si el código de máquina del sistema operativo conjetura se ejecutara sin modificaciones, se "rompería" "del control del host VMware. Por ejemplo, un SO huésped necesitaría ejecutarse en el anillo de protección más privilegiado y configurar la tabla de interrupciones. Si se le permitiera hacer eso, VMware habría perdido el control del hardware.
VMware reescribe esas instrucciones en el código del sistema operativo antes de ejecutarlo, reemplazándolas con saltos en el código de VMware que simula el efecto deseado.
Entonces, esta técnica es algo análoga a lo que usted describe.
fuente
Hay una variedad de casos en los que un sistema operativo puede "inyectar código" en un programa. Las versiones basadas en 68000 del sistema Apple Macintosh construyen una tabla de todos los puntos de entrada de segmento (ubicados inmediatamente antes de las variables globales estáticas, IIRC). Cuando se inicia un programa, cada entrada en la tabla consiste en una instrucción de captura seguida por el número de segmento y el desplazamiento en el segmento. Si se ejecuta la trampa, el sistema mirará las palabras después de la instrucción de la trampa para ver qué segmento y desplazamiento se requiere, cargará el segmento (si aún no lo está), agregará la dirección de inicio del segmento al desplazamiento y luego reemplace la trampa con un salto a esa dirección recién calculada.
En el software de PC más antiguo, aunque técnicamente esto no fue hecho por el "SO", era común que el código se construyera con instrucciones de captura en lugar de instrucciones matemáticas de coprocesador. Si no se instaló un coprocesador matemático, el controlador de trampa lo emularía. Si se instaló un coprocesador, la primera vez que se toma una trampa, el controlador reemplazará la instrucción de trampa con una instrucción de coprocesador; Las futuras ejecuciones del mismo código utilizarán la instrucción del coprocesador directamente.
fuente