Foundationপ্রথম নীতি থেকে
LEVEL 4লেসন ১/২৯কঠিন১ ঘণ্টা ১৫ মিনিট

অপারেটিং সিস্টেম আসলে কী — bare metal থেকে kernel পর্যন্ত

What Is an Operating System?

OS একটা রহস্যময় 'সিস্টেম সফটওয়্যার' না — এটা তিনটা সুনির্দিষ্ট কাজ করে: একটা hardware-কে বহু ব্যবহারকারীর মধ্যে multiplex করা, তাদের একে অপরের থেকে isolate করা, আর কাঁচা hardware-এর উপর ব্যবহারযোগ্য abstraction (process, file, socket, address space) তৈরি করা। এই লেসনে আমরা bare metal থেকে শুরু করব — যেখানে কোনো OS নেই — তারপর পুরো boot chain ধরে kernel-এ পৌঁছাব, monolithic বনাম microkernel বিতর্ক দেখব, আর শেষে libc ছাড়া একটা প্রোগ্রাম লিখে হাতেকলমে দেখব OS আর library-র সীমারেখা ঠিক কোথায়।

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

  • OS-বিহীন একটা সিস্টেম (Arduino-শ্রেণির bare metal, bootloader-only firmware) আর OS-চালিত সিস্টেমের মধ্যে ঠিক কী কী পার্থক্য — memory-র মালিকানা, interrupt handling, concurrency, fault containment — প্রতিটা কংক্রিটভাবে ব্যাখ্যা করতে পারবেন
  • OS-এর তিনটা মূল কাজ — hardware multiplexing, isolation, আর abstraction — আলাদা করে চিনতে পারবেন, আর যেকোনো OS-বৈশিষ্ট্যকে এই তিনটার কোনো একটায় শ্রেণিবদ্ধ করতে পারবেন
  • পাওয়ার বোতাম থেকে shell prompt পর্যন্ত পূর্ণ boot chain (firmware → bootloader → kernel → init → userland) ধাপে ধাপে বর্ণনা করতে পারবেন, আর নিজের মেশিনে `dmesg`, `systemd-analyze`, `/proc/cmdline` দিয়ে প্রতিটা ধাপ যাচাই করতে পারবেন
  • Monolithic, microkernel আর hybrid kernel architecture-এর পার্থক্য প্রকৃত উদাহরণসহ (Linux, seL4, QNX, XNU, Windows NT) তুলনা করতে পারবেন — বিশেষত driver কোথায় চলে আর তার fault-containment ও performance পরিণতি কী
  • কোনটা OS আর কোনটা OS না — shell, libc, systemd, GNOME, package manager — এই সীমারেখা নির্ভুলভাবে টানতে পারবেন, আর ব্যাখ্যা করতে পারবেন কেন এই বিভ্রান্তিটা এত সাধারণ
  • libc ছাড়া একটা freestanding প্রোগ্রাম (`-nostdlib`) লিখে সরাসরি raw syscall দিয়ে exit করাতে পারবেন, আর `strace` দিয়ে দেখাতে পারবেন libc ঠিক কতটা কাজ আপনার অগোচরে করে দেয়

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

আগে এটা বুঝি

গত module-এ আপনি assembly লিখেছেন, assembler দিয়ে object file বানিয়েছেন, linker দিয়ে একটা ELF executable তৈরি করেছেন, তারপর ./prog টাইপ করে সেটা চালিয়েছেন। শেষ ধাপটার দিকে একবার ভালো করে তাকান — ./prog টাইপ করার পর ঠিক কে সেই ELF ফাইলটা পড়ল? কে সিদ্ধান্ত নিল আপনার .text section কোন memory address-এ যাবে? কে ঠিক করল আপনার প্রোগ্রাম CPU পাবে, আর ঠিক কতক্ষণ পাবে? আর যখন আপনার প্রোগ্রাম একটা NULL pointer dereference করে ক্র্যাশ করল — কে সেটা ধরল, আর কেন পুরো মেশিন রিবুট হয়ে গেল না?

এই সব প্রশ্নের উত্তর একটাই শব্দ: operating system। কিন্তু “operating system” শব্দটা এত ব্যাপকভাবে, এত অস্পষ্টভাবে ব্যবহৃত হয় যে এটা প্রায় অর্থহীন হয়ে গেছে। কেউ বলে “আমার OS হলো Ubuntu”, কেউ বলে “Windows একটা OS”, কেউ ভাবে OS মানে যে ডেস্কটপটা স্ক্রিনে দেখা যায়। এই লেসনের প্রথম কাজ হলো সেই ঝাপসা শব্দটাকে তিনটা সুনির্দিষ্ট, যাচাইযোগ্য কাজে ভেঙে ফেলা।

আর সেটা করার সবচেয়ে পরিষ্কার উপায় হলো উল্টো দিক থেকে শুরু করা — যদি কোনো OS না থাকত, তাহলে ঠিক কী কী হতো না?

একটা কংক্রিট সংখ্যা দিয়ে শুরু করি। আপনার Linux মেশিনে এই কমান্ডটা চালান:

ps aux | wc -l
312

তিনশোরও বেশি process। আর nproc চালালে হয়তো দেখবেন 8 — মাত্র আটটা CPU core। ৩১২টা প্রোগ্রাম, ৮টা core, আর প্রতিটা প্রোগ্রাম কোড লেখা হয়েছে এমনভাবে যেন সে-ই একমাত্র চলছে — কোনো প্রোগ্রামে আপনি লেখেননি “এখন আমি CPU ছেড়ে দিচ্ছি, অন্য কেউ চালাক”। এই বিভ্রমটা কে বানায়, আর কীভাবে — সেটাই এই পুরো module-এর কেন্দ্রীয় প্রশ্ন, আর এই লেসন তার মানচিত্র।

মূল ধারণা

Bare metal — যখন সত্যিই কোনো OS নেই

OS কী বোঝার সবচেয়ে ভালো উপায় হলো একটা এমন সিস্টেম দেখা যেখানে OS নেই। এরকম সিস্টেম প্রচুর আছে, এবং সেগুলো খেলনা না — বিলিয়ন সংখ্যায় শিপ হয়।

একটা Arduino Uno-র ভেতরে আছে একটা ATmega328P microcontroller: ৮-বিট AVR core, ১৬ MHz, ৩২ KB flash, ২ KB SRAM। আপনি avr-gcc দিয়ে একটা C প্রোগ্রাম কম্পাইল করে .hex ফাইল বানান, avrdude দিয়ে সেটা flash-এ লিখে দেন, আর chip রিসেট হলে address 0x0000-এ থাকা reset vector থেকে execution শুরু হয় — সরাসরি আপনার কোডে। মাঝখানে কেউ নেই।

সেই জগতে যা যা নেই:

  • কোনো fork() নেই — একটাই execution stream, চিরকাল।
  • কোনো printf() নেই যা কোথাও যায় — output চাইলে আপনাকে UART peripheral-এর UDR0 register-এ সরাসরি byte লিখতে হবে।
  • কোনো malloc() নেই যার পেছনে virtual memory আছে — heap মানে শুধু সেই ২ KB SRAM-এর একটা অংশ যেটা আপনি নিজে ভাগ করে নিয়েছেন। Stack আর heap মুখোমুখি বাড়তে বাড়তে ধাক্কা খেলে কেউ আপনাকে সতর্ক করবে না — ডেটা নীরবে নষ্ট হবে।
  • কোনো memory protection নেই — একটা বাজে pointer যেকোনো address-এ লিখতে পারে, নিজের কোড-সহ।
  • কোনো scheduler নেই — আপনার while (1) লুপ থেকে বেরোনোর একমাত্র উপায় interrupt, আর সেই interrupt handler-ও আপনাকেই লিখতে হবে।

এটাই bare metal। এবং লক্ষ করুন, এটা কোনো ঘাটতি না — এটা একটা ট্রেড-অফ। একটা pacemaker বা ABS ব্রেক কন্ট্রোলারে আপনি ঠিক এই determinism-ই চান: কোনো scheduler নেই মানে কোনো অপ্রত্যাশিত latency নেই, interrupt থেকে response পর্যন্ত সময়টা cycle-নির্ভুলভাবে গোনা যায়।

বিষয়Bare metal (Arduino, U-Boot)OS-চালিত (Linux)
Memory-র মালিকআপনার প্রোগ্রাম, পুরোটাইkernel; process পায় একটা virtual address space
Interrupt handlerআপনি নিজে লেখেন, vector table-এ বসানkernel লেখে; আপনার কোড কখনো দেখেই না
একসাথে কয়টা প্রোগ্রামএকটাশত শত, বিভ্রম-সহ যে প্রত্যেকে একা
Bug-এর পরিণতিপুরো ডিভাইস হ্যাং/ক্র্যাশএকটা process মরে; বাকিরা টেরও পায় না
I/O করার উপায়Peripheral register-এ সরাসরি লেখাwrite() syscall, driver-এর মধ্য দিয়ে
প্রোগ্রাম বদলানোপুরো firmware আবার flash করাএকটা ফাইল কপি করা

OS-এর তিনটা কাজ

উপরের টেবিলের প্রতিটা সারি আসলে তিনটা মৌলিক কাজের কোনো একটার প্রকাশ। এই তিনটা মনে রাখলে পুরো module-এর প্রতিটা লেসন এদের কোনো একটার গভীরে নামা হিসেবে পড়া যাবে।

১. Multiplexing — একটা hardware, বহু ব্যবহারকারী

আপনার কাছে ৮টা CPU core আছে, কিন্তু ৩১২টা process। আপনার কাছে ১৬ GB RAM আছে, কিন্তু সব process-এর virtual address space যোগ করলে সেটা হয়তো ১০০ GB। একটাই নেটওয়ার্ক কার্ড, কিন্তু ব্রাউজার, apt, আর একটা background sync ডেমন — তিনজনই একসাথে প্যাকেট পাঠাচ্ছে।

OS এই দুষ্প্রাপ্য সম্পদগুলো ভাগ করে দুইভাবে:

  • Time-division — CPU: প্রতিটা process কয়েক মিলিসেকেন্ড চলে, তারপর জোর করে সরিয়ে দেওয়া হয় (preemption)। Linux-এ timer interrupt-এর হার দেখা যায় CONFIG_HZ দিয়ে — সাধারণ ডেস্কটপ কার্নেলে ২৫০ বা ১০০০, অর্থাৎ প্রতি ৪ ms বা ১ ms-এ একবার kernel-এর হাতে নিয়ন্ত্রণ ফিরে আসে।
  • Space-division — RAM: প্রতিটা process পায় physical page-এর একটা আলাদা সেট, page table-এর মাধ্যমে তার নিজের virtual address-এ ম্যাপ করা। ডিস্ক: প্রতিটা ফাইল আলাদা block পায়।

২. Isolation — কেউ কারো ঘরে ঢুকতে পারবে না

Multiplexing একা যথেষ্ট না। যদি ৩১২টা process একই RAM ভাগ করে নেয় কিন্তু একে অপরের memory পড়তে-লিখতে পারে, তাহলে আপনার ব্রাউজারে খোলা ব্যাংকের পাসওয়ার্ড যেকোনো প্রোগ্রাম পড়ে নিতে পারবে, আর যেকোনো একটা buggy প্রোগ্রামের একটা ভুল pointer পুরো সিস্টেম নামিয়ে দিতে পারবে।

তাই OS-এর দ্বিতীয় কাজ isolation, আর এখানে একটা অত্যন্ত গুরুত্বপূর্ণ কথা আছে যা পরের লেসনের পুরো ভিত্তি:

Isolation কখনোই software-এর সদিচ্ছার উপর নির্ভর করে না — এটা hardware দিয়ে বলপূর্বক প্রয়োগ করা হয়।

দুইটা hardware ব্যবস্থা এই কাজটা করে:

  • MMU + page table — প্রতিটা process-এর নিজস্ব page table আছে। Process A যদি এমন একটা address-এ পড়তে চায় যেটা তার page table-এ ম্যাপ করা নেই, MMU নিজেই একটা page fault তৈরি করে — কোনো instruction চলে না, ডেটাও ফাঁস হয় না। (Virtual memory এই module-এর একটা বড় অংশ।)
  • Privilege level (ring) — কিছু instruction শুধু kernel চালাতে পারে। User-mode কোড cli (interrupt বন্ধ করা) বা page table-এর base register (cr3) বদলানোর চেষ্টা করলে CPU সেটাকে exception বানিয়ে kernel-কে জানায়। এটাই পরের লেসনের বিষয়, আর সেখানে আমরা এটা হাতেকলমে ভেঙে দেখব।

Isolation-এর আরেকটা কম-আলোচিত দিক হলো fault containment। আপনার একটা C প্রোগ্রামে *(int*)0 = 1; লিখে চালান — শুধু সেই প্রোগ্রামটা Segmentation fault বলে মরে, আপনি shell prompt-এ ফিরে আসেন, বাকি ৩১১টা process কিছুই টের পায় না। Arduino-তে একই বাগ মানে ডিভাইস রিসেট।

৩. Abstraction — কাঁচা hardware-এর উপর ব্যবহারযোগ্য ধারণা

Hardware যা দেয় তা ব্যবহারের অযোগ্যভাবে নিচু স্তরের। একটা NVMe SSD-র সাথে কথা বলতে হলে আপনাকে PCIe BAR-এ ম্যাপ করা doorbell register-এ ৬৪-বাইটের submission queue entry লিখতে হবে, তারপর completion queue polling করতে হবে। এই কাজটা প্রতিটা প্রোগ্রামারকে করতে বললে কেউ কিছু লিখতে পারত না — আর দুইটা প্রোগ্রাম একসাথে করলে ডিস্ক নষ্ট হয়ে যেত।

তাই OS প্রতিটা hardware বাস্তবতার উপর একটা পরিষ্কার, স্থিতিশীল abstraction বসায়:

Abstractionনিচে আসল hardware বাস্তবতা কীযে API দিয়ে ধরা হয়
ProcessCPU register-এর একটা সেট + কিছু page table entry + kernel-এর একটা task_structfork, execve, wait4, exit_group
Address spacePhysical page-এর একটা বিক্ষিপ্ত সংগ্রহ, page table দিয়ে সাজানোmmap, brk, munmap
FileNVMe/SATA ডিভাইসে ছড়ানো কিছু block, একটা inode দিয়ে বাঁধাopen, read, write, close, lseek
SocketNIC-এর DMA ring buffer, DMA descriptor, MAC/IP/TCP headersocket, connect, send, recv
Threadএকই page table ভাগ করা আরেকটা register-অবস্থাclone, futex

এই abstraction-গুলোর সৌন্দর্য হলো তাদের অভিন্নতাread(fd, buf, n) — এই একটা কল কাজ করে যখন fd একটা SSD-র ফাইল, একটা নেটওয়ার্ক socket, একটা কীবোর্ড, একটা pipe, বা /dev/urandom। আপনার কোডকে জানতেই হয় না নিচে কী আছে। এই “সবকিছুই একটা ফাইল” নীতিটা Unix-এর সবচেয়ে প্রভাবশালী ডিজাইন-সিদ্ধান্ত, আর এই module-এর file descriptor ও VFS লেসনগুলোর কেন্দ্রীয় বিষয়।

   PROCESS-এর দৃষ্টিতে জগৎ                  আসল HARDWARE
  ┌───────────────────────────┐         ┌──────────────────────────┐
  │  "আমার নিজের CPU"          │  ←───  │  8 core, 312 process     │
  │  "আমার নিজের 128 TB RAM"   │  ←───  │  16 GB DRAM + swap       │
  │  "/home/me/notes.txt"     │  ←───  │  NVMe LBA 8823440..8823456│
  │  "একটা TCP connection"     │  ←───  │  NIC DMA ring, IRQ 132   │
  │  "getchar() থেকে একটা অক্ষর"│  ←───  │  USB HID interrupt xfer  │
  └───────────────────────────┘         └──────────────────────────┘
              ▲                                     ▲
              │      ┌───────────────────────┐      │
              └──────│        KERNEL         │──────┘
                     │ multiplex + isolate   │
                     │     + abstract        │
                     └───────────────────────┘
তিন স্তর -- hardware যা দেয়, kernel যা তৈরি করে, আর process যা দেখে। প্রতিটা তীর একটা abstraction।

Boot sequence — পাওয়ার বোতাম থেকে shell prompt

OS আকাশ থেকে পড়ে না। পাওয়ার বোতাম চাপার মুহূর্তে RAM ফাঁকা, kernel ডিস্কে একটা সাধারণ ফাইল হিসেবে পড়ে আছে, আর CPU একটা নির্দিষ্ট hardwired address থেকে instruction আনা শুরু করে। মাঝখানের ধাপগুলো এরকম:

ধাপকে চালায়কোথা থেকে আসেমূল কাজহস্তান্তর করে
১. FirmwareUEFI (বা পুরনো BIOS)Motherboard-এর SPI flash chipRAM training, PCIe enumeration, ডিস্ক খুঁজে বের করাEFI System Partition (FAT32) থেকে একটা .efi প্রোগ্রাম
২. BootloaderGRUB2 / systemd-boot / rEFIndESP-তে ফাইল হিসেবে (/boot/efi/EFI/...)menu দেখানো, kernel + initramfs RAM-এ লোড, boot_params ভরাkernel entry point-এ jump
৩. Kernel (early)vmlinuz-এর ভেতরের decompressor/boot/vmlinuz-*নিজেকে decompress করা, page table বানানো, ৬৪-বিট mode-এ যাওয়াstart_kernel()
৪. Kernel (init)start_kernel() in init/main.cএকই বাইনারিscheduler, memory manager, driver, VFS চালু; initramfs মাউন্টPID 1 হিসেবে userspace প্রোগ্রাম execve
৫. init / systemd/sbin/initsystemdআসল root filesystemunit dependency সমাধান করে সব service সমান্তরালে চালুgetty, display manager
৬. Userlandlogin shell, GNOME, আপনার সব প্রোগ্রাম/usr/bin/...আপনি যা করেন

লক্ষ করুন ধাপ ৫ থেকে আমরা আর kernel-এ নেই — systemd একটা সাধারণ user-space প্রোগ্রাম, শুধু তার PID 1 আর তার কিছু বিশেষ দায়িত্ব আছে (orphan process দত্তক নেওয়া — যেটা পঞ্চম লেসনের বিষয়)। এই সীমারেখাটা এই লেসনের একটা মূল শিক্ষা, আর নিচে “যা OS না” অংশে আবার ফিরে আসবে।

Kernel architecture — কোথায় কোন কোড চলবে

সব OS-ই উপরের তিনটা কাজ করে। পার্থক্য হলো কতটুকু কোড সর্বোচ্চ privilege-এ চলবে সেই সিদ্ধান্তে। এটাই কম্পিউটার সায়েন্সের সবচেয়ে পুরনো স্থাপত্য-বিতর্কগুলোর একটা।

Monolithic kernel — filesystem, নেটওয়ার্ক stack, device driver, scheduler, memory manager, সবকিছু একই privileged address space-এ, একটাই বড় প্রোগ্রাম হিসেবে। Linux-এর সবচেয়ে বিশুদ্ধ উদাহরণ। একটা read() কল করলে kernel-এর ভেতরে VFS → ext4 → block layer → NVMe driver পর্যন্ত পুরো পথটা সাধারণ function call — কোনো message passing নেই, তাই দ্রুত। কিন্তু একটা buggy driver পুরো kernel-এর যেকোনো ডেটা কাঠামো নষ্ট করতে পারে, ফল kernel panic।

Microkernel — kernel-এ থাকে শুধু একেবারে অপরিহার্য: address space, thread, আর IPC (message passing)। Filesystem, driver, নেটওয়ার্ক stack — সব সাধারণ user-space process। একটা ফাইল পড়া মানে filesystem server-কে একটা message পাঠানো, যে আবার disk driver server-কে message পাঠায়। একটা driver ক্র্যাশ করলে শুধু সেই process মরে; supervisor সেটাকে আবার চালু করতে পারে, বাকি সিস্টেম চলতেই থাকে।

Hybrid — বাস্তবে সবচেয়ে প্রচলিত আপস। XNU (macOS/iOS)-এ Mach microkernel-এর IPC ও memory ধারণা আছে, কিন্তু BSD layer আর অধিকাংশ driver একই address space-এ চলে — অর্থাৎ microkernel-এর ইন্টারফেস, monolithic-এর পারফরম্যান্স। Windows NT-ও একই ধাঁচের।

MonolithicMicrokernelHybrid
উদাহরণLinux, FreeBSDseL4, MINIX 3, QNX, L4XNU (macOS), Windows NT
Privileged কোডের আকারবড় — Linux ৬.x-এর সোর্স ট্রি ৩ কোটির বেশি লাইন, একটা distro kernel বাইনারি সাধারণত ১০-১৫ MB compressedখুব ছোট — seL4-এর verified kernel প্রায় ১০ হাজার লাইন Cমাঝামাঝি
Driver কোথায়Kernel-এর ভেতরে (module হিসেবেও)User space processবেশিরভাগ kernel-এ
একটা driver ক্র্যাশ করলেKernel panic — পুরো মেশিনশুধু সেই server মরে, restart সম্ভবসাধারণত panic
একটা ফাইল পড়ার খরচকয়েকটা function callকয়েকটা IPC round tripকয়েকটা function call
শক্তিশালী দিকপারফরম্যান্স, ড্রাইভার-প্রাচুর্যFault containment, verifiabilityব্যবহারিক ভারসাম্য

যা OS না — সীমারেখাটা ঠিক কোথায়

এই লেসনের সবচেয়ে বেশি ব্যবহারিক অংশটা সম্ভবত এটাই, কারণ প্রতিদিনের ভাষায় আমরা যাকে “OS” বলি তার বেশিরভাগই আসলে kernel না।

  • Shell (bash, zsh, fish) OS না। এটা একটা সাধারণ প্রোগ্রাম — /usr/bin/bash, আপনার নিজের লেখা প্রোগ্রামের মতোই একটা ELF ফাইল, একই privilege-এ চলে। আপনি নিজে একটা shell লিখতে পারেন (পঞ্চম লেসনে ঠিক তা-ই করব)।
  • libc (glibc, musl) OS না। এটা একটা library, আপনার process-এর address space-এ ম্যাপ হওয়া একগাদা user-space কোড। printf একটা libc ফাংশন যা শেষমেশ write syscall করে — kernel printf চেনে না, শুধু write চেনে।
  • systemd OS না (যদিও এটা PID 1)। User-space process, execve দিয়ে চালু হওয়া।
  • GNOME/KDE/Explorer/Finder OS না। এগুলো user-space অ্যাপ্লিকেশনের সংগ্রহ। Linux server-এ প্রায়ই এদের কিছুই ইনস্টল করা থাকে না, তবুও OS পুরোপুরি কাজ করে।
  • apt/dnf/Homebrew OS না। প্যাকেজ ম্যানেজার সাধারণ প্রোগ্রাম যা ফাইল ডাউনলোড করে জায়গামতো রাখে।

তাহলে কী OS? সংকীর্ণ, প্রযুক্তিগত অর্থে: kernel — সেই কোড যা privileged mode-এ চলে, hardware-কে সরাসরি নিয়ন্ত্রণ করে, আর syscall interface-এর মাধ্যমে সেবা দেয়। বিস্তৃত, প্রচলিত অর্থে “operating system” বলতে বোঝানো হয় kernel + সেই ন্যূনতম userland যা ছাড়া সিস্টেম ব্যবহারযোগ্য না (init, libc, coreutils, shell) — অর্থাৎ একটা distribution। এই দ্বৈততাই “Linux বনাম GNU/Linux” নামকরণ-বিতর্কের মূল: Linux কড়া অর্থে শুধু kernel-টাই, আর Ubuntu হলো সেই kernel + GNU userland + আরও হাজারটা প্যাকেজ।

ভেতরে কী ঘটছে

পাওয়ার বোতাম থেকে prompt — প্রকৃত পথ, ঠিকানা ও ফাংশন-নাম সহ

উপরের টেবিলটা মানচিত্র। এখন প্রকৃত পথটা, x86-64 Linux-এর কংক্রিট বাস্তবতায়:

Power-on থেকে shell prompt -- x86-64 UEFI Linux
  1. CPU reset৬৪-বিট CPU রিসেট হয় 16-bit real mode-এ, প্রথম instruction আনে physical address 0xFFFFFFF0 থেকে -- এটা SPI flash-এ ম্যাপ করা firmware, RAM এখনো ব্যবহারযোগ্য না (DRAM controller-ই চালু হয়নি)
  2. UEFI firmwareCache-as-RAM দিয়ে শুরু, তারপর DRAM training, PCIe enumeration, boot device খোঁজা; ESP (FAT32 partition) থেকে EFI/BOOT/BOOTX64.EFI লোড করে চালায়
  3. Bootloader (GRUB2 / systemd-boot)একটা EFI অ্যাপ্লিকেশন; menu দেখায়, /boot/vmlinuz-6.8.0 আর /boot/initrd.img RAM-এ পড়ে, boot_params কাঠামো ভরে (কমান্ড লাইন, memory map, framebuffer তথ্য), তারপর kernel entry-তে jump
  4. Kernel decompressorvmlinuz আসলে একটা ছোট setup stub + gzip/zstd-সংকুচিত vmlinux; stub নিজেকে unpack করে, KASLR-এর জন্য random offset বাছে, একটা প্রাথমিক identity-mapped page table বানিয়ে long mode-এ ঢোকে
  5. arch/x86/kernel/head_64.S : startup_64প্রকৃত ৬৪-বিট kernel entry -- GDT/IDT বসায়, per-CPU এলাকা তৈরি করে, তারপর C কোডে লাফ দেয়
  6. init/main.c : start_kernel()এখান থেকে সব C-তে: setup_arch(), mm_init() (buddy allocator, slab), sched_init(), trap_init(), rcu_init(), time_init(), console_init() -- এই মুহূর্ত থেকেই dmesg-এর টাইমস্ট্যাম্প শুরু
  7. rest_init() → kernel_thread(kernel_init)kernel দুটো thread বানায়: PID 1 হবে init, PID 2 হবে kthreadd (সব kernel thread-এর জনক); মূল boot thread নিজে PID 0 (swapper/idle) হয়ে idle loop-এ চলে যায়
  8. kernel_init() → run_init_process("/sbin/init")initramfs মাউন্ট করে driver লোড হয়, আসল root filesystem মাউন্ট হয়, তারপর একটা execve -- এই একটা কলেই PID 1 kernel thread থেকে user-space process-এ রূপান্তরিত হয়
  9. systemd (PID 1, user space)unit dependency graph সমাধান করে সব target সমান্তরালে চালু করে; এখান থেকে সবকিছুই সাধারণ process, প্রতিটা syscall দিয়ে kernel-এর সাথে কথা বলে
  10. getty → login → bashfork + execve-র একটা শৃঙ্খল -- ঠিক যে যন্ত্রটা এই module-এর পঞ্চম লেসনে নিজে হাতে বানাব

এই ধাপগুলোর মধ্যে দুইটা মুহূর্ত বিশেষভাবে খেয়াল করার মতো।

প্রথম মুহূর্ত — execve("/sbin/init") এর আগে পর্যন্ত সব কোড privileged mode-এ চলছিল। এই একটা কলের পর প্রথমবারের মতো CPU user mode-এ নামে, আর সেই মুহূর্ত থেকে kernel আর “চলছে” না — সে অপেক্ষা করছে। এই “kernel চলছে না, অপেক্ষা করছে” ধারণাটা এই লেসনের চতুর্থ misconception-এর বিষয়, আর পরের দুটো লেসনের কেন্দ্রীয় মেকানিজম।

দ্বিতীয় মুহূর্ত — initramfs। একটা মুরগি-ডিম সমস্যা আছে: root filesystem / হয়তো একটা LVM volume-এর উপর, যেটা আবার একটা encrypted device-এর উপর, যেটা আবার একটা NVMe ডিস্কে। এই তিনটার driver দরকার root মাউন্ট করতে — কিন্তু driver-গুলো তো /lib/modules/-এ, অর্থাৎ root-এর ভেতরেই। সমাধান: bootloader RAM-এ একটা ছোট, সম্পূর্ণ, সংকুচিত filesystem (initramfs, সাধারণত ৩০-৮০ MB) লোড করে দেয় যেটায় ঠিক প্রয়োজনীয় module-গুলো আছে। Kernel সেটা মাউন্ট করে, driver লোড করে, আসল root মাউন্ট করে, তারপর switch_root দিয়ে সরে যায়।

Kernel “চলছে” মানে কী — তিনটা প্রেক্ষাপট

Kernel কোড কখন execute হয়? ঠিক তিনটা পরিস্থিতিতে, আর একটাও “background-এ একটা প্রোগ্রাম হিসেবে” না:

প্রেক্ষাপটকীভাবে ঢোকেউদাহরণps-এ দেখা যায়?
Process contextএকটা process syscall করে trap করেছেআপনার read() কলহ্যাঁ — সেই process-ই, D/S state-এ
Interrupt contextHardware একটা IRQ তুলেছেনেটওয়ার্ক প্যাকেট এসেছে, timer tickনা — কোনো process-এর হয়ে চলছে না
Kernel threadkernel নিজে schedulable thread বানিয়েছেkworker, ksoftirqd, kswapdহ্যাঁ — নামের চারপাশে বর্গবন্ধনী সহ

ps aux-এ যে entry-গুলো [kworker/0:1], [ksoftirqd/2], [kswapd0] — এই বর্গবন্ধনীগুলো মানে এদের কোনো user-space address space নেই; এরা kernel-এর নিজের thread, শুধু scheduler-এর সুবিধার জন্য task_struct পেয়েছে। এদের /proc/PID/cmdline ফাঁকা, ঠিক এই কারণেই।

উদাহরণ

একটা বাস্তব boot-এর সময়-বিভাজন

তত্ত্ব যথেষ্ট হয়েছে — একটা প্রকৃত মেশিনের boot প্রোফাইল দেখি, প্রতিটা সংখ্যা উপরের ধাপগুলোর সাথে মিলিয়ে।

systemd-analyze
Startup finished in 4.512s (firmware) + 2.201s (loader) + 1.874s (kernel)
                     + 3.397s (initrd) + 8.219s (userspace) = 20.205s
graphical.target reached after 8.211s in userspace.

পাঁচটা সংখ্যা, উপরের টেবিলের পাঁচটা ধাপের সাথে সরাসরি মিলে যায়:

systemd-analyze লেবেলকোন ধাপ৪.৫/২.২/১.৯/৩.৪/৮.২ সেকেন্ডে কী ঘটল
firmware — ৪.৫১২sধাপ ১UEFI POST: DRAM training (সবচেয়ে ধীর অংশ, প্রতি boot-এ আবার), PCIe enumeration, USB scan
loader — ২.২০১sধাপ ২GRUB menu timeout + ১৪ MB vmlinuz ও ৬৭ MB initrd ডিস্ক থেকে পড়া
kernel — ১.৮৭৪sধাপ ৩-৪decompress + start_kernel() + সব built-in driver probe
initrd — ৩.৩৯৭sধাপ ৪-এর শেষinitramfs-এ module লোড, LUKS unlock, LVM scan, আসল root মাউন্ট
userspace — ৮.২১৯sধাপ ৫-৬systemd-এর সব unit — নেটওয়ার্ক, লগিং, display manager

কে ঠিক কতটা সময় নিল, তা unit ধরে ধরে দেখা যায়:

systemd-analyze blame | head -8
3.108s NetworkManager-wait-online.service
1.844s snapd.service
1.201s systemd-udev-settle.service
 892ms dev-nvme0n1p2.device
 611ms systemd-journal-flush.service
 405ms udisks2.service
 288ms polkit.service
 141ms systemd-logind.service

এখানে একটা শিক্ষণীয় বিষয় লুকানো আছে: শীর্ষ entry-টা (NetworkManager-wait-online, ৩.১ সেকেন্ড) কোনো CPU কাজ করছে না — সে অপেক্ষা করছে, DHCP উত্তরের জন্য। অর্থাৎ ২০ সেকেন্ডের boot-এর প্রায় ১৫% শুধু একটা নেটওয়ার্ক timeout। এটাই OS-এর concurrency-র দিকটার একটা সরাসরি উদাহরণ, আর কেন systemd সমান্তরাল unit চালু করার উপর এত জোর দেয়।

Kernel-এর নিজের boot log, প্রথম ২০ লাইন

dmesg-এর টাইমস্ট্যাম্পগুলো শুরু হয় start_kernel()-এ timer চালু হওয়ার মুহূর্ত থেকে — অর্থাৎ উপরের kernel ধাপের ভেতরের সময়:

dmesg | head -14
[    0.000000] Linux version 6.8.0-45-generic (buildd@lcy02-amd64-072)
               (x86_64-linux-gnu-gcc-13, GNU ld 2.42) #45-Ubuntu SMP PREEMPT_DYNAMIC
[    0.000000] Command line: BOOT_IMAGE=/boot/vmlinuz-6.8.0-45-generic
               root=UUID=3f2a... ro quiet splash
[    0.000000] BIOS-provided physical RAM map:
[    0.000000] BIOS-e820: [mem 0x0000000000000000-0x000000000009ffff] usable
[    0.000000] BIOS-e820: [mem 0x00000000000a0000-0x00000000000fffff] reserved
[    0.000000] BIOS-e820: [mem 0x0000000000100000-0x00000000bffdffff] usable
[    0.000000] NX (Execute Disable) protection: active
[    0.000000] efi: EFI v2.7 by American Megatrends
[    0.004221] KASLR enabled
[    0.011043] Memory: 16221244K/16777216K available
[    0.238117] smp: Bringing up secondary CPUs ...
[    0.291884] smp: Brought up 1 node, 8 CPUs
[    0.804119] Freeing unused kernel image (initmem) memory: 4616K
[    1.021553] Run /init as init process

এই ১৪ লাইনে boot-এর পুরো গল্প আছে:

  • Command line: — bootloader-এর হস্তান্তর করা প্যারামিটার। root=UUID=... — ঠিক এইটা kernel-কে বলে দিচ্ছে কোন partition মাউন্ট করতে হবে। এটা রানটাইমে /proc/cmdline-এ পড়া যায়।
  • BIOS-e820 — firmware-এর দেওয়া physical memory map। usable আর reserved অংশগুলো kernel-কে বলে কোন physical address আসলেই RAM আর কোনগুলো hardware-এর জন্য সংরক্ষিত। Kernel-এর buddy allocator ঠিক এই তালিকা থেকেই শুরু করে।
  • NX (Execute Disable) protection: active — গত module-এর NX bit; hardware-চালিত isolation-এর একটা কংক্রিট প্রমাণ, .text ছাড়া কোনো page থেকে instruction fetch হবে না। Level 10-এর security module-এ এটা কেন বাধ্যতামূলক তা দেখব।
  • KASLR enabled — kernel নিজেকে প্রতি boot-এ ভিন্ন random address-এ বসায়, ঠিক যেমন ASLR user process-এর জন্য করে (চতুর্থ লেসনের বিষয়)।
  • smp: Brought up 1 node, 8 CPUs — শুরুতে একটাই CPU চলছিল (boot CPU); ০.২৯ সেকেন্ডে বাকি ৭টা core সক্রিয় হলো। এই মুহূর্তের আগে পর্যন্ত পুরো kernel কার্যত single-threaded।
  • Freeing unused kernel image (initmem) memory: 4616K — যেসব ফাংশন শুধু boot-এর সময় দরকার (__init দিয়ে চিহ্নিত), তাদের কোড কাজ শেষ হওয়ার পর RAM থেকে মুছে ফেলা হচ্ছে। ৪.৬ MB ফেরত।
  • Run /init as init process — এটাই সেই ঐতিহাসিক লাইন, kernel_init()-এর run_init_process()। এর পরের প্রতিটা কাজ user space-এ।

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

EXPERIMENT

নিজের মেশিনের boot chain-টা ধাপে ধাপে খুঁড়ে বের করুন

Linux (systemd-চালিত distro; WSL2-তে আংশিক)· ২০ মিনিট

ধাপ ১ — bootloader ঠিক কোন ফাইলগুলো RAM-এ তুলেছিল?

ls -lh /boot/
-rw-r--r-- 1 root root 279K  Sep 12 09:14 config-6.8.0-45-generic
drwx------ 3 root root 4.0K  Jan  1  1970 efi
drwxr-xr-x 5 root root 4.0K  Oct  3 11:02 grub
-rw-r--r-- 1 root root  67M  Oct  3 11:02 initrd.img-6.8.0-45-generic
-rw------- 1 root root 8.1M  Sep 12 09:14 System.map-6.8.0-45-generic
-rw------- 1 root root  14M  Sep 12 09:14 vmlinuz-6.8.0-45-generic

vmlinuz (১৪ MB, সংকুচিত kernel) আর initrd.img (৬৭ MB) — এই দুইটা ফাইলই bootloader RAM-এ কপি করেছিল। System.map হলো প্রতিটা kernel symbol-এর address তালিকা; kernel নিজে এটা পড়ে না, কিন্তু crash dump বিশ্লেষণে অপরিহার্য।

ধাপ ২ — bootloader kernel-কে ঠিক কী বলেছিল?

cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-6.8.0-45-generic root=UUID=3f2a91b4-... ro quiet splash

এই স্ট্রিংটা GRUB লিখেছিল boot_params কাঠামোয়, আর kernel সেটাকে চিরকালের জন্য /proc/cmdline-এ প্রকাশ করে রেখেছে। root= ছাড়া kernel জানত না কোথা থেকে userland আনতে হবে।

ধাপ ৩ — kernel-এর নিজের ভাষ্য পড়ুন।

dmesg | head -50            # অনুমতি না থাকলে: sudo dmesg | head -50
dmesg | grep -i "Run /init" # userspace-এ হস্তান্তরের সঠিক মুহূর্ত
dmesg | grep -iE "e820|Memory:" | head

Run /init লাইনের টাইমস্ট্যাম্পটা লিখে রাখুন — এটাই আপনার মেশিনে “kernel-এর একার রাজত্ব” ঠিক কত সেকেন্ড টিকেছিল তার পরিমাপ।

ধাপ ৪ — পুরো boot-এর সময়-বিভাজন।

systemd-analyze
systemd-analyze blame | head -10
systemd-analyze critical-chain

critical-chain সবচেয়ে শিক্ষণীয় — এটা dependency graph-এর সবচেয়ে দীর্ঘ পথটা দেখায়, অর্থাৎ boot দ্রুত করতে হলে ঠিক কোন unit-গুলোকে হাত দিতে হবে।

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

Boot chain কোনো তাত্ত্বিক ধারণা না -- প্রতিটা ধাপের প্রমাণ আপনার নিজের চলমান সিস্টেমেই লিখিত আছে: /boot-এ bootloader যে ফাইলগুলো লোড করেছিল, /proc/cmdline-এ তার হস্তান্তর করা প্যারামিটার, dmesg-এ kernel-এর নিজের ভাষ্য, আর systemd-analyze-এ প্রতিটা পর্বের প্রকৃত সময়।

EXPERIMENT

Kernel এই মুহূর্তে কী কী সামলাচ্ছে -- গুনে দেখুন

Linux· ১৫ মিনিট

ধাপ ১ — কতগুলো process, তার কতগুলো kernel thread?

ps aux | wc -l                       # মোট (হেডার লাইন সহ)
ps -eo comm | grep -c '^\['          # kernel thread (নাম বর্গবন্ধনীতে)
nproc                                # কতগুলো CPU core
312
118
8

১১৮টা kernel thread — kworker, ksoftirqd, kswapd0, rcu_preempt, migration/N। এদের কারো user-space memory নেই। যাচাই করুন:

KT=$(ps -eo pid,comm | awk '$2 ~ /^\[?kworker/ {print $1; exit}')
sudo cat /proc/$KT/cmdline | wc -c   # 0 -- কোনো command line নেই
sudo ls /proc/$KT/maps               # ফাঁকা -- কোনো address space নেই

ধাপ ২ — /proc আসলে কী?

ls /proc | head -20
1
10
1012
...
buddyinfo
cmdline
cpuinfo

সংখ্যাগুলো process ID — প্রতিটা চলমান process-এর জন্য একটা ডিরেক্টরি। নামওয়ালা entry-গুলো kernel-এর নিজের তথ্য। এই ফাইলগুলোর কোনোটাই ডিস্কে নেই:

df -h /proc
mount | grep ' /proc '
Filesystem      Size  Used Avail Use% Mounted on
proc               0     0     0    - /proc
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)

আকার শূন্য, তবুও cat /proc/cpuinfo কিলোবাইট ডেটা দেয় — কারণ প্রতিটা read একটা kernel function কল করে যে তখনই টেক্সট বানিয়ে দেয়। এটাই “সবকিছুই একটা ফাইল” abstraction-এর সবচেয়ে চমৎকার প্রয়োগ: kernel-এর ভেতরের তথ্য পড়তে কোনো নতুন syscall লাগেনি, পুরনো open/read দিয়েই হয়ে গেছে।

ধাপ ৩ — interrupt আর context switch গুনুন।

grep -E '^(ctxt|processes|btime|intr)' /proc/stat | cut -c1-60
sleep 5
grep '^ctxt' /proc/stat
intr 284917366 39 0 0 0 0 0 0 0 1 ...
ctxt 418992331
processes 1284471
btime 1727942018

ctxt হলো boot থেকে এ পর্যন্ত মোট context switch। পাঁচ সেকেন্ড পরে আবার পড়ে বিয়োগ করুন — একটা নিষ্ক্রিয় ডেস্কটপে সাধারণত সেকেন্ডে ২,০০০-১৫,০০০ context switch পাবেন। processes হলো boot থেকে সৃষ্ট মোট process — পঞ্চম লেসনে যখন fork() করব, এই সংখ্যাটা বাড়তে দেখা যাবে।

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

আপনার 'নিষ্ক্রিয়' মেশিনটা আসলে শত শত process, ডজনখানেক kernel thread আর সেকেন্ডে হাজার হাজার interrupt সামলাচ্ছে -- আর /proc-এর প্রতিটা ফাইল কোনো ডিস্কে নেই, kernel চাওয়ামাত্র তৈরি করে দেয়।

নিজে বানান

BUILD IT

libc ছাড়া একটা প্রোগ্রাম -- OS আর library-র সীমারেখা হাতেকলমে

C + inline assembly (gcc) · ●●●○○
  1. একটা সাধারণ hello world লিখে strace দিয়ে গুনুন কতগুলো syscall হয়
  2. একই কাজ -nostdlib দিয়ে, _start entry point আর raw syscall inline asm দিয়ে লিখুন
  3. দুইটার আকার (ls -l), dynamic dependency (ldd), আর syscall সংখ্যা (strace -c) তুলনা করুন
  4. exit status ফেরত দিয়ে প্রমাণ করুন freestanding প্রোগ্রামটা সত্যিই kernel-এর সাথে কথা বলছে
  5. ভুল করে return করে দেখুন কী হয় -- এবং কেন হয় তা ব্যাখ্যা করুন

ধাপ ১ — স্বাভাবিক সংস্করণ, আর তার লুকানো খরচ।

/* normal.c */
#include <stdio.h>

int main(void) {
    printf("hello\n");
    return 42;
}
gcc -O2 -o normal normal.c
./normal; echo "exit status: $?"
strace -c ./normal 2>&1 | tail -20
hello
exit status: 42
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
  0.00    0.000000           0         2           read
  0.00    0.000000           0         2           write
  0.00    0.000000           0         5           mmap
  0.00    0.000000           0         4           mprotect
  0.00    0.000000           0         2           openat
  0.00    0.000000           0         3           newfstatat
  0.00    0.000000           0         1           execve
  0.00    0.000000           0         1           arch_prctl
  0.00    0.000000           0         1           set_tid_address
  0.00    0.000000           0         2           close
------ ----------- ----------- --------- --------- ----------------
100.00    0.000000                    25           total

আপনি লিখেছিলেন একটা printf। হলো ২৫টা syscall। কে করল বাকি ২৪টা? execve করল kernel, বাকিগুলো করল dynamic linker (ld-linux-x86-64.so.2) আর libc-র startup কোড: openat করে /etc/ld.so.cache আর libc.so.6 খুলল, mmap করে সেগুলো address space-এ বসাল, mprotect দিয়ে permission ঠিক করল, arch_prctl দিয়ে thread-local storage-এর জন্য %fs base বসাল। আপনার আসল কাজ ছিল ওই একটাই write

ধাপ ২ — freestanding সংস্করণ।

/* nolibc.c -- কোনো header নেই, কোনো library নেই, কোনো main নেই */

static long syscall3(long n, long a, long b, long c) {
    long ret;
    __asm__ volatile (
        "syscall"
        : "=a" (ret)
        : "a" (n), "D" (a), "S" (b), "d" (c)
        : "rcx", "r11", "memory"
    );
    return ret;
}

#define SYS_write      1
#define SYS_exit_group 231

static unsigned long slen(const char *s) {
    unsigned long n = 0;
    while (s[n]) n++;
    return n;
}

/* _start হলো প্রকৃত entry point -- ELF header-এ e_entry ঠিক এখানেই দেখাবে।
   কোনো argc/argv সেট আপ করা নেই, কোনো stack frame গোছানো নেই, কিছুই না। */
void _start(void) {
    const char *msg = "hello from bare syscalls\n";
    syscall3(SYS_write, 1, (long)msg, (long)slen(msg));
    syscall3(SYS_exit_group, 42, 0, 0);
    __builtin_unreachable();   /* এই লাইনে কখনো পৌঁছাবে না */
}
gcc -nostdlib -static -no-pie -O2 -o nolibc nolibc.c
./nolibc; echo "exit status: $?"
hello from bare syscalls
exit status: 42

ধাপ ৩ — তুলনা।

ls -l normal nolibc
ldd normal
ldd nolibc
strace -c ./nolibc 2>&1 | tail -8
-rwxr-xr-x 1 you you 16168 Oct  3 12:04 normal
-rwxr-xr-x 1 you you  9016 Oct  3 12:04 nolibc

# ldd normal
        linux-vdso.so.1 (0x00007ffd4a3f9000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2c8e400000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f2c8e6b7000)

# ldd nolibc
        statically linked

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
  0.00    0.000000           0         1           write
  0.00    0.000000           0         1           exit_group
------ ----------- ----------- --------- --------- ----------------
100.00    0.000000                     2           total

২৫টা syscall বনাম ২টা। এই পার্থক্যটাই এই লেসনের কেন্দ্রীয় দাবির সবচেয়ে পরিষ্কার প্রমাণ: printf, malloc, dynamic linking — এসবের একটাও OS না। এরা user-space library কোড, যারা শেষমেশ কয়েকটা syscall-এ নেমে আসে। OS ঠিক ততটুকুই যতটুকু write আর exit_group-এর ওপারে আছে।

ধাপ ৪ — ইচ্ছা করে ভুল করুন।

syscall3(SYS_exit_group, ...) লাইনটা মুছে ফেলে আবার কম্পাইল করে চালান:

Segmentation fault (core dumped)

কেন? কারণ _start কোনো সাধারণ ফাংশন না — একে কেউ call করেনি, তাই stack-এ কোনো return address নেই। _start থেকে ret করলে CPU stack-এ যা আছে (এখানে argc) সেটাকে address ধরে সেখানে লাফ দেয়, আর সঙ্গে সঙ্গে page fault হয়। libc-চালিত প্রোগ্রামে এই কাজটা করে __libc_start_main — সে main-কে call করে, তার return value নিয়ে নিজে exit_group syscall করে। libc ঠিক কী কী করে দেয় — এটা তারই একটা কংক্রিট আইটেম।

নিজে বাড়ান

  1. ARM64-তে port করুন। Raspberry Pi বা Apple Silicon-এর Linux VM-এ inline asm বদলান: instruction হবে svc #0, syscall নম্বর যাবে x8-এ, আর argument x0-x5-এ। ARM64-তে write = ৬৪, exit_group = ৯৪ (x86-64-এর সংখ্যাগুলো সম্পূর্ণ ভিন্ন — যাচাই করুন /usr/include/asm-generic/unistd.h-এ)।
  2. readelf দিয়ে দুইটা বাইনারি ব্যবচ্ছেদ করুন। readelf -h normal nolibc চালিয়ে e_entry তুলনা করুন, আর readelf -l দিয়ে program header গুনুন — গত module-এর ELF লেসনের সাথে সরাসরি মিলিয়ে দেখুন INTERP segment nolibc-এ কেন নেই।
  3. argc/argv উদ্ধার করুন। _start-এ ঢোকার মুহূর্তে rsp যেখানে দেখাচ্ছে সেখানে আছে argc, তার ঠিক পরে argv pointer-এর সারি, তারপর একটা NULL, তারপর envp। একটা naked assembly stub লিখে rsp-এর মান একটা C ফাংশনে পাঠান আর প্রথম argument ছাপান।
  4. নিজের ক্ষুদ্র puts বানান। slen + write মিলিয়ে, তারপর একটা putn যোগ করুন যা একটা long-কে দশমিকে ছাপায় — কোনো sprintf ছাড়া, শুধু বারবার ১০ দিয়ে ভাগ করে। এটাই একটা kernel-এর নিজস্ব printk কীভাবে শুরু হয়।
  5. আকার কমান-প্রতিযোগিতা। gcc -nostdlib -static -no-pie -Os -Wl,--build-id=none -Wl,-n আর strip -s দিয়ে nolibc-কে কত ছোট করতে পারেন? ২ KB-র নিচে নামানো সম্ভব; ELF header হাতে লিখলে ১৫০ বাইটেরও কম সম্ভব।

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

এই ধারণাগুলো বাস্তবে কোথায়

Linux — একটাই kernel, চরম ভিন্ন স্কেলে। নভেম্বর ২০১৭ থেকে TOP500 সুপারকম্পিউটার তালিকার ১০০% মেশিন Linux চালায়। একই kernel একটা ৮ MB RAM-এর রাউটারেও চলে (OpenWrt), আর Frontier-এর ৯০ লক্ষ CPU core-এও। এই বিস্তারটাই monolithic + module ডিজাইনের বাস্তব যুক্তি: যা লাগবে না, compile-ই করবেন না।

Android — Linux kernel, কিন্তু GNU userland নয়। Android-এ Linux kernel আছে (কিছু প্যাচ সহ: binder IPC, ashmem, wakelock), কিন্তু libc হলো Bionic (glibc না), init হলো Android-এর নিজের init (systemd না), আর অ্যাপ চলে ART runtime-এ। এটাই “kernel বনাম distribution” পার্থক্যের সবচেয়ে বড় বাস্তব উদাহরণ — ৩০০ কোটির বেশি ডিভাইস।

seL4 — microkernel-এর প্রমাণিত রূপ। প্রায় ১০ হাজার লাইনের C, কিন্তু তার সাথে আছে Isabelle/HOL-এ লেখা একটা যান্ত্রিক প্রমাণ যে বাইনারি কোডটা তার নির্দিষ্টকরণ (specification) অনুসারেই চলে — কোনো buffer overflow নেই, কোনো null dereference নেই, কোনো infinite loop নেই। DARPA-র HACMS প্রকল্পে একটা Boeing Unmanned Little Bird হেলিকপ্টারের flight control seL4-তে সরানো হয়েছিল, আর red team সেটা ভাঙতে ব্যর্থ হয়েছিল। Level 10-এর security module-এ formal verification-এর এই ধারা আবার আসবে।

QNX — গাড়ির ভেতরে microkernel। BlackBerry QNX ২৫ কোটির বেশি যানবাহনে চলছে (২০২৪-এর হিসাব) — ইনফোটেইনমেন্ট থেকে ADAS পর্যন্ত। কারণটা ঠিক সেই fault containment: একটা CAN bus driver ক্র্যাশ করলে ব্রেক কন্ট্রোলার যেন না মরে।

XNU — hybrid-এর পাঠ্যপুস্তক উদাহরণ, আর ওপেন সোর্স। macOS ও iOS-এর kernel; Mach 3.0-র IPC ও VM subsystem + FreeBSD-র process/syscall layer + Apple-এর I/O Kit (C++-এ লেখা driver framework)। সোর্স Apple প্রকাশ করে (Darwin/XNU), তাই আপনি নিজে syscalls.master ফাইলটা পড়ে দেখতে পারেন — এই module-এর অনেক তুলনায় সেটা কাজে লাগবে।

Unikernel — user/kernel বিভাজন সম্পূর্ণ বাদ। MirageOS বা Unikraft-এ আপনার অ্যাপ্লিকেশন আর “OS” একসাথে কম্পাইল হয়ে একটাই বাইনারি হয়, সবটাই একই privilege-এ, কোনো syscall boundary নেই — কারণ isolation-এর কাজটা নিচের hypervisor করছে। Boot time মিলিসেকেন্ডে, image আকার মেগাবাইটের নিচে। Level 12-এ Firecracker microVM-এর প্রসঙ্গে এটা আবার আসবে।

FreeRTOS বনাম Linux — কখন OS দরকার নেই। FreeRTOS-এ একটা scheduler আছে, task আছে, mutex আছে — কিন্তু কোনো MMU-ভিত্তিক isolation নেই, কোনো virtual memory নেই, কোনো process নেই। ফলে সব task একই address space ভাগ করে। এটা একটা RTOS, পূর্ণ OS না — আর ঠিক সেই কারণেই একটা ৩২ KB RAM-এর chip-এ চলে।

মঙ্গলগ্রহে দুইটা ভিন্ন উত্তর। Perseverance rover চালায় VxWorks (একটা বাণিজ্যিক RTOS, কঠোর real-time গ্যারান্টি সহ), কিন্তু Ingenuity হেলিকপ্টার চালাত Linux একটা Qualcomm Snapdragon 801-এ — ২০২১ সালের এপ্রিলে সেটাই ছিল অন্য গ্রহে Linux-এর প্রথম উড্ডয়ন। দুইটা ভিন্ন সিদ্ধান্ত, একই মিশনে: rover-এ প্রয়োজন ছিল প্রমাণযোগ্য determinism, হেলিকপ্টারে প্রয়োজন ছিল দ্রুত উন্নয়ন আর সমৃদ্ধ ecosystem।

Boot chain-এর নিরাপত্তা — Secure Boot ও BootHole। এই লেসনের boot chain-এর প্রতিটা হস্তান্তর একটা বিশ্বাসের সংযোগ। UEFI Secure Boot প্রতিটা ধাপের স্বাক্ষর যাচাই করে সেই শৃঙ্খল রক্ষা করে। ২০২০ সালে CVE-2020-10713 (“BootHole”) — GRUB2-র grub.cfg পার্সারে একটা buffer overflow — আক্রমণকারীকে Secure Boot পেরিয়ে যাওয়ার সুযোগ দিয়েছিল, অর্থাৎ kernel লোড হওয়ার আগেই কোড চালানো। প্যাচ করতে প্রতিটা distro-র shim পুনরায় স্বাক্ষর করতে হয়েছিল। শিক্ষা: শৃঙ্খলের সবচেয়ে দুর্বল কড়িটাই পুরো OS-এর নিরাপত্তার সীমা — Level 10-এর trust chain আলোচনার ভিত্তি।

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

“Operating system মানে যা আমি স্ক্রিনে দেখি -- উইন্ডো, আইকন, টাস্কবার, ফাইল ম্যানেজার।”

এটা সবচেয়ে সাধারণ বিভ্রান্তি, আর এর সহজ প্রতিষেধক হলো একটা সার্ভার। একটা সাধারণ Ubuntu Server ইনস্টলেশনে GNOME, X11, Wayland — কিছুই ইনস্টল করা থাকে না। কোনো উইন্ডো নেই, কোনো আইকন নেই, শুধু একটা টেক্সট prompt। তবুও সেই মেশিনে OS সম্পূর্ণভাবে কাজ করছে: process চলছে, memory isolate হচ্ছে, ফাইল লেখা হচ্ছে, নেটওয়ার্ক প্যাকেট যাচ্ছে। বিশ্বের বেশিরভাগ Linux ইনস্টলেশন এভাবেই চলে।

উল্টো দিকটাও দেখুন — GNOME বা Windows Explorer ক্র্যাশ করলে (killall gnome-shell চালিয়ে দেখুন) আপনার সব প্রোগ্রাম চলতেই থাকে, নেটওয়ার্ক connection টেকে, ডাউনলোড শেষ হয়। কারণ desktop environment হলো কেবল আরেকটা user-space প্রোগ্রাম, যেটা read/write/mmap করে ঠিক আপনার লেখা প্রোগ্রামের মতোই। OS হলো তার নিচে যা আছে, তার পাশে যা দেখা যায় তা না।

“OS ছাড়া কোনো প্রোগ্রাম চলতেই পারে না -- CPU-র তো একটা OS লাগবেই।”

CPU-র OS-এর কোনো ধারণাই নেই। CPU শুধু জানে: একটা address থেকে instruction আনো, চালাও, পরেরটায় যাও। রিসেটের পর x86-64 CPU physical address 0xFFFFFFF0 থেকে instruction আনতে শুরু করে — সেখানে যা-ই থাকুক, চালাবে। সেটা যদি আপনার লেখা ২০ লাইনের assembly হয়, তা-ই চলবে।

বিলিয়ন সংখ্যক ডিভাইস ঠিক এভাবেই চলে: মাইক্রোওয়েভের কন্ট্রোলার, TV রিমোট, মাউসের ভেতরের chip, গাড়ির ECU-র বড় অংশ, স্পেসক্রাফটের subsystem। এদের কোনো OS নেই, শুধু একটা main() আর একটা অসীম লুপ। Linux kernel নিজেও boot-এর প্রথম কয়েক মিলিসেকেন্ডে ঠিক এভাবেই চলে — OS-বিহীন একটা প্রোগ্রাম হিসেবে, যে ধীরে ধীরে নিজেকে একটা OS-এ পরিণত করছে।

সঠিক কথাটা হলো: OS ছাড়া প্রোগ্রাম চলে, কিন্তু একসাথে অনেকগুলো প্রোগ্রাম নিরাপদে চলে না। OS-এর অস্তিত্বের কারণ সেটাই — multiplexing আর isolation, execution না।

“Microkernel মানে ছোট, কম-ক্ষমতাসম্পন্ন OS -- খেলনা বা গবেষণার জিনিস, বাস্তব কাজের না।”

“Micro” শব্দটা kernel-এ কতটুকু কোড আছে তা বোঝায়, পুরো সিস্টেম কতটা সক্ষম তা না। QNX একটা microkernel, আর সেটা ২৫ কোটির বেশি গাড়িতে ইনফোটেইনমেন্ট থেকে নিরাপত্তা-সংকটাপন্ন ADAS পর্যন্ত চালাচ্ছে — POSIX-সঙ্গতি, নেটওয়ার্ক stack, গ্রাফিক্স, সবই আছে; শুধু সেগুলো kernel-এর ভেতরে না, বাইরে user-space server হিসেবে।

seL4-র ক্ষেত্রে ছোট আকারটাই তার সবচেয়ে বড় শক্তি: ১০ হাজার লাইন বলেই তার সম্পূর্ণ গাণিতিক verification সম্ভব হয়েছে — Linux-এর কোটি-লাইনে সেটা কল্পনাতীত। প্রকৃত ট্রেড-অফটা ক্ষমতা নিয়ে না, পারফরম্যান্স ও প্রকৌশল-জটিলতা নিয়ে: microkernel-এ একটা ফাইল পড়তে হলে কয়েকটা IPC round trip লাগে যেখানে monolithic-এ কয়েকটা function call যথেষ্ট। L4 পরিবার সেই IPC খরচ কমাতে দশকের পর দশক কাজ করেছে (মূল Mach-এ যা ছিল হাজার হাজার cycle, seL4-তে তা কয়েকশো)। এটা একটা প্রকৌশলগত আপস, ক্ষমতার সীমা না।

“Kernel সবসময় background-এ চলছে -- এটা একটা বিশেষ process, ps-এ খুঁজলে পাওয়া যাবে।”

ps aux চালিয়ে “kernel” নামে কিছু খুঁজে দেখুন — পাবেন না। কারণ kernel কোনো process না, তার কোনো PID নেই, আর সে কোনো CPU সময়ের জন্য অন্যদের সাথে প্রতিযোগিতা করে না।

Kernel হলো privileged mode-এ চলা কোডের একটা সংগ্রহ, যা ঠিক তিনটা ঘটনায় সক্রিয় হয় — একটা process syscall করলে (তখন সে সেই process-এরই context-এ চলে), একটা hardware interrupt এলে, অথবা একটা kernel thread schedule হলে। এই তিনটার কোনোটা না ঘটলে kernel আক্ষরিক অর্থে থেমে আছে — CPU hlt চালিয়ে idle C-state-এ, বিদ্যুৎ বাঁচাচ্ছে।

ps-এ বর্গবন্ধনীর নামগুলো ([kworker/0:1], [kswapd0]) দেখে বিভ্রান্ত হবেন না — ওগুলো kernel thread, kernel নিজে না। এদের কাজ নির্দিষ্ট asynchronous দায়িত্ব (deferred work, page reclaim), আর এদের কোনো user-space address space নেই — /proc/PID/maps ফাঁকা, cmdline শূন্য-দৈর্ঘ্যের। এই “kernel একটা library, process না” ধারণাটাই পরের দুই লেসনের ভিত্তি: mode switch কীভাবে হয়, আর syscall ঠিক কী।

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

1

একটা Arduino-তে চলা প্রোগ্রামে আপনি ভুল করে একটা array-র সীমা ছাড়িয়ে লিখলেন। একই ভুল একটা Linux প্রোগ্রামে করলেন। দুই ক্ষেত্রে ফলাফল কেন সম্পূর্ণ ভিন্ন — আর ঠিক কোন hardware উপাদানটা এই পার্থক্যের জন্য দায়ী?

যুক্তি

পার্থক্যের একক কারণ হলো MMU (Memory Management Unit) — ATmega328P-তে যেটা নেই, আর x86-64-এ যেটা আছে ও kernel যেটাকে সক্রিয়ভাবে কনফিগার করে রেখেছে।

Arduino (ATmega328P)Linux (x86-64)
MMU আছে?নাহ্যাঁ, page table সহ
Address কী?সরাসরি physical SRAM addressVirtual address, প্রতি অ্যাক্সেসে অনুবাদিত
Array সীমা ছাড়ালেপাশের ভেরিয়েবল, stack, বা নিজের return address নীরবে নষ্টম্যাপ-না-করা page হলে page fault → SIGSEGV
কে টের পায়কেউ না, যতক্ষণ না আচরণ অদ্ভুত হয়Kernel, তাৎক্ষণিকভাবে
পরিণতিপুরো ডিভাইস হ্যাং বা রহস্যময় আচরণশুধু সেই process মরে, বাকি ৩১১টা অক্ষত

সূক্ষ্মতা যা মনে রাখা জরুরি: Linux-এও সব overflow ধরা পড়ে না। যদি আপনি একটা ১০০-বাইট array-র ১০৫ নম্বর বাইটে লেখেন, সেই address সম্ভবত একই page-এর ভেতরেই পড়বে (page ৪০৯৬ বাইট), তাই MMU কিছুই বলবে না — আপনি নীরবে পাশের ভেরিয়েবল নষ্ট করবেন, ঠিক Arduino-র মতোই। MMU-র সুরক্ষার দানা (granularity) হলো page, byte না। এই সীমাবদ্ধতাই stack canary, ASAN আর guard page-এর মতো কৌশলের অস্তিত্বের কারণ — Level 10-এর security module-এ এগুলো বিস্তারিত আসবে, আর Level 11-এ দেখব ASAN-এর মতো টুলের রানটাইম খরচ কেন ২-৩ গুণ।

2

নিচের প্রতিটাকে OS-এর তিনটা কাজের (multiplexing / isolation / abstraction) কোনো একটায় ফেলুন — এবং যদি একাধিকের সাথে জড়িত মনে হয়, ব্যাখ্যা করুন প্রধান কাজটা কোনটা।

(ক) open("/tmp/a.txt", O_RDONLY) (খ) CPU scheduler-এর timeslice (গ) প্রতিটা process-এর আলাদা page table (ঘ) TCP socket (ঙ) Swap partition

প্রয়োগ
আইটেমপ্রধান কাজযুক্তি
(ক) open()AbstractionNVMe-র LBA, inode, extent — সব লুকিয়ে একটা int fd দিচ্ছে। গৌণভাবে isolation-ও আছে (permission check), কিন্তু মূল কাজটা হলো “ডিস্ক ব্লকের সংগ্রহ” → “ফাইল” রূপান্তর
(খ) TimesliceMultiplexingএকটা CPU-কে সময়ের ভাগে বহু process-এর মধ্যে ভাগ করা — বিশুদ্ধ time-division
(গ) আলাদা page tableIsolation (তারপরই multiplexing)মূল উদ্দেশ্য: A যেন B-র memory ছুঁতে না পারে। কিন্তু একই যন্ত্র physical RAM-কে space-division-এ ভাগ করেও দেয়, তাই এটা তিন কাজের মধ্যে সবচেয়ে বেশি বহুমুখী উদাহরণ
(ঘ) TCP socketAbstractionNIC-এর DMA ring, IRQ, packet header, retransmission — সব লুকিয়ে “একটা নির্ভরযোগ্য byte stream”। NIC-কে বহু connection-এ multiplex করাও এর অংশ
(ঙ) SwapMultiplexing১৬ GB physical RAM-কে তার চেয়ে বেশি চাহিদার মধ্যে ভাগ করা — অর্থাৎ একটা দুষ্প্রাপ্য সম্পদের space-division, ডিস্ককে overflow হিসেবে ব্যবহার করে

সবচেয়ে শিক্ষণীয় ঘটনা (গ) — page table একই সাথে তিনটা কাজেই অংশ নেয়: isolation দেয় (অন্যের page ম্যাপ করা নেই), multiplexing দেয় (physical page-কে ভাগ করে), আর abstraction দেয় (প্রতিটা process একটা টানা, বিশাল address space দেখে যদিও নিচে physical page বিক্ষিপ্ত)। এই তিন-ইন-এক চরিত্রই বোঝায় কেন virtual memory এই module-এর সবচেয়ে বড় বিষয়, আর কেন Level 11-এর performance module-এ TLB miss একটা প্রধান আলোচ্য — এই বিলাসিতার একটা প্রকৃত hardware খরচ আছে।

3

strace ./hello চালিয়ে দেখলেন ২৫টা syscall, যার মধ্যে মাত্র একটা write। বাকি ২৪টা কে করল, আর -static দিয়ে কম্পাইল করলে এই সংখ্যাটা কেন কমে যাবে? সংখ্যাটা কি ২-এ নামবে?

যুক্তি

বাকি ২৪টার উৎস তিনটা:

উৎসকী কী syscallকেন
Kernel নিজেexecve (১টা)প্রোগ্রাম চালু করার কাজটাই একটা syscall
Dynamic linker (ld-linux)openat, read, newfstatat, mmap, mprotect, close (~১৮টা)libc.so.6 খুঁজে বের করা, ম্যাপ করা, relocation প্রয়োগ, permission ঠিক করা
libc startuparch_prctl, set_tid_address, brk, set_robust_list (~৫টা)TLS-এর জন্য %fs base বসানো, thread bookkeeping, heap শুরু

-static দিয়ে: dynamic linker-এর পুরো অংশটা (~১৮টা) উবে যায়, কারণ libc আগেই বাইনারিতে ঢোকানো আছে — কিছু খোঁজার বা ম্যাপ করার নেই। বাস্তবে সংখ্যাটা ২৫ থেকে নেমে সাধারণত ৭-১০ হয়।

কিন্তু ২-এ নামবে না, কারণ libc-র startup কোড (__libc_start_main) স্ট্যাটিক লিঙ্কেও চলে: তাকে TLS বসাতে arch_prctl করতে হয়, heap শুরু করতে brk করতে হয়, আর stdio buffer-এর সিদ্ধান্ত নিতে stdout terminal কি না জানতে newfstatat/ioctl করতে হয়।

২-এ নামানোর একমাত্র উপায় libc-টাই সরিয়ে দেওয়া — ঠিক এই লেসনের BuildIt-এ যা করা হয়েছে (-nostdlib + নিজের _start + raw syscall): write + exit_group, আর কিছুই না।

এই স্তরবিন্যাস — kernel, dynamic linker, libc startup, আপনার কোড — Level 5-এর compilers module-এ (linking ও relocation) আর Level 11-এ (startup latency, প্রতিটা mmap-এর page fault খরচ) সরাসরি ফিরে আসবে; serverless cold start-এর আলোচনায় (Level 12) এই ২৫ বনাম ২-এর পার্থক্যটাই মিলিসেকেন্ড-স্তরের অর্থ বহন করে।

4

আপনার সার্ভারে systemd-analyze দেখাচ্ছে: firmware 9.2s + loader 0.4s + kernel 1.1s + initrd 0.9s + userspace 2.3s। মোট ১৩.৯ সেকেন্ড। Boot দ্রুত করার জন্য আপনি প্রথমে কোথায় হাত দেবেন, আর কী কী নির্দিষ্ট পদক্ষেপ নেবেন?

প্রয়োগ

অগ্রাধিকার পরিষ্কার: firmware — মোট সময়ের ৬৬%। সবচেয়ে সাধারণ ভুল হলো systemd-analyze blame-এর তালিকা দেখে userspace অপ্টিমাইজ করতে বসা, যেখানে সেখানে জেতার সুযোগ মাত্র ২.৩ সেকেন্ডের ভেতরে।

পর্বসময়অংশসম্ভাব্য পদক্ষেপ
firmware৯.২s৬৬%UEFI-তে Fast Boot চালু, ব্যবহার-না-করা controller (legacy USB, serial, unused SATA) নিষ্ক্রিয়, PXE/network boot বন্ধ, Option ROM কমানো, memory retraining এড়াতে firmware আপডেট
loader০.৪s৩%GRUB timeout শূন্যে নামানো; সামান্য লাভ
kernel১.১s৮%অপ্রয়োজনীয় built-in driver বাদ দিয়ে custom kernel; পরিশ্রম বেশি, লাভ কম
initrd০.৯s৬%dracut --hostonly বা সীমিত MODULES=dep দিয়ে ছোট initramfs
userspace২.৩s১৭%blame/critical-chain দেখে সবচেয়ে দীর্ঘ পথের unit; সাধারণত NetworkManager-wait-online নিষ্ক্রিয় করাই সবচেয়ে বড় একক লাভ

সবচেয়ে বাস্তবসম্মত পরিকল্পনা: firmware-এ ৪-৫ সেকেন্ড (সার্ভার BIOS-এ memory training আর PCIe/Option ROM scan-ই প্রধান খরচ) + userspace-এ ১-১.৫ সেকেন্ড — অর্থাৎ ১৩.৯ থেকে প্রায় ৭-৮ সেকেন্ডে নামা সম্ভব, kernel পুনর্গঠনের ঝুঁকি ছাড়াই।

একটা গভীর সিদ্ধান্ত যা প্রশ্নের বাইরে যায়: যদি boot time সত্যিই সংকটাপন্ন হয় (auto-scaling fleet, spot instance), তাহলে boot দ্রুত করার বদলে boot এড়ানো ভালো — VM snapshot/restore, বা Firecracker microVM যা ১২৫ ms-এ boot করে কারণ সেখানে firmware ধাপটাই নেই, একটা ন্যূনতম VMM সরাসরি kernel লোড করে। এটাই Level 12-এর cloud module-এর cold start আলোচনার মূল সূত্র: সবচেয়ে দ্রুত ধাপ সেটাই যেটা একেবারে চালানোই হয় না।

5

আপনি একটা মেডিকেল ইনফিউশন পাম্পের সফটওয়্যার আর্কিটেকচার ঠিক করছেন। ডিভাইসটায় আছে: একটা মোটর কন্ট্রোলার (কঠোর টাইমিং, ±১ ms), একটা টাচস্ক্রিন UI, একটা Wi-Fi আপলোড ফিচার, আর একটা লগিং সাবসিস্টেম। এই লেসনের তিনটা kernel architecture-এর মধ্যে কোনটা বাছবেন এবং কেন? দুইটা বিকল্প নকশাও বিবেচনা করুন।

ডিজাইন

মূল সংঘাতটা চিহ্নিত করা সবচেয়ে জরুরি: একই ডিভাইসে দুইটা পরস্পরবিরোধী চাহিদা আছে — মোটর কন্ট্রোলের কঠোর real-time determinism ও নিরাপত্তা, আর UI/Wi-Fi-র সমৃদ্ধ ecosystem (গ্রাফিক্স stack, TLS, HTTP client)। একটা একক kernel দিয়ে দুটোই ভালোভাবে করা কঠিন।

নকশা ১ — সব-Linux (monolithic), PREEMPT_RT প্যাচ সহ। দ্রুততম উন্নয়ন, সব driver ও library পাওয়া যায়। কিন্তু Wi-Fi driver বা গ্রাফিক্স stack-এর একটা বাগ kernel panic ঘটালে মোটরও থেমে যায় — একটা ইনফিউশন পাম্পে যা প্রাণঘাতী। PREEMPT_RT worst-case latency মাইক্রোসেকেন্ড-স্তরে নামায়, কিন্তু “প্রমাণ” দেয় না, আর regulatory certification (IEC 62304, FDA) কোটি-লাইনের kernel-এর জন্য কার্যত অসম্ভব।

নকশা ২ — সব-RTOS (FreeRTOS/VxWorks)। টাইমিং নিখুঁত, ছোট ও সার্টিফাই-যোগ্য। কিন্তু কোনো MMU-isolation নেই মানে Wi-Fi stack-এর একটা buffer overflow সরাসরি মোটর কন্ট্রোল কোডের memory লিখতে পারে — অর্থাৎ ঠিক সেই আক্রমণ-পৃষ্ঠটা যেটা সবচেয়ে বড় (নেটওয়ার্ক) আর সবচেয়ে সংকটাপন্ন উপাদানটা (মোটর) একই অরক্ষিত জায়গায় বসে থাকে। উপরন্তু টাচস্ক্রিন UI লেখা যন্ত্রণাদায়ক।

নকশা ৩ (সুপারিশ) — microkernel বা hypervisor দিয়ে বিভাজন। seL4 (বা QNX, বা Jailhouse-এর মতো partitioning hypervisor) নিচে; তার উপর:

অংশকোথায় চলেযুক্তি
মোটর কন্ট্রোলseL4-এর একটা আলাদা, সর্বোচ্চ-অগ্রাধিকার partition; ছোট, সার্টিফাই-যোগ্য কোডকঠোর timing, ন্যূনতম কোড, formally verified kernel-এর গ্যারান্টি
Wi-Fi + TLSসম্পূর্ণ আলাদা partition, নেটওয়ার্ক ছাড়া অন্য কিছুতে অ্যাক্সেস নেইআক্রমণ-পৃষ্ঠকে একটা বাক্সে বন্দি করা
টাচস্ক্রিন UIএকটা অতিথি Linux, নিম্ন অগ্রাধিকারেসমৃদ্ধ graphics stack পাওয়া যায়, ক্র্যাশ করলে শুধু UI মরে
লগিংনিজস্ব partition, শুধু append-only চ্যানেলনিরীক্ষার অখণ্ডতা রক্ষা

মূল সিদ্ধান্ত-নীতি: যে উপাদানটা ব্যর্থ হলে মানুষ মারা যেতে পারে, তার কোড যেন যতটা সম্ভব ছোট হয় এবং যতটা সম্ভব কম কিছুর উপর নির্ভর করে। এই নীতিটাই seL4-র অস্তিত্বের কারণ — এই লেসনের microkernel আলোচনার সরাসরি প্রয়োগ, আর DARPA HACMS-এ Boeing হেলিকপ্টারের ক্ষেত্রে ঠিক এই যুক্তিতেই সিদ্ধান্ত হয়েছিল।

ট্রেড-অফ যা স্বীকার করতেই হবে: partition-এর মধ্যে যোগাযোগ IPC দিয়ে করতে হবে (function call না) — উন্নয়ন ধীর, ডিবাগিং কঠিন, আর প্রতিটা IPC-র একটা মাপযোগ্য latency আছে। Level 9-এর distributed systems module-এ দেখব এই “সবকিছু message passing” মডেলটার নিজস্ব সমস্যা আছে (partial failure, ordering), আর Level 12-এ দেখব একই বিভাজন-নীতি cloud-এ VM ও container isolation-এর রূপে ফিরে আসে।

এরপর কী

পরের লেসন — Kernel space বনাম user space

এই লেসনে বারবার একটা শব্দ ব্যবহার করেছি যার সংজ্ঞা এখনো দিইনি: privileged mode। বলেছি isolation “hardware দিয়ে বলপূর্বক প্রয়োগ করা হয়”, বলেছি kernel “সর্বোচ্চ privilege-এ চলে”, বলেছি user-mode কোড cli চালাতে পারে না। কিন্তু ঠিক কীভাবে? CPU কী দেখে সিদ্ধান্ত নেয় “এই instruction-টা তুমি চালাতে পারবে না”? আর যদি একটা প্রোগ্রাম জোর করে চেষ্টা করে, ঠিক কী ঘটে — কোন সংকেত, কোন register, কোন handler?

পরের লেসনে আমরা সেই দেয়ালটার গঠন দেখব: x86-এর ring 0-3 (আর কেন বাস্তবে মাত্র দুটোই ব্যবহৃত হয়), ARM-এর EL0-EL3, mode switch-এর প্রকৃত মেকানিজম, আর একটা প্রশ্ন যা প্রথমে অদ্ভুত শোনায় — kernel-এর কোড কেন প্রতিটা process-এর address space-এ ম্যাপ করা থাকে? /proc/self/maps খুলে আমরা সেই বিভাজনটা নিজের চোখে দেখব। আর শেষে দেখব ২০১৮ সালের Meltdown কীভাবে এই বিশ্বাসটাই ভেঙে দিয়েছিল, আর তার প্রতিকার KPTI কেন আজও প্রতিটা syscall-কে কিছুটা ধীর করে রেখেছে।

আর সবচেয়ে মজার অংশ — আমরা একটা প্রোগ্রাম লিখব যেটা ইচ্ছা করে একটা privileged instruction চালানোর চেষ্টা করবে, তারপর signal handler দিয়ে সেই ব্যর্থতাটা ধরে দেখাবে দেয়ালটা সত্যিই আছে।

আরও পড়ুন

  • Operating Systems: Three Easy Pieces — Chapter 2: Introduction to Operating Systems — Remzi H. Arpaci-Dusseau, Andrea C. Arpaci-Dusseau · OS-কে virtualization + concurrency + persistence — এই তিন স্তম্ভে ভাগ করার ক্লাসিক উপস্থাপনা; এই লেসনের তিন-কাজের কাঠামোর সবচেয়ে কাছের প্রামাণ্য উৎস
  • Linux kernel source — init/main.c (start_kernel, rest_init, kernel_init) · kernel boot-এর প্রকৃত entry point; এই লেসনের boot-chain আলোচনার প্রতিটা ধাপ এখানে লাইন ধরে যাচাই করা যায়
  • seL4: Formal Verification of an OS Kernel — Gerwin Klein, Kevin Elphinstone, Gernot Heiser, et al. (SOSP 2009) · প্রায় ১০ হাজার লাইনের microkernel-এর সম্পূর্ণ গাণিতিক verification — microkernel-এর 'ছোট মানে দুর্বল না' যুক্তির সবচেয়ে শক্ত প্রমাণ
  • The UEFI Specification & Linux Boot Protocol (Documentation/arch/x86/boot.rst) · bootloader আর kernel-এর মধ্যে প্রকৃত handoff চুক্তি — boot_params struct, কোন register-এ কী থাকে, ঠিক কোথায় লেখা আছে