പൂജ്യത്തിൽ നിന്ന് ഒരു ഓപ്പറേറ്റിംഗ് സിസ്റ്റം നിർമ്മിക്കുക എന്നത് C ഉപയോഗിച്ച് എഴുതുന്ന കേർണൽ ഹാക്കർമാരുടെ ജോലിയാണെന്ന് തോന്നാം. എന്നാൽ ഒരു ഉച്ചതിരിഞ്ഞ് പൈത്തണിൽ (Python) ലളിതമായ ഒരു സിമുലേഷൻ നിങ്ങൾക്ക് തയ്യാറാക്കാം, പ്രോസസ് മാനേജ്‌മെന്റിന്റെ ലോജിക് ഒരു ഹൈ-ലെവൽ ഭാഷയിലും അത്രത്തോളം കഠിനമാണെന്ന് നിങ്ങൾ വേഗത്തിൽ തിരിച്ചറിയും. ഞാൻ ഇത് വളരെ പ്രയാസപ്പെട്ടാണ് പഠിച്ചത്. ഒരു ചെറിയ ഓഎസ് സിമുലേറ്റർ എഴുതാൻ ഞാൻ ഇരുന്നു. ലക്ഷ്യം ലളിതമായിരുന്നു: കുറച്ച് പ്രോസസ്സുകൾ സൃഷ്ടിക്കുക, അവ ഷെഡ്യൂൾ ചെയ്യുക, അവയുടെ ജോലി കഴിഞ്ഞാൽ അവ പൂർത്തിയായതായി അടയാളപ്പെടുത്തുക. കോഡ് ചെറുതായിരുന്നു. ലോജിക് കൃത്യമാണെന്ന് എനിക്ക് തോന്നി. എന്നാൽ ഞാൻ അത് റൺ ചെയ്തപ്പോൾ, ഒന്നും അവസാനിച്ചില്ല.

എന്തുകൊണ്ട് പൈത്തണിൽ ഒരു മിനി ഓഎസ് നിർമ്മിക്കണം?

ഒരു യഥാർത്ഥ ഓപ്പറേറ്റിംഗ് സിസ്റ്റം മെമ്മറി പേജിംഗ്, ഫയൽ സിസ്റ്റങ്ങൾ, ഹാർഡ്‌വെയർ ഇന്ററപ്റ്റുകൾ, ഡിവൈസ് ഡ്രൈവർമാർ എന്നിവ ഒരേസമയം കൈകാര്യം ചെയ്യുന്നു. ഒരു സിമുലേഷൻ ഇവയെല്ലാം ഒഴിവാക്കി പ്രധാന ആശയത്തിൽ ശ്രദ്ധ കേന്ദ്രീകരിക്കാൻ നിങ്ങളെ അനുവദിക്കുന്നു: അതായത് 'സ്റ്റേറ്റ്' (state). നിങ്ങൾ ഒരു പ്രോസസ് നിർവചിക്കുന്നു. അതിന് ഒരു PID, ഒരു ബർസ്റ്റ് ടൈം (burst time), ഒരു ലൈഫ്സൈക്കിൾ സ്റ്റാറ്റസ് എന്നിവ ഉണ്ടായിരിക്കും. Ready. Running. Finished. ഒരു ഷെഡ്യൂളർ ലൂപ്പ് അടുത്ത പ്രോസസ് തിരഞ്ഞെടുക്കുന്നു, അതിന്റെ സ്റ്റേറ്റ് മാറ്റുന്നു, ഒരു ടൈം സ്ലൈസ് സിമുലേറ്റ് ചെയ്യുന്നു, തുടർന്ന് അത് 'done' എന്ന അവസ്ഥയിലേക്ക് മാറ്റുന്നു.

പൈന്റർ അരിത്മെറ്റിക്കും (pointer arithmetic) മെമ്മറി അലൈൻമെന്റും ഒഴിവാക്കാൻ കഴിയുന്നതുകൊണ്ട് ഇത്തരം പരീക്ഷണങ്ങൾക്ക് പൈത്തൺ മികച്ചൊരു മാധ്യമമാണ്. ഒരു ലിസ്റ്റിലെ ഡിക്ഷണറികൾ നിങ്ങളുടെ പ്രോസസ് ടേബിളായി മാറുന്നു. ഒരു while ലൂപ്പ് നിങ്ങളുടെ കേർണൽ ഷെഡ്യൂളറായി മാറുന്നു. സ്റ്റാൻഡേർഡ് ലൈബ്രറി ടൂളുകൾ ഉപയോഗിച്ച് നിങ്ങൾക്ക് റൗണ്ട്-റോബിൻ ഷെഡ്യൂളിംഗോ പ്രയോറിറ്റി ക്യൂകളോ നടപ്പിലാക്കാം. ഇത് വളരെ എളുപ്പമാണെന്ന് തോന്നും, അതുകൊണ്ടാണ് അതിനുശേഷം ഉണ്ടായ ബഗ് എന്നെ ഇത്രയധികം പ്രകോപിപ്പിച്ചത്.

സെറ്റപ്പ് (The Setup)

എന്റെ സിമുലേഷനിൽ process_table എന്ന പേരുള്ള ഒരു ലിസ്റ്റ് ഉപയോഗിച്ചു. ഓരോ എൻട്രിയും ഇപ്രകാരമുള്ള ഒരു ഡിക്ഷണറി ആയിരുന്നു:

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

ഷെഡ്യൂളർ ഒരു ലളിതമായ while ലൂപ്പ് ആണ് പ്രവർത്തിപ്പിച്ചത്. സ്റ്റാറ്റസ് "finished" അല്ലാത്ത ആദ്യത്തെ പ്രോസസ്സിനായി അത് ടേബിൾ പരിശോധിച്ചു. അത് കണ്ടെത്തുമ്പോൾ, ആ പ്രോസസിനെ ഒരു സിമുലേറ്റഡ് സൈക്കിളിനായി പ്രവർത്തിപ്പിക്കാൻ execute_tick(p) എന്ന ഹെൽപ്പർ ഫംഗ്ഷൻ അത് വിളിച്ചു. execute_tick-നുള്ളിൽ, ഞാൻ പ്രോസസ് സ്റ്റാറ്റസ് "running" എന്ന് സെറ്റ് ചെയ്യുകയും, ബർസ്റ്റ് ടൈം കുറയ്ക്കുകയും, ബാക്കിയുള്ള ജോലി പൂജ്യമായോ എന്ന് പരിശോധിക്കുകയും ചെയ്തു. അങ്ങനെയാണെങ്കിൽ, ഞാൻ സ്റ്റാറ്റസ് "finished" എന്ന് അപ്ഡേറ്റ് ചെയ്തു. എല്ലാ പ്രോസസ്സുകളും 'finished' അവസ്ഥയിൽ എത്തിക്കഴിഞ്ഞാൽ ഔട്ടർ ലൂപ്പ് അവസാനിക്കേണ്ടതായിരുന്നു.

കാണാൻ വളരെ ലളിതമായിരുന്നു ഈ പ്രക്രിയ. ഒരു റെഡി പ്രോസസ് കണ്ടെത്തുക. അത് പ്രവർത്തിപ്പിക്കുക. പൂർത്തിയായോ എന്ന് പരിശോധിക്കുക. ഇത് ആവർത്തിക്കുക. ഷെഡ്യൂളർ പ്രവർത്തിക്കുന്നത് നിരീക്ഷിക്കാൻ ഞാൻ പ്രിന്റ് സ്റ്റേറ്റ്‌മെന്റുകളും ചേർത്തിരുന്നു. പ്രോസസ്സുകൾ തിരഞ്ഞെടുക്കപ്പെടുന്നത് എനിക്ക് കാണാമായിരുന്നു. ലൂപ്പ് പ്രവർത്തിച്ചുകൊണ്ടേയിരുന്നു. എന്നിട്ടും പ്രോസസ്സുകൾ ഒരിക്കലും അവസാനിക്കാത്ത, എന്നെന്നേക്കുമായി പ്രവർത്തിച്ചുകൊണ്ടിരിക്കുന്ന ഒരു അവസ്ഥയിൽ അകപ്പെട്ടതുപോലെ തോന്നി.

ലക്ഷണങ്ങൾ (The Symptom)

ഇത് ഏറ്റവും മോശമായ തരം പരാജയമാണ്: നിശബ്ദമായ പരാജയം. ടെർമിനലിൽ ഒരു സ്റ്റാക്ക് ട്രാസും (stack trace) തെളിഞ്ഞില്ല. ഒരു IndexError-ഓ KeyError-ഓ എനിക്ക് വഴി കാണിച്ചില്ല. ഇന്റർപ്രെറ്റർ തികച്ചും കൃത്യമായിരുന്നു. പ്രോഗ്രാം പ്രവർത്തിച്ചില്ല എന്ന് മാത്രം. പ്രോസസ്സുകൾ തുടങ്ങി, പക്ഷേ അവ ഒരിക്കലും അവസാനിച്ചില്ല. ആ പ്രക്രിയയുടെ ഓരോ ഘട്ടവും പരിശോധിക്കാൻ ഞാൻ മണിക്കൂറുകൾ ചിലവഴിച്ചു.

ലൂപ്പ് കണ്ടീഷൻ തെറ്റായിരുന്നോ? ബർസ്റ്റ് ടൈം കണക്കാക്കുന്നതിൽ എനിക്ക് എന്തെങ്കിലും പിശക് പറ്റിയോ? പ്രോസസ് ടേബിൾ അപ്ഡേറ്റ് ചെയ്യുന്നതിന് പകരം കോപ്പി ചെയ്യപ്പെടുകയാണോ ചെയ്തത്? എന്റെ ടെർമിനേഷൻ കണ്ടീഷൻ തെറ്റായ കീ (key) ആണോ പരിശോധിക്കുന്നത്? ഞാൻ കൂടുതൽ പ്രിന്റുകൾ ചേർത്തു. ഓരോ ബൂളിയൻ എക്സ്പ്രഷനും ഞാൻ പരിശോധിച്ചു. യഥാർത്ഥത്തിൽ പ്രശ്നമായ ആ ഒരു വരി ഒഴികെ മറ്റെല്ലാ കാര്യങ്ങളെക്കുറിച്ചും ഞാൻ സംശയിച്ചു.

കുറ്റവാളി (The Culprit)

അപ്പോഴാണ് ഞാൻ അത് കണ്ടത്. execute_tick-നുള്ളിൽ ഞാൻ ഇങ്ങനെ എഴുതിയിരുന്നു:

p["status"] == "running"

രണ്ട് സമചിഹ്നങ്ങൾ. അതൊരു താരതമ്യമായിരുന്നു (comparison), അസൈൻമെന്റല്ല (assignment). പരിഹാരം ഒരു അക്ഷരം മാത്രം അകലെയായിരുന്നു:

p["status"] = "running"

പൈത്തണിൽ, p["status"] == "running" എന്നത് തികച്ചും സാധുവായ ഒരു എക്സ്പ്രഷനാണ്. ഇത് True അല്ലെങ്കിൽ False എന്ന് വിലയിരുത്തുന്നു, എന്നാൽ ഞാൻ അത് മറ്റൊന്നിനും അസൈൻ ചെയ്യാത്തതിനാൽ ഇന്റർപ്രെറ്റർ ആ ഫലം ഉപേക്ഷിക്കുന്നു. ആ വരി കൊണ്ട് യാതൊരു പ്രയോജനവുമില്ല. ഡിക്ഷണറി എൻട്രി മാറ്റമില്ലാതെ തുടരുകയും, പ്രോസസ് അതിന്റെ ലൈഫ്സൈക്കിളിലൂടെ മുന്നോട്ട് പോകാതിരിക്കുകയും ചെയ്തു.

ഞാൻ അത് ഒരു സമചിഹ്നമാക്കി മാറ്റി. സ്ക്രിപ്റ്റ് വീണ്ടും റൺ ചെയ്തു. സിമുലേഷൻ കൃത്യമായി പ്രവർത്തിച്ചു. പ്രോസസ്സുകൾ പ്ലാൻ ചെയ്തതുപോലെ തന്നെ ready, running, finished എന്നീ ഘട്ടങ്ങളിലൂടെ കടന്നുപോയി. ഒരു ചെറിയ ടൈപ്പിംഗ് പിശക് എനിക്ക് മണിക്കൂറുകൾ നഷ്ടപ്പെടുത്തി.

എന്തുകൊണ്ടാണ് ഈ ബഗുകൾ ഒളിച്ചിരിക്കുന്നത്?

ഇത് ഇത്രയധികം ബുദ്ധിമുട്ടാകാൻ കാരണം, സിന്റാക്സ് (syntax) പൂർണ്ണമായും തെറ്റല്ലെങ്കിൽ പൈത്തൺ ഒരു എക്സ്പ്രഷൻ സ്റ്റേറ്റ്‌മെന്റിനെ (expression statement) പിശകായി കാണിക്കില്ല എന്നതാണ്. ആ ബഗ് ഒരു സെമാന്റിക് ടൈപ്പോ (semantic typo) ആയിരുന്നു. പ്രോഗ്രാം സ്റ്റാറ്റസ് താരതമ്യം ചെയ്തു, ഒരു ബൂളിയൻ ഫലം നൽകി, അത് ഉപേക്ഷിച്ചു. ആ താരതമ്യം False നൽകാൻ സാധ്യതയുള്ളതുകൊണ്ട്, പ്രോസസ് അതിന്റെ പഴയ അവസ്ഥയിൽ തന്നെ തുടർന്നു, ഔട്ടർ ലൂപ്പിന് അത് അവസാനിപ്പിക്കാൻ കാരണവും ഉണ്ടായില്ല.

നിങ്ങൾ ഇതിനോടൊപ്പം 'confirmation bias' കൂടി കൂട്ടിച്ചേർക്കുന്നു. ഒരു അസൈൻമെന്റ് നൽകാൻ നിങ്ങൾ ഉദ്ദേശിച്ചതുകൊണ്ട് തന്നെ നിങ്ങൾ ഒരു അസൈൻമെന്റ് ടൈപ്പ് ചെയ്തു എന്ന് നിങ്ങൾക്ക് അറിയാം. അഞ്ചാം തവണ കോഡ് വായിക്കുമ്പോൾ, നിങ്ങളുടെ തലച്ചോറ് ആ ചിഹ്നത്തെ സ്വയം തിരുത്തി വായിക്കുന്നു. ഇതാണ് 'rubber ducking' ഫലപ്രദമാകാൻ കാരണം. എഴുതിയതിനും നിങ്ങൾ ഉദ്ദേശിച്ചതിനും ഇടയിലുള്ള വ്യത്യാസം കാണത്തക്കവിധം ഓരോ വരിയും സാവധാനം വിശദീകരിക്കാൻ ഇത് നിങ്ങളെ നിർബന്ധിക്കുന്നു.

വലിയ ക്രാഷുകളെക്കാൾ കണ്ടെത്താൻ പ്രയാസമാണ് ഇത്തരത്തിലുള്ള ചെറിയ ബഗ്ഗുകൾ. ഒരു segfault അല്ലെങ്കിൽ syntax error ഉടൻ തന്നെ ശ്രദ്ധയിൽപ്പെടും. എന്നാൽ ഒരു 'silent no-op' സ്റ്റേറ്റിനെ (state) തെറ്റിക്കുകയും പ്രോഗ്രാം തടസ്സമില്ലാതെ മുന്നോട്ട് നീങ്ങാൻ അനുവദിക്കുകയും ചെയ്യുന്നു. പരാജയം പിന്നീട് (downstream) ആണ് സംഭവിക്കുന്നത്, അതിനാൽ യഥാർത്ഥ കാരണത്തേക്കാൾ ഉപരിയായി അതിന്റെ ലക്ഷണങ്ങളെ (symptoms) പരിഹരിക്കാനാണ് നിങ്ങളുടെ പ്രവണത.

മികച്ചൊരു പ്രതിരോധം

നിങ്ങളുടെ കണ്ണുകളെ മാത്രം വിശ്വസിക്കാനാവില്ല. ഈ സംഭവത്തിന് ശേഷം, തെറ്റുകൾ നേരത്തെ കണ്ടെത്താൻ സഹായിക്കുന്ന ചില ശീലങ്ങൾ ഞാൻ മാറ്റിപ്പണിതിട്ടുണ്ട്.

ഒന്നാമതായി, നിങ്ങൾ ഒരു dictionary ഉപയോഗിച്ചാണ് state നിലനിർത്തുന്നതെങ്കിൽ, process states-നായി ഒരു dataclass അല്ലെങ്കിൽ enum.Enum ഉപയോഗിക്കുന്നത് പരിഗണിക്കുക. നിങ്ങളുടെ സ്റ്റാറ്റസുകൾ constants ആയോ enum members ആയോ നിർവചിക്കുക:

from enum import Enum

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

explicit types ഉപയോഗിക്കുന്നതിലൂടെ, mypy പോലുള്ള ടൂളുകൾക്ക് static analysis സമയത്ത് സംശയാസ്പദമായ താരതമ്യങ്ങൾ (comparisons) ചൂണ്ടിക്കാണിക്കാൻ കഴിയും. ഒരു അസൈൻമെന്റ് വരേണ്ട സ്ഥാനത്ത് അബദ്ധവശാൽ ഒരു താരതമ്യം നടക്കുകയാണെങ്കിൽ, ടൈപ്പുകൾ പ്രതീക്ഷിച്ചതുപോലെ ഇല്ലാതിരിക്കുമ്പോൾ അത് കണ്ടെത്താൻ വളരെ എളുപ്പമായിരിക്കും.

രണ്ടാമതായി, scheduler logic എഴുതുന്നതിന് മുമ്പ് state transitions-നായി unit tests എഴുതുക. ഒരു പ്രോസസ്സ് സൃഷ്ടിച്ച്, അത് ഒരു 'tick' പ്രവർത്തിപ്പിക്കുകയും, ഷെഡ്യൂളർ റൺ ചെയ്യിച്ച്, അവസാന സ്റ്റേറ്റ് FINISHED ആണെന്ന് ഉറപ്പുവരുത്തുന്ന (assert) ഒരു ലളിതമായ ടെസ്റ്റ് ഉടൻ തന്നെ പരാജയപ്പെടുമായിരുന്നു. ആ പരാജയം, മുഴുവൻ ലൂപ്പിലൂടെയും അലഞ്ഞുതിരിയുന്നതിന് പകരം, പ്രശ്നം സ്റ്റേറ്റ് അപ്ഡേറ്റ് ലോജിക്കിലാണെന്ന് കണ്ടെത്താൻ എന്നെ സഹായിക്കുമായിരുന്നു.