మొదటి నుండి ఒక ఆపరేటింగ్ సిస్టమ్ను నిర్మించడం అనేది C భాషలో కెర్నల్ హ్యాకర్లు చేసే పనిలా అనిపిస్తుంది. కానీ మీరు ఒక మధ్యాహ్నంలోనే పైథాన్లో ఒక సరళీకృత సిమ్యులేషన్ను సిద్ధం చేయవచ్చు, మరియు ప్రాసెస్ మేనేజ్మెంట్ యొక్క లాజిక్ కూడా హై-లెవల్ లాంగ్వేజ్లో అంతే కఠినంగా ఉంటుందని మీరు త్వరగా కనుగొంటారు. నేను దీనిని కష్టపడి నేర్చుకున్నాను. నేను ఒక చిన్న OS సిమ్యులేటర్ను రాయడానికి కూర్చున్నాను. లక్ష్యం చిన్నదే: కొన్ని ప్రాసెస్లను సృష్టించడం, వాటిని షెడ్యూల్ చేయడం మరియు వాటి పని పూర్తయినప్పుడు వాటిని 'finished'గా గుర్తించడం. కోడ్ చాలా చిన్నదిగా ఉంది. లాజిక్ కూడా చాలా పక్కాగా అనిపించింది. కానీ నేను దానిని రన్ చేసినప్పుడు, ఏదీ ఆగిపోలేదు.
పైథాన్లో మినీ OS ని ఎందుకు నిర్మించాలి?
నిజమైన ఆపరేటింగ్ సిస్టమ్ మెమరీ పేజింగ్, ఫైల్ సిస్టమ్స్, హార్డ్వేర్ ఇంటరప్ట్స్ మరియు డివైస్ డ్రైవర్లను మేనేజ్ చేస్తుంది. ఒక సిమ్యులేషన్ వీటన్నింటినీ తొలగించి, మీరు కేవలం ప్రధాన అంశంపై దృష్టి పెట్టేలా చేస్తుంది: అది 'స్టేట్' (state). మీరు ఒక ప్రాసెస్ను నిర్వచిస్తారు. దానికి ఒక PID, బర్స్ట్ టైమ్ (burst time) మరియు లైఫ్సైకిల్ స్టేటస్ ఉంటుంది. Ready. Running. Finished. ఒక షెడ్యూలర్ లూప్ తదుపరి అభ్యర్థిని ఎంచుకుంటుంది, దాని స్టేట్ను మారుస్తుంది, ఒక టైమ్ స్లైస్ను సిమ్యులేట్ చేస్తుంది మరియు దానిని 'done' స్థితికి మారుస్తుంది.
పైథాన్ ఇటువంటి ప్రయోగాలకు అద్భుతమైన సాధనం, ఎందుకంటే ఇది పాయింటర్ అర్థమెటిక్ మరియు మెమరీ అలైన్మెంట్ను పట్టించుకోకుండా ఉండనిస్తుంది. డిక్షనరీల జాబితా మీ ప్రాసెస్ టేబుల్గా మారుతుంది. ఒక while లూప్ మీ కెర్నల్ షెడ్యూలర్గా మారుతుంది. మీరు స్టాండర్డ్ లైబ్రరీ టూల్స్తోనే రౌండ్-రాబిన్ షెడ్యూలింగ్ లేదా ప్రియారిటీ క్యూలను అమలు చేయవచ్చు. ఇది చాలా సులభంగా అనిపిస్తుంది, అందుకే ఆ తర్వాత వచ్చిన బగ్ నన్ను ఎంతగానో విసిగించింది.
సెటప్
నా సిమ్యులేషన్ process_table అనే జాబితాను ఉపయోగించింది. ప్రతి ఎంట్రీ ఈ క్రింది విధంగా ఉండే ఒక డిక్షనరీ:
{
"pid": 1,
"burst_time": 3,
"status": "ready"
}
షెడ్యూలర్ ఒక సాధారణ while లూప్ను నడిపింది. స్టేటస్ "finished" కాకుండా ఉన్న మొదటి ప్రాసెస్ కోసం అది టేబుల్ను స్కాన్ చేసింది. దానిని కనుగొన్నప్పుడు, ఆ ప్రాసెస్ను ఒక సిమ్యులేటెడ్ సైకిల్ కోసం నడపడానికి execute_tick(p) అనే హెల్పర్ ఫంక్షన్ను పిలిచింది. execute_tick లోపల, నేను ప్రాసెస్ స్టేటస్ను "running"గా సెట్ చేసి, బర్స్ట్ టైమ్ను తగ్గించాను మరియు మిగిలిన పని సున్నాకి చేరుకుందో లేదో తనిఖీ చేశాను. ఒకవేళ సున్నా అయితే, నేను స్టేటస్ను "finished"గా అప్డేట్ చేశాను. ప్రతి ప్రాసెస్ 'finished' స్థితికి చేరుకున్న తర్వాత వెలుపలి లూప్ ముగియాల్సి ఉంది.
కాగితం మీద చూస్తే, ఈ ఫ్లో చాలా స్పష్టంగా ఉంది. సిద్ధంగా ఉన్న ప్రాసెస్ను కనుగొనండి. దానిని రన్ చేయండి. పని పూర్తయిందో లేదో తనిఖీ చేయండి. పూర్తయ్యే వరకు మళ్ళీ మళ్ళీ చేయండి. షెడ్యూలర్ తన పనిని ఎలా చేస్తుందో చూడటానికి నేను ప్రింట్ స్టేట్మెంట్లను కూడా జోడించాను. ప్రాసెస్లు ఎంపిక చేయబడటం నేను చూడగలిగాను. లూప్ నిరంతరం తిరుగుతూనే ఉంది. అయినప్పటికీ, ప్రాసెస్లు ఎప్పటికీ ముగియకుండా, నిరంతరం రన్ అవుతూనే ఉన్నట్లు అనిపించింది.
లక్షణం
ఇది అత్యంత అధ్వాన్నమైన వైఫల్యం: నిశ్శబ్ద వైఫల్యం. టెర్మినల్లో ఎటువంటి స్టాక్ ట్రేస్ (stack trace) కనిపించలేదు. ఎటువంటి IndexError లేదా KeyError నాకు దారి చూపలేదు. ఇంటర్ప్రెటర్ ఎటువంటి తప్పు లేకుండా పనిచేస్తోంది. ప్రోగ్రామ్ అనుకున్నట్లుగా పనిచేయడం లేదు. ప్రాసెస్లు ప్రారంభమయ్యాయి, కానీ అవి ఎప్పటికీ ముగియలేదు. ఆ ఫ్లోను సరిచూసుకోవడానికి నేను గంటల సమయం వెచ్చించాను.
లూప్ కండిషన్ తప్పుగా ఉందా? బహుశా బర్స్ట్ టైమ్ కాలిక్యులేషన్లో ఏదైనా పొరపాటు జరిగి ఉండవచ్చు. ప్రాసెస్ టేబుల్ అప్డేట్ అవ్వడానికి బదులుగా కాపీ అవుతుందా? నా టెర్మినేషన్ కండిషన్ తప్పు కీని తనిఖీ చేస్తోందా? నేను మరిన్ని ప్రింట్లను జోడించాను. ప్రతి బూలియన్ ఎక్స్ప్రెషన్ను తనిఖీ చేశాను. నిజంగా ముఖ్యమైన ఆ ఒక్క లైన్ తప్ప మిగిలిన ప్రతిదీ నేను ప్రశ్నించాను.
నిందితుడు
అప్పుడు నాకు అది కనిపించింది. execute_tick లోపల, నేను ఇలా రాశాను:
p["status"] == "running"
రెండు సమాన గుర్తులు. అది పోలిక (comparison), అసైన్మెంట్ (assignment) కాదు. పరిష్కారం కేవలం ఒకే ఒక అక్షరంతో ఉంది:
p["status"] = "running"
పైథాన్లో, p["status"] == "running" అనేది పూర్తిగా సరైన ఎక్స్ప్రెషన్. ఇది True లేదా Falseని ఇస్తుంది, కానీ నేను దానిని దేనికీ అసైన్ చేయనందున ఇంటర్ప్రెటర్ ఆ ఫలితాన్ని పక్కన పెడుతుంది. ఆ లైన్ వల్ల ఎటువంటి ఉపయోగం ఉండదు. డిక్షనరీ ఎంట్రీ మునుపటి స్టేటస్తోనే అలాగే ఉండిపోయింది, తద్వారా ప్రాసెస్ తన లైఫ్సైకిల్లో ముందుకు సాగలేదు.
నేను దానిని ఒకే సమాన గుర్తుతో మార్చాను. స్క్రిప్ట్ను మళ్ళీ రన్ చేశాను. సిమ్యులేషన్ సరిగ్గా పనిచేయడం మొదలుపెట్టింది. ప్రాసెస్లు ప్లాన్ చేసినట్లే ready, running, మరియు finished స్టేట్ల ద్వారా తిరిగాయి. ఒక చిన్న అక్షరం వల్ల నాకు గంటల సమయం వృథా అయింది.
ఈ బగ్లు ఎందుకు దాగి ఉంటాయి?
ఇది ఇంతగా ఇబ్బంది పెట్టడానికి కారణం ఏమిటంటే, ఒక ఎక్స్ప్రెషన్ స్టేట్మెంట్ సింటాక్స్ పరంగా తప్పు కానంత వరకు పైథాన్ దానిని ఎర్రర్గా గుర్తించదు. ఆ బగ్ ఒక సెమాంటిక్ టైపో (semantic typo). ప్రోగ్రామ్ స్టేటస్ను పోల్చింది, ఒక బూలియన్ ఫలితాన్ని ఇచ్చింది మరియు దానిని పక్కన పడేసింది. పోలిక (comparison) ఫలితం False వచ్చే అవకాశం ఉన్నందున, ప్రాసెస్ తన పాత స్టేట్లోనే ఉండిపోయింది మరియు వెలుపలి లూప్ ఆగిపోవడానికి ఎటువంటి కారణం లేకుండా పోయింది.
మీరు దీనికి కన్ఫర్మేషన్ బయాస్ను (confirmation bias) జోడిస్తారు. మీరు ఒక అసైన్మెంట్ చేయాలనుకున్నారు కాబట్టి, మీరు ఒక అసైన్మెంట్నే టైప్ చేశారని మీ మెదడు నమ్ముతుంది. మీరు ఐదవసారి కోడ్ను చదివినప్పుడు, మీ మెదడు ఆ సింబల్ను ఆటోకరెక్ట్ చేస్తుంది. అందుకే 'రబ్బర్ డక్కింగ్' (rubber ducking) పనిచేస్తుంది. ఇది మీరు ప్రతి లైన్ను నెమ్మదిగా వివరించేలా చేస్తుంది, తద్వారా మీరు రాసిన దానికి మరియు మీరు అనుకున్న దానికి మధ్య ఉన్న తేడా స్పష్టంగా కనిపిస్తుంది.
ఇలాంటి చిన్న బగ్స్ (bugs), పెద్ద పెద్ద క్రాష్ల (crashes) కంటే కనుగొనడం కష్టం. ఒక segfault లేదా syntax error వెంటనే తెలిసిపోతుంది. ఒక సైలెంట్ no-op కేవలం స్టేట్ను (state) పాడు చేస్తుంది మరియు ప్రోగ్రామ్ తప్పుగా ముందుకు సాగేలా చేస్తుంది. వైఫల్యం తర్వాతి దశలో (downstream) జరుగుతుంది, మరియు మీ సహజ ప్రవృత్తి కారణాన్ని వెతకడం కంటే లక్షణాన్ని (symptom) డీబగ్ చేయడం వైపు మళ్లుతుంది.
మెరుగైన రక్షణ
మీరు కేవలం మీ కళ్ళను మాత్రమే నమ్మలేరు. ఈ సంఘటన తర్వాత, తప్పును ముందే పట్టుకోవడానికి నేను కొన్ని అలవాట్లను మార్చుకున్నాను.
మొదటిది, మీరు ఒక డిక్షనరీలో (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 సమయంలో అనుమానాస్పద పోలికలను (comparisons) గుర్తించగలవు. అసైన్మెంట్ ఉండాల్సిన చోట పొరపాటున పోలిక (comparison) జరిగినప్పుడు, టైప్స్ ఆశించిన విధంగా లేకపోతే దానిని గుర్తించడం చాలా సులభం అవుతుంది.
రెండవది, మీరు scheduler లాజిక్ను రాసే ముందే state transitions కోసం unit tests రాయండి. ఒక ప్రాసెస్ను సృష్టించి, ఒక టిక్ (tick) పనిని ఇచ్చి, schedulerని రన్ చేసి, చివరి స్టేట్ FINISHED అని నిర్ధారించే ఒక సాధారణ టెస్ట్ వెంటనే ఫెయిల్ అయ్యేది. ఆ వైఫల్యం వల్ల నేను మొత్తం loop అంతా వెతకాల్సిన అవసరం లేకుండా, నేరుగా state update logic పైనే దృష్టి పెట్టేవాడిని.
