I/O ইন্টারফেসিং — পোর্ট-ম্যাপড বনাম মেমরি-ম্যাপড I/O
এই পাঠে যা শিখবেন
- ডিভাইস রেজিস্টার কী এবং CPU কীভাবে তার মাধ্যমে পেরিফেরাল ডিভাইসের সাথে যোগাযোগ করে
- পোর্ট-ম্যাপড I/O ও মেমরি-ম্যাপড I/O স্কিমের গঠনগত পার্থক্য
- কেন মেমরি-ম্যাপড I/O আধুনিক সিস্টেমে বেশি ব্যবহৃত হয়
- মেমরি-ম্যাপড I/O ঠিকানা ক্যাশ করা কেন বিপজ্জনক — একটি বাস্তব সঠিকতার (correctness) সমস্যা
১ · ডিভাইস রেজিস্টার — CPU ও ডিভাইসের যোগাযোগ-বিন্দু
কীবোর্ড, ডিস্ক, নেটওয়ার্ক কার্ডের মতো প্রতিটি পেরিফেরাল ডিভাইসের ভেতরে ছোট, দ্রুত স্টোরেজ লোকেশন থাকে — ডিভাইস রেজিস্টার — যেখানে CPU কমান্ড লিখতে পারে (যেমন "ডেটা পাঠাও") এবং স্ট্যাটাস পড়তে পারে (যেমন "কাজ শেষ হয়েছে কিনা")। এটি সরাসরি L26-এর CPU রেজিস্টার ধারণার একটি প্রতিরূপ, শুধু এখন প্রয়োগ হচ্ছে ডিভাইস-সাইড হার্ডওয়্যারে, CPU-এর ভেতরের রেজিস্টারে নয়। মূল প্রশ্ন হলো — CPU কীভাবে এই রেজিস্টারগুলো ঠিকানা করে অ্যাক্সেস করবে? এর দুটি প্রধান উত্তর আছে।
২ · পোর্ট-ম্যাপড I/O — আলাদা অ্যাড্রেস স্পেস
পোর্ট-ম্যাপড I/O ডিভাইস রেজিস্টারের জন্য মেমরি থেকে সম্পূর্ণ আলাদা একটি অ্যাড্রেস
স্পেস ব্যবহার করে — ঐতিহাসিকভাবে x86-এর IN/OUT নির্দেশনা এর একটি বাস্তব উদাহরণ। এই
স্কিমের জন্য ISA-তে বিশেষ, dedicated I/O নির্দেশনা দরকার হয় — সরাসরি L23/L27-এ শেখা ISA ডিজাইন
সিদ্ধান্তের একটি বাস্তব প্রয়োগ।
৩ · মেমরি-ম্যাপড I/O — একই অ্যাড্রেস স্পেস পুনর্ব্যবহার
মেমরি-ম্যাপড I/O (MMIO) — আধুনিক বেশিরভাগ সিস্টেমে বেশি প্রচলিত পদ্ধতি — ডিভাইস রেজিস্টারকে সাধারণ মেমরির একই অ্যাড্রেস স্পেসে বসিয়ে দেয়। CPU সাধারণ LOAD/STORE নির্দেশনা (L27) দিয়েই, নির্দিষ্ট সংরক্ষিত মেমরি ঠিকানায় পড়ে/লিখে, ডিভাইসের সাথে যোগাযোগ করে — কোনো নতুন নির্দেশনা ISA-তে যোগ করার দরকারই পড়ে না, যা সত্যিকারভাবে এলিগ্যান্ট, কারণ এটি সব নিয়মিত মেমরি-অ্যাক্সেস হার্ডওয়্যার পুনর্ব্যবহার করে।
মেমরি-ম্যাপড I/O ঠিকানাগুলোকে সাধারণত M8-এর ক্যাশ সিস্টেম দ্বারা non-cacheable হিসেবে গণ্য করতে হয়। কারণ একটি ডিভাইস রেজিস্টারের মান বাহ্যিক হার্ডওয়্যার ইভেন্টেও বদলাতে পারে (শুধু CPU-এর লেখায় নয়) — যদি সেটা ক্যাশ করা হয়, ক্যাশ পুরনো (stale) মান পরিবেশন করতে পারে, যা একটি প্রকৃত সঠিকতা-ঝুঁকি (correctness hazard) — যদি না বিশেষভাবে হ্যান্ডল করা হয়।
আলাদা অ্যাড্রেস স্পেস · বিশেষ IN/OUT নির্দেশনা দরকার · ISA-তে বাড়তি জটিলতা।
একই অ্যাড্রেস স্পেস · সাধারণ LOAD/STORE যথেষ্ট · আধুনিক সিস্টেমে বেশি প্রচলিত।
# দুই ধরনের I/O স্কিম সিমুলেশন -- একটি ডিভাইস কমান্ড কীভাবে প্রতিটি স্কিমে পাঠানো হয়
# --- পোর্ট-ম্যাপড I/O: মেমরি থেকে সম্পূর্ণ আলাদা অ্যাড্রেস স্পেস ---
port_mapped_io = {}
def io_write(port, value):
port_mapped_io[port] = value
return f"io_write(port=0x{port:02X}, value={value}) -> বিশেষ IN/OUT-স্টাইল নির্দেশনা দিয়ে ডিভাইসে কমান্ড পাঠানো হলো"
def io_read(port):
return port_mapped_io.get(port)
print(io_write(port=0x3F8, value=1))
print("io_read(0x3F8):", io_read(0x3F8))
print()
# --- মেমরি-ম্যাপড I/O: একই অ্যাড্রেস স্পেস, রিজার্ভড রেঞ্জ ডিভাইসের জন্য ---
memory = {}
DEVICE_ADDR_START, DEVICE_ADDR_END = 0xF000, 0xF00F
device_log = []
def handle_device_write(address, value):
"""রিজার্ভড রেঞ্জে লেখা হলে -- সাধারণ মেমরিতে না গিয়ে ডিভাইস হ্যান্ডলারে রুট হয়"""
device_log.append((address, value))
return f"mmio_write(0x{address:04X}, {value}) -> ঠিকানা ডিভাইস-রেঞ্জে পড়ায় ডিভাইস হ্যান্ডলারে রুট হলো"
def mmio_write(address, value):
"""সাধারণ memory[address] = value সিনট্যাক্স -- ঠিকানার রেঞ্জ চেক করেই সিদ্ধান্ত নেওয়া হয়"""
if DEVICE_ADDR_START <= address <= DEVICE_ADDR_END:
return handle_device_write(address, value)
memory[address] = value
return f"mmio_write(0x{address:04X}, {value}) -> সাধারণ মেমরি ঠিকানায় লেখা হলো"
print(mmio_write(0x1000, 42)) # সাধারণ মেমরি ঠিকানা
print(mmio_write(0xF002, 1)) # ডিভাইস রেজিস্টার ঠিকানা -- একই syntax!
print()
print("memory ডিকশনারি: ", memory)
print("device_log: ", device_log)
print()
print("নিশ্চিত: একই mmio_write() ফাংশন, শুধু ঠিকানার রেঞ্জ দেখেই সাধারণ মেমরি বনাম ডিভাইস রেজিস্টার আলাদা করেছে।")
mmio_write(0x1000, 42) ও mmio_write(0xF002, 1) দুটোই ঠিক একই ফাংশন
কল, একই সিনট্যাক্স — কিন্তু সম্পূর্ণ ভিন্ন ফলাফল দেয়, শুধুমাত্র ঠিকানার মান DEVICE_ADDR_START-
DEVICE_ADDR_END রেঞ্জের ভেতরে কিনা তার ওপর ভিত্তি করে। এটাই মেমরি-ম্যাপড I/O-এর মূল এলিগ্যান্স —
সফটওয়্যারের দৃষ্টিকোণ থেকে কোনো "বিশেষ" কিছু করতে হয় না, হার্ডওয়্যারই ঠিকানার রেঞ্জ দেখে সঠিক পথে রুট করে দেয়।
পোর্ট-ম্যাপড ও মেমরি-ম্যাপড I/O দুটোই একই মৌলিক সমস্যার সমাধান — CPU কীভাবে ডিভাইস রেজিস্টার ঠিকানা করবে — কিন্তু ভিন্ন ট্রেড-অফে। মেমরি-ম্যাপড I/O সাধারণ মেমরি-অ্যাক্সেস মেকানিজম পুনর্ব্যবহার করে সরলতা আনে, কিন্তু এর জন্য মূল্য দিতে হয় ক্যাশ সিস্টেমকে ডিভাইস-রেঞ্জ ঠিকানা "non-cacheable" হিসেবে বিশেষভাবে চিহ্নিত করে — নাহলে stale ডেটার ঝুঁকি তৈরি হয়।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ মেমরি-ম্যাপড I/O যদি এতই এলিগ্যান্ট, তাহলে পোর্ট-ম্যাপড I/O কেন এখনো (যেমন ঐতিহাসিক x86-এ) ব্যবহৃত হয়েছে?
পোর্ট-ম্যাপড I/O-এর একটি বাস্তব সুবিধা ছিল — যেহেতু এটি সম্পূর্ণ আলাদা অ্যাড্রেস স্পেস ব্যবহার করে, ডিভাইস রেজিস্টারের জন্য মূল্যবান মেমরি অ্যাড্রেস স্পেসের কোনো অংশ "রিজার্ভ" করে রাখতে হয় না — বিশেষত পুরনো সিস্টেমে যেখানে মোট অ্যাড্রেসযোগ্য মেমরি সীমিত ছিল, এটি একটি বাস্তব বিবেচ্য বিষয় ছিল। এছাড়া, দুটো আলাদা অ্যাড্রেস স্পেস থাকায় হার্ডওয়্যার ডিজাইনাররা মাঝে মাঝে সহজেই বুঝতে পারতেন কোন অ্যাক্সেসটা মেমরির, কোনটা ডিভাইসের — কোনো রেঞ্জ-চেক লজিক ছাড়াই।
প্র ০২ যদি একটি ডিভাইস রেজিস্টারের ঠিকানা ভুলবশত ক্যাশ করা হয়, বাস্তবে ঠিক কী সমস্যা ঘটতে পারে তার একটি কংক্রিট উদাহরণ দিন।
ধরা যাক একটি ডিভাইস রেজিস্টার "ডেটা প্রস্তুত?" স্ট্যাটাস দেখায় — CPU বারবার এই ঠিকানা পড়ে (polling) দেখে কাজ শেষ হয়েছে কিনা। যদি এই ঠিকানা ক্যাশ করা হয়, CPU-এর প্রথম রিডেই মান ক্যাশে বসে যাবে — এরপর ডিভাইস বাহ্যিকভাবে (হার্ডওয়্যার ইভেন্টে) মান আপডেট করলেও, CPU ক্যাশ থেকে সেই পুরনো মানই বারবার পড়তে থাকবে, কখনো "প্রস্তুত" অবস্থা দেখতেই পাবে না — প্রোগ্রাম চিরকাল আটকে থাকতে পারে (infinite polling loop), যদিও ডিভাইস আসলে অনেক আগেই কাজ শেষ করে ফেলেছে।
প্র ০৩
উপরের কোড সেলে mmio_write ফাংশনটি ঠিকানার রেঞ্জ চেক করে সিদ্ধান্ত নেয়। বাস্তব হার্ডওয়্যারে এই "রেঞ্জ চেক" আসলে কোন সার্কিট দিয়ে বাস্তবায়িত হয়?
বাস্তবে এটি একটি কম্প্যারেটর সার্কিটের (L12-এর সরাসরি প্রয়োগ) কাজ — ঠিকানার নির্দিষ্ট উচ্চ-বিটগুলো একটি নির্দিষ্ট প্যাটার্নের সাথে তুলনা করে দেখা হয় সেটা ডিভাইস-রেঞ্জের ভেতরে পড়ে কিনা। যদি মেলে, বাস কন্ট্রোলার সেই অ্যাক্সেসটা মূল মেমরি চিপের বদলে সংশ্লিষ্ট ডিভাইস কন্ট্রোলারের দিকে রুট করে দেয় — অনেকটা L10-এর ডিমাল্টিপ্লেক্সারের মতোই একটি সিদ্ধান্ত-ভিত্তিক রাউটিং কাজ, শুধু কোড দিয়ে না বরং কম্প্যারেটর ও রাউটিং সার্কিট দিয়ে।
অনুশীলন
-
চিন্তা করুন: উপরের কোড সেলে
mmio_write(0xF010, 5)কল করলে (লক্ষ্য করুন — এটিDEVICE_ADDR_END = 0xF00F-এর ঠিক পরের ঠিকানা) কী ঘটবে?যেহেতু 0xF010 > 0xF00F (রেঞ্জের ঠিক বাইরে), শর্ত
DEVICE_ADDR_START <= address <= DEVICE_ADDR_ENDমিথ্যা হবে — তাই এই লেখাটি ডিভাইস হ্যান্ডলারে না গিয়ে সাধারণmemoryডিকশনারিতেই সংরক্ষিত হবে, ঠিক যেমন একটি সম্পূর্ণ অসংশ্লিষ্ট মেমরি ঠিকানা হয়। এটাই দেখায় রেঞ্জের সীমানা নির্ভুলভাবে সংজ্ঞায়িত করা কতটা গুরুত্বপূর্ণ — এক-বিট ভুল সীমানা একটি ডিভাইস রেজিস্টারকে ভুলবশত সাধারণ মেমরি হিসেবে গণ্য করাতে পারে, বা উল্টোটা। -
পরীক্ষা করুন: পোর্ট-ম্যাপড I/O স্কিমে কোনো "রেঞ্জ চেক" নেই কেন —
io_writeফাংশনটি সরাসরিport_mapped_ioডিকশনারিতে লেখে, কোনো শর্ত ছাড়াই।কারণ পোর্ট-ম্যাপড I/O-তে
io_write/io_readনিজেরাই একটি সম্পূর্ণ আলাদা, dedicated "জগৎ" — এই ফাংশনগুলো ব্যবহার করার মানেই হলো আপনি ইতিমধ্যে জানেন এটা I/O অ্যাক্সেস, মেমরি অ্যাক্সেস নয়। রেঞ্জ চেকের দরকার নেই কারণ ভাষাগতভাবেই (ISA-স্তরে আলাদা নির্দেশনা ব্যবহার করেই) সিদ্ধান্তটা আগে থেকে নেওয়া হয়ে গেছে — ঠিক এই পার্থক্যটাই মেমরি-ম্যাপড I/O-এর সাথে তুলনায় পোর্ট-ম্যাপড I/O-এর মূল গঠনগত পার্থক্য।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ পরবর্তী পাঠ — বাস আর্কিটেকচার: সিস্টেম বাস ও প্রোটোকল, যা এই পাঠের রুটিং-ধারণা আরও গভীরে নিয়ে যাবে।
- রেজিস্টার ফাইল ও CPU রেজিস্টার L26 এই পাঠের ডিভাইস রেজিস্টার ধারণার ভিত্তি — CPU-এর নিজস্ব রেজিস্টার কীভাবে কাজ করে, আগে দেখুন।