Foundationপ্রথম নীতি থেকে
LEVEL 12লেসন ৫/২২কঠিন৫০ মিনিট

Paravirtualization ও virtio — গেস্ট যখন নিজেই জানে সে ভার্চুয়াল

Paravirtualization and virtio: Cooperative I/O Instead of Faithful Emulation

Paravirtualization মানে guest OS-কে সত্য বলে দেওয়া যে সে ভার্চুয়াল, আর তাকে hypercall দিয়ে সরাসরি hypervisor-এর সাথে সহযোগিতা করতে শেখানো — ঐতিহাসিকভাবে Xen-এর পুরো CPU-এমুলেশনে, আজ প্রধানত virtio-র ring-buffer I/O-তে, যেখানে batching VM-exit সংখ্যা ৯৯%-এর বেশি কমিয়ে দেয়।

এই লেসন শেষে আপনি পারবেন

  • Full-device-emulation আর paravirtualization-এর মৌলিক পার্থক্য — unmodified guest driver বনাম modified, সহযোগিতামূলক guest OS — নিজের ভাষায় ব্যাখ্যা করতে পারবেন
  • Hypercall ধারণাটা syscall-এর সাথে সরাসরি তুলনা করে বলতে পারবেন কেন এটা trap-and-decode-এর চেয়ে সস্তা
  • ঐতিহাসিক Xen PV কীভাবে Popek-Goldberg trap-detection সমস্যা পুরোপুরি পাশ কাটিয়ে গিয়েছিল, আর কেন VT-x/EPT আসার পর সেই দরকার কমে গেল তা ব্যাখ্যা করতে পারবেন
  • virtqueue-ভিত্তিক ring buffer মেকানিজম — descriptor, avail ring, used ring, kick/notify — ধাপে ধাপে trace করতে পারবেন
  • batching-এর মাধ্যমে VM-exit সংখ্যা কীভাবে নাটকীয়ভাবে কমে তা নিজে সিমুলেট করে সংখ্যায় প্রমাণ করতে পারবেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

গত লেসনে (virtualization-cloud/kvm-qemu) আমরা দেখেছি KVM/QEMU কীভাবে একটা guest VM চালায় — VT-x guest mode-এ CPU সরাসরি চলে, আর QEMU userspace-এ বসে device emulation করে। সেই মডেলের একটা কেন্দ্রীয়, ইচ্ছাকৃত প্রতিশ্রুতি ছিল: guest-কে কিছুই বলা হয় না। Guest OS-এর ভেতরের disk driver বা network driver ঠিক সেই একই, unmodified driver যেটা real hardware-এ চলত — সে ভাবে সে একটা আসল Intel e1000 NIC বা আসল AHCI disk controller-এর সাথে কথা বলছে, রেজিস্টারে রেজিস্টারে, byte-for-byte বাস্তব প্রোটোকল মেনে। QEMU সেই illusion-টা এত নিখুঁতভাবে বজায় রাখে যে driver কোনোদিন সন্দেহও করে না।

কিন্তু এই সততার একটা দাম আছে, যেটা আগের লেসনের শেষে ইঙ্গিত হিসেবে রেখে এসেছিলাম। Driver যখন একটা register-এ লিখতে যায় (যেমন “disk-এ এই sector-টা লেখো”), সেই write একটা MMIO (memory-mapped I/O) access — আর guest mode-এ MMIO access সবসময় একটা VM-exit ট্রিগার করে (kvm-qemu লেসনের VM-exit আলোচনার সরাসরি প্রয়োগ)। CPU guest mode থেকে বেরিয়ে আসে, KVM সেটা QEMU-র কাছে পাঠায়, QEMU decode করে বোঝে ঠিক কোন register-এ কী লেখা হয়েছিল (একটা আসল e1000 datasheet অনুসরণ করে), তারপর তার internal state আপডেট করে, প্রয়োজনে আসল network packet পাঠায়, তারপর নিয়ন্ত্রণ guest-এ ফিরিয়ে দেয়। একটা মাত্র “packet পাঠাও” নির্দেশের জন্য পুরো এই world-switch + decode + emulate + world-switch-ফেরত চক্রটা লাগে — আর high-throughput I/O-তে (একটা ব্যস্ত network card, বা একটা database-এর disk write) এই চক্র সেকেন্ডে হাজার হাজার বার ঘটতে পারে।

আজকের প্রশ্ন সরল: যদি guest OS-কে সত্যটা বলে দেওয়া হয় — যে সে আসলে ভার্চুয়াল, আসল হার্ডওয়্যার নয় — তাহলে কি সে hypervisor-এর সাথে আরও সস্তায়, সরাসরি কথা বলতে পারে? উত্তর হ্যাঁ, আর এই ধারণার নামই paravirtualization — এই লেসনের প্রথম অর্ধেক তার ঐতিহাসিক জন্ম (Xen-এর পুরো CPU emulation প্রতিস্থাপন) দেখাবে, দ্বিতীয় অর্ধেক দেখাবে কেন সেই একই ধারণা আজ, VT-x/EPT-এর যুগেও, virtio-র রূপে I/O-তে সর্বত্র বেঁচে আছে — এমনকি একটা সম্পূর্ণ hardware-virtualized, unmodified-guest-OS KVM VM-এও।

মূল ধারণা

মূল ধারণা — trap-and-decode বনাম explicit hypercall

আগের লেসনের মডেলটা আবার নাম দিয়ে রাখি: full-device-emulation — guest driver unmodified, hardware-এর প্রতিটা register-access বিশ্বস্তভাবে trap হয়ে decode হয়। এই মডেলের একটা লুকানো অনুমান আছে — guest-কে কখনো জিজ্ঞেস করা হয় না সে কী চায়, তার আচরণ থেকে অনুমান করে নেওয়া হয় (একটা নির্দিষ্ট register-এ লেখা মানে “নিশ্চয়ই disk write শুরু করতে চাইছে”)। Hypervisor-কে তাই পুরো একটা real hardware protocol-এর interpreter হতে হয়।

Paravirtualization এই অনুমানটাই বাতিল করে দেয়। এর কেন্দ্রীয় সিদ্ধান্ত: guest OS-এর কার্নেল modify করা হয় — সে জানে সে virtualized পরিবেশে চলছে, আর hardware-এর register প্রোগ্রাম করার ভান না করে, সরাসরি hypervisor-কে একটা explicit request পাঠায়। এই request-এর নাম hypercall।

পার্থক্যটা একটা সমীকরণে ধরা যায়:

full emulation:    guest instr.→traphypervisor→decode + reverse-engineer intentaction\text{full emulation:}\;\; \text{guest instr.} \xrightarrow{\text{trap}} \text{hypervisor} \xrightarrow{\text{decode + reverse-engineer intent}} \text{action}paravirtualization:    guest hypercall (explicit intent)→hypervisor→action\text{paravirtualization:}\;\; \text{guest hypercall (explicit intent)} \xrightarrow{} \text{hypervisor} \xrightarrow{} \text{action}

মধ্যম ধাপটা — “decode + reverse-engineer intent” — সম্পূর্ণ উধাও। Guest নিজেই বলে দিচ্ছে সে কী চায়; hypervisor-কে আর গোয়েন্দাগিরি করতে হয় না। এটাই paravirtualization-এর পুরো লাভের উৎস, আর এই লেসনের বাকি অংশ শুধু এই এক ধারণার দুইটা ভিন্ন যুগ, ভিন্ন প্রয়োগ — প্রথমে পুরো CPU-তে (Xen, ঐতিহাসিক), পরে শুধু I/O-তে (virtio, বর্তমান)।

কেন এটা “বিনামূল্যে” আসে না

Paravirtualization-এর দাম স্পষ্ট: guest OS-এর সোর্স কোড modify করতে হয় — সে জেনেশুনে ভার্চুয়ালাইজেশন-সচেতন কোড চালায়। এটা closed-source OS-এর জন্য (পুরনো Windows) কার্যত অসম্ভব ছিল — শুধু Linux-এর মতো open-source kernel-ই সহজে modify করা যেত। এই সীমাবদ্ধতাটাই আগের লেসনের full-device-emulation মডেল কেন এখনও দরকার তার একটা বড় কারণ — unmodified guest সমর্থন করতেই হয়, সব guest paravirtualization-সচেতন হবে এই ধরে নেওয়া যায় না। প্রশ্নটা তাই কখনোই “paravirtualization বনাম emulation, একটা জিতবে” ছিল না — বরং “কোথায়, কোন শর্তে, কোনটা ব্যবহার করব” — আজকের লেসনের বাকি অংশ ঠিক এই প্রশ্নের ঐতিহাসিক আর বর্তমান উত্তর দেখাবে।

ভেতরে কী ঘটছে

ভেতরে কী ঘটছে — Xen PV থেকে virtio পর্যন্ত

পর্ব ১: Xen PV, ২০০৩ — যখন হার্ডওয়্যার সাহায্য করত না

২০০৩ সালে Xen প্রকাশিত হওয়ার সময় x86-এ VT-x/AMD-V-এর মতো কোনো hardware virtualization extension ছিল না। আগের লেসনগুলোর Popek-Goldberg আলোচনা মনে করুন — x86-এর কিছু instruction sensitive কিন্তু unprivileged: guest ring 1-এ চালালে সেগুলো trap করে না, চুপচাপ ভুল (বা নিরাপত্তাহীন) আচরণ করে, কারণ CPU নিজেই জানে না guest আসলে ring 0 ভেবে চলছে। এই একগুচ্ছ “সমস্যাযুক্ত” instruction-এর কারণেই x86 classic trap-and-emulate দিয়ে সহজে virtualize করা যেত না — VMware-এর মতো কোম্পানি এই সমস্যা সমাধানে binary translation ব্যবহার করেছিল (guest code চালানোর আগে on-the-fly rewrite করে সমস্যাযুক্ত instruction বাদ দেওয়া), একটা জটিল, ব্যয়বহুল কৌশল।

Xen সম্পূর্ণ ভিন্ন পথ নিল: guest kernel নিজেই modify করে দাও, যাতে সে সমস্যাযুক্ত instruction-টা কখনোই না চালায়। যেখানে আসল Linux kernel LGDT বা page-table-write-এর মতো sensitive instruction সরাসরি চালাত, Xen-modified (“Xen-ified”) Linux kernel সেই জায়গায় একটা explicit hypercall বসাল — HYPERVISOR_set_gdt(), HYPERVISOR_mmu_update()-এর মতো ফাংশন কল। ফলাফল একটা চমৎকার observation:

এই কৌশল বাস্তবে দারুণ কাজ করেছিল — Xen PV guest-এর overhead native hardware-এর ৫% এর কাছাকাছি নেমে এসেছিল, যেখানে সেই সময়ের বিকল্প (VMware-এর binary translation, বা memory virtualization-এর জন্য shadow page table কৌশল — guest-এর page table-এর একটা hypervisor-নিয়ন্ত্রিত ছায়া-কপি রেখে প্রতিটা পরিবর্তন ট্র্যাক করা, ব্যয়বহুল কারণ প্রতিটা guest page-table write-ই trap করতে হতো) অনেক ধীরগতির ছিল। Memory virtualization-এও Xen PV একই কৌশল প্রয়োগ করেছিল — guest kernel সরাসরি নিজের page table update করত hypercall দিয়ে (mmu_update), hypervisor সেটা validate করে বসাত — shadow page table-এর মতো heavy trap-based tracking দরকার হতো না।

পর্ব ২: VT-x/EPT আসার পর — কেন Xen PV-র মূল কারণ হারিয়ে গেল

kvm-qemu লেসনে যা দেখেছেন — VT-x (২০০৫-০৬) guest mode নামে একটা নতুন CPU mode যোগ করল, যেখানে sensitive instruction-গুলো স্বয়ংক্রিয়ভাবে trap করে (Popek-Goldberg সমস্যাটা হার্ডওয়্যারেই সমাধান হয়ে গেল), আর EPT (২০০৮) memory virtualization-এর জন্য shadow page table-এর প্রয়োজনই মুছে দিল (hardware নিজেই দুই-স্তরের address translation করে)। এই দুইটা hardware feature ঠিক সেই দুইটা সমস্যাই সমাধান করল যেগুলোর জন্য Xen PV আবিষ্কৃত হয়েছিল — CPU virtualization আর memory virtualization।

ফলাফল: পূর্ণ Xen PV (পুরো kernel modify করে CPU + MMU virtualization) ধীরে ধীরে অপ্রাসঙ্গিক হয়ে গেল। HVM (Hardware Virtual Machine, VT-x-ভিত্তিক) mode-এ unmodified guest kernel চালানো এখন সম্ভব, আর পারফরম্যান্সও প্রতিযোগিতামূলক — modified kernel maintain করার (patch rebase, upstream-এর সাথে sync রাখা) ইঞ্জিনিয়ারিং খরচ আর তার লাভের অনুপাত উল্টে গেল। কিন্তু এখানেই সবচেয়ে গুরুত্বপূর্ণ, প্রায়ই ভুল বোঝা টার্নিং পয়েন্ট:

পর্ব ৩: virtio — I/O-র জন্য paravirtualization, standardized

Rusty Russell-এর ২০০৮ সালের পেপারের observation ছিল সরল কিন্তু গুরুত্বপূর্ণ: Xen-এর নিজস্ব PV block/net driver ছিল, KVM-এর নিজস্ব ছিল, VMware-এর নিজস্ব ছিল — প্রতিটা hypervisor একই ধারণা (paravirtualized I/O) নিজের মতো করে পুনর্বার বানাচ্ছিল, guest OS-এ একগাদা hypervisor-নির্দিষ্ট driver বহন করতে হচ্ছিল। virtio এই বিশৃঙ্খলার সমাধান — একটা hypervisor-স্বাধীন standard: একই virtio-net driver Xen, KVM, VMware, বা যেকোনো compliant hypervisor-এর উপরে অপরিবর্তিত চলে, কারণ virtqueue-র ডেটা-লেআউট আর protocol একটা open স্পেসিফিকেশন (আজ OASIS-এর অধীনে)।

গুরুত্বপূর্ণ প্রেক্ষাপট: একটা আধুনিক KVM/QEMU VM প্রায়ই সম্পূর্ণ hardware-virtualized (VT-x/EPT ব্যবহার করে, guest kernel unmodified) — কিন্তু সেই একই VM-এ guest disk আর network driver হতে পারে virtio driver, কারণ CPU/memory-তে paravirtualization দরকার না থাকলেও, disk/network I/O-তে এখনও দরকার। এটাই আধুনিক ভার্চুয়ালাইজেশন স্ট্যাকের বাস্তবতা — hybrid: CPU/MMU hardware-virtualized, I/O paravirtualized। “পুরোপুরি PV” বা “পুরোপুরি HVM” এই বাইনারি চিন্তা আজকের বাস্তবতাকে ভুল বর্ণনা করে।

virtqueue-র মেকানিজম — একটা shared ring buffer

virtio-র কেন্দ্রীয় ডেটা-স্ট্রাকচার একটা virtqueue — guest আর host-এর মধ্যে shared memory-তে বসানো একটা ring buffer, যার তিনটা অংশ:

  1. Descriptor table — প্রতিটা entry একটা বাফারের ঠিকানা আর দৈর্ঘ্য ({addr, len, flags, next}) বহন করে — আসল ডেটা এখানে কপি হয় না, শুধু pointer থাকে (zero-copy design-এর ভিত্তি)।
  2. Available ring — guest এখানে লেখে: “এই descriptor index-গুলো host-এর জন্য প্রস্তুত, নিয়ে যাও।”
  3. Used ring — host এখানে লেখে: “এই descriptor index-গুলো আমি প্রসেস করে ফেলেছি, guest ফেরত নাও।”
একটা virtio-net packet পাঠানোর ধাপ — full-emulation-এর সাথে তুলনা করে পড়ুন
  1. guest: বাফার তৈরিnetwork packet একটা memory buffer-এ লেখা হয় — এখনও কোনো VM-exit নেই
  2. guest: descriptor বসানোbuffer-এর {addr, len} একটা descriptor entry-তে লেখা হয় — plain memory write, VM-exit নেই
  3. guest: available ring আপডেটdescriptor index available ring-এ যোগ হয় — এটাও plain memory write, VM-exit নেই
  4. guest: kick/notify (ঐচ্ছিকভাবে batched)একটামাত্র lightweight notification (এক ধরনের hypercall) host-কে জানায় "নতুন কাজ আছে" — এখানেই একমাত্র VM-exit
  5. host (QEMU/vhost): descriptor পড়াshared memory থেকেই সরাসরি buffer পড়ে, আসল packet পাঠায় — decode-এর কোনো ধাপ নেই, format আগে থেকেই জানা
  6. host: used ring আপডেট + guest-কে interruptসম্পন্ন descriptor used ring-এ, guest-কে জানানো হয় ফলাফল প্রস্তুত

লক্ষ্য করুন — পুরো ধাপগুলোর মধ্যে মাত্র একটাতে VM-exit ঘটে (kick/notify), বাকিগুলো সাধারণ shared-memory read/write, কোনো trap-decode-emulate চক্র ছাড়াই। এই একটা পার্থক্যই — “প্রতিটা register-access = ১টা VM-exit” (full-emulation) বনাম “একগুচ্ছ কাজ শেষে ১টা VM-exit” (virtio) — এই লেসনের build অংশে সংখ্যায় পরিমাপ করা হবে।

Batching — virtio-র আসল অস্ত্র

এখানেই virtio-র সবচেয়ে গুরুত্বপূর্ণ optimization: guest একাধিক descriptor available ring-এ জমা করে রাখতে পারে, তারপর একটামাত্র kick পাঠাতে পারে পুরো ব্যাচের জন্য। একটা disk-heavy workload-এ (হাজার হাজার sector write) guest হয়তো ৬৪টা write একসাথে ring-এ রেখে একবার kick করবে — full-emulation মডেলে সেই ৬৪টা write-ই ৬৪টা আলাদা VM-exit দাবি করত। এই ব্যাচিং-ই সেই মূল কারণ যা virtio-কে full-device-emulation-এর চেয়ে বহুগুণ দ্রুত করে তোলে উচ্চ-throughput workload-এ।

উদাহরণ

সংখ্যায় — একটা virtio-net TX-এর হিসাব

ধরুন একটা guest VM সেকেন্ডে ৫০,০০০-টা ছোট network packet পাঠাচ্ছে (একটা ব্যস্ত API server-এর বাস্তবসম্মত লোড)। দুই মডেলে VM-exit খরচ তুলনা করি:

মডেলপ্রতি packet VM-exit?সেকেন্ডে VM-exit সংখ্যা
Full-device-emulation (আগের লেসনের e1000 emulation)হ্যাঁ, প্রতিটাতে (কমপক্ষে) ১টা~৫০,০০০
virtio-net, batch-হীন (batch size ১)হ্যাঁ, প্রতিটাতে ১টা~৫০,০০০
virtio-net, ৩২-batchনা — প্রতি ৩২টাতে ১টা kick~১,৫৬৩
virtio-net, ২৫৬-batch (heavy bulk transfer)না — প্রতি ২৫৬টাতে ১টা kick~১৯৫

লক্ষ্য করুন দ্বিতীয় সারিটা — batch size ১-এ virtio-র কোনো সুবিধাই নেই full-emulation-এর তুলনায়, VM-exit সংখ্যা একই। এটা একটা গুরুত্বপূর্ণ, প্রায়ই উপেক্ষিত সত্য: virtio নিজে থেকেই দ্রুত না — batching-এর সুযোগ কাজে লাগালেই দ্রুত। একটা latency-sensitive workload (যেখানে প্রতিটা packet অবিলম্বে পাঠানো জরুরি, batching-এর জন্য অপেক্ষা করা যায় না) virtio ব্যবহার করেও পুরো লাভ পায় না — এটাই misconception অংশে আবার আসবে।

একটা আনুমানিক, order-of-magnitude হিসাব — একটা VM-exit round-trip-এর খরচ প্রায় ১-২ মাইক্রোসেকেন্ড (world-switch, cache/TLB প্রভাব, host scheduler জাগানো সব মিলিয়ে; নির্দিষ্ট সংখ্যা CPU generation ও EPT/VPID সেটিংসের উপর নির্ভরশীল, তাই “প্রায়” ধরে নিন):

সাশ্রয় সময়≈(VM-exitemulation−VM-exitvirtio)×VM-exit খরচ\text{সাশ্রয় সময়} \approx (\text{VM-exit}_{\text{emulation}} - \text{VM-exit}_{\text{virtio}}) \times \text{VM-exit খরচ}

৫০,০০০ − ১৯৫ = ৪৯,৮০৫টা VM-exit বাঁচল; প্রায় ১.৫ মাইক্রোসেকেন্ড (\mu s) প্রতি-VM-exit খরচ ধরলে সেকেন্ডে প্রায় ৭৫ মিলিসেকেন্ড CPU সময় বাঁচল — শুধু world-switch overhead-এই, প্যাকেট প্রসেসিং-এর আসল কাজের বাইরে। উচ্চ-throughput সার্ভারে এটা সহজেই core-utilization-এর একটা পরিমাপযোগ্য শতাংশ।

ছবিতে

FULL-DEVICE-EMULATION (আগের লেসন)          VIRTIO (এই লেসন)
────────────────────────────────           ────────────────────────────────
 guest driver (unmodified)                  guest driver (virtio-aware)
   │ register write #1  ──VM-exit──►QEMU      │ descriptor #1 ─┐ shared mem,
   │ register write #2  ──VM-exit──►QEMU      │ descriptor #2 ─┤ কোনো VM-exit
   │ register write #3  ──VM-exit──►QEMU      │ descriptor #3 ─┘ নেই এখনো
   │ ...                                      │ available ring update (কোনো VM-exit নেই)
   │ register write #N  ──VM-exit──►QEMU      │ ONE kick ──VM-exit──► host
                                               │
   N-টা operation = N-টা VM-exit               N-টা operation = ১-টা VM-exit
                                               (batch size N হলে)
দুই মডেলের পাশাপাশি ছবি — full-device-emulation-এ প্রতিটা register access VM-exit দাবি করে; virtio-তে shared-memory ring আর batched kick দিয়ে সেই সংখ্যা নাটকীয়ভাবে কমে।

নিজে চালিয়ে দেখুন

EXPERIMENT

একটা বাস্তব KVM cloud VM-এ virtio driver নিজের চোখে দেখুন

Linux guest, KVM/QEMU বা যেকোনো major cloud provider (AWS, GCP, Azure, DigitalOcean) VM· ১০ মিনিট

একটা Linux cloud VM-এ (বা লোকাল QEMU/KVM guest-এ) নিচের কমান্ডগুলো চালিয়ে দেখুন কোন driver গুলো block/network device চালাচ্ছে:

lspci -k | grep -A 3 -i "ethernet\|scsi\|non-volatile"
lsmod | grep virtio
ethtool -i eth0 2>/dev/null || ethtool -i ens3

প্রত্যাশিত ফলাফলের ধরন (নির্দিষ্ট cloud provider ও device-naming ভেদে সামান্য ভিন্ন হতে পারে, কিন্তু driver-এর নাম-প্যাটার্ন এভাবেই দেখা যাবে):

00:03.0 Ethernet controller: Red Hat, Inc. Virtio network device
        Kernel driver in use: virtio-pci
        Kernel modules: virtio_pci

virtio_net             65536  0
virtio_pci             28672  0
virtio_ring            36864  4 virtio_net,virtio_pci,virtio_blk,virtio_scsi
virtio_blk             20480  3

driver: virtio_net
version: 1.0.0
firmware-version:
bus-info: virtio0

লক্ষ্য করুন virtio_net আর virtio_blk (বা virtio_scsi) মডিউল লোড হয়ে আছে, আর Kernel driver in use: virtio-pci — এটাই এই লেসনের প্রধান দাবির সরাসরি প্রমাণ: এই VM-এর guest kernel সম্পূর্ণ unmodified, সাধারণ upstream Linux (VT-x/EPT-এর কল্যাণে তার আর দরকার নেই) — অথচ তার I/O driver paravirtualized, কারণ device emulation-এ VT-x/EPT কোনো সাহায্য করে না। “PV drivers” বা “guest tools” যা cloud image-এ pre-installed থাকে বলা হয় — বেশিরভাগ ক্ষেত্রেই এটাই: virtio driver সেট।

এটা কী প্রমাণ করে

আজকের বেশিরভাগ cloud VM-ই paravirtualized I/O ব্যবহার করে — এটা কোনো পাঠ্যপুস্তকীয় বিমূর্ততা না, প্রতিটা প্রধান cloud provider-এর ডিফল্ট ব্যবস্থা।

নিজে বানান

BUILD IT

VM-exit Counter — Full-Emulation বনাম virtio Batching সিমুলেটর

Python · ●●○○○
  1. দুইটা ফাংশন লিখুন — full_emulation_exits(n) সবসময় n রিটার্ন করে (প্রতিটা op = ১ VM-exit)
  2. virtio_exits(n, batch_size) লিখুন — ceil(n / batch_size) রিটার্ন করে (batch-এ জমিয়ে ১টা kick)
  3. batch size-এর একটা রেঞ্জ (১ থেকে ২৫৬) জুড়ে দুইটা সংখ্যা আর শতাংশ-হ্রাস প্রিন্ট করুন
  4. একটা আনুমানিক per-VM-exit খরচ (মাইক্রোসেকেন্ড) দিয়ে মোট সাশ্রয় সময় হিসাব করুন
  5. batch size ১-এ virtio-র কোনো সুবিধা নেই তা নিজের সংখ্যায় যাচাই করুন — উপরের misconception-এর ভিত্তি

এই সিমুলেটরটা এই লেসনের কেন্দ্রীয় দাবি — “batching VM-exit সংখ্যা নাটকীয়ভাবে কমায়” — কে বিমূর্ত দাবি না রেখে concrete সংখ্যায় নামিয়ে আনে। কোড আসলে চালিয়ে নিচের exact output যাচাই করা হয়েছে:

# virtio_vmexit_sim.py — VM-exit সংখ্যা তুলনা: full-emulation বনাম virtio ring-buffer batching
import math

def full_emulation_exits(n_ops):
    # প্রতিটা I/O operation = ১টা guest MMIO/PIO trap = ১টা VM-exit
    return n_ops

def virtio_exits(n_ops, batch_size):
    # guest batch_size-টা descriptor virtqueue-তে জমা করে রাখে,
    # তারপর একটামাত্র "kick" (notification) দিয়ে হোস্টকে জানায়
    return math.ceil(n_ops / batch_size)

N = 10_000  # মোট I/O operation (যেমন ১০,০০০-টা ৪KiB disk write)
VMEXIT_COST_US = 1.5  # আনুমানিক, order-of-magnitude world-switch খরচ (মাইক্রোসেকেন্ড)

print(f"মোট I/O operation N = {N}")
print(f"{'batch size':>10} | {'virtio VM-exits':>16} | {'full-emu VM-exits':>18} | {'reduction':>10} | {'সাশ্রয় সময় (ms)':>18}")
print("-" * 82)
fe = full_emulation_exits(N)
for batch in (1, 2, 4, 8, 16, 32, 64, 128, 256):
    vt = virtio_exits(N, batch)
    reduction = 100 * (1 - vt / fe)
    saved_us = (fe - vt) * VMEXIT_COST_US
    print(f"{batch:>10} | {vt:>16} | {fe:>18} | {reduction:>9.2f}% | {saved_us/1000:>17.3f}")

Exact output (python3 virtio_vmexit_sim.py চালিয়ে পাওয়া, হুবহু এটাই আসবে):

মোট I/O operation N = 10000
batch size |  virtio VM-exits |  full-emu VM-exits |  reduction |  সাশ্রয় সময় (ms)
----------------------------------------------------------------------------------
         1 |            10000 |              10000 |      0.00% |             0.000
         2 |             5000 |              10000 |     50.00% |             7.500
         4 |             2500 |              10000 |     75.00% |            11.250
         8 |             1250 |              10000 |     87.50% |            13.125
        16 |              625 |              10000 |     93.75% |            14.062
        32 |              313 |              10000 |     96.87% |            14.530
        64 |              157 |              10000 |     98.43% |            14.764
       128 |               79 |              10000 |     99.21% |            14.882
       256 |               40 |              10000 |     99.60% |            14.940

তিনটা পর্যবেক্ষণ output থেকে সরাসরি: (১) batch ১-এ virtio-র কোনো লাভ নেই — ১০,০০০ = ১০,০০০, সংখ্যা এখানে মিথ্যা বলে না। (২) batch ৩২-তেই ৯৬.৮৭% হ্রাস — diminishing returns শুরু হয়ে যায়, batch অনেক বড় করলেও (১২৮ → ২৫৬) সাশ্রয় সময় প্রায় স্থির থাকে (~১৪.৮৮ ms থেকে ~১৪.৯৪ ms, মাত্র ০.০৬ ms বাড়ল) — বাস্তব virtio implementation তাই batch size-কে অসীম বড় করে না, একটা যুক্তিসঙ্গত সীমায় (ring queue depth) থামে। (৩) সাশ্রয় সময়ের একক এখানে মিলিসেকেন্ড-স্তরে হলেও, এটা প্রতি সেকেন্ডে ১০,০০০ operation-এর জন্য — একটা busy production server-এ এই সংখ্যা প্রতি সেকেন্ডে বারবার ঘটে, তাই সঞ্চিত সাশ্রয় CPU-utilization-এ সহজেই লক্ষণীয়।

নিজে বাড়ান: VMEXIT_COST_US বদলে দেখুন হালকা (০.৫ µs, নতুন CPU/VPID-optimized) বনাম ভারী (৩-৪ µs, পুরনো CPU) hardware-এ সাশ্রয়ের হিসাব কীভাবে বদলায় — আর একটা তৃতীয় ফাংশন লিখুন যা interrupt-coalescing (host থেকে guest-এর দিকে ফেরত ইনটারাপ্টও batching করা) যোগ করলে মোট round-trip সংখ্যা আরও কীভাবে কমে তা মডেল করে।

বাস্তব সিস্টেমে

বাস্তব সিস্টেমে paravirtualization ও virtio

  • AWS EC2, Nitro system-এর আগে ও পরে — পুরনো Xen-ভিত্তিক EC2 instance টাইপ (m3, c3-জাতীয়) সরাসরি Xen PV বা Xen PVHVM (hybrid — HVM CPU/memory + PV I/O drivers) ব্যবহার করত। আধুনিক Nitro instance-এও (m5, c5 ইত্যাদি) নেটওয়ার্ক ও storage I/O virtio-ভিত্তিক প্রোটোকলের (ENA, NVMe নিজেই একটা standardized “PV-like” queue-based ইন্টারফেস) উপর দাঁড়িয়ে — একই মূলনীতি, নতুন প্রজন্মের হার্ডওয়্যার-অফলোডেড রূপে।

  • Google Compute Engine, Azure — উভয়ই ডিফল্টভাবে virtio-net (বা GCE-র gVNIC-এর মতো তুলনামূলক আরও optimized ভ্যারিয়েন্ট) আর virtio-blk/virtio-scsi ব্যবহার করে standard VM image-এ। “Install VirtIO drivers” cloud console-এর ডকুমেন্টেশনে Windows guest-এর জন্য একটা পরিচিত ধাপ — Linux guest-এ সাধারণত kernel-এ built-in থাকায় আলাদা install লাগে না।

  • Windows guest-এ “PV drivers” ইনস্টল — Windows-এর নেটিভ কার্নেল virtio-aware না, তাই cloud provider-রা একটা signed virtio driver package সরবরাহ করে (যেমন Fedora Project-এর virtio-win ISO, RedHat-স্পন্সরড, বহু hypervisor প্ল্যাটফর্মে ব্যবহৃত) — এটা ইনস্টল না করলে Windows guest হয় ধীর, emulated IDE/e1000 driver ব্যবহার করে চলবে (আগের লেসনের full-device-emulation পথ), অথবা কিছু ক্ষেত্রে boot-ই করবে না যদি boot disk virtio-blk-এ থাকে আর driver অনুপস্থিত থাকে।

  • VirtualBox-এর “Guest Additions” — একটা ভিন্ন hypervisor product, কিন্তু একই ধারণা — install করলে network/graphics/clipboard driver paravirtualized, দ্রুততর পথে চলে। নাম আলাদা (“Guest Additions” বনাম “PV drivers” বনাম “virtio-win”), অন্তর্নিহিত কৌশল একই।

  • NVMe over virtio-এর মতো hybrid design-এর উত্থান — আধুনিক cloud NVMe storage প্রায়ই একটা queue-based, batching-বান্ধব ইন্টারফেস উন্মুক্ত করে যা virtqueue-র নকশা-দর্শনের সাথে হুবহু মেলে (submission queue/completion queue জোড়া, ঠিক available/used ring-এর মতো) — দেখায় paravirtualization-এর “batched, explicit ring-based interface” ধারণাটা শুধু ভার্চুয়ালাইজেশনেই থামেনি, আধুনিক real hardware storage protocol ডিজাইনকেও প্রভাবিত করেছে।

  • Xen আজও বেঁচে আছে, কিন্তু ভিন্ন ভূমিকায় — pure PV mode আজ কার্যত পরিত্যক্ত, কিন্তু Xen নিজেই টিকে আছে (AWS-এর পুরনো ইতিহাস, Qubes OS-এর মতো security-কেন্দ্রিক প্রজেক্ট) HVM + PV-drivers hybrid মডেলে — ঠিক এই লেসনের কেন্দ্রীয় দাবির জীবন্ত উদাহরণ: CPU/memory hardware-assisted, I/O paravirtualized।

যে ভুলগুলো সবাই করে

“Paravirtualization মানেই পুরনো, মৃত প্রযুক্তি — VT-x/EPT আসার পর এটার আর কোনো প্রাসঙ্গিকতা নেই।”

সম্পূর্ণ উল্টো — শুধু তার প্রয়োগক্ষেত্র বদলেছে। CPU আর memory virtualization-এ (Xen-এর মূল ব্যবহার) সত্যিই এর প্রয়োজন কমেছে, কারণ VT-x/EPT সেই দুইটা সমস্যা হার্ডওয়্যারে সমাধান করেছে। কিন্তু I/O-তে paravirtualization আজও প্রায় সর্বত্র — virtio-net, virtio-blk, virtio-scsi ছাড়া একটা আধুনিক production cloud VM কল্পনা করা কঠিন। “মৃত প্রযুক্তি” বলাটা এই ভুল ধারণা থেকে আসে যে paravirtualization মানে শুধু “Xen PV নামের সেই নির্দিষ্ট, পুরনো কৌশল” — যেখানে আসলে এটা একটা সাধারণ ডিজাইন-নীতি (explicit hypercall বনাম trap-and-decode) যা জায়গা বদলে আজও সক্রিয়।

“virtio driver ইনস্টল করা মানেই automatically সব I/O দ্রুত হয়ে যাবে, batch size নিয়ে ভাবার দরকার নেই।”

এই লেসনের build অংশের সংখ্যাই এর বিপরীত প্রমাণ — batch size ১-এ virtio-র VM-exit সংখ্যা full-device-emulation-এর সমান, কোনো লাভ নেই। virtio-র পুরো সুবিধা আসে batching থেকে, driver বা guest OS নিজে থেকে batch করলে তবেই। একটা latency-sensitive workload (যেমন একটা RPC যেখানে প্রতিটা request অবিলম্বে পাঠানো জরুরি, batching-এর জন্য অপেক্ষা করলে latency বাড়ে) হয়তো ইচ্ছাকৃতভাবেই কম batch করে — এবং তখন virtio ও full-emulation-এর VM-exit সংখ্যা কাছাকাছি থাকতে পারে, যদিও virtio-র protocol overhead (কোনো register-decode লাগে না) তখনও কিছুটা সুবিধা দেয়।

“virtio মানে Linux-নির্দিষ্ট একটা KVM feature।”

virtio একটা hypervisor-স্বাধীন, open standard (আজ OASIS-এর অধীনে) — এর জন্ম হয়েছিল ঠিক এই কারণে যে Xen, KVM, VMware প্রতিটাই আলাদা আলাদা PV driver বানাচ্ছিল, একই কাজের জন্য। একই virtio-net driver অপরিবর্তিত KVM, Xen (HVM+PV-drivers mode), এমনকি অন্য compliant hypervisor-এও চলে — guest OS কোনো নির্দিষ্ট hypervisor-এর জন্য আলাদা driver লেখার দরকার নেই। “Linux-নির্দিষ্ট” ভুল ধারণাটা আসে এই থেকে যে Linux kernel-এ virtio driver বহু আগে থেকেই built-in (upstream-এ merge হয়েছে), যেখানে Windows-এ আলাদা install লাগে — কিন্তু driver-এর উপস্থিতি/অনুপস্থিতি standard-টার hypervisor-নিরপেক্ষতাকে বদলায় না।

“Hypercall আর ঐতিহাসিক trap-and-emulate আসলে একই জিনিস — দুটোই তো guest থেকে hypervisor-এ নিয়ন্ত্রণ পাঠায়।”

দুইটাই নিয়ন্ত্রণ hypervisor-এ পাঠায় ঠিকই, কিন্তু কী পাঠানো হয় এবং কেন পাঠানো হয় তাতে মৌলিক পার্থক্য। Trap-and-emulate-এ guest জানে না সে trap ট্রিগার করছে — সে ভাবছে সে সরাসরি hardware-এ লিখছে, hypervisor চুপচাপ সেই write আটকে দেয় (guest-এর অজান্তে)। Hypercall-এ guest সচেতনভাবে, ইচ্ছাকৃতভাবে hypervisor-কে ডাকছে — ঠিক যেমন একটা userspace প্রোগ্রাম জানে সে syscall চালাচ্ছে, ভুলবশত না। এই পার্থক্যটাই decode ধাপ বাদ দেয়: hypercall-এ hypervisor আগে থেকেই জানে ঠিক কোন hypercall নম্বর, কোন argument আসবে (একটা প্রকাশিত API contract), সেখানে trap-and-emulate-এ hypervisor-কে প্রতিবার অনুমান করে বের করতে হয় guest আসলে কী করার চেষ্টা করছিল।

বুঝেছেন কি না দেখুন

1

Full-device-emulation আর paravirtualization-এর মধ্যে মূল পার্থক্যটা এক বাক্যে বলুন — guest driver, hypervisor-এর কাজ, দুইটাই উল্লেখ করে।

স্মরণ

Full-device-emulation-এ guest driver unmodified থাকে, সে বাস্তব হার্ডওয়্যার register প্রোগ্রাম করার ভান করে, আর hypervisor প্রতিটা access trap করে decode করে guest-এর intent অনুমান করে; paravirtualization-এ guest kernel modified, সে সরাসরি একটা explicit hypercall চালিয়ে hypervisor-কে বলে দেয় সে কী চায় — decode/অনুমান করার ধাপ থাকে না।

2

Xen PV কীভাবে Popek-Goldberg trap-detection সমস্যা “সমাধান” করেছিল? এটা কি আসলেই সমস্যাটা সমাধান করেছিল, নাকি অন্য কিছু করেছিল?

যুক্তি

Popek-Goldberg সমস্যাটা ছিল: x86-এর কিছু sensitive instruction হার্ডওয়্যারে trap করে না, তাই hypervisor সেগুলো ধরতে পারে না। Xen PV এই সমস্যাটা সমাধান করেনি — বরং সম্পূর্ণ পাশ কাটিয়ে গেছে: guest kernel-কেই এমনভাবে modify করা হয়েছিল যাতে সেই সমস্যাযুক্ত instruction-গুলো guest কখনোই না চালায় — তার জায়গায় একটা hypercall বসানো হয়েছিল, যেটা design অনুযায়ীই trap করে। যেহেতু সমস্যাযুক্ত instruction চালানোই হচ্ছে না, “সেটা detect করা যাচ্ছে না” এই প্রশ্নটাই অবান্তর হয়ে যায়।

3

একটা আধুনিক KVM VM-এ VT-x/EPT পুরোপুরি সক্রিয়, guest kernel সম্পূর্ণ unmodified upstream Linux। তবু এই VM-এ virtio-blk driver ব্যবহার করা হচ্ছে disk I/O-র জন্য। এটা কি একটা দ্বন্দ্ব — “সম্পূর্ণ hardware-virtualized” আর “paravirtualized I/O” কি একসাথে থাকতে পারে?

প্রয়োগ

দ্বন্দ্ব নেই — এটাই আধুনিক বাস্তবতার স্বাভাবিক রূপ। VT-x/EPT CPU execution আর memory address translation-কে hardware-এ virtualize করে, এই দুইটাতে আর কোনো paravirtualization দরকার নেই (তাই kernel unmodified থাকতে পারে, সাধারণ sensitive instruction হার্ডওয়্যারেই trap করে)। কিন্তু disk device emulation-এ VT-x/EPT কোনো সাহায্য করে না — একটা real AHCI/e1000 controller অনুকরণ করা তখনও ব্যয়বহুল থেকে যায়। তাই সেই একটা নির্দিষ্ট জায়গায় (I/O), guest ইচ্ছাকৃতভাবে virtio driver ব্যবহার করে — এই দুইটা কৌশল ভিন্ন স্তরে কাজ করে (CPU/memory বনাম I/O device), তাই একসাথে থাকা সম্পূর্ণ স্বাভাবিক, বরং এটাই আজকের standard practice।

4

build অংশের সিমুলেশনে batch size ৩২ থেকে ২৫৬-এ যাওয়ার সাশ্রয় (~১৪.৫৩ ms থেকে ~১৪.৯৪ ms) খুবই ছোট, কিন্তু ১ থেকে ৩২-এ যাওয়ার সাশ্রয় (০ থেকে ~১৪.৫৩ ms) বিশাল। এই diminishing-returns প্যাটার্নটা গণিতগতভাবে কেন ঘটে?

যুক্তি

VM-exit সংখ্যা ceil(N / batch) — batch দ্বিগুণ করলে VM-exit সংখ্যা প্রায় অর্ধেক হয়, কিন্তু সাশ্রয় নির্ভর করে full_emulation - virtio_exits = N - N/batch = N × (1 - 1/batch)-এর উপর। Batch ছোট থেকে বড় হওয়ার শুরুতে 1/batch দ্রুত ছোট হয় (১ → ০.৫ → ০.২৫…), কিন্তু batch যত বড় হয় 1/batch তত ছোট এমনিতেই হয়ে যায় (৩২-এ ০.০৩১, ২৫৬-এ ০.০০৪) — ফলে আরও দ্বিগুণ করলে অবশিষ্ট 1/batch-এর উপর প্রভাব কমতে থাকে। এটা একটা classic 1/x আকৃতির diminishing-returns curve, exponential batch-বৃদ্ধিতেও।

5

আপনি একটা নতুন cloud storage backend ডিজাইন করছেন যেখানে দুই ধরনের workload আছে — (ক) একটা log-shipping service যা প্রতি সেকেন্ডে হাজার হাজার ছোট append লেখে, latency নিয়ে চিন্তিত নয়, শুধু throughput চায়; (খ) একটা database transaction commit path যেখানে প্রতিটা write-এর latency সরাসরি user-facing response time-কে প্রভাবিত করে। virtio batching নীতি দুইটাতে কীভাবে ভিন্ন সেট করবেন, আর কেন?

ডিজাইন

(ক)-এর জন্য বড় batch size (যেমন এই লেসনের ১২৮-২৫৬-এর রেঞ্জ) — throughput-focused, প্রতিটা লেখা কিছু মিলিসেকেন্ড দেরিতে kick হলেও সমস্যা নেই, VM-exit সংখ্যা সর্বনিম্ন রাখাই লক্ষ্য, আর build-এর সংখ্যা দেখায় ১২৮+ batch-এ diminishing returns শুরু হয়ে গেলেও এখনও ~৯৯% হ্রাস বজায় থাকে। (খ)-এর জন্য ছোট batch (এমনকি কার্যত batch-হীন, প্রতিটা commit অবিলম্বে kick) — এখানে সিদ্ধান্তটা raw VM-exit-count optimization-এর বিপরীতে যায়, কারণ latency-র দাম batching-এর সাশ্রয়ের চেয়ে বেশি গুরুত্বপূর্ণ; misconception-এর দ্বিতীয় পয়েন্টের সরাসরি প্রয়োগ — virtio driver থাকলেই automatically সব দ্রুত হয় না, batch policy workload-এর priority অনুযায়ী সচেতনভাবে বেছে নিতে হয় (many real block-device drivers এখানে একটা tunable “notify-after-N” বা explicit flush/fsync-triggered kick প্যাটার্ন ব্যবহার করে ঠিক এই trade-off সামলাতে)।

6

একজন সহকর্মী একটা পুরনো Windows Server 2003 VM migrate করছেন KVM-এ, আর দেখছেন disk throughput ভয়াবহ ধীর, CPU-তে qemu-system-x86_64 প্রসেস প্রায় একটা পুরো core খেয়ে ফেলছে অথচ guest-এর ভেতরের disk-activity সামান্যই। সবচেয়ে সম্ভাব্য কারণ কী, আর কীভাবে যাচাই করবেন?

প্রয়োগ

সবচেয়ে সম্ভাব্য কারণ: VM-টা এখনও emulated IDE/AHCI controller ব্যবহার করছে (full-device-emulation পথ), virtio-blk driver ইনস্টল করা হয়নি — Windows guest নেটিভভাবে virtio-aware না, তাই migrate করার পর ডিফল্টভাবে emulated controller-এই পড়ে যায়। প্রতিটা disk operation তখন একটা VM-exit + QEMU-তে register-decode দাবি করছে, যা ঠিক ব্যাখ্যা করে কেন qemu-system-x86_64 প্রসেসের CPU খরচ বেশি অথচ guest-এর দৃশ্যমান কাজ কম — সময়টা useful I/O-তে না, trap-emulate overhead-এ যাচ্ছে। যাচাই: guest-এর ভেতরে Device Manager-এ disk controller-এর নাম দেখা (IDE ATA/ATAPI controller বনাম Red Hat VirtIO SCSI controller), অথবা host-এ QEMU-র command line/libvirt XML-এ disk-এর bus অ্যাট্রিবিউট (ide বনাম virtio) পরীক্ষা করা। সমাধান: virtio-win ISO থেকে driver ইনস্টল করে disk bus virtio-blk/virtio-scsi-তে পরিবর্তন করা।

এরপর কী

এরপর কী

এই লেসন পর্যন্ত পুরো VM stack-টা একবার সম্পূর্ণ ঘুরে দেখা হলো — theory (Popek-Goldberg, hypervisor টাইপ), hardware সাহায্য (VT-x, EPT), বাস্তবায়ন (kvm-qemu), আর এখন paravirtualized I/O (Xen PV-র ইতিহাস, virtio-র বর্তমান)। এই সবকিছুর একটা অভিন্ন থিম ছিল: একটা দ্বিতীয়, পূর্ণাঙ্গ kernel চালিয়ে isolation পাওয়া — guest OS নিজের memory space, নিজের scheduler, নিজের সবকিছু নিয়ে।

কিন্তু একটা সম্পূর্ণ ভিন্ন পথ আছে isolated, deployable workload পাওয়ার — যেটা দ্বিতীয় কোনো kernel চালায়ই না। পরের লেসনে (virtualization-cloud/containers-vs-vms) আমরা container-এর দিকে যাব — আর ঠিক কেন এগুলোকে “lightweight VM” বলাটা একটা সাধারণ কিন্তু বিভ্রান্তিকর shorthand, সেটা স্পষ্ট করে দেখাব। Isolation যদি একটা দ্বিতীয় kernel ছাড়াই সম্ভব হয়, তাহলে VM আর container-এর নিরাপত্তা-সীমা আসলে কতটা ভিন্ন — সেই প্রশ্নই পরের লেসনের driving question।

আরও পড়ুন

  • Xen and the Art of Virtualization — Paul Barham, Boris Dragovic, Keir Fraser, Steven Hand, Tim Harris, Alex Ho, Rolf Neugebauer, Ian Pratt, Andrew Warfield — SOSP 2003 · Paravirtualization-এর জন্মপত্র — sensitive instruction-কে hypercall দিয়ে প্রতিস্থাপনের মূল যুক্তি এখানেই প্রথম প্রকাশিত
  • Virtual I/O Device (VIRTIO) Version 1.2 Specification — OASIS Open · virtqueue, descriptor ring, avail/used ring-এর প্রামাণ্য স্পেসিফিকেশন
  • virtio: Towards a De-Facto Standard for Virtual I/O Devices — Rusty Russell — ACM SIGOPS Operating Systems Review, 42(5), 2008 · virtio-র মূল ডিজাইন পেপার — কেন প্রতিটা hypervisor-এর জন্য আলাদা PV driver লেখার বদলে একটা common front-end/back-end স্তর দরকার ছিল