Construir un sistema operativo desde cero suena como un trabajo para hackers de kernel que escriben en C. Pero puedes montar una simulación simplificada en Python en una tarde, y descubrirás rápidamente que la lógica de la gestión de procesos es igual de implacable en un lenguaje de alto nivel. Aprendí esto por las malas. Me senté a escribir un pequeño simulador de SO. El objetivo era modesto: crear unos pocos procesos, programarlos y marcarlos como terminados cuando su trabajo finalizara. El código era corto. La lógica parecía infalible. Luego lo ejecuté, y nada moría.
¿Por qué construir un mini SO en Python?
Un sistema operativo real hace malabarismos con la paginación de memoria, sistemas de archivos, interrupciones de hardware y controladores de dispositivos. Una simulación elimina todo eso y te permite concentrarte en la idea central: el estado. Defines un proceso. Tiene un PID, un tiempo de ráfaga (burst time) y un estado de ciclo de vida. Listo. En ejecución. Terminado. Un bucle de planificación (scheduler loop) elige al siguiente candidato, avanza su estado, simula una fracción de tiempo (time slice) y lo transiciona a terminado.
Python es un vehículo excelente para este tipo de experimentos porque te permite ignorar la aritmética de punteros y la alineación de memoria. Una lista de diccionarios se convierte en tu tabla de procesos. Un bucle while se convierte en tu planificador del kernel. Puedes implementar planificación round-robin o colas de prioridad con nada más que las herramientas de la biblioteca estándar. Se siente accesible, que es exactamente por lo que el error que siguió fue tan enfurecedor.
La configuración
Mi simulación utilizaba una lista llamada process_table. Cada entrada era un diccionario con esta forma:
{
"pid": 1,
"burst_time": 3,
"status": "ready"
}
El planificador ejecutaba un bucle while sencillo. Escaneaba la tabla en busca del primer proceso cuyo estado no fuera "finished". Cuando encontraba uno, llamaba a una función auxiliar, execute_tick(p), para ejecutar ese proceso durante un ciclo simulado. Dentro de execute_tick, establecía el estado del proceso a "running", decrementaba el tiempo de ráfaga y comprobaba si el trabajo restante llegaba a cero. Si era así, actualizaba el estado a "finished". Se suponía que el bucle externo terminaría una vez que cada proceso alcanzara el estado "finished".
En el papel, el flujo era limpio. Buscar un proceso listo. Ejecutarlo. Comprobar si ha terminado. Repetir hasta finalizar. Incluso añadí sentencias print para observar al planificador hacer su trabajo. Podía ver cómo se seleccionaban los procesos. El bucle seguía dando vueltas. Sin embargo, los procesos parecían entrar en un presente eterno, ejecutándose siempre, sin avanzar nunca.
El síntoma
Este es el peor tipo de fallo: el silencioso. Ningún rastro de la pila (stack trace) apareció en la terminal. Ningún IndexError o KeyError me dio una pista que seguir. El intérprete estaba perfectamente feliz. El programa simplemente no se comportaba como debía. Los procesos comenzaban, pero nunca terminaban. Pasé horas rastreando el flujo.
¿Estaba mal la condición del bucle? Quizás tenía un error de desfase (off-by-one error) en el cálculo del tiempo de ráfaga. ¿Se estaba sombreando o copiando la tabla de procesos en lugar de actualizarse in situ? ¿Estaba mi condición de terminación comprobando la clave incorrecta? Añadí más print. Audité cada expresión booleana. Cuestioné todo excepto la única línea que realmente importaba.
El culpable
Entonces lo vi. Dentro de execute_tick, había escrito:
p["status"] == "running"
Dos signos de igual. Una comparación, no una asignación. La solución estaba a un solo carácter de distancia:
p["status"] = "running"
En Python, p["status"] == "running" es una expresión perfectamente válida. Se evalúa como True o False, y luego el intérprete descarta el resultado porque nunca lo asigné a nada. La línea no hace absolutamente nada útil. La entrada del diccionario permanecía intacta, preservando cualquier estado que tuviera antes, y el proceso nunca avanzaba en su ciclo de vida.
Lo cambié por un solo signo de igual. Ejecuté el script de nuevo. La simulación cobró vida. Los procesos pasaban por listo, en ejecución y terminado exactamente como estaba planeado. Una pulsación de tecla extra me había costado horas.
Por qué se esconden estos errores
La razón por la que esto duele tanto es que Python no marca una sentencia de expresión como un error a menos que sea una sintaxis totalmente inválida. El error era un error tipográfico semántico. El programa comparaba el estado, producía un booleano y lo descartaba. Debido a que la comparación en sí misma podía devolver False, el proceso se quedaba atascado en su estado anterior y el bucle externo no tenía motivos para romperse.
A esto le sumas el sesgo de confirmación. Sabes que escribiste una asignación porque tu intención era realizar una asignación. Cuando lees el código por quinta vez, tu cerebro autocorrigió el símbolo. Por eso funciona el rubber ducking. Te obliga a articular cada línea con la suficiente lentitud como para que la brecha entre lo que está escrito y lo que querías decir se vuelva visible.
Los errores pequeños como este son más difíciles de encontrar que los fallos catastróficos. Un segfault o un error de sintaxis se manifiestan de inmediato. Un no-op silencioso simplemente corrompe el estado y permite que el programa siga avanzando con dificultad. El fallo ocurre más adelante en el flujo, y tu instinto es depurar el síntoma en lugar de la causa.
Una mejor defensa
No puedes confiar solo en tus ojos. Después de este episodio, cambié algunos hábitos que habrían detectado el error antes.
Primero, si estás manteniendo el estado en un diccionario, considera usar una dataclass o un enum.Enum para los estados del proceso. Define tus estados como constantes o miembros de un enum:
from enum import Enum
class ProcessState(Enum):
READY = "ready"
RUNNING = "running"
FINISHED = "finished"
Con tipos explícitos, herramientas como mypy pueden señalar comparaciones sospechosas durante el análisis estático. Una comparación accidental donde debería haber una asignación resulta mucho más fácil de detectar cuando los tipos no coinciden con lo esperado.
Segundo, escribe pruebas unitarias para las transiciones de estado antes de escribir la lógica del scheduler. Una prueba simple que cree un proceso con un solo tick de trabajo, ejecute el scheduler y verifique que el estado final sea FINISHED habría fallado de inmediato. Ese fallo habría limitado la búsqueda a la lógica de actualización de estado, en lugar de dejarme vagar por todo el bucle
