בניית מערכת הפעלה מאפס נשמעת כמו עבודה עבור האקרים של קרנל (kernel hackers) שכותבים ב-C. אבל אפשר להריץ סימולציה מפושטת ב-Python בצהריים אחד, ותגלו במהירות שללוגיקה של ניהול תהליכים אין פחות חוסר סובלנות גם בשפה ברמה גבוהה (high-level language). למדתי זאת בדרך הקשה. התיישבתי לכתוב סימולטור מערכת הפעלה זעיר. המטרה הייתה צנועה: ליצור כמה תהליכים, לתזמן אותם, ולסמן אותם כסיימו כאשר עבודתם הושלמה. הקוד היה קצר. הלוגיקה הרגישה חסינה לחלוטין. ואז הרצתי אותו, ושום דבר לא "מת".
למה לבנות מיני מערכת הפעלה ב-Python?
מערכת הפעלה אמיתית מתמרנת דפי זיכרון (memory paging), מערכות קבצים, הפסקות חומרה (hardware interrupts) ומנהלי התקנים (device drivers). סימולציה מסירה את כל זה ומאפשרת לך להתמקד ברעיון הליבה: מצב (state). אתה מגדיר תהליך. יש לו PID, זמן ריצה (burst time) וסטטוס מחזור חיים. מוכן (Ready). רץ (Running). הסתיים (Finished). לולאת מתזמן (scheduler loop) בוחרת את המועמד הבא, מקדמת את המצב שלו, מדמה מנת זמן (time slice), ומעבירה אותו למצב "הסתיים".
Python היא כלי מצוין לניסוי מסוג זה מכיוון שהיא מאפשרת לך להתעלם מאריתמטיקת מצביעים (pointer arithmetic) ויישור זיכרון (memory alignment). רשימה של מילונים הופכת לטבלת התהליכים שלך. לולאת while הופכת למתזמן הקרנל שלך. ניתן לממש תזמון round-robin או תורים בעדיפות (priority queues) באמצעות כלי הספרייה הסטנדרטית בלבד. זה מרגיש נגיש, וזו בדיוק הסיבה שהבאג שבא לאחר מכן היה כל כך מתסכל.
ההגדרה
הסימולציה שלי השתמשה ברשימה בשם process_table. כל רשומה הייתה מילון במבנה הבא:
{
"pid": 1,
"burst_time": 3,
"status": "ready"
}
המתזמן הריץ לולאת while פשוטה. הוא סרק את הטבלה עבור התהליך הראשון שהסטטוס שלו לא היה "finished". כשמצא אחד כזה, הוא קרא לפונקציית עזר, execute_tick(p), כדי להריץ את התהליך למחזור סימולציה אחד. בתוך execute_tick, הגדרתי את סטטוס התהליך כ-"running", הפחתתי את זמן הריצה (burst time), ובדקתי אם העבודה הנותרת הגיעה לאפס. אם כן, עדכנתי את הסטטוס ל-"finished". הלולאה החיצונית הייתה אמורה להסתיים ברגע שכל תהליך הגיע למצב "finished".
על הנייר, הזרימה הייתה נקייה. מצא תהליך מוכן. הרץ אותו. בדוק אם הושלם. חזור על הפעולה עד לסיום. אפילו הוספתי פקודות print כדי לצפות במתזמן עושה את עבודתו. יכולתי לראות את התהליכים נבחרים. הלולאה המשיכה לפעול ללא הפסקה. ובכל זאת, נראה היה שהתהליכים נכנסים לזמן הווה נצחי, רצים לנצח, ומעולם לא ממשיכים הלאה.
התסמין
זהו סוג הכי גרוע של כישלון: הכשל השקט. שום stack trace לא נשפך לטרמינל. שום IndexError או KeyError לא נתנו לי רמז לעקוב אחריו. האינטרפרטר היה מרוצה לחלוטין. התוכנית פשוט לא התנהגה כצפוי. תהליכים התחילו, אך הם מעולם לא הסתיימו. ביליתי שעות בניסיון לשחזר את זרימת התוכנית.
האם תנאי הלולאה היה שגוי? אולי הייתה לי שגיאת off-by-one בחישוב זמן הריצה. האם טבלת התהליכים עברה shadowing או הועתקה במקום להתעדכן במקומה? האם תנאי הסיום שלי בדק את המפתח הלא נכון? הוספתי עוד פקודות print. ביצעתי ביקורת (audit) על כל ביטוי בוליאני. הטיתי ספק בכל דבר, חוץ מהשורה האחת שבאמת הייתה משנה.
האשם
ואז ראיתי את זה. בתוך execute_tick, כתבתי:
p["status"] == "running"
שני סימני שווה. השוואה, לא השמה. התיקון היה במרחק של תו אחד בלבד:
p["status"] = "running"
ב-Python, הביטוי p["status"] == "running" הוא ביטוי תקף לחלוטין. הוא מתקבל כ-True או False, ואז האינטרפרטר משליך את התוצאה מכיוון שמעולם לא השבתי אותה לשום דבר. השורה הזו לא עושה דבר מועיל. הרשומה במילון נותרה ללא שינוי, תוך שמירה על הסטטוס שהיה לה קודם לכן, והתהליך מעולם לא התקדם לאורך מחזור החיים שלו.
שיניתי זאת לסימן שווה בודד. הרצתי את הסקריפט שוב. הסימולציה התחילה "לנשום". התהליכים עברו במעגל בין ready, running, ו-finished בדיוק כפי שתוכנן. לחיצה אחת מיותרת על המקלדת עלתה לי שעות.
למה הבאגים האלה מתחבאים
הסיבה שזה כל כך כואב היא ש-Python לא מסמנת ביטוי (expression statement) כשגיאה אלא אם כן התחביר (syntax) אינו תקני לחלוטין. הבאג היה טעות הקלדה סמנטית. התוכנית השוותה את הסטטוס, הפיקה ערך בוליאני, והשליכה אותו. מכיוון שההשוואה עצמה יכלה להחזיר 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
