RTOS ফান্ডামেন্টাল — টাস্ক ও শিডিউলিং
এই পাঠে যা শিখবেন
- RTOS কী, এবং এটি সাধারণ-উদ্দেশ্যের OS শিডিউলিং থেকে কীভাবে আলাদা
- RTOS-এ "টাস্ক" ধারণাটি ও প্রায়োরিটি-ভিত্তিক প্রি-এম্পটিভ শিডিউলিং কীভাবে কাজ করে
- সমান-প্রায়োরিটি টাস্কের মধ্যে রাউন্ড-রবিন শেয়ারিং
- একটি সত্যিকারের, চলমান শিডিউলার সিমুলেশন — একটি প্রকৃত টিক-বাই-টিক এক্সিকিউশন ট্রেস তৈরি করে যা পরবর্তী পাঠে (L30) সেমাফোর/মিউটেক্স আলোচনার ভিত্তি হবে
১ · RTOS কী, এবং কেন দরকার
Operating Systems কোর্সে সাধারণ-উদ্দেশ্যের OS প্রসেস/থ্রেড শিডিউলিং (রাউন্ড-রবিন, মাল্টিলেভেল ফিডব্যাক কিউ ইত্যাদি) কভার করা হয়েছে — সেখানে লক্ষ্য মূলত থ্রুপুট ও ফেয়ারনেস সর্বোচ্চ করা। RTOS শিডিউলিং-এর লক্ষ্য সম্পূর্ণ ভিন্ন — প্রতিটি টাস্কের টাইমিং গ্যারান্টি নিশ্চিত করা (L28-এর হার্ড রিয়েল-টাইম সংজ্ঞা মনে করুন)। এই কারণে RTOS শিডিউলার সবসময় predictable ও deterministic হতে হয় — একটি সাধারণ OS-এর "গড়ে ভালো পারফরম্যান্স" যথেষ্ট নয়, এখানে "প্রতিবারই ডেডলাইনের মধ্যে" নিশ্চিত করতে হয়।
একটি RTOS হলো একটি ছোট, হালকা অপারেটিং সিস্টেম কার্নেল যা মাইক্রোকন্ট্রোলারে চলে এবং একাধিক স্বাধীন টাস্কTaskRTOS-এ একটি স্বাধীনভাবে শিডিউল হওয়া কাজের একক — নিজস্ব স্ট্যাক ও স্টেট সহ, সাধারণ OS-এর একটি থ্রেডের অনুরূপ। (একটি সাধারণ OS-এর "থ্রেড"-এর অনুরূপ, কিন্তু সাধারণত অনেক হালকা) সামলাতে পারে — প্রতিটি টাস্কের নিজস্ব স্ট্যাক ও স্টেট থাকে, আর RTOS শিডিউলার ঠিক করে কোন মুহূর্তে কোন টাস্ক CPU পাবে। বাস্তব RTOS-এর উদাহরণ — FreeRTOS ও Zephyr — দুটোই ওপেন-সোর্স, ব্যাপকভাবে ব্যবহৃত, বাস্তব সিলিকনে চলে (এই কোর্সের সিমুলেশন তাদের আচরণ অনুকরণ করে, বাস্তব RTOS কার্নেল চালায় না)।
২ · প্রায়োরিটি-ভিত্তিক প্রি-এম্পটিভ শিডিউলিং
সবচেয়ে সাধারণ RTOS শিডিউলিং নীতি হলো ফিক্সড-প্রায়োরিটি প্রি-এম্পটিভ শিডিউলিং: প্রতিটি টাস্কের একটি প্রায়োরিটি থাকে, আর শিডিউলার নিয়মিত বিরতিতে (প্রতি "টিক"-এ) পরীক্ষা করে — সব রেডি (কার্যকর হওয়ার জন্য প্রস্তুত) টাস্কের মধ্যে সবচেয়ে বেশি প্রায়োরিটিরটি কে, আর সেটিকেই CPU দেয়। যদি চলমান একটি নিম্ন-প্রায়োরিটি টাস্কের মাঝপথেই একটি উচ্চ-প্রায়োরিটি টাস্ক রেডি হয়ে যায়, শিডিউলার তাৎক্ষণিকভাবে চলমান টাস্কটিকে প্রি-এম্পট করে উচ্চ-প্রায়োরিটিরটি চালায় — L27-এ ইন্টারাপ্টের ক্ষেত্রে যেমনটা দেখা গিয়েছিল, একই নীতি এখানে টাস্ক-লেভেলে প্রযোজ্য। সমান প্রায়োরিটির একাধিক টাস্ক রেডি থাকলে সাধারণত রাউন্ড-রবিন (পালাক্রমে, ন্যায্যভাবে) ভাগাভাগি করা হয়।
৩ · একটি সত্যিকারের শিডিউলার সিমুলেশন
নিচের কোডে পাঁচটি Task — প্রতিটির নাম, প্রায়োরিটি (১ = সর্বোচ্চ), রেডি হওয়ার টিক, ও কতটুকু
কাজ বাকি (work_ticks) — এবং একটি শিডিউলার লুপ যা প্রতি টিকে সব রেডি টাস্কের মধ্যে সবচেয়ে
বেশি প্রায়োরিটিরটি বেছে নেয় (সমান প্রায়োরিটিতে যেটা সবচেয়ে বেশিদিন ধরে অপেক্ষা করছে সেটিকে রাউন্ড-রবিন
নিয়মে অগ্রাধিকার দিয়ে)। ট্রেসে লক্ষ করুন — Comm_Send (সর্বোচ্চ প্রায়োরিটি) মাঝপথে এসে
চলমান Sensor_Read-কে সত্যিই প্রি-এম্পট করছে, আর Sensor_Read পরে আবার চালিয়ে
গিয়ে সম্পন্ন হচ্ছে।
class Task:
def __init__(self, name, priority, ready_tick, work_ticks):
self.name = name
self.priority = priority # ১ = সর্বোচ্চ প্রায়োরিটি, সংখ্যা বাড়লে প্রায়োরিটি কমে
self.ready_tick = ready_tick
self.remaining = work_ticks
self.last_run_tick = -1
tasks = [
Task("Comm_Send", priority=1, ready_tick=2, work_ticks=2),
Task("Sensor_Read", priority=2, ready_tick=0, work_ticks=4),
Task("Logger_A", priority=3, ready_tick=0, work_ticks=2),
Task("Logger_B", priority=3, ready_tick=0, work_ticks=2),
Task("Display_Update", priority=4, ready_tick=0, work_ticks=4),
]
TOTAL_TICKS = 15
trace = []
for tick in range(TOTAL_TICKS):
ready = [t for t in tasks if t.ready_tick <= tick and t.remaining > 0]
if not ready:
trace.append((tick, None))
continue
# প্রায়োরিটি অনুযায়ী সাজানো, সমান প্রায়োরিটিতে যে সবচেয়ে বেশিদিন অপেক্ষা করছে (round-robin) তাকে আগে
ready.sort(key=lambda t: (t.priority, t.last_run_tick))
chosen = ready[0]
chosen.remaining -= 1
chosen.last_run_tick = tick
trace.append((tick, chosen.name))
prev = None
switches = 0
for tick, name in trace:
if name is None:
print(f"tick {tick:>2}: CPU আইডল (কোনো টাস্ক রেডি নয়)")
else:
marker = " <- কনটেক্সট সুইচ" if name != prev else ""
if marker:
switches += 1
print(f"tick {tick:>2}: {name} রান করছে{marker}")
prev = name
print(f"\nমোট কনটেক্সট সুইচ: {switches}")
print("\nপ্রতিটি টাস্কের চূড়ান্ত অবস্থা:")
for t in tasks:
status = "সম্পন্ন" if t.remaining == 0 else f"অসম্পন্ন (বাকি {t.remaining} টিক)"
print(f" {t.name:<15} priority={t.priority} শেষ রান tick={t.last_run_tick:>2} -> {status}")
Sensor_Read (priority ২) tick ০-১-এ রান করে, কিন্তু tick ২-এ যখন
সর্বোচ্চ-প্রায়োরিটি Comm_Send রেডি হয়ে যায়, শিডিউলার সত্যিই Sensor_Read-কে
থামিয়ে Comm_Send-কে CPU দেয় (প্রকৃত প্রি-এম্পশন — Sensor_Read-এর বাকি কাজ
ঠিক যেখানে ছিল সেখানেই সংরক্ষিত থাকে, শূন্য থেকে শুরু হয় না)। Comm_Send শেষ হলে tick ৪-এ
Sensor_Read ঠিক যেখানে থেমেছিল সেখান থেকেই আবার চলে সম্পন্ন হয়। আর সমান-প্রায়োরিটির
Logger_A/Logger_B tick ৬-৯-এ পালাক্রমে (রাউন্ড-রবিন) CPU ভাগ করে নেয় —
কারণ প্রতিবার যেটা কম সম্প্রতি রান করেছে সেটিই last_run_tick অনুযায়ী পরের পালা পায়।
RTOS শিডিউলার প্রতি টিকে সবচেয়ে বেশি প্রায়োরিটির রেডি টাস্ককে CPU দেয়, প্রয়োজনে চলমান নিম্ন-প্রায়োরিটি টাস্ককে প্রি-এম্পট করে (যার কাজ পরে যেখান থেকে থেমেছিল সেখান থেকেই আবার শুরু হয়), আর সমান-প্রায়োরিটির টাস্কগুলো রাউন্ড-রবিন নিয়মে CPU ভাগ করে নেয়। কিন্তু একটি টাস্ক যদি একটি শেয়ার্ড রিসোর্সের জন্য অপেক্ষা করে (যেমন একটি মিউটেক্স) — তখন এই সহজ নিয়মেও একটি বিপজ্জনক সমস্যা দেখা দিতে পারে, যা পরের পাঠের বিষয়।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১
উপরের সিমুলেশনে Display_Update-এর প্রায়োরিটি সবচেয়ে কম (৪) — এটি কি শেষ পর্যন্ত
সম্পন্ন হয়, নাকি চিরকাল অপেক্ষা করতে থাকে?
এই নির্দিষ্ট উদাহরণে এটি সম্পন্ন হয় — কারণ বাকি সব উচ্চ-প্রায়োরিটি টাস্কের কাজ সীমিত এবং একসময় শেষ
হয়ে যায়, এরপর Display_Update-এর জন্য CPU খালি হয়ে যায়। কিন্তু বাস্তব সিস্টেমে যদি
উচ্চ-প্রায়োরিটি টাস্কগুলো ক্রমাগত (কখনো না থেমে) নতুন কাজ পেতে থাকে, সবচেয়ে কম প্রায়োরিটির টাস্ক
কার্যত স্টার্ভ (starve) করতে পারে — কখনোই CPU সময় না পাওয়া। এটি ফিক্সড-প্রায়োরিটি
শিডিউলিং-এর একটি পরিচিত সীমাবদ্ধতা।
প্র ০২ একটি সাধারণ (নন-RTOS) অপারেটিং সিস্টেমের শিডিউলার আর এই RTOS শিডিউলারের মূল লক্ষ্যের পার্থক্য কী?
সাধারণ OS শিডিউলার (যেমন Linux-এর CFS) মূলত থ্রুপুট ও ফেয়ারনেস সর্বোচ্চ করতে চায় — গড়ে সবাইকে মোটামুটি ন্যায্যভাবে CPU সময় দেওয়া। RTOS শিডিউলার এর বিপরীতে প্রেডিক্টেবিলিটি চায় — একটি নির্দিষ্ট টাস্ক তার ডেডলাইনের মধ্যে নিশ্চিতভাবে CPU পাবে কিনা তা আগে থেকে বিশ্লেষণযোগ্য হতে হয়, এমনকি এর জন্য কম-প্রায়োরিটি টাস্ককে "অন্যায্যভাবে" অপেক্ষা করানো লাগলেও।
প্র ০৩
কোডে ready.sort(key=lambda t: (t.priority, t.last_run_tick)) লাইনে দ্বিতীয়
ক্রাইটেরিয়া t.last_run_tick কেন ব্যবহার করা হয়েছে?
এটি সমান-প্রায়োরিটি টাস্কের মধ্যে রাউন্ড-রবিন আচরণ তৈরি করার জন্য — যে টাস্কটি সবচেয়ে দেরিতে (বা
কখনো না) রান করেছে, তার last_run_tick সবচেয়ে ছোট (বা -1), তাই সাজানোর পর
সেটিই আগে আসে এবং পরের পালা পায়। এটি ছাড়া, একই প্রায়োরিটির একটি টাস্ক বারবার নির্বাচিত হয়ে অন্যগুলো
কখনো CPU সময় না-ও পেতে পারত।
অনুশীলন
-
চিন্তা করুন: উপরের কোড সেলে
Comm_Send-এরready_tickযদি ২-এর বদলে ০ করা হয় (অর্থাৎ শুরু থেকেই রেডি), ট্রেসের শুরুটা কীভাবে বদলাবে বলে মনে করেন?তাহলে tick ০ থেকেই
Comm_Send(priority ১)Sensor_Read-এর (priority ২) চেয়ে বেশি প্রায়োরিটি পাবে, তাই শুরু থেকেইComm_Sendরান করবে —Sensor_Readএকদমই প্রি-এম্পট হবে না, বরংComm_Sendসম্পন্ন হওয়া পর্যন্ত অপেক্ষা করেই তারপর শুরু হবে। -
পরীক্ষা করুন: কোড সেলে
Logger_A-এরwork_ticks২-এর বদলে ৫ করে Run চেপে দেখুন মোট কনটেক্সট সুইচ সংখ্যা কীভাবে বদলায়।Logger_A-এর কাজ বেশি হওয়ায় এটিLogger_B-এর সাথে আরও বেশিবার পালাক্রমে (রাউন্ড-রবিন) সুইচ হবে এর আগে সম্পন্ন হওয়ার জন্য — ফলে মোট কনটেক্সট সুইচ সংখ্যা বেড়ে যাবে, এবংDisplay_Updateশুরু হওয়ার tick-ও পিছিয়ে যাবে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পরবর্তী পাঠ L30 সেমাফোর, মিউটেক্স ও প্রায়োরিটি ইনভার্সন — এই শিডিউলারের উপর ভিত্তি করেই একটি ক্লাসিক, বাস্তব বাগ ক্লাস দেখানো হবে।
- Operating Systems কোর্স সহোদর কোর্স সাধারণ প্রসেস/থ্রেড শিডিউলিং অ্যালগরিদমের গভীর আলোচনা সেই কোর্সেই আছে।
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ বেয়ার-মেটাল বনাম RTOS বনাম এমবেডেড লিনাক্স, লো-পাওয়ার মোড, ওয়াচডগ টাইমার — বাকি M7-M8 মডিউল আসছে এরপরই।