সরাসরি পর্বে যান
পর্ব ৫৮ · ৬৪-এর মধ্যে · মডিউল ৯

xv6 — একটি ছোট শিক্ষণ-OS-এর ভেতরে ঘোরা

A Guided Tour of xv6 — Boot, Processes, System Calls, Page Tables, File System and Locks in a Teaching OS

Taught in Bangla, this lesson is a guided tour of xv6, the RISC-V teaching operating system: boot to the shell prompt, processes, the scheduler and swtch, the full path of a write system call, Sv39 page tables with walk() and kalloc, the seven-layer file system, spinlocks, push_off and sleeplocks, the shell's use of fork, exec and pipe in sh.c, and how to run and modify xv6 yourself.

এই পর্বে যা শিখবেন

  • শিক্ষণ-OS কেন আছে — Unix V6 আর Lions-এর বই থেকে MINIX, Pintos আর xv6 পর্যন্ত
  • xv6-এর সোর্সের মানচিত্র: কোন ফাইলে কী, আর কোর্সের কোন পর্বের সাথে মেলে
  • বুট থেকে $ prompt পর্যন্ত, আর একটি write system call-এর পুরো যাত্রা
  • Sv39 page walk হাতে করা, kalloc-এর free list আর xv6 ফাইলের সর্বোচ্চ আকারের হিসাব
  • নিজের কম্পিউটারে xv6 চালানো আর কোন কোন অনুশীলনী দিয়ে kernel-এ হাত দেওয়া শুরু করবেন

১ · কেন একটি “খেলনা” OS — আসলে খেলনা নয়

এতগুলো পর্বে আপনি process, scheduling, lock, paging আর file system আলাদা আলাদা শিখেছেন। কিন্তু একটি প্রশ্ন থেকেই যায়: এগুলো একটি আসল kernel-এ একসাথে কীভাবে বসে? Linux kernel-এ উত্তর আছে, কিন্তু তার সোর্স কয়েক কোটি লাইনের। একজন মানুষ সারাজীবনেও পুরোটা পড়তে পারবেন না।

সমাধান হলো একটি শিক্ষণ-OS — এমন একটি সম্পূর্ণ, চালানো যায় এমন অপারেটিং সিস্টেম, যা ইচ্ছা করেই ছোট আর সরল রাখা হয়েছে। এতে আসল process, আসল page table, আসল disk driver আছে; শুধু অপ্রয়োজনীয় জটিলতা নেই।

মেডিকেল কলেজের অ্যানাটমি ক্লাসের কঙ্কাল আর মডেলের কথা ভাবুন। একজন শিক্ষার্থী প্রথম দিনেই হাসপাতালের অপারেশন থিয়েটারে ঢোকেন না। আগে একটি পরিষ্কার মডেলে দেখেন কোন হাড় কোথায়, কোন শিরা কোন দিকে যায়। মডেলটি আসল দেহের চেয়ে সরল, কিন্তু ভুল নয় — প্রতিটি অংশ আসল জায়গাতেই আছে। xv6 ঠিক সেই মডেল: Linux-এর চেয়ে বহুগুণ সরল, কিন্তু প্রতিটি অংশ আসল কাজটাই করে।

একটি ঐতিহ্যের গল্প। Bell Labs-এর Ken Thompson আর Dennis Ritchie-র তৈরি Unix Version 6 (V6) ১৯৭৫ সালের দিকে বিশ্ববিদ্যালয়গুলোতে ছড়ায়। অস্ট্রেলিয়ার University of New South Wales-এর অধ্যাপক John Lions ১৯৭৬ সালের দিকে V6-এর পুরো kernel সোর্স আর তার লাইন-ধরে ব্যাখ্যা নিয়ে একটি বই লেখেন — Lions' Commentary on UNIX 6th Edition. পরে Unix-এর লাইসেন্স কড়া হলে বইটির আনুষ্ঠানিক বিতরণ বন্ধ হয়ে যায়, কিন্তু ফটোকপি হাতে হাতে বহু বছর ঘুরেছে; আনুষ্ঠানিকভাবে বই হয়ে বেরোয় ১৯৯০-এর দশকের শেষে।

লাইসেন্সের এই বাধার কারণেই Andrew Tanenbaum ১৯৮৭ সালে নিজের পাঠ্যবইয়ের সাথে MINIX লেখেন। আর ১৯৯১ সালে ফিনল্যান্ডের এক ছাত্র, Linus Torvalds, MINIX ব্যবহার করতে করতেই Linux শুরু করেন। Stanford-এ তৈরি হয় Pintos (২০০০-এর দশকের মাঝামাঝি, x86-এর জন্য), যেখানে শিক্ষার্থীরা নিজেরা thread, user program আর virtual memory যোগ করেন। Berkeley-র Nachos আর Harvard-এর OS/161-ও একই ধারার।

MIT-র অপারেটিং সিস্টেম কোর্স (প্রথমে 6.828, পরে 6.S081, এখন 6.1810 নামে) বহু বছর V6 আর Lions-এর বই পড়াত। কিন্তু PDP-11-এর assembly আর ১৯৭০-এর দশকের C ক্রমে অচেনা হয়ে যাচ্ছিল। তাই ২০০৬ সালের দিকে Russ Cox, Frans Kaashoek আর Robert Morris V6-এর গঠন মেনে আধুনিক ANSI C-তে x86-এর জন্য নতুন করে লেখেন — নাম রাখেন xv6. ২০১৯ সালের দিকে এটি RISC-V-তে নেওয়া হয়, আর x86 সংস্করণটি এখন রক্ষণাবেক্ষণ হয় না। এই পর্বের সব কোড RISC-V সংস্করণের (mit-pdos/xv6-riscv).

📘 Unix V6 + Lions

আসল ঐতিহাসিক kernel, PDP-11-এর জন্য। পড়ার জন্য অসাধারণ, কিন্তু আজকের হার্ডওয়্যারে চালাতে emulator লাগে।

পড়া, ইতিহাস

🧩 MINIX

Microkernel নকশা — driver আর file system আলাদা user-mode প্রসেসে। পাঠ্যবইয়ের সাথে পুরো সোর্স।

নকশার তুলনা

🛠️ Pintos

একটি কাঠামো দেওয়া থাকে; শিক্ষার্থীরা ধাপে ধাপে scheduler, system call আর virtual memory লেখেন।

প্রজেক্ট-ভিত্তিক

🐚 xv6

Unix-ধাঁচের ছোট monolithic kernel, multicore, RISC-V. সাথে একটি বিনামূল্যের বই আর ছোট ছোট lab.

পুরোটা পড়া ও বদলানো

xv6-এর সঙ্গী একটি ছোট বই: “xv6: a simple, Unix-like teaching operating system”, লেখক Russ Cox, Frans Kaashoek আর Robert Morris — MIT-র ওয়েবসাইটে বিনামূল্যে পাওয়া যায়। এই পর্বের প্রতিটি অংশ ওই বইয়ের এক-একটি অধ্যায়ের সাথে মেলে; পর্বটি শেষ করে বইটি ধরলে অনেক সহজ লাগবে।

সোর্সের মানচিত্র — কোন ফাইলে কী

xv6-এর repository-তে দুটি প্রধান ফোল্ডার: kernel/ আর user/. ২০২৬ সালের অক্টোবরের riscv branch-এ kernel/-এর ৪৪টি ফাইলে মোট প্রায় ৬,৬০০ লাইন — header আর assembly মিলিয়ে। সংখ্যাটি version-ভেদে কিছুটা বদলায়, কিন্তু মাত্রাটা একই: কয়েক হাজার লাইন, কয়েক কোটি নয়।

নিচের টেবিলটি এই পর্বের মানচিত্র। ডান দিকের কলাম বলছে, কোর্সের কোন পর্বের ধারণা কোন ফাইলে কোড হয়ে বসেছে।

ফাইলকী করেকোর্সের কোন পর্ব
entry.S, start.c, main.cবুট: stack বসানো, machine mode থেকে supervisor mode, সব subsystem চালুপর্ব ০৭ (বুট)
proc.c, proc.h, swtch.Sপ্রসেস টেবিল, fork/exit/wait, scheduler, sleep/wakeup, context switchপর্ব ০৮–১০, ১৮, ২৫
trampoline.S, trap.c, kernelvec.Suser আর kernel-এর মধ্যে ঢোকা-বেরোনো, interrupt, exceptionপর্ব ০৪, ০৫
syscall.c, sysproc.c, sysfile.csystem call বণ্টন আর প্রতিটির handlerপর্ব ০৫
vm.c, kalloc.c, exec.cpage table, খালি পাতার allocator, প্রোগ্রাম লোডপর্ব ৩২, ৩৩, ৩৫
spinlock.c, sleeplock.cদুই ধরনের lockপর্ব ২৩, ২৪
bio.c, log.c, fs.c, file.c, pipe.cbuffer cache, logging, inode আর directory, file table, pipeপর্ব ১২, ৪১–৪৪
uart.c, console.c, plic.c, virtio_disk.cserial console-এর driver, interrupt controller, disk driverপর্ব ৩৯
user/init.c, user/sh.cপ্রথম user প্রোগ্রাম আর shellপর্ব ০৩, ১১
user/ulib.c, user/usys.plছোট C library; প্রতিটি system call-এর assembly stub তৈরির scriptপর্ব ০৫

Hardware হিসেবে xv6 চলে QEMU-র virt নামের একটি কাল্পনিক RISC-V বোর্ডে: ১২৮ MiB RAM, একটি UART serial port (কীবোর্ড আর স্ক্রিন), একটি virtio disk, একটি PLIC interrupt controller আর ডিফল্টভাবে ৩টি CPU core — RISC-V-এর ভাষায় ৩টি hart. Physical memory-র মানচিত্র memlayout.h-এ লেখা: UART থাকে 0x10000000-এ, disk 0x10001000-এ, আর RAM শুরু হয় 0x80000000 থেকে।

কোড সম্পর্কে একটি কথা। এই পর্বের প্রতিটি “xv6 থেকে” লেবেলের কোড github.com/mit-pdos/xv6-riscv-এর riscv branch থেকে নেওয়া (অক্টোবর ২০২৬-এর commit f780d78), ছোট অংশ, হুবহু — শুধু কিছু মন্তব্য বাদ। xv6 MIT লাইসেন্সে প্রকাশিত, কপিরাইট Frans Kaashoek, Robert Morris, Russ Cox ও MIT. xv6 এখনো বদলায়: বইয়ের পুরোনো সংস্করণ বা পুরোনো checkout-এ কিছু ফাংশনের নাম ও গঠন আলাদা পাবেন, আর যেখানে পার্থক্য বড়, সেখানে বলে দেওয়া হয়েছে।

২ · বুট — বিদ্যুৎ থেকে $ prompt পর্যন্ত

পর্ব ০৭-এ দেখেছিলেন, একটি PC-তে firmware (BIOS বা UEFI) → bootloader → kernel → init — এই শিকল। xv6-এ শিকলটি অনেক ছোট, কারণ QEMU নিজেই kernel ফাইলটি RAM-এর 0x80000000 ঠিকানায় বসিয়ে দেয়; আলাদা bootloader লাগে না। প্রতিটি CPU core সেখান থেকে চলা শুরু করে।

RISC-V-এ তিনটি privilege স্তর আছে। সবচেয়ে ক্ষমতাবান machine mode (M) — চালু হওয়ার সময় CPU এখানে থাকে। Kernel চলে supervisor mode-এ (S), আর সাধারণ প্রোগ্রাম user mode-এ (U). xv6 machine mode-এ থাকে মাত্র কয়েক লাইন — দরকারি সেটিং করেই নেমে আসে supervisor mode-এ।

// start() jumps here in supervisor mode on all CPUs.
void
main()
{
  if (cpuid() == 0) {
    consoleinit();
    printkinit();
    printk("\n");
    printk("xv6 kernel is booting\n");
    printk("\n");
    kinit();            // physical page allocator
    kvminit();          // create kernel page table
    kvminithart();      // turn on paging
    procinit();         // process table
    trapinit();         // trap vectors
    trapinithart();     // install kernel trap vector
    plicinit();         // set up interrupt controller
    plicinithart();     // ask PLIC for device interrupts
    binit();            // buffer cache
    iinit();            // inode table
    fileinit();         // file table
    virtio_disk_init(); // emulated hard disk
    userinit();         // first user process
    /* ... then tell the other harts to start ... */
  }
  /* ... other harts: kvminithart(), trapinithart(), plicinithart() ... */
  scheduler();
}

লক্ষ করুন, main() নিজেই একটি সূচিপত্র — প্রতিটি লাইন একটি subsystem, আর প্রায় প্রতিটি কোর্সের একটি পর্ব। ক্রমটিও অর্থপূর্ণ: page table বানাতে খালি পাতা লাগে, তাই kinit() আগে; প্রথম প্রসেস বানাতে প্রসেস টেবিল লাগে, তাই procinit() আগে। নিচের stepper-এ পুরো যাত্রাটি দেখুন:

1 QEMU: kernel -> RAM 0x800000002 entry.S: sp = stack0 + (hartid+1)*40963 start(): mepc = main; mret -> S-mode4 main(): kinit ... virtio_disk_init5 userinit(): allocproc -> pid 16 scheduler(): swtch -> forkret7 forkret(): fsinit; kexec("/init")8 init: open console; fork; exec("sh")9 sh: write(2, "$ ", 2)
অবস্থা (ধাপের শেষে)
CPU mode
Paging
proc টেবিল
  1. QEMU kernel বসায়। make qemu-এর কমান্ডে -kernel kernel/kernel থাকে। QEMU ফাইলটি RAM-এর 0x80000000-এ রাখে, আর প্রতিটি hart সেখান থেকে machine mode-এ চলা শুরু করে। এখনো paging নেই, প্রসেস নেই, console-এ কিছু নেই।
  2. _entry — C চালানোর আগে stack চাই। entry.S-এর কয়েকটি instruction প্রতিটি hart-এর জন্য ৪০৯৬ byte-এর আলাদা stack বসায় (stack0 অ্যারের ভেতরে, hart নম্বর দিয়ে আলাদা করা), তারপর call start. Stack ছাড়া কোনো C ফাংশন ডাকা যায় না — local variable আর return address রাখার জায়গা লাগে।
  3. start() — machine mode থেকে নামা। সব interrupt আর exception supervisor mode-এ পাঠানোর ব্যবস্থা (medeleg, mideleg), timer চালু, satp = 0 দিয়ে paging আপাতত বন্ধ। তারপর mepc-তে main-এর ঠিকানা রেখে mret — যেন একটি “ফেরা”, কিন্তু আসলে supervisor mode-এ main()-এ প্রবেশ।
  4. main() — সব subsystem চালু। Hart 0 console চালু করে প্রথম বার্তা ছাপে, তারপর page allocator, kernel page table (এখানেই paging চালু হয়), প্রসেস টেবিল, trap, interrupt controller, buffer cache, inode টেবিল, file টেবিল আর disk. প্রসেস টেবিলের ৬৪টি (NPROC) ঘরই এখন UNUSED.
  5. userinit() — প্রথম প্রসেস। allocproc() একটি খালি ঘর নেয়, pid দেয় ১, trapframe-এর জন্য একটি পাতা আর একটি খালি user page table বানায়, আর context.ra-তে রাখে forkret-এর ঠিকানা। তারপর state = RUNNABLE. প্রসেসটি এখনো চলেনি — শুধু “চলার জন্য তৈরি”।
  6. scheduler() — প্রথম context switch. বাকি hart-গুলো এখন জেগে নিজের বার্তা ছাপে (কোনটি আগে, তা প্রতিবার আলাদা হতে পারে)। প্রতিটি hart নিজের scheduler লুপে ঢোকে। একটি hart pid ১-কে RUNNABLE পায়, RUNNING করে, আর swtch করে — ফলে CPU লাফ দেয় forkret-এ, কারণ context.ra-তে সেটিই রাখা ছিল।
  7. forkret() — file system আর /init. প্রথমবার এখানে fsinit() চলে, কারণ disk পড়তে sleep লাগে, আর sleep শুধু একটি প্রসেসের ভেতর থেকেই ডাকা যায় — main() থেকে নয়। তারপর kexec("/init") disk থেকে প্রোগ্রামটি লোড করে, আর trampoline দিয়ে user mode-এ নামা।
  8. /init — console আর shell. init.c console ডিভাইস খোলে (fd ০), দুবার dup করে fd ১ ও ২ বানায়, “init: starting sh” ছাপে, তারপর fork(); সন্তান (pid ২) exec("sh") করে। Init নিজে wait()-এ ঘুমায় — shell বেরিয়ে গেলে আবার চালু করবে, আর অনাথ প্রসেসদেরও সে-ই তুলে নেয়।
  9. Shell — $ prompt. sh.c-এর getcmd() লেখে write(2, "$ ", 2), তারপর কীবোর্ডের জন্য অপেক্ষা করে। পুরো বুট শেষ। পরের অংশে ঠিক এই write-টির পেছনে কী ঘটে, তা অনুসরণ করব।

পুরোনো সংস্করণে একটু আলাদা। ২০২৫ সালের আগের xv6-এ (আর বইয়ের সেই সময়ের সংস্করণে) userinit() প্রথমে initcode নামের কয়েক byte-এর একটি ছোট user প্রোগ্রাম মেমরিতে বসাত, আর সেটিই exec("/init") system call করত। এখনকার কোডে kernel নিজেই forkret() থেকে /init লোড করে। ধারণা একই — প্রথম user প্রসেস /init, যার pid ১ — শুধু পথটি ছোট হয়েছে। পুরোনো টিউটোরিয়াল বা AI-এর উত্তরে initcode.S দেখলে অবাক হবেন না।

৩ · প্রসেস, scheduler আর swtch

পর্ব ০৮-এ xv6-এর struct proc পুরোটা দেখেছিলেন — state, pid, parent, kernel stack, page table, trapframe, context, খোলা ফাইলের অ্যারে। এখানে মনে রাখুন শুধু ছয়টি অবস্থা:

enum procstate { UNUSED, USED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };

পাঠ্যবইয়ের পাঁচ-অবস্থার মডেলের (পর্ব ০৮) সাথে মিলিয়ে নিন: RUNNABLE হলো ready, SLEEPING হলো waiting/blocked, ZOMBIE হলো terminated কিন্তু parent এখনো wait() করেনি (zombie). বাড়তি দুটি — UNUSED মানে টেবিলের ঘরটি খালি, আর USED মানে ঘরটি নেওয়া হয়েছে কিন্তু প্রসেস তৈরি এখনো শেষ হয়নি। xv6-এ প্রসেস টেবিল একটি স্থির অ্যারে, struct proc proc[NPROC], যেখানে NPROC = ৬৪ — তাই ৬৫তম প্রসেস তৈরি হয় না, fork() −১ ফেরত দেয়।

Scheduler — একটি অন্তহীন লুপ

পর্ব ১৮-এর round robin মনে আছে? xv6-এর scheduler তার সবচেয়ে সরল রূপ। প্রতিটি CPU-র নিজের একটি scheduler লুপ, যা প্রসেস টেবিলের ওপর দিয়ে বারবার ঘোরে আর প্রথম যে RUNNABLE প্রসেস পায়, তাকে চালায়:

void
scheduler(void)
{
  struct proc *p;
  struct cpu *c = mycpu();

  c->proc = 0;
  for (;;) {
    intr_on();
    intr_off();

    int found = 0;
    for (p = proc; p < &proc[NPROC]; p++) {
      acquire(&p->lock);
      if (p->state == RUNNABLE) {
        p->state = RUNNING;
        c->proc = p;
        swtch(&c->context, &p->context);

        // Don't re-enable interrupts on release.
        mycpu()->intena = 0;

        c->proc = 0;
        found = 1;
      }
      release(&p->lock);
    }
    if (found == 0) {
      // nothing to run; stop running on this core until an interrupt.
      asm volatile("wfi");
    }
  }
}

কোনো priority নেই, কোনো আলাদা ready queue নেই — ready queue আসলে পুরো proc[] অ্যারে, আর “ক্রম” হলো অ্যারের সূচি। Time quantum হলো timer interrupt-এর ব্যবধান: start.c প্রথম timer বসায় ১০ লাখ টিক পরে (QEMU-তে মোটামুটি ১০০ মিলিসেকেন্ড), আর প্রতিটি timer interrupt-এ usertrap() চলমান প্রসেসকে yield() করায়। কিছুই চালানোর না থাকলে wfi (wait for interrupt) — CPU ঘুমায়, বিদ্যুৎ বাঁচে।

বাস টার্মিনালের একজন ডিসপ্যাচারকে ভাবুন, যাঁর সামনে ৬৪টি প্ল্যাটফর্মের একটি বোর্ড। তিনি বোর্ডের ওপর থেকে নিচে আঙুল চালান; যে বাস “যাত্রী তোলা শেষ, ছাড়ার জন্য তৈরি” দেখায়, তাকে রাস্তায় পাঠান। বাসটি একটি নির্দিষ্ট সময় পরে (timer) বা যাত্রীর জন্য থামতে হলে (sleep) টার্মিনালে ফেরে, আর ডিসপ্যাচার আবার পরের প্ল্যাটফর্ম থেকে খোঁজা শুরু করেন। বোর্ডের নিচে পৌঁছালে আবার ওপর থেকে।

swtch — ১৪টি register রাখা, ১৪টি ফেরত আনা

পর্ব ০৯-এ context switch-এর তত্ত্ব দেখেছিলেন। xv6-এ পুরো কাজটি swtch.S-এর একটি ফাংশন: আগের context-এ ra, sp আর s0–s11 লেখে (১৪টি sd), তারপর নতুন context থেকে একই ১৪টি পড়ে (১৪টি ld), তারপর ret. মোট ২৯টি instruction.

মাত্র ১৪টি কেন, ৩২টি নয়? কারণ swtch একটি সাধারণ C ফাংশনের মতো ডাকা হয়। RISC-V-এর calling convention বলে, ডাকা ফাংশন (callee) শুধু callee-saved register-গুলো (s0–s11, সাথে sp) অক্ষত রাখার দায় নেয়; বাকিগুলো ডাকার আগে caller-এর compiler নিজেই প্রয়োজনমতো stack-এ রেখে দেয়। আর user প্রোগ্রামের সব register? সেগুলো আগেই trapframe-এ জমা হয়েছে, kernel-এ ঢোকার মুহূর্তে (অংশ ৪)।

আরেকটি সূক্ষ্ম বিষয়: xv6-এ এক প্রসেস থেকে সরাসরি আরেক প্রসেসে switch হয় না। পথটি সবসময় প্রসেস A → সেই CPU-র scheduler → প্রসেস B — দুটি swtch. প্রসেস A sched() ডেকে swtch(&p->context, &mycpu()->context) করে scheduler-এ ফেরে, আর scheduler তখন ঠিক swtch-এর পরের লাইন থেকে চলতে থাকে — যেন ফাংশনটি সবে ফেরত এসেছে।

Sleep আর wakeup — condition variable-এর পূর্বপুরুষ

প্রসেস যখন কিছুর জন্য অপেক্ষা করে (disk-এর ব্লক, pipe-এ ডেটা, সন্তানের exit), তখন সে CPU আটকে রাখে না — ঘুমায়। ঘুমায় একটি channel-এর ওপর, যা আসলে যেকোনো একটি ঠিকানা (যেমন disk buffer-এর ঠিকানা)। যে ঘটনাটি ঘটায়, সে wakeup(chan) ডাকে, আর সেই channel-এ ঘুমন্ত সবাই RUNNABLE হয়। বইয়ে আর ২০২৫ সালের সংস্করণে (git tag xv6-riscv-rev5) sleep দেখতে এমন:

void
sleep(void *chan, struct spinlock *lk)
{
  struct proc *p = myproc();

  acquire(&p->lock);  //DOC: sleeplock1
  release(lk);

  // Go to sleep.
  p->chan = chan;
  p->state = SLEEPING;

  sched();

  // Tidy up.
  p->chan = 0;

  // Reacquire original lock.
  release(&p->lock);
  acquire(lk);
}

পর্ব ২৫-এর condition variable-এর সাথে মিলটি স্পষ্ট: pthread_cond_wait(&cv, &m) যেমন mutex ছেড়ে ঘুমায় আর জেগে আবার mutex নেয়, এখানেও তাই। কেন lk নিয়ে ঢুকতে হয়? কারণ “শর্ত মিথ্যা দেখলাম” আর “ঘুমিয়ে পড়লাম” — এর মাঝখানে কেউ wakeup ডেকে ফেললে সংকেতটি হারিয়ে যায় (lost wakeup), আর প্রসেস চিরকাল ঘুমায়। এখানে p->lock ধরার পরে lk ছাড়া হয়, আর wakeup-কেও p->lock নিতে হয় — তাই মাঝখানে ফাঁক থাকে না। জেগে ওঠার পর শর্তটি আবার যাচাই করতে হয় (while লুপে), কারণ একই channel-এ একাধিকজন জাগতে পারে।

সবচেয়ে নতুন কোডে API বদলেছে। ২০২৬ সালের আগস্টে xv6-এর riscv branch-এ sleep দুই ভাগে ভাগ হয়েছে: আগে sleep_prepare(chan) (“আমি এই channel-এ অপেক্ষা করব” — নাম লেখানো), তারপর caller নিজের lock ছাড়ে, তারপর sleep(). মাঝখানে কেউ wakeup ডাকলে সে p->chan শূন্য করে দেয়, আর sleep() সেটা দেখে না ঘুমিয়েই ফিরে আসে। Lost wakeup ঠেকানোর নীতি একই — শুধু “নাম লেখানো” আর “ঘুমানো” আলাদা হয়েছে, অনেকটা Linux-এর prepare_to_wait() → schedule() ধাঁচে। বই বা পুরোনো lab-এ sleep(chan, lk) দেখবেন।

৪ · System call — একটি write-এর পুরো যাত্রা

পর্ব ০৫-এ system call-কে বলেছিলেন user mode থেকে kernel mode-এ যাওয়ার নিয়ন্ত্রিত দরজা। xv6-এ দরজাটির প্রতিটি কবজা দেখা যায়। শুরু user দিক থেকে। user/usys.pl নামের একটি Perl script প্রতিটি system call-এর জন্য তিন লাইনের assembly stub তৈরি করে:

.global write
write:
 li a7, SYS_write     # system call number (16) into a7
 ecall                # trap into the kernel
 ret                  # a0 now holds the return value

RISC-V-এর calling convention অনুযায়ী write(fd, buf, n)-এর তিনটি argument আগে থেকেই a0, a1, a2-তে আছে। Stub শুধু নম্বরটি a7-এ রাখে আর ecall চালায়। ecall একটি বিশেষ instruction: CPU সাথে সাথে supervisor mode-এ যায়, কারণ-register scause-এ লেখে ৮ (“user mode থেকে ecall”), sepc-তে রাখে ecall-এর নিজের ঠিকানা, আর লাফ দেয় stvec register যে ঠিকানা দেখায় সেখানে — xv6-এ সেটি uservec.

এখানে একটি মজার সমস্যা আছে। ecall-এর পরেও CPU চলছে user-এর page table দিয়ে — RISC-V নিজে থেকে page table বদলায় না। তাহলে kernel-এর কোড user-এর ঠিকানা-জগতে কীভাবে দেখা যাবে? xv6-এর উত্তর trampoline page: trampoline.S-এর কোড একটি পাতায় রাখা হয়, আর সেই পাতাটি প্রতিটি user page table-এ এবং kernel page table-এ একই virtual address-এ (TRAMPOLINE, ঠিকানা-জগতের একদম ওপরে) map করা থাকে। ফলে page table বদলানোর মুহূর্তেও পরের instruction একই ঠিকানায় পাওয়া যায়। তার ঠিক নিচের পাতায় থাকে প্রতিটি প্রসেসের trapframe — user-এর ৩১টি general register (x0 সবসময় শূন্য, তাই সেটি বাদ) আর কয়েকটি kernel-তথ্য জমা রাখার জায়গা।

1 sh getcmd(): write(2, "$ ", 2)2 usys.S: li a7, 16; ecall3 uservec: regs -> TRAPFRAME; satp = kernel4 usertrap(): scause 8; epc += 4; syscall()5 syscall(): syscalls[16] = sys_write6 sys_write: argaddr/argint/argfd7 filewrite -> consolewrite -> uartwrite8 a0 = 2; prepare_return; userret; sret
Register আর অবস্থা
Mode
a7
a0 / a1 / a2
Page table
  1. User দিকের ডাক। Shell-এর getcmd() prompt লিখতে চায়: write(2, "$ ", 2) — fd ২ (stderr, যা init আগেই console-এর সাথে জুড়ে দিয়েছে), buffer-এর ঠিকানা, আর ২ byte. C compiler তিনটি argument a0, a1, a2-তে বসায়। এখনো সবকিছু user mode-এ।
  2. Stub আর ecall. write stub a7-এ রাখে SYS_write = ১৬ (kernel/syscall.h), তারপর ecall. CPU supervisor mode-এ যায়, scause = ৮, sepc = এই ecall-এর ঠিকানা, আর লাফ দেয় uservec-এ। Page table এখনো user-এর।
  3. uservec — সব register জমা। Trampoline-এর কোড ৩১টি general register প্রসেসের trapframe-এ লেখে (a0 সাময়িকভাবে sscratch-এ রেখে), তারপর trapframe থেকেই kernel stack-এর ঠিকানা, hart নম্বর, usertrap-এর ঠিকানা আর kernel page table পড়ে। satp-এ kernel page table বসে, TLB পরিষ্কার হয় (sfence.vma), আর usertrap() ডাকা হয়।
  4. usertrap() — কারণ দেখা। scause == 8, তাই এটি system call. Trapframe-এর epc-তে ৪ যোগ হয়, যাতে ফেরার সময় ecall-এর পরের instruction থেকে চলে (নইলে একই ecall অনন্তকাল চলত)। তারপর interrupt আবার চালু, আর syscall().
  5. syscall() — টেবিল দেখে বণ্টন। num = p->trapframe->a7 = ১৬. num টেবিলের সীমার ভেতরে আর ঘরটি খালি নয় — তাই syscalls[16], অর্থাৎ sys_write, ডাকা হয়। ভুল নম্বর হলে “unknown sys call” ছাপা হতো আর ফেরত মান হতো −১.
  6. sys_write — argument আনা। Handler-গুলোর নিজের কোনো parameter নেই; argument পড়ে trapframe থেকে: argaddr(1, &p), argint(2, &n), আর argfd(0, …) fd ২ যাচাই করে প্রসেসের ofile[2] থেকে struct file আনে। Fd টেবিলের বাইরে বা খালি হলে এখানেই −১.
  7. Console পর্যন্ত। filewrite() দেখে ফাইলটির ধরন FD_DEVICE, তাই devsw[CONSOLE].write = consolewrite. সেটি either_copyin() দিয়ে user-এর buffer থেকে byte দুটি kernel-এ আনে — user ঠিকানা সরাসরি বিশ্বাস করা হয় না, page table দেখে অনুবাদ করে কপি — তারপর uartwrite() UART-এ পাঠায়। স্ক্রিনে এলো $ .
  8. ফেরা। sys_write ফেরত দেয় ২; syscall() সেটি লেখে p->trapframe->a0-তে। usertrap() prepare_return() দিয়ে stvec, sepc আর sstatus ঠিক করে, user page table-এর satp মান ফেরত দেয়; trampoline-এর userret page table বদলায়, trapframe থেকে register ফেরত আনে, আর sret — user mode-এ, ecall-এর পরের লাইনে। Stub-এর ret-এর পর C কোড a0-তে পায় ২.

বণ্টনের হৃদয়টি মাত্র কয়েক লাইন। syscalls হলো function pointer-এর একটি অ্যারে, যার সূচি system call নম্বর — পর্ব ০৫-এর syscall table-এর সবচেয়ে স্বচ্ছ রূপ:

static uint64 (*syscalls[])(void) = {
  [SYS_fork]    = sys_fork,
  [SYS_exit]    = sys_exit,
  /* ... */
  [SYS_getpid]  = sys_getpid,
  /* ... */
  [SYS_write]   = sys_write,
  /* ... */
};

void
syscall(void)
{
  int num;
  struct proc *p = myproc();

  num = p->trapframe->a7;
  if (num > 0 && num < NELEM(syscalls) && syscalls[num]) {
    p->trapframe->a0 = syscalls[num]();
  } else {
    printk("%d %s: unknown sys call %d\n", p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}

এবার একই কাঠামো একটি ছোট simulator-এ চালিয়ে দেখুন। এখানে trapframe একটি সাধারণ JavaScript object, user মেমরি একটি ছোট map, আর নম্বরগুলো xv6-এর আসল syscall.h থেকে নেওয়া:

// xv6-style system call dispatch, simulated. Numbers from kernel/syscall.h.
const SYS_getpid = 11, SYS_uptime = 14, SYS_write = 16, NOFILE = 16;
const proc = { pid: 2, name: "sh", ofile: ["console", "console", "console"], trapframe: {} };
const umem = { 0x1f40: "$ " };           // pretend user memory: address -> bytes
let screen = "";
const argraw = n => proc.trapframe["a" + n];
const argint = n => argraw(n) | 0;       // 32-bit integer argument
const argaddr = n => argraw(n);          // pointer argument, checked later by copyin
function sys_getpid() { return proc.pid; }
function sys_uptime() { return 37; }     // ticks since boot (fixed for the demo)
function sys_write() {
  const p = argaddr(1), n = argint(2), fd = argint(0);
  if (fd < 0 || fd >= NOFILE || !proc.ofile[fd]) return -1;  // argfd() fails
  const bytes = (umem[p] || "").slice(0, n);                  // copyin from user memory
  screen += bytes;
  return bytes.length;
}
const syscalls = [];
syscalls[SYS_getpid] = sys_getpid; syscalls[SYS_uptime] = sys_uptime; syscalls[SYS_write] = sys_write;
function syscall() {
  const num = proc.trapframe.a7;
  if (num > 0 && num < syscalls.length && syscalls[num]) proc.trapframe.a0 = syscalls[num]();
  else { console.log(proc.pid + " " + proc.name + ": unknown sys call " + num); proc.trapframe.a0 = -1; }
}
function ecall(num, a0 = 0, a1 = 0, a2 = 0) {   // what the stub + uservec + usertrap do
  proc.trapframe = { a0, a1, a2, a7: num };
  syscall();
  return proc.trapframe.a0;
}
console.log("getpid()             a0 =", ecall(SYS_getpid));
console.log("write(2, 0x1f40, 2)  a0 =", ecall(SYS_write, 2, 0x1f40, 2), " screen:", JSON.stringify(screen));
console.log("write(9, 0x1f40, 2)  a0 =", ecall(SYS_write, 9, 0x1f40, 2));
console.log("uptime()             a0 =", ecall(SYS_uptime));
console.log("ecall 22 (sync)      a0 =", ecall(22));
console.log("table length", syscalls.length, "- slots filled:", syscalls.filter(Boolean).length);

আউটপুট (নির্ধারিত):

getpid()             a0 = 2
write(2, 0x1f40, 2)  a0 = 2  screen: "$ "
write(9, 0x1f40, 2)  a0 = -1
uptime()             a0 = 37
2 sh: unknown sys call 22
ecall 22 (sync)      a0 = -1
table length 17 - slots filled: 3

লক্ষ করুন write-এর লাইনে a0 দুই ভূমিকায়: ঢোকার সময় সেটি ছিল প্রথম argument (fd = ২), ফেরার সময় সেখানেই ফল (২ byte লেখা হয়েছে) — xv6-এ ঠিক এমনই। fd ৯-এ কোনো খোলা ফাইল নেই, তাই argfd-এর ধাপেই −১. নম্বর ২২ আসল xv6-এ SYS_sync, কিন্তু আমাদের টেবিলে মাত্র ১৭টি ঘর (০ থেকে ১৬), তাই সীমা-যাচাই ব্যর্থ হয়ে xv6-এর মতোই “unknown sys call” ছাপা হলো। আর ১৭টি ঘরের মধ্যে ভরা মাত্র ৩টি — বাকিগুলো undefined, তাই syscalls[num] পরীক্ষাটিও জরুরি। নতুন system call যোগ করা মানে এই টেবিলে একটি ঘর ভরা — xv6-এর প্রথম lab ঠিক এটাই করায়।

৫ · মেমরি — Sv39, walk() আর kalloc

খালি পাতার হিসাব — kalloc.c

পর্ব ৩১-এ বিভিন্ন allocator দেখেছিলেন — first fit, buddy, slab. xv6-এর physical memory allocator সবচেয়ে সরল: সব পাতা একই আকারের (৪০৯৬ byte), তাই fragmentation-এর প্রশ্নই নেই। খালি পাতাগুলো একটি free list-এ সাজানো, আর সেই list-এর next pointer রাখা থাকে খালি পাতার ভেতরেই — আলাদা কোনো হিসাবের খাতা লাগে না।

struct run {
  struct run *next;
};

void
kfree(void *pa)
{
  struct run *r;
  /* ... panic if pa is not page-aligned or outside [end, PHYSTOP) ... */
  memset(pa, 1, PGSIZE);   // Fill with junk to catch dangling refs.
  r = (struct run *)pa;
  acquire(&kmem.lock);
  r->next = kmem.freelist;
  kmem.freelist = r;
  release(&kmem.lock);
}

void *
kalloc(void)
{
  struct run *r;
  acquire(&kmem.lock);
  r = kmem.freelist;
  if (r)
    kmem.freelist = r->next;
  release(&kmem.lock);
  if (r)
    memset((char *)r, 5, PGSIZE); // fill with junk
  return (void *)r;
}

kinit() kernel-এর শেষ (end) থেকে PHYSTOP (0x80000000 + ১২৮ MiB) পর্যন্ত প্রতিটি পাতায় kfree ডাকে। ফলে list-এর মাথায় থাকে সবচেয়ে উঁচু ঠিকানার পাতা। একই ধারণাটি নিচে user space-এ, একটি static অ্যারের ওপর — চারটি “পাতা” নিয়ে। ঠিকানা প্রতিবার আলাদা হতে পারে, তাই প্রোগ্রাম ঠিকানা না ছেপে পাতার ক্রমিক নম্বর ছাপে:

#include <stdio.h>
#include <string.h>
#define PGSIZE 4096
#define NPAGES 4
struct run { struct run *next; };                  // same trick as xv6's kalloc.c
static _Alignas(PGSIZE) char mem[NPAGES * PGSIZE]; // our fake "physical memory"
static struct run *freelist;
static int pg(void *p) { return (int)(((char *)p - mem) / PGSIZE); }
static void kfree(void *pa) {
  char *c = pa;
  if (c < mem || c >= mem + sizeof mem || (c - mem) % PGSIZE) { printf("panic: kfree\n"); return; }
  memset(pa, 1, PGSIZE);                           // junk, to catch dangling pointers
  struct run *r = pa; r->next = freelist; freelist = r;   // push on the front
}
static void *kalloc(void) {
  struct run *r = freelist;                        // pop from the front
  if (r) { freelist = r->next; memset(r, 5, PGSIZE); }  return r;
}
static void show(const char *what) {
  printf("%-20s free:", what);
  for (struct run *r = freelist; r; r = r->next) printf(" [%d]", pg(r));
  printf("%s\n", freelist ? "" : " (empty)");
}
int main(void) {
  for (char *p = mem; p + PGSIZE <= mem + sizeof mem; p += PGSIZE) kfree(p);  // kinit
  show("after kinit");
  char *a = kalloc(), *b = kalloc();
  printf("kalloc -> page %d, page %d; a[0] = %d, a[100] = %d\n", pg(a), pg(b), a[0], a[100]);
  show("after 2 kallocs");
  kfree(a);  show("after kfree(a)");
  char *c = kalloc();  printf("next kalloc -> page %d (same as a: %s)\n", pg(c), c == a ? "yes" : "no");
  kalloc(); kalloc();  show("after 2 more");
  printf("kalloc on empty list -> %s\n", kalloc() ? "page" : "0 (out of memory)");
  kfree(mem + 100);               // not page-aligned
}

আউটপুট (Wandbox, gcc):

after kinit          free: [3] [2] [1] [0]
kalloc -> page 3, page 2; a[0] = 5, a[100] = 5
after 2 kallocs      free: [1] [0]
after kfree(a)       free: [3] [1] [0]
next kalloc -> page 3 (same as a: yes)
after 2 more         free: (empty)
kalloc on empty list -> 0 (out of memory)
panic: kfree

পাতা ০, ১, ২, ৩ ক্রমে free করা হয়েছিল, কিন্তু list-এর মাথায় [3] — কারণ প্রতিটি kfree সামনে ঢোকায়; এটি একটি stack (LIFO). সদ্য ফেরত দেওয়া পাতাটিই পরের kalloc-এ আবার আসে (“same as a: yes”) — cache-এর জন্য ভালো, কিন্তু পুরোনো pointer ভুল করে ব্যবহার করলে অন্যের ডেটা নষ্ট হবে। এজন্যই xv6 free করার সময় পাতাটি ১ দিয়ে আর দেওয়ার সময় ৫ দিয়ে ভরে (a[0] = 5): ঝুলন্ত pointer দিয়ে পড়লে আবর্জনা দেখে বাগ তাড়াতাড়ি ধরা পড়ে। খালি list-এ kalloc ০ ফেরত দেয় — আসল xv6-এ তখন fork বা sbrk −১ ফেরত দেয়। আর ভুল ঠিকানা free করলে xv6 সত্যিই panic("kfree") করে থেমে যায়।

Sv39 — তিন স্তরের page table

পর্ব ৩৩-এ x86-64-এর চার স্তরের টেবিল দেখেছিলেন। RISC-V-এর Sv39 একই ধারণা, তিন স্তরে। Virtual address ৩৯ bit (২৩৯ byte = ৫১২ GiB): ওপরের ৯ bit স্তর ২-এর সূচি, পরের ৯ bit স্তর ১-এর, তার পরের ৯ bit স্তর ০-এর, আর শেষ ১২ bit পাতার ভেতরের offset. প্রতিটি page-table পাতায় ৫১২টি ৮-byte-এর PTE — ঠিক এক পাতা (৫১২ × ৮ = ৪০৯৬). xv6 এর মধ্যে ব্যবহার করে ৩৮ bit (MAXVA = ২৩৮), যাতে sign-extension-এর ঝামেলা না থাকে।

#define PGSHIFT 12   // bits of offset within a page
#define PTE_V (1L << 0) // valid
#define PTE_R (1L << 1)
#define PTE_W (1L << 2)
#define PTE_X (1L << 3)
#define PTE_U (1L << 4) // user can access
#define PA2PTE(pa) ((((uint64)pa) >> 12) << 10)
#define PTE2PA(pte) (((pte) >> 10) << 12)
#define PXMASK         0x1FF // 9 bits
#define PXSHIFT(level) (PGSHIFT + (9 * (level)))
#define PX(level, va)  ((((uint64)(va)) >> PXSHIFT(level)) & PXMASK)

pte_t *
walk(pagetable_t pagetable, uint64 va, int alloc)
{
  if (va >= MAXVA)
    panic("walk");

  for (int level = 2; level > 0; level--) {
    pte_t *pte = &pagetable[PX(level, va)];
    if (*pte & PTE_V) {
      pagetable = (pagetable_t)PTE2PA(*pte);
    } else {
      if (!alloc || (pagetable = (pde_t *)kalloc()) == 0)
        return 0;
      memset(pagetable, 0, PGSIZE);
      *pte = PA2PTE(pagetable) | PTE_V;
    }
  }
  return &pagetable[PX(0, va)];
}

walk() হলো hardware যা করে, তার software অনুকরণ — kernel-কে নিজেও টেবিল ভরতে হয়, তাই হাঁটতে জানতে হয়। alloc ১ হলে মাঝের কোনো টেবিল না থাকলে kalloc() দিয়ে নতুন পাতা এনে শূন্য করে বসিয়ে দেয়। এর ওপর দাঁড়িয়ে mappages() — একটি পরিসরের প্রতিটি পাতার জন্য walk(…, 1) ডেকে leaf PTE-তে লেখে PA2PTE(pa) | perm | PTE_V. আর uvmalloc() প্রসেসের মেমরি বাড়ায়: প্রতিটি নতুন পাতার জন্য kalloc(), তারপর mappages(). লক্ষ করুন, মাঝের স্তরের PTE-তে শুধু PTE_V; R, W, X তিনটিই শূন্য থাকলে hardware বোঝে “এটি leaf নয়, পরের টেবিলের ঠিকানা”।

নিচের stepper-এ একটি উদাহরণ-ঠিকানা 0x1234567ABC হাতে হাতে অনুবাদ করুন। ঠিকানাটি এমনভাবে বাছা, যাতে তিনটি সূচিই শূন্য নয়। Page-table পাতাগুলোর physical ঠিকানাও উদাহরণ, তবে বাস্তবসম্মত — kalloc উঁচু ঠিকানা থেকে দেয়, তাই PHYSTOP (0x88000000)-এর ঠিক নিচে।

1 VA -> L2 = 72, L1 = 418, L0 = 359, off = 0xABC2 satp -> root table @ PA 0x87FFF0003 root[72] = 0x21FFF401 -> next @ 0x87FFD0004 L1[418] = 0x21FFF001 -> next @ 0x87FFC0005 L0[359] = 0x21FD2817 -> leaf, V R W U6 PA = 0x87F4A000 | 0xABC = 0x87F4AABC
অবস্থা (ধাপের শেষে)
এখনকার টেবিল
PTE-র ঠিকানা
মেমরি-পড়া
Physical address
  1. ঠিকানা চার ভাগে। 0x1234567ABC-কে ৩৯ bit binary-তে লিখে ওপর থেকে ৯-৯-৯-১২ করে কাটুন: 001001000 = ৭২, 110100010 = ৪১৮, 101100111 = ৩৫৯, আর শেষ ১২ bit = 0xABC. কোডে এটাই PX(2, va), PX(1, va), PX(0, va): ১২, ২১ আর ৩০ bit ডানে সরিয়ে নিচের ৯ bit রাখা।
  2. satp — শুরুর ঠিকানা। satp register-এর ওপরের ৪ bit-এ মোড ৮ (Sv39), নিচে root টেবিলের physical page number. xv6-এর MAKE_SATP(pagetable) ঠিক এটাই বানায়। এখানে root টেবিল আছে physical 0x87FFF000-এ। Context switch-এ প্রসেস বদলালে আসলে এই একটি register বদলায়।
  3. স্তর ২ — প্রথম মেমরি-পড়া। PTE-র ঠিকানা = টেবিলের শুরু + সূচি × ৮ = 0x87FFF240. মান 0x21FFF401: সবচেয়ে নিচের bit (V) = ১, কিন্তু R, W, X শূন্য — তাই এটি পরের টেবিলের pointer. PPN পেতে ১০ bit ডানে সরান: 0x87FFD, অর্থাৎ পরের টেবিল 0x87FFD000-এ। V = ০ হলে এখানেই page fault (বা walk(…, 1)-এ নতুন টেবিল বানানো)।
  4. স্তর ১ — দ্বিতীয় মেমরি-পড়া। 0x87FFD000 + ৪১৮ × ৮ = 0x87FFDD10. মান 0x21FFF001 — আবার শুধু V, তাই pointer: পরের টেবিল 0x87FFC000. লক্ষ করুন, ৯ bit-এর সূচির সর্বোচ্চ মান ৫১১, আর ৫১১ × ৮ = ৪০৮৮ — সবসময় একই পাতার ভেতরে।
  5. স্তর ০ — তৃতীয় মেমরি-পড়া, leaf. 0x87FFC000 + ৩৫৯ × ৮ = 0x87FFCB38. মান 0x21FD2817: নিচের ১০ bit-এর flag 0x17 = V (১) + R (২) + W (৪) + U (১৬). R আছে, তাই এটি leaf. U আছে, তাই user mode থেকে ছোঁয়া যাবে; X নেই, তাই এখান থেকে কোড চালানো যাবে না। PPN = 0x87F4A.
  6. Physical address — চতুর্থ মেমরি-পড়া আসল ডেটার। PPN-কে ১২ bit বাঁয়ে সরিয়ে offset জুড়ুন: 0x87F4A000 + 0xABC = 0x87F4AABC. TLB না থাকলে একটি ছোঁয়ায় চারবার মেমরি — এজন্যই TLB (পর্ব ৩৩). Kernel নিজে যখন user-এর ঠিকানা থেকে পড়ে (copyin), তখন hardware-এর বদলে এই একই কাজ walkaddr() দিয়ে software-এ করে।

একই হিসাব একটি ছোট C প্রোগ্রামে, xv6-এর PX macro হুবহু ব্যবহার করে — xv6-এর নিজের দুটি বিশেষ পাতা, trampoline আর trapframe-সহ:

#include <stdio.h>
#include <stdint.h>
typedef uint64_t uint64;
#define PGSIZE 4096
#define PGSHIFT 12                                   // the next five lines are as in xv6
#define PXMASK 0x1FF
#define PXSHIFT(level) (PGSHIFT + (9 * (level)))
#define PX(level, va) ((((uint64)(va)) >> PXSHIFT(level)) & PXMASK)
#define MAXVA (1L << (9 + 9 + 9 + 12 - 1))
#define TRAMPOLINE (MAXVA - PGSIZE)
#define TRAPFRAME (TRAMPOLINE - PGSIZE)

static void show(const char *name, uint64 va) {
  if (va >= MAXVA) { printf("%-11s 0x%010llx  >= MAXVA: walk() would panic\n", name, (unsigned long long)va); return; }
  printf("%-11s 0x%010llx  L2=%3u L1=%3u L0=%3u off=0x%03x\n", name, (unsigned long long)va,
         (unsigned)PX(2, va), (unsigned)PX(1, va), (unsigned)PX(0, va), (unsigned)(va & 0xFFF));
}
int main(void) {
  show("text start", 0x0);
  show("data", 0x2A7C);
  show("example", 0x1234567ABCULL);
  show("TRAPFRAME", TRAPFRAME);
  show("TRAMPOLINE", TRAMPOLINE);
  show("too high", (uint64)MAXVA);
  printf("MAXVA = 2^38 = %llu GiB; one L2 entry covers %d GiB, one L1 entry %d MiB\n",
         (unsigned long long)(MAXVA >> 30), 1, 512 * 4096 / (1 << 20));
  return 0;
}

আউটপুট (Wandbox, gcc):

text start  0x0000000000  L2=  0 L1=  0 L0=  0 off=0x000
data        0x0000002a7c  L2=  0 L1=  0 L0=  2 off=0xa7c
example     0x1234567abc  L2= 72 L1=418 L0=359 off=0xabc
TRAPFRAME   0x3fffffe000  L2=255 L1=511 L0=510 off=0x000
TRAMPOLINE  0x3ffffff000  L2=255 L1=511 L0=511 off=0x000
too high    0x4000000000  >= MAXVA: walk() would panic
MAXVA = 2^38 = 256 GiB; one L2 entry covers 1 GiB, one L1 entry 2 MiB

“example” লাইনটি ঠিক উপরের stepper: ৭২/৪১৮/৩৫৯/0xABC. সবচেয়ে শিক্ষণীয় শেষ দুটি সারি। TRAPFRAME আর TRAMPOLINE স্তর ২-এর ঘর ২৫৫-এ — ২৫৬টি ঘরের শেষটি, কারণ xv6 ৩৮ bit ব্যবহার করে বলে স্তর ২-এর মাত্র অর্ধেক (০–২৫৫) কাজে লাগে। দুটি পাতা একই স্তর-০ টেবিলের পাশাপাশি ঘরে (৫১০ আর ৫১১). আর প্রোগ্রামের কোড-ডেটা থাকে একদম নিচে, স্তর ২-এর ঘর ০-এ। তাই একটি ছোট প্রসেসের page table-এ লাগে মাত্র পাঁচটি টেবিল-পাতা: একটি root, দুটি স্তর-১ (ঘর ০ আর ২৫৫-এর জন্য), দুটি স্তর-০ — xv6-এর ২৫৬ GiB-এর ঠিকানা-জগতের জন্য মাত্র ২০ KiB. Multilevel টেবিলের পুরো সুবিধা এখানেই।

User আর kernel-এর সীমানা

xv6-এ প্রতিটি প্রসেসের নিজের user page table, আর kernel-এর একটি আলাদা page table — Linux-এর মতো kernel-কে প্রতিটি প্রসেসের ঠিকানা-জগতে map করা হয় না (শুধু trampoline পাতাটি দুই জায়গাতেই)। তাই kernel যখন sys_write-এ user-এর buffer পড়ে, তখন সরাসরি pointer ব্যবহার করতে পারে না; copyin আর copyout user page table-এ walkaddr() দিয়ে প্রতিটি পাতার physical ঠিকানা বের করে, PTE_U আছে কি না দেখে, তারপর কপি করে। ভুল বা অন্যের ঠিকানা দিলে −১ — kernel crash করে না। নিরাপত্তার দিক থেকে এটিই মূল কথা: user-এর দেওয়া কোনো ঠিকানা kernel কখনো অন্ধভাবে বিশ্বাস করে না।

৬ · File system — সাত তলার দালান

পর্ব ৪১–৪৪-এ file system-এর interface, directory, inode আর journaling আলাদা দেখেছিলেন। xv6 এগুলোকে পরিষ্কার স্তরে সাজায় — প্রতিটি স্তর শুধু ঠিক নিচের স্তরটিকে ব্যবহার করে:

স্তরফাইলকী করে
৭ · File descriptorfile.c, sysfile.cofile[fd] → struct file (offset, পড়া/লেখার অনুমতি); pipe, device আর inode — সব এক interface-এ
৬ · Pathnamefs.c (namei)/usr/a-কে ভেঙে ধাপে ধাপে directory খোঁজা
৫ · Directoryfs.c (dirlookup)Directory = একটি বিশেষ inode, যার ভেতরে struct dirent-এর সারি (inode নম্বর + ১৪ অক্ষরের নাম)
৪ · Inodefs.cফাইলের আকার, link সংখ্যা, block-এর ঠিকানা; bmap() দিয়ে ফাইলের n-তম block খোঁজা
৩ · Logginglog.cকয়েকটি block-এর বদল একসাথে — হয় সব, নয় কিছুই না (crash consistency)
২ · Buffer cachebio.cDisk block-এর মেমরি-কপি, প্রতিটি buffer-এ sleeplock, LRU নিয়মে পুনর্ব্যবহার
১ · Diskvirtio_disk.cQEMU-র virtio disk-এ ১০২৪ byte-এর block পড়া-লেখা

Buffer cache (buffer cache) দুটি কাজ করে: ধীর disk-এর বারবার পড়া এড়ানো, আর নিশ্চিত করা যে একটি block-এর মেমরি-কপি একটিই থাকে — দুটি প্রসেস একই block-এর দুই কপি আলাদাভাবে বদলালে একটির কাজ হারাত। bread() block-টি এনে তার sleeplock ধরে দেয়, brelse() ছেড়ে দেয়। xv6-এ buffer মাত্র ৩০টি (NBUF) — শেখার জন্য যথেষ্ট, বাস্তবের জন্য খুবই ছোট।

Logging হলো পর্ব ৪৪-এর write-ahead logging-এর সবচেয়ে সরল রূপ। প্রতিটি file system system call begin_op() আর end_op()-এর মাঝে চলে; মাঝখানের সব বদলানো block প্রথমে disk-এর log এলাকায় লেখা হয়, তারপর log-এর header block-এ সংখ্যা লেখা হয় — এই একটি লেখাই “commit”. তারপর block-গুলো আসল জায়গায় কপি, আর শেষে header মুছে ফেলা। মাঝপথে বিদ্যুৎ গেলে বুটের সময় recover_from_log() header দেখে: commit হয়ে থাকলে আবার কপি করে (redo), না হলে log উপেক্ষা করে। এজন্যই filewrite() বড় লেখাকে কয়েক block-এর টুকরোয় ভাঙে — একটি transaction-এ log-এ সর্বোচ্চ ১০টি block (MAXOPBLOCKS).

Inode আর সর্বোচ্চ ফাইল-আকার — একটি পূর্ণ হিসাব

#define BSIZE   1024 // block size

#define NDIRECT   12
#define NINDIRECT (BSIZE / sizeof(uint))
#define MAXFILE   (NDIRECT + NINDIRECT)

// On-disk inode structure
struct dinode {
  short type;              // File type
  short major;             // Major device number (T_DEVICE only)
  short minor;             // Minor device number (T_DEVICE only)
  short nlink;             // Number of links to inode in file system
  uint size;               // Size of file (bytes)
  uint addrs[NDIRECT + 1]; // Data block addresses
};

অর্থাৎ inode-এ ১২টি direct block pointer আর একটি single indirect block-এর pointer (addrs[12]). Double বা triple indirect নেই। এবার পরীক্ষার ধাঁচে হিসাব:

হিসাব — xv6-এ একটি ফাইল সর্বোচ্চ কত বড়?

ধাপ ১ — একটি indirect block-এ কতটি ঠিকানা: block-এর আকার BSIZE = ১০২৪ byte, প্রতিটি block নম্বর একটি uint = ৪ byte. তাই NINDIRECT = ১০২৪ ÷ ৪ = ২৫৬.

ধাপ ২ — মোট block: MAXFILE = NDIRECT + NINDIRECT = ১২ + ২৫৬ = ২৬৮টি block.

ধাপ ৩ — byte-এ: ২৬৮ × ১০২৪ = ২৭৪,৪৩২ byte = ঠিক ২৬৮ KiB. প্রথম ১২ KiB পৌঁছানো যায় সরাসরি; বাকি ২৫৬ KiB-এর যেকোনো byte পড়তে আগে indirect block-টি পড়তে হয়।

ধাপ ৪ — কোন byte কোন block-এ: byte ৫০,০০০ → block সূচি ৫০,০০০ ÷ ১০২৪ = ৪৮ (ভাগফল), কারণ ৪৮ × ১০২৪ = ৪৯,১৫২; ভেতরে offset ৫০,০০০ − ৪৯,১৫২ = ৮৪৮. সূচি ৪৮ ≥ ১২, তাই indirect block-এর ঘর ৪৮ − ১২ = ৩৬. bmap() ঠিক এই হিসাবটি করে: bn < NDIRECT হলে সরাসরি, নইলে bn -= NDIRECT করে indirect block-এ।

২৬৮ KiB খুবই ছোট — একটি ছবিও ধরে না। এজন্যই xv6-এর একটি পরিচিত lab হলো “big files”: একটি direct pointer-কে double indirect বানিয়ে সীমা বাড়ানো (অনুশীলন ১-এ হিসাবটি করবেন)। Linux-এর ext2/ext3-এ একই ১২টি direct pointer, সাথে single, double আর triple indirect; ext4 এর বদলে extent ব্যবহার করে (পর্ব ৪৩).

৭ · Lock — spinlock, push_off আর sleeplock

xv6 multicore — তিনটি hart একসাথে kernel-এর ভেতরে থাকতে পারে, তাই প্রায় প্রতিটি ভাগ করা ডেটার নিজস্ব lock. পর্ব ২৩-এর spinlock এখানে সরাসরি:

void
acquire(struct spinlock *lk)
{
  push_off(); // disable interrupts to avoid deadlock.
  if (holding(lk))
    panic("acquire");

  while (__atomic_exchange_n(&lk->locked, 1, __ATOMIC_ACQUIRE) != 0)
    ;

  // Record info about lock acquisition for holding() and debugging.
  lk->cpu = mycpu();
}

__atomic_exchange_n RISC-V-এ একটি amoswap.w.aq instruction হয়ে যায় — “১ লেখা আর পুরোনো মানটি ফেরত দেওয়া, একই মুহূর্তে”, ঠিক পর্ব ২৩-এর test-and-set. পুরোনো মান ০ হলে lock পাওয়া গেল; ১ হলে অন্য কেউ ধরে আছে, তাই আবার চেষ্টা — ঘুরতে থাকা। __ATOMIC_ACQUIRE compiler আর CPU-কে বলে, critical section-এর কোনো পড়া-লেখা যেন lock নেওয়ার আগে সরে না যায় (memory barrier); release()-এ উল্টো দিকের __ATOMIC_RELEASE.

কিন্তু প্রথম লাইনে interrupt বন্ধ কেন? ধরুন একটি hart tickslock ধরে আছে, আর ঠিক তখন timer interrupt এলো। Interrupt handler-ও tickslock নিতে চায়, তাই ঘুরতে থাকে — কিন্তু lock-টি ছাড়বে যে কোড, সে তো এই একই CPU-তে থেমে আছে handler শেষ হওয়ার অপেক্ষায়। চিরস্থায়ী deadlock, মাত্র একটি CPU-তেই। সমাধান: lock ধরে থাকার পুরো সময় সেই CPU-তে interrupt বন্ধ (interrupt masking). push_off()/pop_off() জোড়ায় জোড়ায় গোনে (noff), কারণ একটি lock ধরে আরেকটি ধরা যায়; শেষ lock ছাড়লে তবেই, আর শুরুতে interrupt চালু থাকলে তবেই (intena), আবার চালু হয়।

অফিসের রেকর্ড রুমে একটি ফাইল ক্যাবিনেটের চাবি একটাই। কর্মী চাবি নিয়ে ফাইল ঘাঁটছেন, এমন সময় ফোন এলো: “এখনই ওই ক্যাবিনেট থেকে একটি কাগজ দিন।” তিনি ফোন কানে নিয়ে চাবির জন্য অপেক্ষা করছেন — অথচ চাবি তাঁর নিজের পকেটেই, আর ফোন রাখার আগে তিনি পকেটে হাত দেবেন না! xv6-এর নিয়ম: চাবি হাতে থাকাকালীন ফোন ধরা নিষেধ (interrupt বন্ধ); চাবি ফেরত দিয়ে তবেই ফোন।

Spinlock অল্প সময়ের জন্য ভালো, কিন্তু disk-এর জন্য কয়েক মিলিসেকেন্ড অপেক্ষায় CPU ঘুরিয়ে নষ্ট করা অপচয় — আর interrupt বন্ধ রেখে disk-এর কাজ শেষ হওয়ার খবরও আসবে না। তাই buffer আর inode-এর জন্য xv6-এ sleeplock: ভেতরে একটি ছোট spinlock আর একটি locked ফ্ল্যাগ; lock ব্যস্ত পেলে acquiresleep() sleep করে, ছাড়ার সময় releasesleep() wakeup করে। Sleeplock ধরে থাকা অবস্থায় interrupt চালু থাকে, প্রসেস ঘুমাতেও পারে — কিন্তু interrupt handler-এ এটি ব্যবহার করা যায় না, কারণ handler ঘুমাতে পারে না।

৮ · Shell — sh.c-এ fork, exec আর pipe

পর্ব ১০ আর ১১-এ shell-এর মূল লুপ দেখেছিলেন: কমান্ড পড়া, fork(), সন্তানে exec(), অভিভাবকে wait(). xv6-এর sh.c মাত্র প্রায় ৫০০ লাইনে এটি করে — একটি ছোট recursive-descent parser দিয়ে কমান্ডকে একটি গাছে (EXEC, REDIR, PIPE, LIST, BACK) ভাঙে, তারপর runcmd() গাছটি চালায়। cd একমাত্র ব্যতিক্রম — সেটি সন্তানে চালালে শুধু সন্তানের directory বদলাত, তাই shell নিজেই chdir করে। Pipe-এর অংশটি পড়ার মতো:

  case PIPE:
    pcmd = (struct pipecmd *)cmd;
    if (pipe(p) < 0)
      panic("pipe");
    if (fork1() == 0) {
      close(1);
      dup(p[1]);
      close(p[0]);
      close(p[1]);
      runcmd(pcmd->left);
    }
    if (fork1() == 0) {
      close(0);
      dup(p[0]);
      close(p[0]);
      close(p[1]);
      runcmd(pcmd->right);
    }
    close(p[0]);
    close(p[1]);
    wait(0);
    wait(0);
    break;

ls | wc-এর জন্য: বাঁ দিকের সন্তান fd ১ বন্ধ করে dup(p[1]) — dup সবসময় সবচেয়ে ছোট খালি fd দেয়, তাই pipe-এর লেখার মুখ এখন fd ১. ডান দিকের সন্তান একইভাবে pipe-এর পড়ার মুখকে fd ০ বানায়। এই “আগে বন্ধ, তারপর dup” কৌশলটিই Unix-এর redirection-এর মূল, আর Linux-এর dup2() একে এক ধাপে করে। অপ্রয়োজনীয় মুখগুলো বন্ধ করা জরুরি: পড়ার দিক কখনো end-of-file পায় না, যতক্ষণ কোথাও লেখার মুখ খোলা থাকে (পর্ব ১২). xv6-এর shell-এ job control, signal বা quoting নেই — Bash-এর তুলনায় খুবই ছোট, কিন্তু মূল যন্ত্রটি হুবহু একই।

৯ · নিজে চালান আর নিজে বদলান

xv6 পড়া এক জিনিস, চালিয়ে একটি লাইন বদলে দেখা আরেক জিনিস। দরকার দুটি জিনিস: একটি RISC-V cross-compiler (আপনার x86 বা ARM কম্পিউটারে বসে RISC-V-এর machine code বানায়) আর QEMU (qemu-system-riscv64; এখনকার Makefile অন্তত version 7.2 চায়)। Linux বা WSL-এ সবচেয়ে সহজ; macOS-এ Homebrew দিয়ে। নিচের কমান্ডগুলো এই পাতায় চালানো যাবে না — নিজের terminal-এ চালাবেন; প্যাকেজের নাম distro-ভেদে আলাদা হতে পারে, তাই MIT-র 6.1810 কোর্স-পাতার “tools” নির্দেশনা মিলিয়ে নিন।

# toolchain + QEMU (package names can differ on your distro/version)
sudo apt install git build-essential gcc-riscv64-linux-gnu qemu-system-misc

git clone https://github.com/mit-pdos/xv6-riscv.git
cd xv6-riscv
make qemu

# ... a few seconds of compiling, then:
xv6 kernel is booting

hart 2 starting
hart 1 starting
init: starting sh
$ ls
$ echo hello xv6
hello xv6

# to leave QEMU: press Ctrl-a, release, then press x

QEMU থেকে বেরোতে Ctrl+a চেপে ছেড়ে দিন, তারপর x. make qemu-gdb দিয়ে চালালে QEMU একটি debugger-এর জন্য অপেক্ষা করে; আরেকটি terminal থেকে RISC-V-সক্ষম gdb জুড়ে break syscall দিয়ে উপরের stepper-এর প্রতিটি ধাপ আসল CPU-তে দেখতে পারবেন। user/-এ নতুন একটি প্রোগ্রাম লিখলে Makefile-এর UPROGS তালিকায় নামটি যোগ করতে হয়, নইলে সেটি disk image-এ ঢুকবে না।

কোথা থেকে শুরু — ক্লাসিক অনুশীলনী

MIT-র কোর্সের lab-গুলো বছরে বছরে কিছুটা বদলায়, তবে কয়েকটি ধারণা বহু বছর ধরে ঘুরে ফিরে আসে। নিচে শুধু দিকনির্দেশনা — সমাধান নয়; নিজে করাটাই শেখা, আর কোনো কোর্সের অংশ হিসেবে করলে সেই কোর্সের সততার নিয়ম মানুন।

অনুশীলনীকী করতে হয় (মোটামুটি)কোন ফাইল ছুঁতে হয়কোর্সের পর্ব
নতুন system call (যেমন trace, sysinfo)নম্বর, টেবিলের ঘর, handler, user stub; sysinfo-এ খালি মেমরি আর প্রসেস গুনে copyoutsyscall.h/.c, sysproc.c, usys.pl, user.h০৫
Page table ছাপাএকটি প্রসেসের তিন স্তরের টেবিল হেঁটে valid PTE দেখানোvm.c৩২, ৩৩
Copy-on-write forkfork-এ পাতা কপি না করে ভাগ করা, লেখার চেষ্টায় page fault-এ তখন কপি; প্রতিটি পাতার reference countvm.c, trap.c, kalloc.c৩৫
Lazy allocationsbrk-এ সাথে সাথে পাতা না দিয়ে প্রথম ছোঁয়ার page fault-এ দেওয়াsysproc.c, trap.c, vm.c৩৫
mmap/munmapএকটি ফাইলকে ঠিকানা-জগতে map করা, page fault-এ ফাইল থেকে পড়াproc.h, sysfile.c, trap.c৩৮, ৪১
Big files, symbolic linkDouble indirect block যোগ; symlink system callfs.h, fs.c, sysfile.c৪২, ৪৩
Lock-এর ভিড় কমানোপ্রতিটি CPU-র আলাদা free list; buffer cache-এ hash bucketkalloc.c, bio.c২৩, ২৯

একটি খবর: xv6-এর সাম্প্রতিক সংস্করণে (২০২৫ থেকে) sbrk-এর lazy allocation-এর একটি সরল রূপ মূল কোডেই এসে গেছে — usertrap()-এ vmfault() দেখতে পাবেন। তাই পুরোনো lab-নির্দেশনা নতুন কোডে হুবহু না মিলতে পারে; যে বছরের lab করছেন, সেই বছরের branch ব্যবহার করুন।

আরও গভীরে: xv6 বড় kernel-এর অনেক কৌশল ইচ্ছা করে বাদ দেয়। Linux-এ প্রতিটি CPU-র নিজের run queue (পর্ব ২১), xv6-এ একটি ভাগ করা টেবিল যা সব CPU পালা করে স্ক্যান করে — ৬৪টি প্রসেসে সমস্যা নেই, হাজারো প্রসেসে অচল। xv6-এর log প্রতিটি পুরো block কপি করে (physical redo log) আর সব system call-কে একটি transaction-এ জড়ো করে; ext4-এর journal আর ZFS-এর copy-on-write অনেক বেশি কৌশলী। MIT-র গবেষকেরা xv6-এর পাশাপাশি গবেষণার জন্যও শিক্ষণ-ধাঁচের kernel ব্যবহার করেছেন — যেমন multicore scalability নিয়ে sv6, আর Go ভাষায় লেখা Biscuit, যেখানে প্রশ্ন ছিল garbage collector-ওয়ালা ভাষায় kernel লিখলে দাম কত।

১০ · AI-এর যুগে xv6 পড়া

AI xv6 সম্পর্কে অনেক কিছু “জানে”, কারণ সোর্স আর বই দুটোই বহু বছর ধরে ইন্টারনেটে আছে। সমস্যা হলো, xv6-এর অন্তত তিনটি আলাদা যুগ — x86 সংস্করণ, ২০১৯–২০২৪-এর RISC-V সংস্করণ, আর ২০২৫-এর পরের বদলানো কোড — AI-এর উত্তরে প্রায়ই মিশে যায়।

AI-এর ভুলকেন ভুলকীভাবে যাচাই করবেন
RISC-V-এর প্রশ্নে int $T_SYSCALL, eax, trapasm.Sএগুলো পুরোনো x86 xv6-এর; RISC-V-এ ecall, a7, trampoline.Sনিজের checkout-এ grep করুন
“usertrapret() ডাকে”, “initcode.S চালায়”, “sleep(chan, lk)”Version-ভেদে সত্য বা মিথ্যা; সাম্প্রতিক কোডে নাম ও গঠন বদলেছেযে branch/tag ব্যবহার করছেন, সেখানে ফাংশনটি খুঁজুন
নির্দিষ্ট “লাইন ৩৪২-এ” বলালাইন নম্বর প্রতিটি commit-এ সরে; প্রায়ই বানানোফাংশনের নাম দিয়ে খুঁজুন, লাইন নম্বর দিয়ে নয়
সর্বোচ্চ ফাইল-আকার “১২ × ৪ KiB + …”xv6-এর BSIZE ১০২৪, ৪০৯৬ নয়; double indirect নেইfs.h-এর ধ্রুবক দিয়ে নিজে গুণ করুন
“Build না হলে sudo make চালান”Root লাগে না; সমস্যা সাধারণত toolchain-এর prefix বা QEMU-র versionMakefile-এর TOOLPREFIX আর qemu-system-riscv64 --version দেখুন
Lab-এর পুরো সমাধান দিয়ে দেওয়াশেখা হয় না; অনেক কোর্সে এটি সততার নিয়ম ভাঙা; প্রায়ই সূক্ষ্ম race বা leak থাকেইঙ্গিত চান, কোড নয়; usertests চালান

AI-কে “শিক্ষক” হিসেবে ব্যবহার করুন, “উত্তরদাতা” হিসেবে নয়। কোন version, কোন ফাইল, আর আপনি কী বুঝেছেন — তিনটিই বলে দিন:

আমি xv6-riscv-এর riscv branch পড়ছি (git log-এ সর্বশেষ commit-এর তারিখ: [এখানে লিখুন]). kernel/trap.c-এর usertrap() আর kernel/trampoline.S-এর uservec নিয়ে প্রশ্ন: ১. ecall-এর ঠিক পরে CPU কোন page table দিয়ে চলছে, আর trampoline পাতা কেন দুই page table-এ একই virtual address-এ map করা — সহজ বাংলায় বোঝাও। ২. usertrap()-এ p->trapframe->epc += 4 না করলে কী হতো? ৩. আমার বোঝা: "trapframe-এ user-এর সব register থাকে, আর context-এ শুধু callee-saved register". এটা ঠিক কি না বলো, ভুল হলে কোথায়। ৪. কোনো ফাংশনের নাম বা গঠন যদি পুরোনো xv6 (x86 বা ২০২৪-এর আগের RISC-V) থেকে বলো, তা স্পষ্ট করে উল্লেখ করো। লাইন নম্বর দিও না। কোনো lab-এর সমাধান-কোড দিও না।

AI-এর উত্তর মানার আগে — চেকলিস্ট

পরীক্ষা ও ইন্টারভিউতে xv6 বা শিক্ষণ-OS নিয়ে প্রায়ই যা জিজ্ঞেস করা হয়:

  • RISC-V xv6-এ system call নম্বর আর ফেরত মান কোথায় থাকে? — নম্বর a7-এ, argument a0–a5-এ, ফেরত মান a0-তে (kernel লেখে p->trapframe->a0-এ)।
  • Trampoline পাতা কেন লাগে? — Trap-এর সময় page table বদলাতে হয়; যে কোড বদলায় তাকে দুই page table-এই একই ঠিকানায় থাকতে হয়, নইলে পরের instruction হারিয়ে যায়।
  • acquire() কেন interrupt বন্ধ করে? — একই CPU-তে interrupt handler একই lock চাইলে deadlock হতো।
  • BSIZE ১০২৪, ১২টি direct ও ১টি single indirect pointer, ৪-byte block নম্বর — সর্বোচ্চ ফাইল? — (১২ + ২৫৬) × ১০২৪ = ২৭৪,৪৩২ byte = ২৬৮ KiB.
  • Sv39-এ একটি virtual address কীভাবে ভাগ হয়? — ৯/৯/৯/১২: তিনটি ৯-bit page-table সূচি আর ১২-bit offset; প্রতিটি টেবিলে ৫১২টি ৮-byte PTE.

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১একটি ১৯৭০-এর দশকের kernel-এর সোর্স আর তার ব্যাখ্যার বই কীভাবে কয়েক প্রজন্মের OS প্রকৌশলী তৈরি করল? “পুরো সিস্টেম পড়া” কেন এত শক্তিশালী শিক্ষা?

Lions-এর বইয়ের আগে অপারেটিং সিস্টেম ছিল প্রায় কালো বাক্স। বড় কোম্পানির OS-এর সোর্স বাইরের কেউ দেখতে পেতেন না; পাঠ্যবই ধারণা শেখাত, কিন্তু সেগুলো একটি আসল সিস্টেমে কীভাবে জোড়া লাগে, তা দেখার সুযোগ ছিল না। Unix V6 ছিল ব্যতিক্রম: kernel মোটামুটি দশ হাজার লাইনের কম, প্রায় পুরোটা C-তে, আর বিশ্ববিদ্যালয়গুলো লাইসেন্স নিয়ে সোর্স পেত। Lions সেই সোর্সের প্রতিটি অংশ লাইন ধরে ব্যাখ্যা করলেন।

পরে Unix-এর লাইসেন্স কড়া হলে সোর্স নিয়ে ক্লাসে পড়ানো কঠিন হয়ে পড়ে, আর বইটির আনুষ্ঠানিক বিতরণ বন্ধ থাকে বহু বছর। তবু ফটোকপির ফটোকপি বিভিন্ন দেশের বিশ্ববিদ্যালয়ে ঘুরতে থাকে — কম্পিউটার জগতের সবচেয়ে বিখ্যাত “নিষিদ্ধ বই”গুলোর একটি হয়ে ওঠে। এই বাধাই পরোক্ষভাবে MINIX-এর জন্ম দেয়, আর MINIX থেকে অনুপ্রাণিত হয়ে Linux.

পুরো সিস্টেম পড়া কেন আলাদা? আলাদা আলাদা ধারণা শেখা আর সেগুলোর সংযোগস্থল দেখা — দুটি ভিন্ন দক্ষতা। পাঠ্যবই বলে “context switch-এ register জমা হয়”; xv6 দেখায় কোন register, কোথায়, কেন মাত্র ১৪টি, আর scheduler-এর lock কে ধরে কে ছাড়ে। সবচেয়ে কঠিন বাগ প্রায় সবসময় দুটি subsystem-এর সীমানায় — যেমন sleep আর lock-এর মিলন, বা page table বদল আর trap-এর মিলন। এগুলো শুধু পুরো সিস্টেম পড়েই বোঝা যায়।

আরেকটি বিষয় হলো আত্মবিশ্বাস। একবার একটি সম্পূর্ণ kernel-এর প্রতিটি লাইন বুঝে ফেললে Linux-এর কোডও আর রহস্যময় লাগে না — সেটি বড়, কিন্তু একই ধাঁচের। বহু অভিজ্ঞ kernel প্রকৌশলী বলেছেন, শুরুটা হয়েছিল এমন কোনো ছোট সিস্টেম পড়ে।

মূল উপলব্ধি: জটিল সিস্টেম শেখার সবচেয়ে কার্যকর উপায় হলো এমন একটি ছোট কিন্তু সম্পূর্ণ নমুনা, যার প্রতিটি অংশ আসল কাজটাই করে। xv6 সেই ঐতিহ্যের আধুনিক উত্তরসূরি।

প্র ০২xv6 monolithic, MINIX microkernel. ১৯৯২ সালের Tanenbaum–Torvalds বিতর্ক থেকে আজ কী শেখার আছে, আর শেখার জন্য কোন নকশা ভালো?

১৯৯২ সালের শুরুতে Usenet-এর comp.os.minix গ্রুপে Andrew Tanenbaum “LINUX is obsolete” শিরোনামে একটি পোস্ট দেন। তাঁর যুক্তি: monolithic kernel পুরোনো ধারণা, ভবিষ্যৎ microkernel-এর — যেখানে file system আর driver আলাদা user-mode প্রসেসে চলে, একটি driver crash করলে পুরো সিস্টেম পড়ে না। আর Linux x86-এর সাথে বাঁধা, অন্য CPU-তে নেওয়া কঠিন হবে। Linus Torvalds জবাবে বলেন, তাত্ত্বিক সৌন্দর্যের চেয়ে বাস্তবে কাজ করা আর গতি বেশি জরুরি, আর MINIX-এর নিজেরও বহু সীমাবদ্ধতা আছে। বিতর্কটি বেশ উত্তপ্ত হয়েছিল, এবং আজও OS ক্লাসে উদাহরণ হিসেবে উঠে আসে।

ইতিহাস কী বলল? দুই পক্ষই আংশিক ঠিক। Linux monolithic থেকেও loadable module দিয়ে নমনীয় হয়েছে আর প্রায় সব CPU-তে চলে — বহনযোগ্যতার ভয় সত্যি হয়নি। অন্যদিকে microkernel-এর ধারণা হারিয়ে যায়নি: seL4-এর মতো microkernel-এর গাণিতিকভাবে প্রমাণিত সঠিকতা আছে, অনেক embedded ও নিরাপত্তা-সংবেদনশীল সিস্টেমে microkernel চলে, আর বিভিন্ন প্রতিবেদন অনুযায়ী Intel-এর কিছু chipset-এর ভেতরের management engine-এ MINIX 3-এর একটি রূপ চলে। Windows NT আর macOS-এর XNU দুটোই “hybrid” — মাঝামাঝি পথ (পর্ব ০৬).

শেখার জন্য কোনটি? xv6 monolithic বেছে নিয়েছে, কারণ এতে একটি system call-এর পুরো পথ একটি ঠিকানা-জগতে, সরাসরি ফাংশন-ডাকে অনুসরণ করা যায় — অংশ ৪-এর stepper-এ যেমন দেখলেন। Microkernel-এ একই write হতো কয়েকটি message: user প্রসেস → kernel → file-server প্রসেস → driver প্রসেস। এটি নকশার দিক থেকে মূল্যবান শিক্ষা (বিচ্ছিন্নতা, message passing), কিন্তু প্রথমবার kernel পড়ার জন্য বেশি স্তর।

একজন শিক্ষার্থীর জন্য সেরা পথ সম্ভবত দুটোই দেখা: আগে xv6-এ পুরো যন্ত্রটি বোঝা, তারপর MINIX বা seL4-এর নকশা পড়ে জিজ্ঞেস করা — “এই অংশটি user space-এ সরালে কী লাভ, কী দাম?”

মূল উপলব্ধি: নকশার বিতর্কে “কে জিতল” প্রশ্নটি অর্থহীন; প্রশ্ন হলো কোন লক্ষ্যে — গতি, নির্ভরযোগ্যতা, প্রমাণযোগ্যতা, না শেখানো — কোন trade-off গ্রহণযোগ্য।

প্র ০৩MIT কেন একটি কাজ করা x86 xv6 ফেলে RISC-V-এ নতুন করে লিখল? একটি শিক্ষণ-OS-এর জন্য CPU বাছাইয়ে কী কী বিবেচনা করতে হয়?

x86 xv6 প্রায় এক যুগেরও বেশি ভালোই চলছিল। কিন্তু x86 চার দশকের পশ্চাৎ-সামঞ্জস্যের বোঝা বয়ে বেড়ায়, আর একটি শিক্ষণ-OS-কে সেই বোঝার অনেকটাই সামলাতে হতো। বুট শুরু হতো ১৯৭৮ সালের 8086-এর মতো ১৬-bit real mode-এ, তারপর protected mode-এ যেতে হতো; segmentation পুরোপুরি এড়ানো যেত না (পর্ব ৩৪); interrupt-এর জন্য IDT, TSS, APIC আর IOAPIC-এর জটিল বিন্যাস; আর এর বেশিরভাগের সাথে OS-এর মূল ধারণার সম্পর্ক সামান্য। শিক্ষার্থীর মনোযোগ যেত ঐতিহাসিক খুঁটিনাটিতে।

RISC-V-এর privileged specification তুলনায় ছোট আর পরিষ্কার: তিনটি privilege mode, কয়েকটি control register (stvec, sepc, scause, satp), একটি সহজ page table ফরম্যাট। একটি ecall-এর পরে hardware কী করে, তা কয়েক লাইনে বলা যায় — যা অংশ ৪-এ দেখলেন। তার ওপর RISC-V একটি উন্মুক্ত ISA, লাইসেন্সের ঝামেলা নেই, আর গবেষণা ও শিল্পে দ্রুত ছড়াচ্ছে। বইয়ের লেখকেরা নিজেরাও বলেছেন, RISC-V-তে যাওয়ার মূল লক্ষ্য ছিল সরলতা।

দামটা কী? প্রথমত, বেশিরভাগ শিক্ষার্থীর হাতে RISC-V কম্পিউটার নেই, তাই QEMU-র ওপর নির্ভর করতে হয় — যদিও x86-এর সময়েও কার্যত QEMU-তেই চলত। দ্বিতীয়ত, cross-compiler বসাতে হয়, যা নতুনদের জন্য প্রথম বাধা। তৃতীয়ত, পুরোনো বিপুল পরিমাণ টিউটোরিয়াল, প্রশ্নোত্তর আর lab সমাধান x86 সংস্করণের, যা নতুন সংস্করণের সাথে মেলে না — AI-এর উত্তরে এই মিশ্রণ আজও দেখা যায়।

সাধারণ নীতি: শিক্ষণ-OS-এর জন্য CPU বাছাইয়ে জিজ্ঞেস করতে হয় — (১) privileged architecture কতটা ছোট, (২) emulator কতটা নির্ভরযোগ্য আর debugger-বান্ধব, (৩) toolchain কতটা সহজলভ্য, (৪) শিক্ষার্থীর ভবিষ্যৎ কাজে এটি কতটা প্রাসঙ্গিক। Pintos x86-এ রয়ে গেছে, কারণ তার লক্ষ্য কিছুটা আলাদা — বাস্তব PC-র কাছাকাছি থাকা।

মূল উপলব্ধি: শেখানোর সরঞ্জামে “যা বাদ দেওয়া হয়েছে” তা “যা রাখা হয়েছে” তার মতোই গুরুত্বপূর্ণ। জটিলতা যদি ধারণা না শেখায়, তবে সেটি শুধু শব্দ।

প্র ০৪xv6-এর sleep একটি lock হাতে নিয়ে ঢোকে, আর ২০২৬ সালে API-টিই বদলে গেল। Lost wakeup সমস্যাটি এত সূক্ষ্ম কেন, আর বড় kernel একে কীভাবে সামলায়?

Disk driver-এর উদাহরণ ভাবুন। একটি প্রসেস দেখে buffer এখনো disk থেকে আসেনি, তাই ঘুমাতে যায়। ঠিক সেই মুহূর্তে — “আসেনি দেখলাম” আর “ঘুমালাম”-এর মাঝখানে — অন্য CPU-তে disk interrupt আসে, handler wakeup ডাকে, কিন্তু তখনো কেউ ঘুমায়নি, তাই কেউ জাগে না। এরপর প্রসেসটি ঘুমিয়ে পড়ে, আর যে সংকেতের অপেক্ষায় সে ঘুমাল, তা ইতিমধ্যে চলে গেছে। কোনো error নেই, কোনো crash নেই — শুধু একটি প্রসেস চিরকালের জন্য আটকে। এমন বাগ লাখে একবার ঘটে, তাই test-এ ধরা পড়ে না।

পুরোনো সমাধান (sleep(chan, lk)): শর্তটি যে lock রক্ষা করে (lk), সেটি হাতে নিয়ে sleep-এ ঢোকা বাধ্যতামূলক। sleep আগে p->lock নেয়, তারপর lk ছাড়ে, আর wakeup-কেও প্রতিটি প্রসেসের p->lock নিতে হয়। তাই “শর্ত দেখা” থেকে “SLEEPING হওয়া” পর্যন্ত সবসময় কোনো না কোনো lock ধরা থাকে — wakeup ফাঁক দিয়ে ঢুকতে পারে না। পর্ব ২৫-এর pthread_cond_wait ঠিক একই চুক্তি মানে, আর Mesa-ধাঁচের monitor-এর মতো জেগে উঠে শর্ত আবার while দিয়ে যাচাই করতে হয়।

নতুন সমাধান (sleep_prepare + sleep): ২০২৬ সালে xv6 অপেক্ষার নিবন্ধন আগে করে নেয়: sleep_prepare(chan) p->chan বসায়, তারপর caller নিজের lock ছাড়ে, তারপর sleep(). মাঝে wakeup এলে সে p->chan শূন্য করে দেয়; sleep() দেখে chan শূন্য, তাই না ঘুমিয়েই ফেরে। সংকেতটি হারায় না, কারণ সেটি প্রসেসের নিজের অবস্থায় “লেখা হয়ে থাকে”। এতে sleep-কে আর কোনো নির্দিষ্ট lock-এর ধরন জানতে হয় না — commit বার্তার ভাষায় API-টি “আরও modular”.

Linux একই নকশা বহু বছর ধরে ব্যবহার করে: prepare_to_wait() প্রসেসকে wait queue-তে তুলে state বদলায়, তারপর শর্ত যাচাই, তারপর schedule(), শেষে finish_wait(). User space-এ futex-এর FUTEX_WAIT-ও একই নীতিতে কাজ করে: kernel ঘুমানোর আগে আবার মিলিয়ে দেখে ঠিকানার মান এখনো প্রত্যাশিত কি না, না হলে ঘুমায় না।

মূল উপলব্ধি: “যাচাই করা” আর “অপেক্ষা শুরু করা” — এই দুটি কাজকে হয় একটি lock-এর নিচে একসাথে, নয়তো “আগে নাম লেখানো, পরে যাচাই” ক্রমে করতে হয়। সব sleep/wakeup, condition variable আর futex-এর মূল চুক্তি এটাই।

প্র ০৫xv6 আর Linux-এর মধ্যে মৌলিক পার্থক্যগুলো কী কী, আর xv6-এর সরলীকরণগুলো কি “ভুল শেখায়”?

xv6-এ ইচ্ছা করে যা নেই বা খুব সরল, তার তালিকা দীর্ঘ:

  • প্রসেস ও scheduling: স্থির ৬৪টি প্রসেস, thread নেই, signal নেই (শুধু kill একটি ফ্ল্যাগ বসায়), priority নেই; একটি ভাগ করা টেবিলের round robin. Linux-এ লাখো task, প্রতিটি CPU-র run queue, CFS/EEVDF (পর্ব ২১).
  • মেমরি: fork পুরো মেমরি কপি করে (copy-on-write নেই), swap নেই, shared library বা mmap নেই; সব পাতা ৪ KiB. Linux-এ COW, demand paging, huge page, page cache, OOM killer (পর্ব ৩৮).
  • File system: ২৬৮ KiB-এর ফাইল, ১৪ অক্ষরের নাম, user/permission নেই, ৩০টি buffer. Linux-এ VFS, ext4/XFS/Btrfs, সব কিছুর জন্য বিশাল page cache.
  • Driver: শুধু UART আর virtio disk; network বা USB নেই। Linux-এর সোর্সের সবচেয়ে বড় অংশই driver.

তাহলে কি ভুল শেখায়? না — সরল করা আর ভুল করা এক নয়। xv6-এর প্রতিটি অংশ বাস্তব নীতি মেনে চলে: trap-এর পথ, page table-এর গঠন, lock-এর নিয়ম, log-এর commit বিন্দু — Linux-এও একই নীতি, শুধু বাড়তি স্তর আর অপ্টিমাইজেশন। বরং বাদ পড়া জিনিসগুলোই সবচেয়ে ভালো অনুশীলনী হয়ে ওঠে: COW fork, lazy allocation, mmap — শিক্ষার্থী নিজে যোগ করতে গিয়ে বোঝেন কেন Linux-এ এগুলো আছে আর কোথায় কোথায় ঝামেলা লুকিয়ে।

সতর্কতা অবশ্য আছে। xv6-এর কিছু সিদ্ধান্ত বড় পরিসরে চলে না — যেমন scheduler-এর পুরো টেবিল স্ক্যান, বা প্রতিটি অপেক্ষায় সব প্রসেস ঘুরে wakeup. পরীক্ষায় বা ইন্টারভিউতে যদি জিজ্ঞেস করা হয় “Linux-এ কীভাবে হয়”, তখন xv6-এর উত্তর দিলে ভুল হবে। তাই xv6 পড়ার সময় প্রতিটি অংশে নিজেকে প্রশ্ন করুন: “এটি ১০,০০০ প্রসেস বা ১২৮টি core-এ চলবে কি? না চললে বড় kernel কী করে?” — পর্ব ৫৫-এর Linux আর পর্ব ৫৬-এর Windows-এর সাথে মিলিয়ে।

মূল উপলব্ধি: একটি ভালো মডেলের মূল্য তার সম্পূর্ণতায় নয়, তার সততায় — যা দেখায় তা সত্য, আর যা বাদ দেয় তা স্পষ্টভাবে বাদ। xv6 ঠিক সেই অর্থে সৎ মডেল।

অনুশীলন

হিসাবগুলো খাতায় করুন, তারপর উত্তর মিলিয়ে নিন।

  1. বড় ফাইলের হিসাব: xv6-এ BSIZE = ১০২৪, block নম্বর ৪ byte, inode-এ ১২টি direct ও ১টি single indirect pointer. (ক) সর্বোচ্চ ফাইল-আকার কত byte? (খ) যদি ১২টি direct-এর একটিকে double indirect pointer বানানো হয় (অর্থাৎ ১১ direct + ১ single + ১ double), তাহলে সর্বোচ্চ কত block আর কত byte? (গ) পরিবর্তিত নকশায় byte ৩০০,০০০ পড়তে কয়টি block disk থেকে পড়তে হবে (cache খালি ধরে, inode আগেই মেমরিতে)?

    (ক) এক indirect block-এ ১০২৪ ÷ ৪ = ২৫৬টি ঠিকানা। মোট (১২ + ২৫৬) = ২৬৮ block × ১০২৪ = ২৭৪,৪৩২ byte (২৬৮ KiB).

    (খ) Double indirect: ২৫৬ × ২৫৬ = ৬৫,৫৩৬ block. মোট ১১ + ২৫৬ + ৬৫,৫৩৬ = ৬৫,৮০৩ block = ৬৫,৮০৩ × ১০২৪ = ৬৭,৩৮২,২৭২ byte, প্রায় ৬৪.৩ MiB. একটি direct pointer ছেড়ে দিয়ে সীমা প্রায় ২৪৬ গুণ বাড়ল।

    (গ) Block সূচি = ৩০০,০০০ ÷ ১০২৪ = ২৯২ (ভাগফল; ২৯২ × ১০২৪ = ২৯৯,০০৮, offset ৯৯২). Direct অংশ ০–১০, single indirect ১১–২৬৬, তাই ২৯২ পড়ে double indirect-এ: ২৯২ − ২৬৭ = ২৫ নম্বর ঘর, অর্থাৎ প্রথম স্তরের ঘর ২৫ ÷ ২৫৬ = ০, দ্বিতীয় স্তরের ঘর ২৫. পড়তে হবে: double indirect block (১), তার ঘর ০ যে block দেখায় (২), আর আসল ডেটা block (৩) — মোট ৩টি disk-পড়া।

  2. Sv39 হাতে: virtual address 0x403A7C-এর L2, L1, L0 সূচি আর offset বের করুন। তারপর বলুন: একটি প্রসেসের কোড, ডেটা আর stack সবই প্রথম ২ MiB-এর ভেতরে, আর ওপরে trapframe ও trampoline — তার user page table-এ মোট কয়টি page-table পাতা লাগবে, আর কেন?

    0x403A7C ÷ ২৩০ = ০ → L2 = ০. (0x403A7C >> ২১) = 0x403A7C ÷ 0x200000 = ২ (কারণ 0x400000 = ২ × 0x200000) → L1 = ২. (0x403A7C >> ১২) = 0x403, আর 0x403 & 0x1FF = 0x003 → L0 = ৩. Offset = 0xA7C. (লক্ষ করুন, 0x403A7C প্রথম ২ MiB-এর বাইরে — L1 সূচি ২ মানে ৪–৬ MiB-এর এলাকা।)

    পাতার হিসাব: নিচের এলাকা (L2 = ০, L1 = ০) আর ওপরের দুটি পাতা (L2 = ২৫৫, L1 = ৫১১, L0 = ৫১০ ও ৫১১). লাগবে ১টি root, স্তর ১-এ দুটি টেবিল (L2-এর ঘর ০ আর ২৫৫-এর জন্য), আর স্তর ০-এ দুটি টেবিল (প্রতিটি দিকের জন্য একটি) — মোট ৫টি পাতা, ২০ KiB. Trapframe আর trampoline একই স্তর-০ টেবিল ভাগ করে, কারণ তাদের L2 আর L1 সূচি একই।

  3. আউটপুট অনুমান করুন: xv6-ধাঁচের kalloc/kfree (সামনে ঢোকানো, সামনে থেকে নেওয়া) তিনটি পাতা ০, ১, ২ নিয়ে। kinit পাতা ০, ১, ২ ক্রমে kfree করে। তারপর: x = kalloc(); y = kalloc(); kfree(y); kfree(x); z = kalloc();. প্রতিটি ধাপের পর free list আর x, y, z কোন পাতা?

    kinit-এর পর list: [2] [1] [0]. x = kalloc() → x = পাতা ২, list [1] [0]. y = kalloc() → y = পাতা ১, list [0]. kfree(y) → list [1] [0]. kfree(x) → list [2] [1] [0]. z = kalloc() → z = পাতা ২ (= x), list [1] [0].

    অর্থাৎ শেষে যে পাতা ফেরত গেল, সেটিই আগে আবার আসে — LIFO. যদি কোডের কোথাও x-এর পুরোনো pointer রয়ে যায়, সেটি এখন z-এর ডেটায় হাত দেবে — এটিই use-after-free বাগ। xv6 পাতাটি আবর্জনা (১ আর ৫) দিয়ে ভরে, যাতে এমন বাগ অদ্ভুত মান দেখিয়ে তাড়াতাড়ি ধরা পড়ে।

  4. একটি system call-এর নকশা লিখুন: xv6-এ getppid() (অভিভাবকের pid) যোগ করতে কোন কোন ফাইলে কী বদলাতে হবে, তালিকা করুন, আর kernel-এর ফাংশনটি C-তে লিখুন। মনে রাখুন, struct proc-এর মন্তব্য অনুযায়ী parent পড়তে wait_lock ধরতে হয়।

    যা বদলাতে হবে: (১) kernel/syscall.h-এ নতুন নম্বর, যেমন #define SYS_getppid 23 (বর্তমান শেষ নম্বর ২২, SYS_sync; আপনার checkout-এ মিলিয়ে নিন). (২) kernel/syscall.c-এ extern uint64 sys_getppid(void); আর টেবিলে [SYS_getppid] = sys_getppid. (৩) kernel/sysproc.c-এ handler. (৪) user/usys.pl-এ entry("getppid");. (৫) user/user.h-এ int getppid(void);. (৬) পরীক্ষার জন্য একটি ছোট user প্রোগ্রাম আর Makefile-এর UPROGS-এ তার নাম।

    Kernel-এর অংশ (নকশা — আপনার xv6-এ build করে যাচাই করুন): wait_lock সংজ্ঞায়িত proc.c-তে, তাই সেখানেই একটি সহায়ক ফাংশন রাখা পরিষ্কার:

    // proc.c (declare it in defs.h too)
    int
    kgetppid(void)
    {
      struct proc *p = myproc();
      int ppid = 0;              // init has no parent
      acquire(&wait_lock);
      if (p->parent)
        ppid = p->parent->pid;
      release(&wait_lock);
      return ppid;
    }
    
    // sysproc.c
    uint64
    sys_getppid(void)
    {
      return kgetppid();
    }

    এখানে argument নেই, তাই argint লাগেনি; ফেরত মানটি syscall() নিজেই a0-তে বসাবে। Shell থেকে চালানো প্রোগ্রামে ফল আসা উচিত shell-এর pid (সাধারণত ২). wait_lock না ধরলে অভিভাবক ঠিক তখন exit করলে parent বদলে init হয়ে যেতে পারে — ছোট কিন্তু আসল race.

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমRISC-V xv6-এর বুটের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. _entry (entry.S) — প্রতিটি hart-এর stack বসানো
  2. start() — machine mode-এর সেটিং, mret দিয়ে supervisor mode
  3. main() — allocator, page table, প্রসেস টেবিল, disk চালু
  4. userinit() — pid ১ তৈরি, RUNNABLE
  5. scheduler() → swtch → forkret() — /init লোড
  6. /init → fork + exec("sh") — $ prompt

RISC-V xv6-এ একটি user প্রোগ্রাম system call করলে system call-এর নম্বর কোন register-এ থাকে?

  • a0
  • a7
  • sp
  • ra

RISC-V-এর Sv39 page table-এ একটি virtual address কীভাবে ভাগ হয়?

  • ১০/১০/১২ bit — দুটি সূচি আর offset
  • ৯/৯/৯/৯/১২ bit — চারটি সূচি আর offset
  • ৯/৯/৯/১২ bit — তিনটি সূচি আর offset
  • ১৩/১৩/১৩ bit — offset ছাড়া তিনটি সূচি

একটি file system-এ block-এর আকার ১০২৪ byte, block নম্বর ৪ byte, আর inode-এ ১২টি direct ও ১টি single indirect pointer (xv6-এর মতো). একটি ফাইল সর্বোচ্চ কত বড় হতে পারে?

  • ২৬৮ KiB (২৭৪,৪৩২ byte)
  • ১২ KiB
  • ১০৭২ KiB
  • প্রায় ৬৪ MiB

xv6-এর acquire() spinlock নেওয়ার আগে সেই CPU-তে interrupt বন্ধ করে (push_off). কেন?

  • অন্য CPU-গুলো যাতে একই সময়ে lock নিতে না পারে
  • Spinlock দ্রুত করার জন্য
  • যাতে scheduler কখনো চলতে না পারে
  • একই CPU-তে interrupt handler একই lock চাইলে যে deadlock হতো, তা ঠেকাতে

xv6-এর scheduler কোন নীতিতে পরের প্রসেস বাছে?

  • সবচেয়ে বেশি priority-র প্রসেস আগে
  • প্রসেস টেবিল ক্রমানুসারে ঘুরে প্রথম RUNNABLE প্রসেস, timer interrupt-এ পালা বদল
  • সবচেয়ে ছোট CPU burst-এর প্রসেস আগে (SJF)
  • সবচেয়ে কম virtual runtime-এর প্রসেস আগে

xv6-এর kalloc খালি পাতাগুলোর linked list-এর next pointer কোথায় রাখে?

  • একটি আলাদা bitmap-এ, প্রতিটি পাতার জন্য একটি bit
  • Disk-এর একটি বিশেষ এলাকায়
  • প্রতিটি খালি পাতার নিজের ভেতরে, শুরুতে
  • Kernel page table-এর PTE-তে

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. xv6 = Unix V6-এর আধুনিক পুনর্লিখন, MIT-র শিক্ষণ-OS; RISC-V, multicore, kernel কয়েক হাজার লাইন; সঙ্গে বিনামূল্যের বই।
  2. বুট: _entry → start() (M → S) → main() → userinit() → scheduler() → forkret() → /init → sh.
  3. System call: নম্বর a7-এ, ecall → uservec (trampoline) → usertrap() → syscalls[a7]() → ফল a0-তে; user ঠিকানা শুধু copyin/copyout দিয়ে।
  4. মেমরি: Sv39 = ৯/৯/৯/১২, তিন স্তর, প্রতিটি টেবিল ৫১২ × ৮ byte; walk(), mappages(), আর LIFO free list-এর kalloc.
  5. File system: disk → buffer cache → log → inode → directory → path → fd; সর্বোচ্চ ফাইল (১২ + ২৫৬) × ১০২৪ byte; spinlock-এ interrupt বন্ধ, দীর্ঘ অপেক্ষায় sleeplock.

পরের পর্বে: পড়া শেষ, এবার বানানোর পালা। পর্ব ৫৯-এর ক্যাপস্টোনে আপনি নিজেই একটি ছোট shell, একটি scheduler simulator আর একটি memory allocator লিখবেন — এই কোর্সের প্রায় প্রতিটি মডিউলের ধারণা এক জায়গায়।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান