Construir um sistema operacional do zero parece um trabalho para hackers de kernel escrevendo em C. Mas você pode criar uma simulação simplificada em Python em uma tarde, e descobrirá rapidamente que a lógica de gerenciamento de processos é tão implacável quanto em uma linguagem de alto nível. Aprendi isso da maneira mais difícil. Sentei-me para escrever um minúsculo simulador de SO. O objetivo era modesto: criar alguns processos, escaloná-los e marcá-los como finalizados quando o trabalho estivesse concluído. O código era curto. A lógica parecia à prova de falhas. Então eu o executei, e nada morria.

Por que construir um mini SO em Python?

Um sistema operacional real faz malabarismos com paginação de memória, sistemas de arquivos, interrupções de hardware e drivers de dispositivos. Uma simulação remove tudo isso e permite que você foque na ideia central: estado. Você define um processo. Ele tem um PID, um burst time e um status de ciclo de vida. Pronto. Em execução. Finalizado. Um loop de escalonador escolhe o próximo candidato, avança seu estado, simula um intervalo de tempo (time slice) e o transiciona para concluído.

O Python é um excelente veículo para esse tipo de experimento porque permite ignorar aritmética de ponteiros e alinhamento de memória. Uma lista de dicionários torna-se sua tabela de processos. Um loop while torna-se seu escalonador de kernel. Você pode implementar escalonamento round-robin ou filas de prioridade com nada mais do que ferramentas da biblioteca padrão. Parece acessível, o que é exatamente o motivo pelo qual o bug que se seguiu foi tão enfurecedor.

A Configuração

Minha simulação usava uma lista chamada process_table. Cada entrada era um dicionário com este formato:

{
    "pid": 1,
    "burst_time": 3,
    "status": "ready"
}

O escalonador executava um loop while simples. Ele percorria a tabela em busca do primeiro processo cujo status não fosse "finished". Quando encontrava um, chamava uma função auxiliar, execute_tick(p), para executar esse processo por um ciclo simulado. Dentro de execute_tick, eu definia o status do processo como "running", decrementava o burst time e verificava se o trabalho restante chegava a zero. Se chegasse, eu atualizava o status para "finished". O loop externo deveria terminar assim que cada processo atingisse o estado finalizado.

No papel, o fluxo era limpo. Encontrar um processo pronto. Executá-lo. Verificar a conclusão. Repetir até terminar. Eu até adicionei comandos print para observar o escalonador fazendo seu trabalho. Eu conseguia ver os processos sendo escolhidos. O loop continuava girando. No entanto, os processos pareciam entrar em um presente eterno, rodando para sempre, sem nunca avançar.

O Sintoma

Este é o pior tipo de falha: a silenciosa. Nenhum stack trace apareceu no terminal. Nenhum IndexError ou KeyError me deu uma pista para seguir. O interpretador estava perfeitamente feliz. O programa simplesmente não se comportava. Os processos iniciavam, mas nunca terminavam. Passei horas refazendo o fluxo.

A condição do loop estava errada? Talvez eu tivesse um erro de off-by-one no cálculo do burst time. A tabela de processos estava sendo sobreposta ou copiada em vez de atualizada no local? Minha condição de terminação estava verificando a chave errada? Adicionei mais prints. Auditei cada expressão booleana. Questionei tudo, exceto a única linha que realmente importava.

O Culpado

Então eu vi. Dentro de execute_tick, eu tinha escrito:

p["status"] == "running"

Dois sinais de igual. Uma comparação, não uma atribuição. A correção estava a apenas um caractere de distância:

p["status"] = "running"

Em Python, p["status"] == "running" é uma expressão perfeitamente válida. Ela avalia para True ou False, e então o interpretador descarta o resultado porque eu nunca o atribuí a nada. A linha não faz absolutamente nada de útil. A entrada no dicionário permanecia intacta, preservando qualquer status que tivesse antes, e o processo nunca avançava em seu ciclo de vida.

Eu mudei para um único sinal de igual. Executei o script novamente. A simulação ganhou vida. Os processos ciclaram por pronto, em execução e finalizado exatamente como planejado. Uma tecla extra me custou horas.

Por que esses bugs se escondem

A razão pela qual isso dói tanto é que o Python não sinaliza uma instrução de expressão como um erro, a menos que seja uma sintaxe explicitamente inválida. O bug era um erro de digitação semântico. O programa comparava o status, produzia um booleano e o descartava. Como a própria comparação poderia retornar False, o processo permanecia preso em seu estado anterior, e o loop externo não tinha motivo para interromper.

You compound this with confirmation bias. You know you typed an assignment because you intended an assignment. When you read the code for the fifth time, your brain autocorrects the symbol. This is why rubber ducking works. It forces you to articulate each line slowly enough that the gap between what is written and what you meant becomes visible.

Small bugs like this are harder to find than dramatic crashes. A segfault or a syntax error announces itself immediately. A silent no-op simply corrupts the state and lets the program limp forward. The failure is downstream, and your instinct is to debug the symptom rather than the cause.

A Better Defense

You cannot trust your eyes alone. After this episode, I changed a few habits that would have caught the mistake earlier.

First, if you are maintaining state in a dictionary, consider using a dataclass or an enum.Enum for process states. Define your statuses as constants or enum members:

from enum import Enum

class ProcessState(Enum):
    READY = "ready"
    RUNNING = "running"
    FINISHED = "finished"

With explicit types, tools like mypy can flag suspicious comparisons during static analysis. An accidental comparison where an assignment should live becomes much easier to spot when the types do not align with expectations.

Second, write unit tests for state transitions before you write the scheduler logic. A simple test that creates a process with one tick of work, runs the scheduler, and asserts the final state is FINISHED would have failed immediately. That failure would have narrowed the search to the state update logic rather than letting me wander through the entire loop