Створення операційної системи з нуля звучить як робота для хакерів ядра, що пишуть на C. Але ви можете розгорнути спрощену симуляцію на Python за один вечір і швидко зрозумієте, що логіка керування процесами є такою ж безжальною і у високорівневих мовах. Я засвоїв це на власному гіркому досвіді. Я взявся писати крихітний симулятор ОС. Мета була скромною: створити кілька процесів, запланувати їх виконання та позначити як завершені, коли робота буде виконана. Код був коротким. Логіка здавалася бездоганною. Потім я запустив його, і ніщо не завершувалося.

Навіщо створювати міні-ОС на Python?

Справжня операційна система маневрує між сторінковим керуванням пам'яттю, файловими системами, апаратними перериваннями та драйверами пристроїв. Симуляція відкидає все це і дозволяє зосередитися на основній ідеї: стані. Ви визначаєте процес. Він має PID, час виконання (burst time) та статус життєвого циклу: Ready, Running, Finished. Цикл планувальника обирає наступного кандидата, змінює його стан, симулює квант часу та переводить його у стан завершення.

Python — чудовий інструмент для такого експерименту, оскільки він дозволяє ігнорувати арифметику покажчиків та вирівнювання пам'яті. Список словників стає вашою таблицею процесів. Цикл while стає вашим планувальником ядра. Ви можете реалізувати чергу round-robin або черги з пріоритетами, використовуючи лише інструменти стандартної бібліотеки. Це здається простим, і саме тому наступна помилка була такою дратувальною.

Налаштування

У моїй симуляції використовувався список під назвою process_table. Кожен запис був словником такого вигляду:

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

Планувальник працював у простому циклі while. Він сканував таблицю в пошуках першого процесу, статус якого не був "finished". Коли він знаходив такий процес, він викликав допоміжну функцію execute_tick(p), щоб запустити цей процес на один симульований цикл. Всередині execute_tick я встановлював статус процесу на "running", зменшував час виконання і перевіряв, чи не досягла кількість роботи нуля. Якщо так, я оновлював статус на "finished". Зовнішній цикл мав завершитися, щойно кожен процес досягне стану "finished".

На папері алгоритм був чітким. Знайти готовий процес. Запустити його. Перевірити завершення. Повторювати, доки не буде зроблено. Я навіть додав оператори print, щоб спостерігати за роботою планувальника. Я бачив, як обираються процеси. Цикл продовжував працювати. Проте процеси, здавалося, увійшли в стан вічного теперішнього часу: вони працювали вічно, ніколи не завершуючись.

Симптом

Це найгірший вид помилки: тиха помилка. Жоден стек викликів (stack trace) не з'явився в терміналі. Жодна помилка IndexError чи KeyError не залишила підказки, за якою можна було б простежити шлях. Інтерпретатор почувався чудово. Програма просто поводилася не так, як слід. Процеси запускалися, але ніколи не завершувалися. Я витратив години, намагаючись відстежити логіку.

Чи була умова циклу неправильною? Можливо, я припустився помилки на одиницю (off-by-one error) при розрахунку часу виконання? Чи не копіювалася таблиця процесів замість оновлення на місці? Чи не перевіряла моя умова завершення неправильний ключ? Я додавав більше print. Я перевіряв кожен логічний вираз. Я піддавав сумніву все, крім одного рядка, який насправді мав значення.

Винуватець

Потім я це побачив. Всередині execute_tick я написав:

p["status"] == "running"

Два знаки рівності. Порівняння замість присвоєння. Виправлення було на відстані одного символу:

p["status"] = "running"

У Python p["status"] == "running" — це цілком коректний вираз. Він повертає True або False, а потім інтерпретатор відкидає результат, оскільки я нічого йому не присвоїв. Цей рядок не робить абсолютно нічого корисного. Запис у словнику залишався незмінним, зберігаючи той статус, який він мав раніше, і процес ніколи не проходив через свій життєвий цикл.

Я замінив його на один знак рівності. Я знову запустив скрипт. Симуляція ожила. Процеси проходили через стани ready, running та finished саме так, як і планувалося. Один зайвий символ коштував мені годин.

Чому такі помилки ховаються

Причина такої прикрості полягає в тому, що Python не позначає вираз як помилку, якщо він не є грубою синтаксичною помилкою. Ця помилка була семантичною друкарською помилкою. Програма порівнювала статус, отримувала логічне значення і викидала його. Оскільки саме порівняння могло повернути False, процес залишався в попередньому стані, і зовнішньому циклу не було причин перериватися.

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