शून्यापासून ऑपरेटिंग सिस्टम तयार करणे म्हणजे C मध्ये कोडिंग करणारे 'kernel hackers' यांचे काम वाटते. पण तुम्ही एका दुपारी Python मध्ये एक सोपे सिम्युलेशन तयार करू शकता, आणि तुम्हाला लवकरच समजेल की प्रोसेस मॅनेजमेंटचे लॉजिक हाय-लेव्हल भाषेतही तितकेच कठीण असते. मला हे अनुभवातून शिकायला मिळाले. मी एक छोटा OS सिम्युलेटर लिहिण्यासाठी बसलो. उद्दिष्ट साधे होते: काही प्रोसेसेस तयार करणे, त्यांचे शेड्युलिंग करणे आणि काम पूर्ण झाल्यावर त्यांना 'finished' म्हणून मार्क करणे. कोड छोटा होता. लॉजिक अगदी अचूक वाटत होते. पण जेव्हा मी तो रन केला, तेव्हा एकही प्रोसेस पूर्ण होऊन थांबत नव्हती.

Python मध्ये मिनी OS का बनवावा?

एक खरी ऑपरेटिंग सिस्टम मेमरी पेजिंग, फाईल सिस्टम, हार्डवेअर इंटरप्ट्स आणि डिव्हाइस ड्रायव्हर्स हाताळते. सिम्युलेशन या सर्व गोष्टी काढून टाकते आणि तुम्हाला मुख्य कल्पनेवर लक्ष केंद्रित करू देते: 'state' (स्थिती). तुम्ही एक प्रोसेस परिभाषित करता. तिला PID, burst time आणि lifecycle status असतो. Ready. Running. Finished. शेड्युलर लूप पुढचा उमेदवार निवडतो, त्याची स्थिती बदलतो, एक 'time slice' सिम्युलेट करतो आणि त्याला 'done' स्थितीत नेतो.

Python अशा प्रयोगांसाठी एक उत्तम माध्यम आहे कारण तुम्हाला पॉइंटर अरिथमेटिक (pointer arithmetic) आणि मेमरी अलाइनमेंट (memory alignment) बद्दल काळजी करण्याची गरज नसते. डिक्शनरीची एक लिस्ट तुमची 'process table' बनते. while लूप तुमचा 'kernel scheduler' बनतो. तुम्ही केवळ स्टँडर्ड लायब्ररी टूल्स वापरून राऊंड-रॉबिन शेड्युलिंग किंवा प्रायोरिटी क्यू (priority queues) लागू करू शकता. हे करणे सोपे वाटते, आणि म्हणूनच त्यानंतर आलेली चूक इतकी त्रासदायक ठरली.

सेटअप (The Setup)

माझ्या सिम्युलेशनमध्ये process_table नावाची लिस्ट वापरली होती. प्रत्येक एन्ट्री खालीलप्रमाणे डिक्शनरी स्वरूपात होती:

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

शेड्युलर एक साधा while लूप चालवत असे. तो टेबलमध्ये अशा पहिल्या प्रोसेसचा शोध घेत असे जिचा स्टेटस "finished" नव्हता. जेव्हा त्याला एखादी प्रोसेस सापडत असे, तेव्हा तो एक सि्युलेटेड सायकलसाठी ती प्रोसेस चालवण्यासाठी execute_tick(p) नावाचे हेल्पर फंक्शन कॉल करत असे. execute_tick मध्ये, मी प्रोसेसचा स्टेटस "running" केला, burst time कमी केला आणि उरलेले काम शून्य झाले आहे का ते तपासले. जर ते शून्य झाले असेल, तर मी स्टेटस "finished" मध्ये अपडेट केले. सर्व प्रोसेसेस 'finished' स्थितीत पोहोचल्यावर बाहेरील लूप थांबायला हवा होता.

कागदावर ही प्रक्रिया अगदी स्पष्ट होती. तयार प्रोसेस शोधा. ती चालवा. काम पूर्ण झाले का ते तपासा. जोपर्यंत सर्व कामे पूर्ण होत नाहीत तोपर्यंत हे पुन्हा पुन्हा करा. शेड्युलरचे काम पाहण्यासाठी मी 'print' स्टेटमेंट्स देखील जोडल्या होत्या. मी प्रोसेसेस निवडल्या जात असल्याचे पाहू शकत होतो. लूप सतत चालू होता. तरीही प्रोसेसेस एका अनंत वर्तमानकाळात अडकल्यासारख्या वाटत होत्या, त्या सतत चालू होत्या पण कधीच पूर्ण होत नव्हत्या.

लक्षण (The Symptom)

हा अपयशाचा सर्वात वाईट प्रकार आहे: शांत अपयश. टर्मिनलवर कोणताही 'stack trace' आला नाही. IndexError किंवा KeyError मुळे मला कोणताही सुगावा लागला नाही. इंटरप्रिटर अगदी व्यवस्थित काम करत होता, पण प्रोग्राम अपेक्षितप्रमाणे वागत नव्हता. प्रोसेसेस सुरू झाल्या, पण त्या कधीच पूर्ण झाल्या नाहीत. मी तासनतास प्रवाहाचा मागोवा घेत राहिलो.

लूपची कंडिशन चुकीची होती का? कदाचित burst time च्या गणनेत काही चूक झाली असावी. प्रोसेस टेबल अपडेट होण्याऐवजी कॉपी किंवा शॅडो (shadow) होत होते का? माझी टर्मिनेशन कंडिशन चुकीची की (key) तपासत होती का? मी अधिक 'print' स्टेटमेंट्स जोडल्या. मी प्रत्येक बुलियन एक्स्प्रेशनची (boolean expression) तपासणी केली. मी महत्त्वाच्या एका ओळीव्यतिरिक्त सर्व गोष्टींवर शंका घेतली.

मुख्य कारण (The Culprit)

मग मला ते दिसले. execute_tick मध्ये मी असे लिहिले होते:

p["status"] == "running"

दोन समान चिन्हे. हे तुलना (comparison) करत होते, असाइनमेंट (assignment) नाही. सुधारणा फक्त एका कॅरेक्टरच्या अंतरावर होती:

p["status"] = "running"

Python मध्ये, p["status"] == "running" हे एक पूर्णपणे वैध एक्स्प्रेशन आहे. ते True किंवा False मूल्य देते, आणि त्यानंतर इंटरप्रिटर त्याचे निकाल टाकून देतो कारण मी ते कशासाठीही असाइन केले नव्हते. ती ओळ काहीही उपयुक्त काम करत नाही. डिक्शनरी एन्ट्री तशीच राहिली, आणि प्रोसेस तिच्या लाइफसायकलमध्ये पुढे जाऊ शकली नाही.

मी ते बदलून एकच समान चिन्ह केले. मी पुन्हा स्क्रिप्ट रन केली. सिम्युलेशन व्यवस्थित सुरू झाले. प्रोसेसेस अगदी नियोजनाप्रमाणे 'ready', 'running' आणि 'finished' या चक्रातून गेल्या. एका अतिरिक्त कीस्ट्रोकमुळे माझे तास वाया गेले होते.

हे बग्स का लपून राहतात?

हे इतके त्रासदायक वाटण्याचे कारण म्हणजे, जोपर्यंत एखादे एक्स्प्रेशन स्टेटमेंट पूर्णपणे चुकीचे सिंटॅक्स (syntax) नसते, तोपर्यंत Python त्याला एरर म्हणून दाखवत नाही. हा बग एक 'semantic typo' होता. प्रोग्रामने स्टेटसची तुलना केली, एक बुलियन मूल्य तयार केले आणि ते फेकून दिले. तुलना स्वतः False देऊ शकत असल्यामुळे, प्रोसेस तिच्या आधीच्या स्थितीतच अडकून राहिली आणि बाहेरील लूपला थांबण्याचे कोणतेही कारण उरले नाही.

तुम्ही याला पुष्टीकरण पूर्वग्रहाने (confirmation bias) अधिक गडद करता. तुम्ही असाइनमेंट टाइप केली आहे हे तुम्हाला माहित असते कारण तुमचा हेतू असाइनमेंट करण्याचा असतो. जेव्हा तुम्ही पाचव्यांदा कोड वाचता, तेव्हा तुमचे मेंदू त्या चिन्हाचे आपोआप दुरुस्ती (autocorrect) करतो. म्हणूनच 'रबर डकिंग' (rubber ducking) प्रभावी ठरते. हे तुम्हाला प्रत्येक ओळ इतक्या संथपणे स्पष्टपणे मांडण्यास भाग पाडते की, जे लिहिले आहे आणि तुमचा जो अर्थ होता, यातील तफावत स्पष्टपणे दिसून येते.

अशा प्रकारचे लहान बग्स शोधणे हे मोठ्या क्रॅशपेक्षा कठीण असते. 'segfault' किंवा 'syntax error' लगेच स्वतःची उपस्थिती दर्शवतात. मात्र, एक शांत 'no-op' फक्त स्टेट (state) खराब करतो आणि प्रोग्रामला अडखळत पुढे जाऊ देतो. बिघाड नंतरच्या टप्प्यात (downstream) दिसून येतो, आणि तुमचा स्वभाव कारणापेक्षा लक्षणांवरून डीबग करण्याचा असतो.

एक उत्तम बचाव

तुम्ही केवळ तुमच्या डोळ्यांवर विश्वास ठेवू शकत नाही. या घटनेनंतर, मी काही सवयी बदलल्या ज्यांनी ही चूक आधीच पकडली असती.

पहिले म्हणजे, जर तुम्ही डिक्शनरीमध्ये (dictionary) स्टेट मेंटेन करत असाल, तर प्रोसेस स्टेट्ससाठी dataclass किंवा enum.Enum वापरण्याचा विचार करा. तुमचे स्टेटस कॉन्स्टंट्स (constants) किंवा 'enum members' म्हणून परिभाषित करा:

from enum import Enum

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

स्पष्ट प्रकारांमुळे (explicit types), mypy सारखी साधने 'static analysis' दरम्यान संशयास्पद तुलनांकडे लक्ष वेधून घेऊ शकतात. जिथे असाइनमेंट असायला हवी होती तिथे चुकून झालेली तुलना शोधणे खूप सोपे होते, जेव्हा प्रकार (types) अपेक्षेप्रमाणे नसतात.

दुसरे म्हणजे, शेड्युलर लॉजिक (scheduler logic) लिहिण्यापूर्वी स्टेट ट्रान्झिशनसाठी (state transitions) युनिट टेस्ट्स लिहा. एक साधी टेस्ट जी एका 'tick' च्या कामासह प्रोसेस तयार करते, शेड्युलर चालवते आणि अंतिम स्टेट FINISHED आहे याची खात्री करते, ती लगेच फेल झाली असती. त्या अपयशामुळे मला संपूर्ण लूपमध्ये भटकण्याऐवजी केवळ 'state update logic' वर लक्ष केंद्रित करणे सोपे झाले असते.