شروع سے ایک آپریٹنگ سسٹم بنانا ایسا لگتا ہے جیسے یہ C میں لکھنے والے کرنل ہیکرز کا کام ہو۔ لیکن آپ ایک دوپہر میں Python میں ایک سادہ سی سمولیشن تیار کر سکتے ہیں، اور آپ جلد ہی دریافت کر لیں گے کہ پروسیس مینجمنٹ (process management) کی منطق ایک ہائی لیول لینگویج میں بھی اتنی ہی سخت ہوتی ہے۔ میں نے یہ مشکل تجربے سے سیکھا۔ میں نے ایک چھوٹا سا OS سمولیٹر لکھنے کا فیصلہ کیا۔ مقصد سادہ تھا: چند پروسیسز بنانا، انہیں شیڈول کرنا، اور کام مکمل ہونے پر انہیں ختم (finished) قرار دینا۔ کوڈ مختصر تھا۔ منطق ناقابلِ تسخیر لگ رہی تھی۔ پھر میں نے اسے چلایا، اور کچھ بھی ختم نہیں ہوا۔
Python میں ایک منی OS کیوں بنائیں؟
ایک حقیقی آپریٹنگ سسٹم میموری پیجنگ (memory paging)، فائل سسٹم، ہارڈ ویئر انٹرپٹس اور ڈیوائس ڈرائیورز کو سنبھالتا ہے۔ ایک سمولیشن ان سب چیزوں کو ہٹا دیتی ہے اور آپ کو بنیادی تصور پر توجہ مرکوز کرنے دیتی ہے: اسٹیٹ (state)۔ آپ ایک پروسیس کی تعریف کرتے ہیں۔ اس کا ایک PID، برسٹ ٹائم (burst time) اور لائف سائیکل اسٹیٹس ہوتا ہے۔ Ready (تیار)۔ Running (چل رہا ہے)۔ Finished (مکمل)۔ ایک شیڈولر لوپ اگلے امیدوار کا انتخاب کرتا ہے، اس کی اسٹیٹ کو آگے بڑھاتا ہے، ایک ٹائم سلائس (time slice) کا سمولن کرتا ہے، اور اسے 'done' کی حالت میں منتقل کر دیتا ہے۔
Python اس طرح کے تجربے کے لیے ایک بہترین ذریعہ ہے کیونکہ یہ آپ کو پوائنٹر اریتھمیٹک (pointer arithmetic) اور میموری الائنمنٹ (memory alignment) کو نظر انداز کرنے کی اجازت دیتا ہے۔ ڈکشنریوں کی ایک لسٹ آپ کی پروسیس ٹیبل بن جاتی ہے۔ ایک while لوپ آپ کا کرنل شیڈولر بن جاتا ہے۔ آپ صرف اسٹینڈرڈ لائبریری کے ٹولز کے ذریعے راؤنڈ رابن شیڈولنگ (round-robin scheduling) یا پرائیورٹی کیوز (priority queues) کو نافذ کر سکتے ہیں۔ یہ بہت آسان محسوس ہوتا ہے، اور یہی وجہ ہے کہ اس کے بعد آنے والا بگ (bug) اتنا پریشان کن تھا۔
سیٹ اپ
میری سمولیشن میں process_table نامی ایک لسٹ استعمال کی گئی تھی۔ ہر انٹری اس طرح کی ڈکشنری تھی:
{
"pid": 1,
"burst_time": 3,
"status": "ready"
}
شیڈولر ایک سادہ while لوپ چلا رہا تھا۔ اس نے ٹیبل میں اس پہلے پروسیس کو تلاش کیا جس کا اسٹیٹس "finished" نہیں تھا۔ جب اسے کوئی پروسیس ملتا، تو وہ ایک ہیلپر فنکشن، execute_tick(p) کو کال کرتا تاکہ اس پروسیس کو ایک سمولیٹڈ سائیکل کے لیے چلایا جا سکے۔ execute_tick کے اندر، میں نے پروسیس کے اسٹیٹس کو "running" پر سیٹ کیا، برسٹ ٹائم کو کم کیا، اور چیک کیا کہ آیا باقی کام صفر ہو گیا ہے۔ اگر ایسا ہوتا، تو میں اسٹیٹس کو "finished" پر اپ ڈیٹ کر دیتا۔ بیرونی لوپ کو اس وقت ختم ہونا چاہیے تھا جب ہر پروسیس 'finished' کی حالت میں پہنچ جائے۔
کاغذ پر، یہ عمل بالکل درست تھا۔ ایک تیار پروسیس تلاش کریں۔ اسے چلائیں۔ تکمیل چیک کریں۔ مکمل ہونے تک دہرائیں۔ میں نے شیڈولر کے کام کو دیکھنے کے لیے پرنٹ اسٹیٹمنٹس (print statements) بھی شامل کیے تھے۔ میں دیکھ سکتا تھا کہ پروسیسز کا انتخاب کیا جا رہا ہے۔ لوپ چلتا رہا۔ پھر بھی ایسا لگتا تھا کہ پروسیسز ایک ابدی حال (eternal present tense) میں داخل ہو گئے ہیں، جو ہمیشہ چلتے رہتے ہیں، لیکن کبھی ختم نہیں ہوتے۔
علامت
یہ ناکامی کی بدترین قسم ہے: خاموش ناکامی۔ ٹرمینل میں کوئی اسٹیک ٹریس (stack trace) ظاہر نہیں ہوا۔ کسی IndexError یا KeyError نے مجھے کوئی سراغ نہیں دیا۔ انٹرپریٹر (interpreter) بالکل ٹھیک کام کر رہا تھا۔ پروگرام بس صحیح طرح سے کام نہیں کر رہا تھا۔ پروسیسز شروع تو ہوئے، لیکن وہ کبھی ختم نہیں ہوئے۔ میں نے گھنٹوں اس کے بہاؤ کا جائزہ لیا۔
کیا لوپ کی شرط غلط تھی؟ شاید برسٹ ٹائم کے حساب کتاب میں مجھ سے کوئی غلطی ہوئی ہو۔ کیا پروسیس ٹیبل اپ ڈیٹ ہونے کے بجائے کاپی ہو رہی تھی؟ کیا میری ٹرمینیشن کنڈیشن (termination condition) غلط کی (key) کو چیک کر رہی تھی؟ میں نے مزید پرنٹ اسٹیٹمنٹس شامل کیے۔ میں نے ہر بولین ایکسپریشن (boolean expression) کا معائنہ کیا۔ میں نے اس ایک لائن کے علاوہ ہر چیز پر شک کیا جو اصل میں اہم تھی۔
اصل وجہ
پھر مجھے وہ نظر آیا۔ execute_tick کے اندر، میں نے لکھا تھا:
p["status"] == "running"
دو برابر کے نشانات۔ ایک موازنہ (comparison)، نہ کہ اسائنمنٹ (assignment)۔ اس کا حل صرف ایک حرف کی دوری پر تھا:
p["status"] = "running"
Python میں، p["status"] == "running" ایک بالکل درست ایکسپریشن ہے۔ یہ True یا False پر مبنی نتیجہ دیتا ہے، اور پھر انٹرپریٹر اس نتیجے کو نظر انداز کر دیتا ہے کیونکہ میں نے اسے کبھی کسی چیز کے ساتھ اسائن نہیں کیا تھا۔ یہ لائن بالکل کوئی مفید کام نہیں کرتی ہے۔ ڈکشنری انٹری اپنی پرانی حالت میں ہی رہی، اور پروسیس کبھی بھی اپنی لائف سائیکل میں آگے نہیں بڑھ سکا۔
میں نے اسے ایک برابر کے نشان میں بدل دیا۔ میں نے اسکرپٹ کو دوبارہ چلایا۔ سمولیشن میں جان آگئی۔ پروسیسز بالکل اسی طرح 'ready'، 'running' اور 'finished' کے مراحل سے گزرے جیسے کہ منصوبہ تھا۔ ایک اضافی کی اسٹروک (keystroke) نے میرے کئی گھنٹے ضائع کر دیے۔
یہ بگ کیوں چھپے رہتے ہیں
اس کا اتنا تکلیف دہ ہونے کی وجہ یہ ہے کہ Python کسی ایکسپریشن اسٹیٹمنٹ کو غلطی (error) کے طور پر نشان زد نہیں کرتا جب تک کہ وہ مکمل طور پر غلط سنٹیکس (syntax) نہ ہو۔ یہ بگ ایک سیمنٹک ٹائپو (semantic typo) تھا۔ پروگرام نے اسٹیٹس کا موازنہ کیا، ایک بولین نتیجہ پیدا کیا، اور اسے پھینک دیا۔ چونکہ موازنہ خود 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
