ਬਿਲਕੁਲ ਸ਼ੁਰੂ ਤੋਂ ਇੱਕ ਓਪਰੇਟਿੰਗ ਸਿਸਟਮ ਬਣਾਉਣਾ C ਵਿੱਚ ਲਿਖਣ ਵਾਲੇ ਕਰਨਲ ਹੈਕਰਾਂ (kernel hackers) ਦਾ ਕੰਮ ਲੱਗਦਾ ਹੈ। ਪਰ ਤੁਸੀਂ ਇੱਕ ਦੁਪਹਿਰ ਵਿੱਚ Python ਵਿੱਚ ਇੱਕ ਸਰਲ ਸਿਮੂਲੇਸ਼ਨ (simulation) ਤਿਆਰ ਕਰ ਸਕਦੇ ਹੋ, ਅਤੇ ਤੁਸੀਂ ਜਲਦੀ ਹੀ ਖੋਜ ਲਵੋਗੇ ਕਿ ਪ੍ਰੋਸੈਸ ਮੈਨੇਜਮੈਂਟ (process management) ਦਾ ਲੌਜਿਕ ਇੱਕ ਹਾਈ-ਲੈਵਲ ਭਾਸ਼ਾ ਵਿੱਚ ਵੀ ਉਨਾ ਹੀ ਸਖ਼ਤ ਹੁੰਦਾ ਹੈ। ਮੈਂ ਇਹ ਬਹੁਤ ਮੁਸ਼ਕਲ ਨਾਲ ਸਿੱਖਿਆ। ਮੈਂ ਇੱਕ ਛੋਟਾ ਜਿਹਾ OS ਸਿਮੂਲੇਟਰ ਲਿਖਣ ਬੈਠਾ ਸੀ। ਉਦੇਸ਼ ਸਾਧਾਰਨ ਸੀ: ਕੁਝ ਪ੍ਰੋਸੈਸ ਬਣਾਉਣਾ, ਉਹਨਾਂ ਨੂੰ ਸ਼ੈਡਿਊਲ ਕਰਨਾ, ਅਤੇ ਕੰਮ ਖਤਮ ਹੋਣ 'ਤੇ ਉਹਨਾਂ ਨੂੰ 'finished' ਵਜੋਂ ਚਿੰਨ੍ਹਿਤ ਕਰਨਾ। ਕੋਡ ਛੋਟਾ ਸੀ। ਲੌਜਿਕ ਬਿਲਕੁਲ ਸਹੀ ਲੱਗ ਰਿਹਾ ਸੀ। ਫਿਰ ਮੈਂ ਇਸਨੂੰ ਚਲਾਇਆ, ਅਤੇ ਕੁਝ ਵੀ ਖਤਮ ਹੀ ਨਹੀਂ ਹੋ ਰਿਹਾ ਸੀ।
Python ਵਿੱਚ ਇੱਕ ਮਿੰਨੀ OS ਕਿਉਂ ਬਣਾਇਆ ਜਾਵੇ?
ਇੱਕ ਅਸਲੀ ਓਪਰੇਟਿੰਗ ਸਿਸਟਮ ਮੈਮੋਰੀ ਪੇਜਿੰਗ (memory paging), ਫਾਈਲ ਸਿਸਟਮ, ਹਾਰਡਵੇਅਰ ਇੰਟਰਪਟਸ (hardware interrupts), ਅਤੇ ਡਿਵਾਈਸ ਡਰਾਈਵਰਾਂ ਨੂੰ ਸੰਭਾਲਦਾ ਹੈ। ਇੱਕ ਸਿਮੂਲੇਸ਼ਨ ਇਹ ਸਭ ਕੁਝ ਹਟਾ ਦਿੰਦੀ ਹੈ ਅਤੇ ਤੁਹਾਨੂੰ ਮੁੱਖ ਵਿਚਾਰ 'ਸਟੇਟ' (state) 'ਤੇ ਧਿਆਨ ਕੇਂਦਰਿਤ ਕਰਨ ਦੀ ਇਜਾਜ਼ਤ ਦਿੰਦੀ ਹੈ। ਤੁਸੀਂ ਇੱਕ ਪ੍ਰੋਸੈਸ ਨੂੰ ਪਰਿਭਾਸ਼ਿਤ ਕਰਦੇ ਹੋ। ਇਸਦਾ ਇੱਕ PID, ਇੱਕ ਬਰਸਟ ਟਾਈਮ (burst time), ਅਤੇ ਇੱਕ ਲਾਈਫਸਾਈਕਲ ਸਟੇਟਸ ਹੁੰਦਾ ਹੈ। Ready. Running. Finished. ਇੱਕ ਸ਼ੈਡਿਊਲਰ ਲੂਪ ਅਗਲੇ ਉਮੀਦਵਾਰ ਨੂੰ ਚੁਣਦਾ ਹੈ, ਉਸਦੀ ਸਟੇਟ ਨੂੰ ਅੱਗੇ ਵਧਾਉਂਦਾ ਹੈ, ਇੱਕ ਟਾਈਮ ਸਲਾਈਸ (time slice) ਦਾ ਸਿਮੂਲੇਸ਼ਨ ਕਰਦਾ ਹੈ, ਅਤੇ ਉਸਨੂੰ 'done' ਵਿੱਚ ਬਦਲ ਦਿੰਦਾ ਹੈ।
Python ਇਸ ਤਰ੍ਹਾਂ ਦੇ ਪ੍ਰਯੋਗ ਲਈ ਇੱਕ ਉੱਤਮ ਸਾਧਨ ਹੈ ਕਿਉਂਕਿ ਇਹ ਤੁਹਾਨੂੰ ਪੁਆਇੰਟਰ ਅਰਿਥਮੈਟਿਕ (pointer arithmetic) ਅਤੇ ਮੈਮੋਰੀ ਅਲਾਈਨਮੈਂਟ (memory alignment) ਨੂੰ ਨਜ਼ਰਅੰਦਾਜ਼ ਕਰਨ ਦੀ ਇਜਾਜ਼ਤ ਦਿੰਦਾ ਹੈ। ਡਿਕਸ਼ਨਰੀਆਂ (dictionaries) ਦੀ ਇੱਕ ਲਿਸਟ ਤੁਹਾਡੀ ਪ੍ਰੋਸੈਸ ਟੇਬਲ ਬਣ ਜਾਂਦੀ ਹੈ। ਇੱਕ 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' ਸਟੇਟ ਵਿੱਚ ਪਹੁੰਚ ਜਾਵੇ।
ਕਾਗਜ਼ 'ਤੇ, ਪ੍ਰਵਾਹ ਸਾਫ਼ ਸੀ। ਇੱਕ ਤਿਆਰ ਪ੍ਰੋਸੈਸ ਲੱਭੋ। ਉਸਨੂੰ ਚਲਾਓ। ਮੁਕੰਮਲ ਹੋਣ ਦੀ ਜਾਂਚ ਕਰੋ। ਹੋਣ ਤੱਕ ਦੁਹਰਾਓ। ਮੈਂ ਸ਼ੈਡਿਊਲਰ ਨੂੰ ਕੰਮ ਕਰਦੇ ਦੇਖਣ ਲਈ ਪ੍ਰਿੰਟ ਸਟੇਟਮੈਂਟਾਂ ਵੀ ਜੋੜੀਆਂ ਸਨ। ਮੈਂ ਪ੍ਰੋਸੈਸਾਂ ਨੂੰ ਚੁਣੇ ਜਾਣ ਦੀ ਪ੍ਰਕਿਰਿਆ ਦੇਖ ਸਕਦਾ ਸੀ। ਲੂਪ ਚਲਦਾ ਰਿਹਾ। ਫਿਰ ਵੀ ਪ੍ਰੋਸੈਸ ਇੱਕ ਅਨੰਤ ਵਰਤਮਾਨ ਕਾਲ (eternal present tense) ਵਿੱਚ ਪ੍ਰਵੇਸ਼ ਕਰਦੇ ਜਾਪਦੇ ਸਨ, ਹਮੇਸ਼ਾ ਚਲਦੇ ਰਹਿੰਦੇ, ਕਦੇ ਅੱਗੇ ਨਾ ਵਧਦੇ।
ਲੱਛਣ
ਇਹ ਅਸਫਲਤਾ ਦੀ ਸਭ ਤੋਂ ਮਾੜੀ ਕਿਸਮ ਹੈ: ਚੁੱਪ-ਚਾਪ ਹੋਣ ਵਾਲੀ ਅਸਫਲਤਾ। ਟੈਰਮੀਨਲ ਵਿੱਚ ਕੋਈ ਸਟੈਕ ਟ੍ਰੇਸ (stack trace) ਨਹੀਂ ਆਇਆ। ਕਿਸੇ IndexError ਜਾਂ KeyError ਨੇ ਮੈਨੂੰ ਕੋਈ ਇਸ਼ਾਰਾ ਨਹੀਂ ਦਿੱਤਾ। ਇੰਟਰਪ੍ਰੀਟਰ (interpreter) ਬਿਲਕੁਲ ਠੀਕ ਸੀ। ਪ੍ਰੋਗਰਾਮ ਬਸ ਸਹੀ ਤਰ੍ਹਾਂ ਕੰਮ ਨਹੀਂ ਕਰ ਰਿਹਾ ਸੀ। ਪ੍ਰੋਸੈਸ ਸ਼ੁਰੂ ਹੋਏ, ਪਰ ਉਹ ਕਦੇ ਖਤਮ ਨਹੀਂ ਹੋਏ। ਮੈਂ ਘੰਟਿਆਂ ਬੱਧੀ ਇਸ ਦੇ ਪ੍ਰਵਾਹ ਦੀ ਜਾਂਚ ਕੀਤੀ।
ਕੀ ਲੂਪ ਦੀ ਸ਼ਰਤ ਗਲਤ ਸੀ? ਸ਼ਾਇਦ ਬਰਸਟ ਟਾਈਮ ਦੀ ਗਣਨਾ ਵਿੱਚ ਮੇਰੇ ਤੋਂ ਕੋਈ ਗਲਤੀ ਹੋ ਗਈ ਸੀ। ਕੀ ਪ੍ਰੋਸੈਸ ਟੇਬਲ ਅਪਡੇਟ ਹੋਣ ਦੀ ਬਜਾਏ ਸ਼ੈਡੋ ਹੋ ਰਿਹਾ ਸੀ ਜਾਂ ਕਾਪੀ ਹੋ ਰਿਹਾ ਸੀ? ਕੀ ਮੇਰੀ ਸਮਾਪਤੀ ਦੀ ਸ਼ਰਤ ਗਲਤ ਕੀ (key) ਦੀ ਜਾਂਚ ਕਰ ਰਹੀ ਸੀ? ਮੈਂ ਹੋਰ ਪ੍ਰਿੰਟਸ ਜੋੜੇ। ਮੈਂ ਹਰ ਬੂਲੀਅਨ ਐਕਸਪ੍ਰੈਸ਼ਨ (boolean expression) ਦੀ ਜਾਂਚ ਕੀਤੀ। ਮੈਂ ਉਸ ਇੱਕ ਲਾਈਨ ਨੂੰ ਛੱਡ ਕੇ ਸਭ ਕੁ
ਤੁਸੀਂ ਇਸ ਨੂੰ confirmation bias ਨਾਲ ਹੋਰ ਗੁੰਝਲਦਾਰ ਬਣਾ ਦਿੰਦੇ ਹੋ। ਤੁਹਾਨੂੰ ਪਤਾ ਹੁੰਦਾ ਹੈ ਕਿ ਤੁਸੀਂ ਇੱਕ assignment ਟਾਈਪ ਕੀਤਾ ਹੈ ਕਿਉਂਕਿ ਤੁਹਾਡਾ ਇਰਾਦਾ ਇੱਕ assignment ਦਾ ਹੀ ਸੀ। ਜਦੋਂ ਤੁਸੀਂ ਪੰਜਵੀਂ ਵਾਰ ਕੋਡ ਪੜ੍ਹਦੇ ਹੋ, ਤਾਂ ਤੁਹਾਡਾ ਦਿਮਾਗ ਆਪਣੇ ਆਪ ਸਿੰਬਲ ਨੂੰ ਸਹੀ ਕਰ ਲੈਂਦਾ ਹੈ। ਇਹੀ ਕਾਰਨ ਹੈ ਕਿ rubber ducking ਕੰਮ ਕਰਦੀ ਹੈ। ਇਹ ਤੁਹਾਨੂੰ ਹਰ ਲਾਈਨ ਨੂੰ ਇੰਨੀ ਹੌਲੀ ਅਤੇ ਸਪਸ਼ਟ ਰੂਪ ਵਿੱਚ ਬੋਲਣ ਲਈ ਮਜਬੂਰ ਕਰਦੀ ਹੈ ਕਿ ਜੋ ਲਿਖਿਆ ਗਿਆ ਹੈ ਅਤੇ ਜੋ ਤੁਹਾਡਾ ਮਤਲਬ ਸੀ, ਉਹਨਾਂ ਵਿਚਕਾਰਲਾ ਅੰਤਰ ਸਾਹਮਣੇ ਆ ਜਾਂਦਾ ਹੈ।
ਇਸ ਤਰ੍ਹਾਂ ਦੇ ਛੋਟੇ bugs ਇਸ ਤਰ੍ਹਾਂ ਦੇ ਵੱਡੇ crashes ਨਾਲੋਂ ਲੱਭਣ ਵਿੱਚ ਵਧੇਰੇ ਮੁਸ਼ਕਲ ਹੁੰਦੇ ਹਨ। ਇੱਕ segfault ਜਾਂ syntax error ਤੁਰੰਤ ਸਾਹਮਣੇ ਆ ਜਾਂਦਾ ਹੈ। ਇੱਕ ਚੁੱਪ ਰਹਿਣ ਵਾਲਾ no-op ਸਿਰਫ਼ state ਨੂੰ ਖਰਾਬ ਕਰ ਦਿੰਦਾ ਹੈ ਅਤੇ ਪ੍ਰੋਗਰਾਮ ਨੂੰ ਅੱਗੇ ਵਧਣ ਦਿੰਦਾ ਹੈ। ਅਸਫਲਤਾ downstream ਦਿਖਾਈ ਦਿੰਦੀ ਹੈ, ਅਤੇ ਤੁਹਾਡੀ instinct ਕਾਰਨ ਦੀ ਬਜਾਏ symptom ਨੂੰ debug ਕਰਨ ਦੀ ਹੁੰਦੀ ਹੈ।
ਇੱਕ ਬਿਹਤਰ ਰੱਖਿਆ
ਤੁਸੀਂ ਸਿਰਫ਼ ਆਪਣੀਆਂ ਅੱਖਾਂ 'ਤੇ ਭਰੋਸਾ ਨਹੀਂ ਕਰ ਸਕਦੇ। ਇਸ ਘਟਨਾ ਤੋਂ ਬਾਅਦ, ਮੈਂ ਆਪਣੀਆਂ ਕੁਝ ਆਦਤਾਂ ਬਦਲ ਦਿੱਤੀਆਂ ਹਨ ਜੋ ਇਸ ਗਲਤੀ ਨੂੰ ਪਹਿਲਾਂ ਹੀ ਫੜ ਸਕਦੀਆਂ ਸਨ।
ਪਹਿਲਾਂ, ਜੇਕਰ ਤੁਸੀਂ ਇੱਕ dictionary ਵਿੱਚ state ਨੂੰ ਬਣਾਈ ਰੱਖ ਰਹੇ ਹੋ, ਤਾਂ process states ਲਈ dataclass ਜਾਂ enum.Enum ਦੀ ਵਰਤੋਂ ਕਰਨ ਬਾਰੇ ਵਿਚਾਰ ਕਰੋ। ਆਪਣੇ statuses ਨੂੰ constants ਜਾਂ enum members ਵਜੋਂ ਪਰਿਭਾਸ਼ਿਤ ਕਰੋ:
from enum import Enum
class ProcessState(Enum):
READY = "ready"
RUNNING = "running"
FINISHED = "finished"
Explicit types ਦੇ ਨਾਲ, mypy ਵਰਗੇ tools static analysis ਦੌਰਾਨ ਸ਼ੱਕੀ comparisons ਵੱਲ ਇਸ਼ਾਰਾ ਕਰ ਸਕਦੇ ਹਨ। ਜਿੱਥੇ ਇੱਕ assignment ਹੋਣੀ ਚਾਹੀਦੀ ਸੀ, ਉੱਥੇ ਗਲਤੀ ਨਾਲ ਕੀਤੀ ਗਈ comparison ਨੂੰ ਫੜਨਾ ਬਹੁਤ ਆਸਾਨ ਹੋ ਜਾਂਦਾ ਹੈ ਜਦੋਂ types ਉਮੀਦ ਅਨੁਸਾਰ ਨਹੀਂ ਹੁੰਦੇ।
ਦੂਜਾ, scheduler logic ਲਿਖਣ ਤੋਂ ਪਹਿਲਾਂ state transitions ਲਈ unit tests ਲਿਖੋ। ਇੱਕ ਸਧਾਰਨ test ਜੋ ਇੱਕ tick ਦੇ ਕੰਮ ਦੇ ਨਾਲ ਇੱਕ process ਬਣਾਉਂਦਾ ਹੈ, scheduler ਨੂੰ ਚਲਾਉਂਦਾ ਹੈ, ਅਤੇ assert ਕਰਦਾ ਹੈ ਕਿ ਅੰਤਿਮ state FINISHED ਹੈ, ਉਹ ਤੁਰੰਤ ਫੇਲ੍ਹ ਹੋ ਗਿਆ ਹੁੰਦਾ। ਉਹ ਅਸਫਲਤਾ ਮੈਨੂੰ ਪੂਰੇ loop ਵਿੱਚ ਭਟਕਣ ਦੇਣ ਦੀ ਬਜਾਏ, ਖੋਜ ਨੂੰ ਸਿਰਫ਼ state update logic ਤੱਕ ਸੀਮਤ ਕਰ ਦਿੰਦੀ।
