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

Linux kernel-এর ভেতর — গঠন, module আর eBPF

Inside the Linux Kernel — Source Tree, Boot, Kernel Coding Rules, Data Structures, Modules, /proc and /sys, eBPF and the Development Process

Taught in Bangla, this lesson tours the Linux kernel: the source tree, boot from start_kernel() to PID 1, kernel coding rules, core data structures such as task_struct, list_head and container_of, writing a complete loadable module with Kbuild and parameters, /proc, /sys, sysctl and kernel config, the history of BPF and modern eBPF with the verifier, maps and JIT, and how the kernel is developed through mailing lists, merge windows and Rust.

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

  • Kernel-এর source tree-র কোন ফোল্ডারে কী থাকে, আর আগের পর্বগুলোর বিষয় সেখানে কোথায় বসে
  • start_kernel() থেকে PID 1 পর্যন্ত kernel-এর প্রথম মুহূর্তগুলো, আর kernel-এ কোড লেখার কঠিন নিয়ম
  • task_struct, list_head, container_of, red-black tree আর xarray — নিজে চালিয়ে দেখা
  • একটি পূর্ণাঙ্গ module: Kbuild, parameter, EXPORT_SYMBOL, taint; আর /proc, /sys, Kconfig ও kernel build
  • eBPF-এর ইতিহাস, verifier, map, JIT, BTF/CO-RE, bpftrace — আর kernel কীভাবে তৈরি হয়: mailing list থেকে LTS

১ · Kernel-এর মানচিত্র — source tree-তে কী কোথায়

এই কোর্সের ৫৪টি পর্বে আপনি kernel-এর অনেক অংশ আলাদা আলাদা দেখেছেন: scheduler, page allocator, VFS, driver, namespace. এবার পুরো ভবনটি দেখার পালা। এই পর্ব মূলত জোড়া লাগাবে: আগে শেখা জিনিস আসল kernel-এর কোথায় থাকে, কীভাবে একসাথে চলে, আর আজকের kernel-এ নতুন কী এসেছে।

একটি বড় সরকারি হাসপাতালে ঢুকলে প্রথমেই চোখে পড়ে একটি বোর্ড: “জরুরি বিভাগ — নিচতলা, শিশু ওয়ার্ড — তিনতলা, ফার্মেসি — পেছনের ভবন, রেকর্ড-রুম — বেসমেন্ট”। বোর্ড না চিনলে আপনি করিডোরে ঘুরতে থাকবেন। Linux kernel-এর source tree সেই বোর্ড। কয়েক কোটি লাইনের কোড পড়ে শেষ করা কারও পক্ষে সম্ভব নয়, কিন্তু কোন ফোল্ডারে কী থাকে জানলে যেকোনো প্রশ্নের উত্তর কোথায় খুঁজবেন, তা জানবেন।

Linux-এর মূল source থাকে kernel.org-এ, Linus Torvalds-এর git repository-তে (“mainline”). মোটামুটি লাখখানেক ফাইল (সংস্করণভেদে কম-বেশি), আর উপরের স্তরে বিশ-পঁচিশটি ফোল্ডার। সবচেয়ে গুরুত্বপূর্ণগুলো, আর এই কোর্সের কোন পর্বে তাদের কাজ দেখেছেন:

ফোল্ডারকী থাকেকোর্সে কোথায় দেখেছেন
arch/CPU-নির্ভর কোড: বুটের প্রথম assembly, page table-এর ধরন, context switch, interrupt আর syscall-এর প্রবেশপথ (arch/x86/entry/)। x86, arm64, riscv — প্রতিটির নিজের ফোল্ডারপর্ব ৪, ৫, ৯
kernel/মূল কেন্দ্র: sched/ (scheduler), fork.c, signal.c, locking/, rcu/, cgroup/, bpf/, সময় ও timerপর্ব ১০, ১৩, ২১, ২৯, ৫১
mm/মেমরি: buddy allocator, SLUB, page fault, page cache, reclaim, swap, OOM killer, mmapপর্ব ৩২–৩৭, ৩৮
fs/VFS আর প্রতিটি file system: ext4/, btrfs/, proc/, nfs/, journaling-এর jbd2/পর্ব ৪১, ৪২, ৪৩, ৪৪
block/Block layer আর I/O scheduler (mq-deadline, BFQ, Kyber)পর্ব ৪০
drivers/Device driver: GPU, নেটওয়ার্ক কার্ড, USB, NVMe, সাউন্ড বাদে প্রায় সব যন্ত্রপর্ব ৩৯
net/নেটওয়ার্ক-স্ট্যাক: socket, TCP/IP, netfilter (firewall)পর্ব ১২ (socket)
ipc/System V IPC (shared memory, semaphore, message queue), POSIX message queueপর্ব ১২
security/LSM কাঠামো আর SELinux, AppArmor, Landlock, keyringপর্ব ৪৯
init/main.c-এ start_kernel(), initramfs খোলা, PID 1 চালু করাপর্ব ৭ (অংশ ২-এ ভেতর থেকে)
include/Header ফাইল: include/linux/ (kernel-এর ভেতরের), include/uapi/ (user space-এর সাথে চুক্তি — syscall নম্বর, struct)পর্ব ৫ (ABI)
tools/User space-এর টুল, যা kernel-এর সাথেই থাকে: perf, bpftool, selftestsপর্ব ৫৭-এ perf

আরও কয়েকটি ছোট কিন্তু চেনার মতো ফোল্ডার: virt/ (KVM, পর্ব ৫০), lib/ (kernel-এর নিজের সাধারণ library — red-black tree, sort, string), crypto/, sound/, scripts/ (build-এর সহায়ক, checkpatch.pl), Documentation/ আর ২০২২ থেকে rust/.

মূল ধারণা

Kernel-এর বেশিরভাগ কোড আসলে driver. নানা হিসাবে drivers/ একাই মোট লাইনের অর্ধেকের বেশি, কোনো কোনো হিসাবে প্রায় দুই-তৃতীয়াংশ। অর্থাৎ scheduler আর memory manager-এর কেন্দ্রটি আকারে তুলনামূলক ছোট। আর আপনার ল্যাপটপে এই হাজারো driver-এর অল্প কয়েকটিই কাজে লাগে — বাকিগুলো build-এ বাদ যায়, বা module হয়ে ডিস্কে পড়ে থাকে (অংশ ৫, ৬)।

ফোল্ডারগুলো আলাদা হলেও কাজের সময় সবাই একটি monolithic kernel-এর (পর্ব ৬) অংশ — একই address space, একে অপরের ফাংশন সরাসরি ডাকে। একটি read() কীভাবে কয়েকটি বিভাগ পার হয়, নিচের ছবিতে দেখুন:

একটি read(fd, buf, 4096) — kernel-এর কোন কোন ফোল্ডার পার হয়

ফাইলটি page cache-এ না থাকলে পুরো পথ; থাকলে mm/-এ থেকেই ফেরত

  1. arch/x86/entrysyscalluser → kernel mode
  2. fs/vfs_read → ext4কোন ফাইল, কোন block
  3. mm/page cacheRAM-এ আছে কি?
  4. block/bio → requestসারি আর merge
  5. drivers/nvmeDMA + IRQহার্ডওয়্যার
ছবি ৫৫.১ — অপেক্ষার সময় task ঘুমায় (kernel/sched/), আর ডেটা এলে interrupt তাকে জাগায় — এক syscall-এ প্রায় পুরো kernel.

২ · start_kernel() থেকে PID 1 — kernel-এর প্রথম মুহূর্ত

পর্ব ৭-এ বুটকে বাইরে থেকে দেখেছেন: firmware, bootloader, সংকুচিত vmlinuz, initramfs, তারপর systemd. এবার kernel-এর ভেতর থেকে, C কোডের ভাষায়। Bootloader থেকে আসার পর arch/-এর assembly কোড kernel খুলে (decompress) একটি ন্যূনতম page table বসায়, তারপর সব architecture-এর জন্য এক সাধারণ C ফাংশন ডাকে: init/main.c-এর start_kernel(). তখন শুধু একটি CPU চলে, interrupt বন্ধ, আর চলমান “task” একটিই — যাকে পরে idle task (PID 0, নাম swapper) বলা হবে।

void __init start_kernel(void)
{
        setup_arch(&command_line);   /* RAM map, early page tables, CPU features */
        mm_core_init();              /* buddy + SLUB ready: kmalloc() works now */
        sched_init();                /* one run queue per CPU                   */
        init_IRQ();                  /* interrupt controller                    */
        time_init();                 /* clock and timer tick                    */
        console_init();              /* printk() now reaches the console        */
        local_irq_enable();
        rest_init();                 /* never returns                           */
}

static void rest_init(void)
{
        user_mode_thread(kernel_init, ...);   /* becomes PID 1 */
        kernel_thread(kthreadd, ...);         /* becomes PID 2 */
        cpu_startup_entry(CPUHP_ONLINE);      /* this task becomes the idle loop */
}

__init চিহ্নটি খেয়াল করুন: এই ফাংশনগুলো একটি আলাদা section-এ রাখা হয়, আর বুট শেষ হলে kernel সেই মেমরি ফেরত দেয় — dmesg-এ “Freeing unused kernel image (initmem) memory” লাইনটি এই কারণে। এবার ধাপে ধাপে দেখুন, কে কখন জন্মায়:

kernel-এর অবস্থা
এখন যে ফাংশন
চলমান CPU
যে task আছে
মেমরি বরাদ্দ
dmesg-এ যা দেখা যায়
  1. প্রবেশ। Architecture-এর assembly থেকে লাফ দিয়ে start_kernel(). শুধু বুট CPU চলছে, interrupt বন্ধ। একমাত্র task হলো স্থিরভাবে compile-করা init_task — PID 0. প্রথম log লাইনটি kernel-এর version আর compiler-এর পরিচয়।
  2. যন্ত্রকে চেনা। setup_arch() firmware-এর দেওয়া RAM-এর মানচিত্র পড়ে (x86-এ e820, ARM-এ সাধারণত device tree), kernel command line ভাঙে, আর আসল page table বসায়। এখনো সত্যিকারের allocator নেই; memblock নামের একটি সরল তালিকায় লেখা থাকে কোন RAM খালি।
  3. মেমরি ব্যবস্থাপক জাগল। mm_core_init() memblock-এর খালি RAM পর্ব ৩৮-এর buddy allocator-এর হাতে দেয় আর SLUB-এর cache তৈরি করে। এখন থেকে kmalloc() চলে। Log-এ “Memory: … available”.
  4. Scheduler, interrupt, ঘড়ি। sched_init() প্রতিটি CPU-র জন্য run queue বানায় (পর্ব ২১), init_IRQ() interrupt controller সাজায়, time_init() timer tick চালু করে। তারপর interrupt খোলা হয়।
  5. প্রথম দুই সন্তান। rest_init() দুটি thread বানায়: kernel_init — পরে এটিই PID 1, আর kthreadd — PID 2, পরের সব kernel thread-এর বাবা (ps -ef-এ বন্ধনীতে নাম লেখা [kworker/…], [ksoftirqd/0]). তারপর PID 0 নিজে idle loop-এ চলে যায় — কোনো CPU-র কাজ না থাকলে এটিই চলে।
  6. বাকি সব জাগানো। PID 1 এখনো kernel-এর ভেতরে। সে বাকি CPU-গুলো চালু করে, তারপর do_initcalls(): built-in প্রতিটি driver আর subsystem-এর initcall নির্দিষ্ট স্তর মেনে (core → subsys → fs → device → late) একে একে চলে, initramfs RAM-এ খোলা হয়। শেষে __init মেমরি মুক্ত।
  7. User space-এ লাফ। run_init_process() initramfs-এর /init (বা না থাকলে /sbin/init) execve করে। একই PID 1 এখন একটি user-space প্রোগ্রাম — সাধারণত systemd. কিছুই না পেলে kernel panic: “No working init found”.

মনে রাখার মতো তিনটি সংখ্যা: PID 0 idle task, PID 1 init (kernel-এর ভেতরে জন্ম, পরে user space), PID 2 kthreadd. ps -o pid,comm -p 1,2 দিয়ে শেষ দুটি দেখা যায় (container-এর ভেতরে PID 1 অন্য — পর্ব ৫১)।

৩ · Kernel-এ কোড লেখার নিয়ম — অপারেশন থিয়েটারের শৃঙ্খলা

Kernel C-তে লেখা, কিন্তু সাধারণ C-এর অভ্যাস এখানে প্রায়ই বিপজ্জনক। অপারেশন থিয়েটারের কথা ভাবুন: বাইরের জুতা নিষেধ, চিকিৎসক নিজের জীবাণুমুক্ত যন্ত্র ব্যবহার করেন, মাঝপথে কেউ বিশ্রামে যেতে পারেন না, আর ভুলের দাম রোগীর জীবন। Kernel-এর একটি বাগও পুরো যন্ত্রের — ভেতরে এক অংশকে আরেক অংশ থেকে রক্ষার দেয়াল নেই। তাই কয়েকটি কঠিন নিয়ম:

📚 C library নেই

printf, malloc, fopen — কিছুই নেই; glibc নিজেই তো kernel-এর ওপর চলে। বদলে kernel-এর নিজের: printk/pr_info, kmalloc/kfree, memcpy, snprintf (lib/-এ)।

📏 ছোট, স্থির stack

প্রতিটি thread-এর kernel stack মাত্র কয়েক KiB — x86-64-এ সাধারণত ১৬ KiB (Linux ৩.১৫ থেকে; আগে ৮ KiB). বড় array বা গভীর recursion নিষেধ; বড় buffer লাগলে kmalloc.

🔢 সাধারণত floating point নেই

প্রতিবার kernel-এ ঢোকার সময় FPU/SIMD register সংরক্ষণ করা ব্যয়বহুল, তাই kernel ডিফল্টে সেগুলো ছোঁয় না। খুব দরকারে (crypto, RAID-এর হিসাব) kernel_fpu_begin() … kernel_fpu_end()-এর মাঝে, ছোট সময়ের জন্য।

⚠️ ভুল মানে oops

NULL pointer ছুঁলে segfault নয়, kernel oops — stack trace সহ log, আর সেটিং অনুযায়ী পুরো যন্ত্রের kernel panic (পর্ব ৬)।

ঘুমানো যাবে, নাকি যাবে না — দুই রকম context

Kernel-এর কোড সবসময় দুই অবস্থার একটিতে থাকে। Process context: কোনো একটি task-এর পক্ষে চলছে, যেমন syscall-এর ভেতরে; এখানে ঘুমানো (sleep) যায় — ডিস্কের অপেক্ষায় scheduler-কে CPU ছেড়ে দেওয়া, mutex-এ অপেক্ষা করা। Atomic context: interrupt handler, softirq (পর্ব ৩৯), বা spinlock ধরে থাকা অবস্থা; এখানে ঘুমানো নিষেধ, কারণ পেছনে কোনো ঘুমাতে-পারা task নেই, বা যে spinlock ধরে আছে সে ঘুমালে অন্য CPU অনন্তকাল ঘুরতে থাকবে (পর্ব ২৩)। ট্রাফিক মোড়ের মাঝখানে দাঁড়ানো সার্জেন্ট হাত নামিয়ে বিশ্রাম নিতে পারেন না — পুরো মোড় আটকে যাবে।

মেমরি চাওয়ার সময় এই পার্থক্য সরাসরি দেখা যায়, GFP flag-এ (“get free pages”):

  • GFP_KERNEL — স্বাভাবিক পছন্দ; মেমরি কম থাকলে allocator page ফেরত আনতে ঘুমাতে পারে, তাই শুধু process context-এ।
  • GFP_ATOMIC — কখনো ঘুমায় না, জরুরি সংরক্ষিত মেমরি থেকেও নিতে পারে; interrupt বা spinlock-এর ভেতরে। ব্যর্থ হওয়ার সম্ভাবনা বেশি, তাই NULL পরীক্ষা বাধ্যতামূলক।
  • GFP_NOIO, GFP_NOFS — ঘুমাতে পারে, কিন্তু মেমরি খালি করতে ডিস্ক-I/O বা file system-এ ফিরে যাবে না; block বা file system-এর নিজের কোডে, যাতে নিজের ওপর আটকে না যায়।

ত্রুটি ফেরত দেওয়ার রীতি — ঋণাত্মক errno আর ERR_PTR

Kernel-এ global errno নেই। ফাংশন সফল হলে ০ (বা একটি অর্থবহ সংখ্যা), ব্যর্থ হলে ঋণাত্মক errno ফেরত দেয়: return -ENOMEM;. Syscall-এর পথে এই সংখ্যা user space-এ পৌঁছালে glibc তাকে ধনাত্মক করে errno-তে রাখে আর ফাংশন −১ ফেরত দেয় (পর্ব ৫)। আর যে ফাংশন pointer ফেরত দেয়? ত্রুটির সংখ্যাটিকেই pointer-এর ভেতরে লুকানো হয়: address space-এর একেবারে শেষ ৪০৯৫টি ঠিকানায় কোনো বৈধ বস্তু থাকে না, তাই ERR_PTR(-ENOMEM) একটি “অসম্ভব” pointer বানায়, IS_ERR() তা চেনে, PTR_ERR() সংখ্যাটি ফেরত দেয়:

struct buf *buf_create(size_t len)
{
        struct buf *b;

        if (len == 0 || len > SZ_1M)
                return ERR_PTR(-EINVAL);            /* error hidden in the pointer */
        b = kzalloc(sizeof(*b) + len, GFP_KERNEL);  /* may sleep: process context only */
        if (!b)
                return ERR_PTR(-ENOMEM);
        b->len = len;
        return b;
}

static irqreturn_t mydev_irq(int irq, void *data)    /* atomic context */
{
        struct mydev *d = data;
        struct event *e;

        spin_lock(&d->lock);
        e = kmalloc(sizeof(*e), GFP_ATOMIC);         /* GFP_KERNEL here would be a bug */
        if (e)
                list_add_tail(&e->node, &d->events);
        spin_unlock(&d->lock);
        return IRQ_HANDLED;
}

/* caller */
        b = buf_create(n);
        if (IS_ERR(b))
                return PTR_ERR(b);                   /* pass -EINVAL / -ENOMEM upward */

Interrupt handler-এ GFP_KERNEL লিখলে সাধারণত কিছুই হয় না — মেমরি যথেষ্ট থাকলে allocator ঘুমায় না — তাই বাগটি পরীক্ষায় লুকিয়ে থাকে, আর একদিন মেমরির চাপে যন্ত্র আটকে যায়। Debug kernel (CONFIG_DEBUG_ATOMIC_SLEEP) আগেভাগেই log-এ লেখে BUG: sleeping function called from invalid context.

printk আর dmesg — kernel-এর ডায়েরি

printk() লেখে kernel-এর একটি ring buffer-এ, প্রতিটি লাইনে একটি গুরুত্ব-স্তর: ০ (KERN_EMERG) থেকে ৭ (KERN_DEBUG); সহজ রূপ pr_err(), pr_info(), driver-এ dev_info(). পড়া হয় dmesg বা journalctl -k দিয়ে — অনেক distro-তে শুধু root (kernel.dmesg_restrict = 1).

Kernel-এর তালা — এক নজরে মনে করিয়ে দেওয়া

Kernel-এ একসাথে অনেক CPU আর interrupt, তাই synchronization সর্বত্র। এগুলো মডিউল ৪-এ বিস্তারিত দেখেছেন; এখানে শুধু কোনটি কোন context-এ চলে:

যন্ত্রঅপেক্ষার ধরনatomic context-এ?কোথায় শিখেছেন
spinlock_t (spin_lock_irqsave)ঘুরে ঘুরে অপেক্ষাহ্যাঁ — ধরে থাকা অবস্থায় ঘুমানো নিষেধপর্ব ২৩
struct mutexঘুমিয়ে অপেক্ষানা — শুধু process contextপর্ব ২৪
atomic_t, refcount_tlock নেই, এক নির্দেশেহ্যাঁপর্ব ২৩
Per-CPU ডেটাভাগই হয় না — প্রতি CPU-র নিজের কপিহ্যাঁ (preemption বন্ধ রেখে)পর্ব ২৩, ৩৮
RCU (rcu_read_lock)reader অপেক্ষা করে না; writer পুরোনো কপি পরে মুছেreader: হ্যাঁপর্ব ২৯
seqlock_treader ভুল হলে আবার পড়েহ্যাঁপর্ব ২৯

এই তালাগুলো ভুল ক্রমে নেওয়া হচ্ছে কি না, debug kernel-এ lockdep প্রতিটি lock-এর ক্রম মনে রেখে আগেভাগে সতর্ক করে (পর্ব ২৭)।

৪ · Kernel-এর ডেটা-কাঠামো — task_struct, list_head আর container_of

Kernel-এর প্রতিটি task-এর জন্য একটি task_struct (include/linux/sched.h) — পর্ব ৮-এর PCB-র আসল রূপ। কয়েক কিলোবাইটের এই struct-এ আগের পর্বগুলোর প্রায় সবকিছুর সূত্র: __state (চলছে, ঘুমাচ্ছে), pid আর tgid, mm (address space, পর্ব ৩৮), files (খোলা fd-এর টেবিল, পর্ব ৪১), signal আর sighand (পর্ব ১৩), cred (UID, capability — পর্ব ৪৭), nsproxy (namespace, পর্ব ৫১), se (scheduler-এর হিসাব, পর্ব ২১)। আর আছে children আর sibling — বাবা-সন্তানের গাছ সাজানোর তালিকা।

সেই তালিকাগুলো কেমন? আপনি C বা DSA কোর্সে linked list শিখেছেন এভাবে: প্রতিটি node-এ ডেটা আর next pointer. Kernel উল্টো পথে যায়। struct list_head-এ শুধু দুটি pointer — next আর prev, কোনো ডেটা নেই। আর এই ছোট node-টি বসানো হয় আসল বস্তুর ভেতরে, একটি member হিসেবে। একে বলে intrusive list.

ট্রেনের বগির কথা ভাবুন। প্রতিটি বগির দুই মাথায় একটি করে কাপলিং (হুক) — বগিটি যাত্রীবাহী হোক বা মালবাহী, হুক একই রকম। ট্রেন জোড়ার সময় কেউ বগির ভেতরে কী আছে তা দেখে না, শুধু হুকে হুক লাগায়। পরে কোনো হুক হাতে পেলে আপনি জানেন বগির শুরু হুক থেকে কতটা পেছনে — সেখানে গেলেই পুরো বগি। list_head সেই হুক, আর “কতটা পেছনে” হিসাবটি container_of.

তালিকায় হাঁটার সময় হাতে থাকে শুধু list_head *, অথচ দরকার পুরো বস্তু। Compiler জানে member-টি struct-এর শুরু থেকে কত বাইট দূরে (offsetof); member-এর ঠিকানা থেকে সেটি বিয়োগ করলেই বস্তুর শুরু। Kernel-এর include/linux/container_of.h-এ এর সংজ্ঞা (সরলীকৃত; আসলটিতে একটি compile-time type পরীক্ষাও আছে):

#define container_of(ptr, type, member) ({                      \
        void *__mptr = (void *)(ptr);                           \
        static_assert(__same_type(*(ptr), ((type *)0)->member)  \
                      || __same_type(*(ptr), void),             \
                      "pointer type mismatch in container_of()"); \
        ((type *)(__mptr - offsetof(type, member))); })

এবার একই ধারণা user space-এ নিজে বানিয়ে চালিয়ে দেখি। নিচের প্রোগ্রামে kernel-এর list_head, list_add_tail, list_del, container_of আর list_for_each_entry-এর ছোট নকল; তিনটি task একটি “run queue”-তে ঢোকে, মাঝেরটি বেরিয়ে যায়, তারপর তালিকায় হাঁটা:

#include <stdio.h>
#include <stddef.h>
/* user-space copy of the kernel idea: the list node lives INSIDE the object */
struct list_head { struct list_head *next, *prev; };
#define container_of(ptr, type, member) \
    ((type *)((char *)(ptr) - offsetof(type, member)))
#define list_for_each_entry(pos, head, member)                           \
    for (pos = container_of((head)->next, __typeof__(*pos), member);     \
         &pos->member != (head);                                         \
         pos = container_of(pos->member.next, __typeof__(*pos), member))
static void list_add_tail(struct list_head *n, struct list_head *head) {
    n->prev = head->prev;  n->next = head;
    head->prev->next = n;  head->prev = n;
}
static void list_del(struct list_head *e) { e->prev->next = e->next; e->next->prev = e->prev; }
struct task { int pid; char comm[12]; int nice; struct list_head run; };

int main(void) {
    struct list_head rq = { &rq, &rq };          /* empty list: points to itself */
    struct task t[3] = { { .pid = 101, .comm = "bash" },
                         { .pid = 102, .comm = "nginx", .nice = -5 },
                         { .pid = 103, .comm = "backup", .nice = 10 } };
    printf("offsetof(struct task, run) = %zu, sizeof(struct task) = %zu\n",
           offsetof(struct task, run), sizeof(struct task));
    for (int i = 0; i < 3; i++) list_add_tail(&t[i].run, &rq);
    list_del(&t[1].run);                          /* nginx leaves the run queue */
    struct task *p;
    list_for_each_entry(p, &rq, run)
        printf("pid=%d comm=%-6s nice=%3d  (node is %td bytes into the task)\n",
               p->pid, p->comm, p->nice, (char *)&p->run - (char *)p);
    struct task *last = container_of(rq.prev, struct task, run);
    printf("rq.prev -> %s; container_of gives &t[2]? %s\n",
           last->comm, last == &t[2] ? "yes" : "no");
    return 0;
}

এই পাতার রানারে একটি আসল রানের আউটপুট (ঠিকানা ছাপা হয়নি, তাই প্রতিবার একই):

offsetof(struct task, run) = 24, sizeof(struct task) = 40
pid=101 comm=bash   nice=  0  (node is 24 bytes into the task)
pid=103 comm=backup nice= 10  (node is 24 bytes into the task)
rq.prev -> backup; container_of gives &t[2]? yes
  • offsetof = 24: pid (৪ বাইট) + comm[12] (১২) + nice (৪) = ২০; pointer-কে ৮-এর গুণিতক ঠিকানায় বসাতে compiler ৪ বাইট ফাঁক (padding) রাখে, তাই run শুরু হয় ২৪-এ; দুটি pointer (১৬ বাইট) মিলে sizeof = ৪০.
  • তালিকায় শুধু দুটি task: list_del শুধু দুই প্রতিবেশীর pointer জুড়ে দিয়েছে — nginx-এর struct মেমরিতে আগের মতোই আছে, কেউ তাকে free করেনি। Kernel-এও তা-ই: তালিকা থেকে সরানো আর মুক্ত করা আলাদা কাজ।
  • Head নিজে কোনো task নয়: খালি তালিকা মানে rq.next == rq.prev == &rq; লুপ থামে যখন pointer আবার head-এ ফেরে। শেষ লাইন দেখাচ্ছে, তালিকা দুই দিকে জোড়া (circular doubly linked): rq.prev সোজা শেষ বস্তুতে যায়, আর container_of ঠিক &t[2] ফেরত দেয়।

ভেতরের হিসাবটি এবার ঠিকানা ধরে দেখুন। ধরা যাক t array শুরু হয়েছে 0x7ffd1000-এ (উদাহরণ ঠিকানা; আসল রানে অন্য হবে), প্রতিটি task ৪০ = 0x28 বাইট:

তালিকায় হাঁটা (list_for_each_entry)
হাতে থাকা pointer
offsetof(task, run)
struct task *pos
পড়া মান
  1. শুরু। Run queue-র head rq. তার next-এ আছে 0x7ffd1018 — এটি কোনো task-এর শুরু নয়, t[0]-এর ভেতরের run member-এর ঠিকানা (হুক)।
  2. দূরত্ব জানা। offsetof(struct task, run) = ২৪ = 0x18. এটি compile-এর সময় স্থির একটি সংখ্যা; চলার সময় কোনো খোঁজাখুঁজি লাগে না।
  3. বিয়োগ। 0x7ffd1018 − 0x18 = 0x7ffd1000 — ঠিক &t[0]. (char *)-এ রূপান্তর করে বিয়োগ করা হয়, যাতে বিয়োগটি বাইটে হয়, struct-এর আকারের গুণিতকে নয়। Cast করে struct task *: এখন pos->pid = ১০১.
  4. পরের হুক। t[0].run.next-এ এখন 0x7ffd1068. কেন t[1]-এর 0x7ffd1040 নয়? কারণ list_del nginx-কে বাদ দিয়ে bash-এর next সোজা backup-এর হুকে জুড়ে দিয়েছে। t[2] শুরু 0x7ffd1000 + 2 × 0x28 = 0x7ffd1050, তার হুক 0x7ffd1068.
  5. আবার বিয়োগ। 0x7ffd1068 − 0x18 = 0x7ffd1050 = &t[2]: pid ১০৩, backup. খেয়াল করুন, একই offsetof সব বস্তুতে খাটে, কারণ সবাই একই ধরনের struct.
  6. থামা। t[2].run.next = &rq — head-এ ফিরে এসেছি। rq কোনো task-এর ভেতরে নেই, তাই এর ওপর container_of চালালে আবর্জনা ঠিকানা আসত; এজন্যই list_for_each_entry বস্তু ব্যবহারের আগে “head-এ ফিরলাম কি?” পরীক্ষা করে।

লাভ তিনটি: যোগ করতে আলাদা node-এর মেমরি লাগে না (atomic context-এ বড় সুবিধা); একটি বস্তু কয়েকটি list_head দিয়ে একসাথে কয়েকটি তালিকায় থাকতে পারে; আর একই list_add/list_del সব ধরনের বস্তুতে চলে (বিস্তারিত ভাবনার প্রশ্ন ৩-এ)।

আরও তিনটি কাঠামো — যেখানে তালিকা যথেষ্ট নয়

  • Red-black tree (lib/rbtree.c, struct rb_node — এটিও intrusive, বস্তুর ভেতরে বসে): সাজানো ক্রমে খোঁজা, যোগ, মোছা O(log n). CFS/EEVDF-এর task-গুলো এতে সাজানো (পর্ব ২১), আর epoll, high-resolution timer-সহ অনেক জায়গায় ব্যবহার হয়। গাছের নিয়ম আর ঘোরানো DSA কোর্সে দেখতে পারেন।
  • XArray (Linux ৪.২০, ২০১৮; Matthew Wilcox): একটি সংখ্যা (index) থেকে pointer-এর বিশাল, ফাঁকা-ফাঁকা “array”, ভেতরে একটি radix tree, RCU দিয়ে lock ছাড়াই পড়া যায়। একটি ফাইলের page cache — “ফাইলের ৫ নম্বর page কোন frame-এ?” — এতেই রাখা (address_space->i_pages).
  • Maple tree (Linux ৬.১, ২০২২): পরিসর (range) রাখার জন্য B-tree-ধাঁচের গাছ। একটি প্রসেসের VMA-গুলো (পর্ব ৩৮-এর /proc/self/maps-এর লাইন) আগে red-black tree-তে ছিল, এখন এতে।

৫ · একটি পূর্ণাঙ্গ module লেখা — Kbuild, parameter, export আর taint

পর্ব ৬-এ সবচেয়ে ছোট “hello” module আর insmod-এর ভেতরের ধাপ দেখেছেন। এবার একটি বাস্তবসম্মত module, যাতে প্রায় প্রতিটি আসল driver-এ থাকা তিনটি জিনিস আছে: বাইরে থেকে দেওয়া parameter, অন্য module-এর জন্য export করা symbol, আর ভুল input-এ ঋণাত্মক errno ফেরত।

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt     /* every pr_info() starts with "greet: " */
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>

static char *who = "world";
module_param(who, charp, 0444);                 /* read-only in /sys */
MODULE_PARM_DESC(who, "Name to greet");

static int times = 1;
module_param(times, int, 0644);                 /* root may change it later */
MODULE_PARM_DESC(times, "How many greetings (1-10)");

int greet_count;                                /* other modules may read this */
EXPORT_SYMBOL_GPL(greet_count);                 /* ...if they are GPL-compatible */

static int __init greet_init(void)
{
        int i;

        if (times < 1 || times > 10)
                return -EINVAL;                 /* loading fails, nothing stays */
        for (i = 1; i <= times; i++)
                pr_info("hello, %s (%d of %d)\n", who, i, times);
        greet_count = times;
        return 0;
}

static void __exit greet_exit(void)
{
        pr_info("goodbye, %s\n", who);
}

module_init(greet_init);
module_exit(greet_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Greeting module with parameters");

এটি সাধারণ gcc greet.c দিয়ে compile হয় না — চলমান kernel-এর ঠিক একই config আর header দিয়ে বানাতে হয়। কাজটি করে kernel-এর নিজের build-ব্যবস্থা Kbuild. আপনার ফোল্ডারে শুধু দুই লাইনের একটি Makefile (রেসিপির লাইনগুলোর শুরুতে Tab):

obj-m := greet.o                        # build greet.ko as a module
KDIR  ?= /lib/modules/$(shell uname -r)/build

all:
	$(MAKE) -C $(KDIR) M=$(CURDIR) modules
clean:
	$(MAKE) -C $(KDIR) M=$(CURDIR) clean

-C $(KDIR) মানে “kernel-এর build ফোল্ডারে গিয়ে তার Makefile চালানো”, আর M= বলে বাইরের module-টি কোথায়। Debian/Ubuntu-তে এর জন্য linux-headers-$(uname -r) প্যাকেজ লাগে। এবার একটি পরীক্ষামূলক VM-এ পুরো যাত্রা — নিচের আউটপুট একটি Ubuntu VM-এর উদাহরণ (এই পাতার রানার container, সেখানে module লোড করা যায় না; সময়ের ছাপ, version আর hash আপনার যন্ত্রে ভিন্ন হবে):

$ make
make -C /lib/modules/6.8.0-45-generic/build M=/home/rahim/greet modules
  CC [M]  /home/rahim/greet/greet.o
  MODPOST /home/rahim/greet/Module.symvers
  CC [M]  /home/rahim/greet/greet.mod.o
  LD [M]  /home/rahim/greet/greet.ko

$ modinfo greet.ko
filename:       /home/rahim/greet/greet.ko
description:    Greeting module with parameters
license:        GPL
depends:
name:           greet
vermagic:       6.8.0-45-generic SMP preempt mod_unload modversions
parm:           who:Name to greet (charp)
parm:           times:How many greetings (1-10) (int)

$ sudo insmod greet.ko times=20
insmod: ERROR: could not insert module greet.ko: Invalid argument

$ sudo insmod greet.ko who=Rajshahi times=2
$ sudo dmesg | tail -4
[ 912.402113] greet: loading out-of-tree module taints kernel.
[ 912.402170] greet: module verification failed: signature and/or required key missing - tainting kernel
[ 912.402689] greet: hello, Rajshahi (1 of 2)
[ 912.402692] greet: hello, Rajshahi (2 of 2)

$ cat /sys/module/greet/parameters/who /sys/module/greet/parameters/times
Rajshahi
2
$ cat /proc/sys/kernel/tainted
12288
$ sudo rmmod greet && sudo dmesg | tail -1
[ 975.118204] greet: goodbye, Rajshahi
  • times=20: greet_init -EINVAL ফেরত দিল, kernel module-টি সরিয়ে দিল, আর insmod সেটিই “Invalid argument” হিসেবে দেখাল — ঋণাত্মক errno user space পর্যন্ত পৌঁছেছে।
  • Parameter: insmod-এ নাম=মান; স্থায়ীভাবে /etc/modprobe.d/*.conf-এ options greet times=3; built-in হলে kernel command line-এ greet.times=3. module_param-এর শেষ সংখ্যাটি /sys-এর ফাইলের অনুমতি — ০৬৪৪ দিলে root চলমান অবস্থায় মান বদলাতে পারেন।
  • EXPORT_SYMBOL_GPL: module-এর global symbol export না করলে অন্য module দেখতে পায় না; _GPL রূপটি শুধু GPL-সামঞ্জস্যপূর্ণ module-কে দেয় (ভাবনার প্রশ্ন ৪)।

Taint — kernel-এর গায়ে লাগা দাগ

উপরের 12288 সংখ্যাটি একটি bitmask. Kernel একবার “অস্বাভাবিক” কিছু দেখলে একটি bit জ্বালায়, আর রিবুট ছাড়া তা আর নেভে না। উদ্দেশ্য শাস্তি নয়, bug report বিচার করা: একটি crash report-এ Tainted: P OE দেখলে kernel ডেভেলপাররা জানেন, এমন module ছিল যার source তাঁরা দেখতে পারবেন না। Oops-এর বার্তায় এই অক্ষরগুলো ছাপা হয়। প্রধান bit-গুলো (Documentation/admin-guide/tainted-kernels.rst):

bitমানঅক্ষরঅর্থ
01Pproprietary (GPL-বহির্ভূত লাইসেন্সের) module লোড হয়েছে
12Fmodule জোর করে লোড করা (insmod -f)
7128Dসম্প্রতি kernel oops বা BUG ঘটেছে
9512Wkernel একটি warning দিয়েছে (WARN_ON)
101024Cdrivers/staging-এর অপরিণত driver লোড হয়েছে
124096Okernel-এর বাইরে (out-of-tree) বানানো module
138192Eস্বাক্ষরবিহীন (unsigned) module
1532768Kচলমান kernel-এ live patch বসানো হয়েছে

তাহলে ১২২৮৮ = ৮১৯২ + ৪০৯৬ = E + O: আমাদের greet বাইরে বানানো আর স্বাক্ষরবিহীন। Secure Boot চালু থাকলে অনেক distro-র kernel স্বাক্ষর বাধ্যতামূলক করে (পর্ব ৪৯-এর lockdown), তখন insmod সরাসরি “Key was rejected by service” বলে ব্যর্থ হয়। সমাধান Secure Boot বন্ধ করা নয়; নিজের চাবি বানিয়ে MOK-এ নথিভুক্ত করে module-এ স্বাক্ষর করা — DKMS-এর মতো ব্যবস্থা এটি স্বয়ংক্রিয়ভাবে করে।

নিজের module শুধু পরীক্ষামূলক যন্ত্রে। Module kernel mode-এ পূর্ণ ক্ষমতায় চলে; একটি ভুল pointer পুরো যন্ত্র ফেলে দিতে পারে, ফাইল সিস্টেম নষ্ট করতে পারে। QEMU বা VirtualBox-এর একটি VM-এ, snapshot রেখে পরীক্ষা করুন — কাজের ল্যাপটপ বা সার্ভারে নয়। আর ইন্টারনেট থেকে নামানো অপরিচিত .ko কখনো লোড করবেন না: kernel-এ ঢোকা কোড আপনার যন্ত্রের সবকিছু দেখতে ও বদলাতে পারে।

৬ · চলমান kernel-এর জানালা — /proc, /sys, sysctl আর kernel config

Kernel-এর ভেতরের অবস্থা দেখার আর কিছু সেটিং বদলানোর জন্য দুটি কাল্পনিক file system আছে, যাদের ফাইল ডিস্কে নেই — পড়ার মুহূর্তে kernel লেখা তৈরি করে। /proc পুরোনো, কিছুটা এলোমেলো: প্রসেসের তথ্য (/proc/<pid>/, পর্ব ১১) আর সিস্টেমের নানা ফাইল একসাথে। তার ভেতরের /proc/sys/ হলো sysctl-এর গাছ — kernel-এর বদলযোগ্য “knob”. /sys (২০০৩-এর দিকে, ২.৬ যুগে) নতুন আর নিয়মমাফিক: kernel-এর বস্তুগুলো (যন্ত্র, driver, module) একটি গাছে, আর প্রতিটি ফাইলে একটিই মান.

নিচের শুধু-পড়ার script-টি এই পাতার রানারের kernel-কে কয়েকটি প্রশ্ন করে। মনে রাখবেন, রানারটি একটি container (পর্ব ৫১) — তাই উত্তর আসে host কম্পিউটারের kernel থেকে:

echo "== which kernel am I talking to? =="
cut -c1-80 /proc/version
for k in osrelease pid_max threads-max tainted kptr_restrict perf_event_paranoid; do
  printf '%-20s %s\n' "$k" "$(cat /proc/sys/kernel/$k 2>/dev/null || echo '(unreadable)')"
done
echo "== /sys/kernel =="
ls /sys/kernel 2>/dev/null | tr '\n' ' '; echo
echo "BTF of the running kernel: $(stat -c %s /sys/kernel/btf/vmlinux 2>/dev/null || echo none) bytes"
echo "== first 3 lines of /proc/kallsyms =="
head -3 /proc/kallsyms 2>/dev/null || echo "(kallsyms unreadable)"
echo "symbols listed: $(wc -l < /proc/kallsyms 2>/dev/null || echo 0)"
echo "== knobs under /proc/sys (files per directory) =="
for d in /proc/sys/*/; do
  n=$(find "$d" -type f 2>/dev/null | wc -l)
  printf '%-8s %s\n' "$(basename "$d")" "$n"
done
all=$(find /proc/sys -type f 2>/dev/null | wc -l)
rd=$(find /proc/sys -type f -readable 2>/dev/null | wc -l)
echo "total: $all files, readable by me: $rd"

একটি আসল রানের আউটপুট (সংখ্যাগুলো যন্ত্র আর সময়ভেদে বদলাতে পারে):

== which kernel am I talking to? ==
Linux version 6.8.0-137-generic (buildd@lcy02-amd64-064) (x86_64-linux-gnu-gcc-1
osrelease            6.8.0-137-generic
pid_max              4194304
threads-max          14727
tainted              0
kptr_restrict        1
perf_event_paranoid  4
== /sys/kernel ==
address_bits boot_params btf cgroup config cpu_byteorder crash_elfcorehdr_size debug fscaps iommu_groups irq kexec_crash_loaded kexec_crash_size kexec_loaded livepatch mm notes oops_count profiling rcu_expedited rcu_normal reboot security slab software_nodes tracing uevent_helper uevent_seqnum vmcoreinfo warn_count 
BTF of the running kernel: 6101218 bytes
== first 3 lines of /proc/kallsyms ==
0000000000000000 A fixed_percpu_data
0000000000000000 A __per_cpu_start
0000000000000000 A cpu_debug_store
symbols listed: 257494
== knobs under /proc/sys (files per directory) ==
abi      1
debug    2
dev      15
fs       43
kernel   148
net      668
user     12
vm       51
total: 940 files, readable by me: 921
  • Kernel-এর পরিচয়: 6.8.0-137-generic — Ubuntu-র বানানো 6.8 kernel (সংখ্যার অর্থ অংশ ৯-এ)। tainted 0: host-এর kernel-এ কোনো দাগ নেই।
  • pid_max 4194304: এটি ২২২, ৬৪-বিট kernel-এ সর্বোচ্চ সম্ভব মান। Kernel নিজে সাধারণত ৩২৭৬৮ (অনেক CPU থাকলে কিছু বেশি) দিয়ে শুরু করে; systemd-এর নতুন সংস্করণ বুটের সময় এটি বাড়িয়ে দেয় — অর্থাৎ একটি sysctl যা user space বদলেছে।
  • threads-max 14727: kernel বুটের সময় RAM থেকে হিসাব করে, যাতে সব thread-এর kernel stack মিলে RAM-এর ১/৮-এর বেশি না নেয়: RAM ÷ (৮ × ১৬ KiB). উল্টো হিসাবে ১৪৭২৭ × ১২৮ KiB ≈ ১.৮ GiB — host-এর RAM মোটামুটি এই মাপের বলে আন্দাজ করা যায়।
  • /sys/kernel: btf (eBPF-এর type-তথ্য, অংশ ৮ — প্রায় ৬ MB), tracing আর debug (ftrace আর debugfs-এর জায়গা), security, livepatch, oops_count. সাবধান: /sys/kernel/config configfs-এর জায়গা, kernel-এর build config নয়।
  • /proc/kallsyms-এ সব ঠিকানা শূন্য: এটি kernel-এর প্রতিটি ফাংশন আর global ভ্যারিয়েবলের নাম-ঠিকানার তালিকা — এখানে আড়াই লাখের বেশি। কিন্তু kptr_restrict = 1: শুধু CAP_SYSLOG-ধারী (সাধারণত root) আসল ঠিকানা দেখেন। কারণ পর্ব ৪৮-এর KASLR: kernel এলোমেলো ঠিকানায় বসে, আর ঠিকানা ফাঁস হলে আক্রমণকারীর কাজ সহজ হয়। perf_event_paranoid 4 Debian/Ubuntu-র বাড়তি কড়া স্তর।
  • ৯৪০টি knob: সবচেয়ে বেশি net/-এ, কারণ প্রতিটি network namespace-এর নিজের নেটওয়ার্ক-সেটিং; ১৯টি আমাদের পড়ার অনুমতির বাইরে।

নিজের যন্ত্রে /proc/sys/kernel/pid_max আর sysctl kernel.pid_max একই জিনিস — নামের বিন্দু path-এর /. বদলাতে sudo sysctl -w kernel.pid_max=…; স্থায়ী করতে /etc/sysctl.d/-এ একটি .conf ফাইল। macOS-এও BSD-র sysctl আছে (sysctl kern.ostype), নামগুলো আলাদা; Windows-এ এমন সেটিং থাকে registry-তে (পর্ব ৫৬), আর চলমান অবস্থা দেখা হয় performance counter বা ETW দিয়ে।

Kconfig আর .config — কোন kernel বানানো হবে

Kernel-এর প্রায় প্রতিটি বৈশিষ্ট্য একটি config symbol, যার সংজ্ঞা প্রতিটি ফোল্ডারের Kconfig ফাইলে: নাম, ধরন, নির্ভরতা আর সাহায্য-বার্তা। এমন symbol হাজার হাজার (সংস্করণভেদে দশ হাজারেরও বেশি)। Driver-এর মতো বেশিরভাগের তিনটি মান হতে পারে: y (kernel-এর ভেতরে স্থায়ী), m (module হিসেবে আলাদা .ko), n (বাদ)। সব উত্তর জমা থাকে একটি টেক্সট ফাইলে, .config:

CONFIG_BPF_SYSCALL=y          # eBPF available
CONFIG_BPF_JIT=y
CONFIG_DEBUG_INFO_BTF=y       # produces /sys/kernel/btf/vmlinux
CONFIG_EXT4_FS=y              # built in: root file system needs it at boot
CONFIG_BTRFS_FS=m             # module: loaded only if used
# CONFIG_DEBUG_ATOMIC_SLEEP is not set

আপনার চলমান kernel কোন config-এ বানানো, তা সাধারণত /boot/config-$(uname -r)-এ থাকে (kernel-এ CONFIG_IKCONFIG_PROC থাকলে /proc/config.gz-এও)। নিজে একটি kernel বানানোর ধাপ — শুধু একটি পরীক্ষামূলক VM-এ, পুরোনো kernel বুট মেনুতে রেখে:

git clone --depth 1 https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
cd linux
cp /boot/config-$(uname -r) .config   # start from your distro's choices
make olddefconfig                     # new options get their default answer
make localmodconfig                   # optional: keep only modules loaded right now
make menuconfig                       # optional: a text menu to browse/change options
make -j"$(nproc)"                     # vmlinux, the compressed image, all modules
sudo make modules_install             # copies .ko files to /lib/modules/<version>/
sudo make install                     # copies the kernel to /boot, updates the bootloader

সময় লাগে যন্ত্রভেদে কয়েক মিনিট থেকে এক ঘণ্টার বেশি; localmodconfig অপ্রয়োজনীয় driver বাদ দিয়ে তা অনেক কমায়। আর নিজের বানানো kernel-এর নিরাপত্তা-হালনাগাদের দায়িত্ব আপনার নিজের।

৭ · BPF-এর জন্মকথা — প্যাকেট ছাঁকার ছোট যন্ত্র থেকে verifier

১৯৯০-এর দশকের শুরুতে নেটওয়ার্ক বিশ্লেষণের একটি বড় সমস্যা ছিল: tcpdump-এর মতো টুল প্রতিটি প্যাকেট kernel থেকে user space-এ কপি করত, আর তারপর বেশিরভাগই ফেলে দিত — কারণ আপনি হয়তো শুধু port ২২-এর প্যাকেট দেখতে চান। Lawrence Berkeley Laboratory-র Steven McCanne আর Van Jacobson ১৯৯২ সালে একটি সমাধান বানান, যা ১৯৯৩ সালের USENIX সম্মেলনে প্রকাশিত হয়: BSD Packet Filter — BPF. ধারণাটি: ছাঁকনির নিয়ম user space থেকে একটি ছোট “প্রোগ্রাম” হিসেবে kernel-এ পাঠানো, আর kernel প্রতিটি প্যাকেটে সেটি চালিয়ে শুধু দরকারিগুলো ওপরে পাঠাবে।

বাড়ির দারোয়ান প্রত্যেক দর্শনার্থীকে ওপরে পাঠালে মালিকের সারাদিন যাবে দরজা খুলতে। তাই মালিক তাঁকে একটি ছোট নিয়মের কার্ড দেন: “যিনি ‘হিসাব বিভাগ’ বলবেন, শুধু তাঁকে পাঠাবেন।” কার্ডের নিয়ম এমন যে দারোয়ান কোনো দর্শনার্থীকে নিয়ে অনন্তকাল আটকে থাকবেন না। BPF প্রোগ্রাম সেই কার্ড, kernel সেই দারোয়ান।

Classic BPF একটি ক্ষুদ্র কাল্পনিক যন্ত্র: একটি accumulator register A, একটি index register X, ছোট একটি scratch মেমরি, আর কয়েক ধরনের নির্দেশ — প্যাকেটের নির্দিষ্ট জায়গা থেকে load, তুলনা, শুধু সামনের দিকে jump, আর return (কত বাইট রাখব; ০ = ফেলে দেওয়া)। পেছনে jump নেই, তাই লুপ নেই, তাই প্রোগ্রাম সবসময় শেষ হয়। tcpdump -d আপনার লেখা ছাঁকনি-নিয়মটির BPF রূপ দেখায় — নিচে একটি Ethernet যন্ত্রে উদাহরণ আউটপুট (রানারে tcpdump নেই):

$ sudo tcpdump -d ip
(000) ldh      [12]                       # load 2 bytes at offset 12: the EtherType
(001) jeq      #0x800      jt 2  jf 3      # IPv4?
(002) ret      #262144                    # yes: keep the packet
(003) ret      #0                         # no: drop it

Linux এই ধারণা ১৯৯০-এর দশকের শেষে socket-এর ছাঁকনি হিসেবে নেয় (Linux Socket Filter), আর আজও যেকোনো প্রোগ্রাম SO_ATTACH_FILTER দিয়ে নিজের socket-এ classic BPF বসাতে পারে — কোনো বিশেষ অনুমতি ছাড়াই, কারণ ছাঁকনি শুধু নিজের প্যাকেট কমাতে পারে। এই পাতার রানারেই চলুক: একটি UDP socket-এ ৪ নির্দেশের ছাঁকনি, যা শুধু ‘A’ দিয়ে শুরু হওয়া বার্তা রাখে, তারপর loopback-এ চারটি বার্তা পাঠানো:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <linux/filter.h>

int main(void) {
    struct sock_filter prog[] = {                  /* classic BPF: 4 instructions */
        BPF_STMT(BPF_LD | BPF_B | BPF_ABS, 8),           /* A = byte 8 = first payload byte */
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 'A', 0, 1),  /* A == 'A' ? next : skip 1 */
        BPF_STMT(BPF_RET | BPF_K, 0xFFFF),               /* keep (up to 65535 bytes) */
        BPF_STMT(BPF_RET | BPF_K, 0),                    /* drop */
    };
    struct sock_fprog fp = { .len = 4, .filter = prog };
    struct sockaddr_in a = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK) };
    int rx = socket(AF_INET, SOCK_DGRAM, 0), tx = socket(AF_INET, SOCK_DGRAM, 0);
    socklen_t len = sizeof a;
    if (bind(rx, (struct sockaddr *)&a, sizeof a) != 0) { perror("bind"); return 1; }
    getsockname(rx, (struct sockaddr *)&a, &len);          /* learn the port */
    if (setsockopt(rx, SOL_SOCKET, SO_ATTACH_FILTER, &fp, sizeof fp) != 0) {
        perror("SO_ATTACH_FILTER"); return 1;
    }
    printf("filter attached: %u instructions\n", fp.len);
    const char *msgs[] = { "A-order#1", "B-spam", "A-order#2", "C-ping" };
    for (int i = 0; i < 4; i++)
        sendto(tx, msgs[i], strlen(msgs[i]), 0, (struct sockaddr *)&a, sizeof a);
    char buf[64]; ssize_t n; int got = 0;
    while ((n = recv(rx, buf, sizeof buf - 1, MSG_DONTWAIT)) > 0) {
        buf[n] = '\0'; printf("received: %s\n", buf); got++;
    }
    printf("sent 4, received %d (kernel ran the filter on each packet)\n", got);
    return 0;
}

এই পাতার রানারে একটি আসল রানের আউটপুট:

filter attached: 4 instructions
received: A-order#1
received: A-order#2
sent 4, received 2 (kernel ran the filter on each packet)

চারটি বার্তা পাঠানো হয়েছে, socket পেয়েছে দুটি — B-spam আর C-ping kernel-এর ভেতরেই, receive queue-তে ঢোকার আগে ফেলে দেওয়া হয়েছে। Offset ৮ কারণ UDP socket-এর ছাঁকনি প্যাকেট দেখে ৮ বাইটের UDP header থেকে। jt 0, jf 1 = “সত্য হলে পরের নির্দেশ, মিথ্যা হলে একটি বাদ দিয়ে” — দুটিই সামনের দিকে। পর্ব ৪৯-এর seccomp-bpf-ও ঠিক এই classic BPF, শুধু প্যাকেটের বদলে প্রতিটি system call-এর নম্বর আর argument দেখে।

Verifier কেন — একটি খেলনা BPF যন্ত্র

Kernel বাইরের একটি প্রোগ্রাম নিজের ভেতরে চালাবে — এটি ভয়ের কথা। তাই প্রোগ্রাম গ্রহণের আগে একবার kernel তাকে যাচাই করে: প্রতিটি jump প্রোগ্রামের ভেতরে পড়ে কি না, পেছনের দিকে কোনো jump (সম্ভাব্য অনন্ত লুপ) আছে কি না, শেষ নির্দেশটি return কি না। নিচের JavaScript-এ একটি খেলনা যন্ত্র আর খেলনা verifier: একটি ছাঁকনি, যা শুধু TCP port ২২ (SSH) রাখে, কয়েকটি নমুনা প্যাকেটে চলে; তারপর একটি লুপ-ওয়ালা প্রোগ্রাম verifier-এর কাছে যায়:

// Toy classic-BPF machine, one register A. Toy packet: [protocol (6 TCP, 17 UDP), port hi, port lo]
function verify(prog) {                 // runs ONCE, before the program is accepted
  for (let pc = 0; pc < prog.length; pc++) {
    const i = prog[pc];
    if (i.op === "jeq" || i.op === "ja") {
      for (const off of i.op === "ja" ? [i.k] : [i.jt, i.jf]) {
        if (off < 0) return "REJECT: insn " + pc + " jumps backwards (possible loop)";
        if (pc + 1 + off >= prog.length) return "REJECT: insn " + pc + " jumps past the end";
      }
    }
  }
  return prog[prog.length - 1].op === "ret" ? "OK" : "REJECT: last insn is not ret";
}
function run(prog, pkt) {
  let A = 0, pc = 0, steps = 0;
  for (;;) {
    const i = prog[pc++]; steps++;
    if (i.op === "ldb") { if (i.k >= pkt.length) return [0, steps]; A = pkt[i.k]; }
    else if (i.op === "ldh") { if (i.k + 1 >= pkt.length) return [0, steps]; A = pkt[i.k] * 256 + pkt[i.k + 1]; }
    else if (i.op === "jeq") pc += (A === i.k) ? i.jt : i.jf;
    else if (i.op === "ja") pc += i.k;
    else if (i.op === "ret") return [i.k, steps];
  }
}
const sshOnly = [ { op: "ldb", k: 0 }, { op: "jeq", k: 6, jt: 0, jf: 3 },
  { op: "ldh", k: 1 }, { op: "jeq", k: 22, jt: 0, jf: 1 },
  { op: "ret", k: 65535 }, { op: "ret", k: 0 } ];
console.log("verifier: " + verify(sshOnly));
const pkts = { "TCP:22": [6, 0, 22], "TCP:80": [6, 0, 80], "UDP:53": [17, 0, 53], "short": [6] };
for (const [name, p] of Object.entries(pkts)) {
  const [v, s] = run(sshOnly, p);
  console.log(name.padEnd(7) + " -> " + (v ? "ACCEPT" : "drop  ") + " after " + s + " insns");
}
const looping = [ { op: "ldb", k: 0 }, { op: "ja", k: -2 }, { op: "ret", k: 0 } ];
console.log("verifier: " + verify(looping));

আউটপুট:

verifier: OK
TCP:22  -> ACCEPT after 5 insns
TCP:80  -> drop   after 5 insns
UDP:53  -> drop   after 3 insns
short   -> drop   after 3 insns
verifier: REJECT: insn 1 jumps backwards (possible loop)
  • TCP:22: ৫টি নির্দেশ — protocol load, ৬-এর সাথে তুলনা (সত্য), port load (০ × ২৫৬ + ২২ = ২২), ২২-এর সাথে তুলনা (সত্য), ret 65535.
  • UDP:53: মাত্র ৩টি — protocol ১৭ ≠ ৬, তাই jf: 3 সোজা শেষ ret 0-তে নিয়ে গেছে। ভালো ছাঁকনি তাড়াতাড়ি “না” বলে।
  • short: প্যাকেটে মাত্র ১ বাইট, অথচ প্রোগ্রাম ১–২ নম্বর বাইট পড়তে চায়। যন্ত্র সীমার বাইরে না পড়ে ফেলে দিয়েছে (0) — classic BPF-এ এই পরীক্ষা চলার সময় হয়; আধুনিক eBPF-এর verifier অনেক ক্ষেত্রে আগেই প্রমাণ চায় যে পড়াটি সীমার ভেতরে (অংশ ৮)।
  • শেষ লাইন: ja -2 পেছনে লাফায়; চালালে প্রোগ্রাম কখনো শেষ হতো না, আর kernel-এর ভেতরে একটি অনন্ত লুপ মানে একটি CPU চিরতরে হারানো। Verifier প্রোগ্রামটি চালানোর আগেই প্রত্যাখ্যান করেছে — নিরাপত্তা আসে প্রমাণ থেকে, চালিয়ে দেখা থেকে নয়।

৮ · আধুনিক eBPF — verifier, map, JIT আর kernel-এর প্রায় সব hook

২০১৪ সালে Alexei Starovoitov-এর কাজ থেকে (পরে Daniel Borkmann-সহ অনেকের অবদানে) classic BPF-এর একটি বড় সম্প্রসারণ Linux-এ আসে: eBPF (“extended”). Linux ৩.১৮-এ নতুন bpf() system call আসে, আর তারপর প্রায় প্রতিটি release-এ নতুন ক্ষমতা। আজ “BPF” বললে সাধারণত eBPF-ই বোঝানো হয়, আর classic রূপটিকে kernel ভেতরে ভেতরে eBPF-এ অনুবাদ করে চালায়। কী বদলাল:

  • বড় যন্ত্র: ১১টি ৬৪-বিট register (r0 ফল, r1–r5 argument, r10 stack-এর frame pointer), ৫১২ বাইটের stack — আধুনিক CPU-র কাছাকাছি, তাই JIT-এ প্রায় এক-এক অনুবাদ।
  • Helper ফাংশন: প্রোগ্রাম kernel-এর যেকোনো ফাংশন ডাকতে পারে না, শুধু অনুমোদিত তালিকা — যেমন bpf_get_current_pid_tgid(), bpf_ktime_get_ns(), bpf_map_lookup_elem().
  • Map: kernel-এ থাকা key-value ভাণ্ডার — hash, array, per-CPU রূপ, ring buffer. eBPF প্রোগ্রাম সেখানে লেখে, user space-এর প্রোগ্রাম bpf() syscall দিয়ে পড়ে; একাধিক প্রোগ্রাম একই map ভাগ করতে পারে।
  • শক্তিশালী verifier: প্রতিটি সম্ভাব্য পথে প্রতিটি register-এর ধরন (সংখ্যা, map-এর মান, প্যাকেটের pointer…) আর সম্ভাব্য পরিসর হিসাব রাখে। Map lookup-এর ফল NULL হতে পারে — পরীক্ষা না করে ব্যবহার করলে প্রত্যাখ্যান। Linux ৫.৩ (২০১৯) থেকে সীমাবদ্ধ লুপ চলে, যদি verifier প্রমাণ করতে পারে লুপটি শেষ হবে।
  • JIT: যাচাই-পাস bytecode CPU-র আসল machine code-এ অনুবাদ হয় (x86-64, arm64, riscv…), তাই গতি প্রায় kernel-এর নিজের কোডের মতো। নিরাপত্তার কারণে অনেক distro-র kernel-এ JIT সবসময় চালু থাকে আর interpreter বাদ দেওয়া হয়।
  • BTF আর CO-RE: task_struct-এর কোন member কত বাইট দূরে, তা kernel-ভেদে বদলায় — অংশ ৪-এর offsetof-এর সমস্যা! BTF হলো kernel-এর সব type-এর সংক্ষিপ্ত বর্ণনা (রানারে দেখা /sys/kernel/btf/vmlinux); লোডের সময় libbpf তা দেখে offset ঠিক করে দেয় — “Compile Once – Run Everywhere”.
প্রোগ্রামের ধরনকোথায় বসে (hook)সাধারণ ব্যবহার
kprobe / kretprobe, fentryপ্রায় যেকোনো kernel ফাংশনের শুরু বা শেষগভীর tracing; ফাংশনের নাম kernel-ভেদে বদলায়, তাই ভঙ্গুর
Tracepointkernel-এর কোডে আগে থেকে বসানো স্থির চিহ্ন (যেমন sys_enter, sched_switch)স্থিতিশীল tracing, observability
XDP (Linux ৪.৮, ২০১৬)network driver-এ, প্যাকেট আসামাত্র, socket buffer বানানোরও আগেDDoS ঠেকানো, load balancer — খুব দ্রুত drop বা redirect
tc (traffic control)নেটওয়ার্ক-স্ট্যাকের ঢোকা ও বেরোনোর পথেcontainer-নেটওয়ার্কিং, নীতি, NAT
cgroup ও socket hookএকটি cgroup-এর socket, connect, bindcontainer ধরে নেটওয়ার্ক-নীতি
BPF LSM (Linux ৫.৭, ২০২০)পর্ব ৪৯-এর LSM hookনিজের নিরাপত্তা-নীতি, অডিট
struct_ops — sched_ext (Linux ৬.১২, ২০২৪)kernel-এর একটি পুরো “ops” টেবিলeBPF-এ লেখা scheduler (পর্ব ২১), TCP congestion control

একটি eBPF প্রোগ্রামের পুরো যাত্রা ধাপে ধাপে দেখুন। উদাহরণ: প্রতিটি প্রসেস কতবার system call করছে, গোনা।

  1. C কোডcount.bpf.cclang -target bpf
  2. লোডbpf(PROG_LOAD)libbpf + BTF
  3. Verifierসব পথনিরাপদ? শেষ হবে?
  4. JITx86-64 codeআসল machine code
  5. Hooksys_enterপ্রতিটি syscall-এ
এই ধাপে
কোথায়
কী আছে
map counts
  1. লেখা। সীমিত C-তে একটি ফাংশন: SEC("tracepoint/raw_syscalls/sys_enter"); ভেতরে pid = bpf_get_current_pid_tgid() >> 32, map-এ সেই pid-এর গণনা খুঁজে ১ বাড়ানো, না থাকলে নতুন entry. সাথে map-এর সংজ্ঞা।
  2. Compile. clang -O2 -g -target bpf একটি ELF object বানায়: eBPF bytecode (প্রতিটি নির্দেশ ৮ বাইট), map-এর বর্ণনা, BTF আর CO-RE relocation-এর তালিকা। এখনো kernel কিছুই জানে না।
  3. লোড। User-space loader (libbpf দিয়ে বানানো, বা bpftrace/bcc) আগে map বানায় (BPF_MAP_CREATE), চলমান kernel-এর BTF দেখে struct-এর offset ঠিক করে, তারপর bpf(BPF_PROG_LOAD). এজন্য সাধারণত root বা CAP_BPF + CAP_PERFMON লাগে।
  4. Verifier. Kernel প্রতিটি পথ ধরে প্রোগ্রামটি “কল্পনায় চালায়”। Map lookup-এর পর NULL পরীক্ষা ভুলে গেলে এখানেই থামে, log-এ লেখে invalid mem access 'map_value_or_null'. সীমার বাইরে stack বা প্যাকেট পড়া, শেষ না-হওয়া লুপ, অননুমোদিত helper — সব প্রত্যাখ্যাত।
  5. JIT. যাচাই-পাস bytecode CPU-র machine code-এ অনুবাদ হয়ে kernel-এর মেমরিতে শুধু-পড়ার-ও-চালানোর page-এ বসে। Kernel একটি file descriptor ফেরত দেয় — প্রোগ্রামটির হাতল।
  6. Hook-এ বসানো। প্রোগ্রামটি raw_syscalls:sys_enter tracepoint-এ জোড়া হলো। এখন যেকোনো CPU-তে যেকোনো প্রসেস syscall করলে, সেই CPU-তেই, সেই মুহূর্তে প্রোগ্রামটি চলে আর map-এর মান বাড়ায় — কোনো context switch নেই, user space-এ কোনো কপি নেই।
  7. ফল পড়া। User-space প্রোগ্রাম প্রতি সেকেন্ডে map-এ হাঁটে (BPF_MAP_GET_NEXT_KEY, BPF_MAP_LOOKUP_ELEM) আর সাজিয়ে ছাপে। Loader বেরিয়ে গেলে, কেউ map বা link ধরে না রাখলে, kernel প্রোগ্রাম আর map দুটোই মুছে দেয়।

bpftrace, bcc আর bpftool — হাতে-কলমে

তৈরি টুল দিয়েই অনেক কাজ হয়: bpftrace — awk আর DTrace-এর ধাঁচের এক-লাইনের ভাষা; bcc — তৈরি টুলের সংগ্রহ; bpftool — লোড-করা প্রোগ্রাম আর map দেখায়। সবগুলোতে root লাগে, রানারে এগুলো নেই — নিচে একটি Ubuntu সার্ভারের উদাহরণ আউটপুট:

# which processes make the most system calls? (Ctrl+C after a few seconds)
$ sudo bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
Attaching 1 probe...
^C

@[systemd-journal]: 57
@[sshd]: 318
@[bash]: 702
@[node]: 9431

# every new program that starts, with its parent (bcc tool; Ubuntu adds -bpfcc)
$ sudo execsnoop-bpfcc
PCOMM            PID     PPID    RET ARGS
cron             24187   1103      0 /bin/sh -c /usr/local/bin/backup.sh
backup.sh        24188   24187     0 /usr/bin/tar -czf /backup/db.tgz /var/lib/db

# what BPF programs are loaded right now?
$ sudo bpftool prog show
47: tracepoint  name sys_enter  tag 6a2b1c0d9e8f7a65  gpl
        loaded_at 2026-10-26T10:12:03+0600  uid 0
        xlated 240B  jited 151B  memlock 4096B  map_ids 12

bpftrace ফল ছোট থেকে বড় ক্রমে ছাপে, তাই সবচেয়ে ব্যস্ত node শেষে। কোনো প্রোগ্রাম না বদলে পুরো সিস্টেমের ছবি — আর পর্ব ৫-এর strace-এর তুলনায় খরচ সাধারণত অনেক কম। xlated আর jited হলো bytecode আর JIT-এর পরের machine code-এর আকার। পূর্ণ ব্যবহার পর্ব ৫৭-এ।

বাস্তবে কোথায় — নেটওয়ার্ক, observability, নিরাপত্তা

  • নেটওয়ার্ক: Cilium Kubernetes-এর নেটওয়ার্কিং আর নেটওয়ার্ক-নীতি eBPF দিয়ে করে (CNCF-এর “graduated” প্রকল্প, ২০২৩); XDP দিয়ে DDoS-প্যাকেট driver-এর স্তরেই ফেলা হয়।
  • Observability আর নিরাপত্তা: Falco, Tetragon-এর মতো টুল প্রসেস চালু, ফাইল খোলা, নেটওয়ার্ক-সংযোগের ঘটনা দেখে সন্দেহজনক আচরণে সতর্ক করে।
  • অন্য OS-এ: Microsoft-এর খোলা প্রকল্প eBPF for Windows সীমিত পরিসরে একই ধারণা আনছে; macOS-এ eBPF নেই, সেখানে ঐতিহাসিকভাবে DTrace আর নিরাপত্তা-টুলের জন্য Endpoint Security কাঠামো।

eBPF-এর ক্ষমতা মানে দায়িত্বও। যে eBPF লোড করতে পারে, সে সারা সিস্টেমের ঘটনা দেখতে পারে — প্রায় root-এর সমান ক্ষমতা। Verifier-এর বাগ কাজে লাগানোর ঘটনা অতীতে ঘটেছে, তাই বেশিরভাগ distro-তে সাধারণ ব্যবহারকারীর eBPF বন্ধ (kernel.unprivileged_bpf_disabled = ১ বা ২)। প্রতিরক্ষার অভ্যাস: kernel হালনাগাদ রাখা, container-কে CAP_BPF/CAP_SYS_ADMIN না দেওয়া, আর নিয়মিত bpftool prog show দিয়ে অপরিচিত প্রোগ্রাম খোঁজা।

আরও গভীরে — verifier আসলে কী হিসাব রাখে? প্রতিটি register-এর জন্য verifier একটি “ধরন” (যেমন PTR_TO_MAP_VALUE_OR_NULL, PTR_TO_PACKET, SCALAR_VALUE), সম্ভাব্য সর্বনিম্ন-সর্বোচ্চ মান (signed আর unsigned দুই রকম), আর “tnum” নামের একটি কাঠামো রাখে — কোন bit নিশ্চিতভাবে জানা আর কোনগুলো অজানা। দুটি পথ একই অবস্থায় পৌঁছালে একটির পরীক্ষা বাদ দেয় (state pruning), তবু একটি প্রোগ্রামের জন্য মোট প্রায় ১০ লাখ নির্দেশ-পরীক্ষার একটি সীমা আছে, যা ছাড়ালে প্রত্যাখ্যান। সাধারণ ব্যবহারকারীর প্রোগ্রামে JIT ধ্রুবক সংখ্যাগুলোকে এলোমেলো মুখোশে ঢাকে (constant blinding), যাতে আক্রমণকারী machine code-এর ভেতরে নিজের পছন্দের বাইট বসাতে না পারে।

৯ · Kernel কীভাবে তৈরি হয় — mailing list, merge window, stable আর Rust

বিশ্বের সবচেয়ে বড় সহযোগী সফটওয়্যার-প্রকল্পগুলোর একটি এখনো মূলত ইমেইলে চলে; প্রতিটি release-এ সাধারণত হাজারো ডেভেলপারের দশ হাজারের বেশি পরিবর্তন। কাজের ধাপ:

  1. Patch লেখা আর পাঠানো: একটি পরিবর্তন = একটি patch, সাথে কেন বদলানো হলো তার বর্ণনা। scripts/checkpatch.pl রীতি যাচাই করে, scripts/get_maintainer.pl MAINTAINERS ফাইল দেখে বলে দেয় কোন maintainer আর mailing list-এ পাঠাতে হবে; তারপর git send-email (প্রায়ই LKML-এও)।
  2. প্রকাশ্য পর্যালোচনা: যে কেউ উত্তর দিতে পারেন; maintainer সন্তুষ্ট হলে নিজের git tree-তে নেন। প্রতিটি patch-এ Signed-off-by: — লেখক Developer Certificate of Origin (DCO, ২০০৪) মেনে ঘোষণা দেন যে কোডটি জমা দেওয়ার অধিকার তাঁর আছে। আরও চিহ্ন: Reviewed-by:, Tested-by:, আর কোন commit-এর বাগ সারানো হলো বোঝাতে Fixes:.
  3. একসাথে পরীক্ষা: শত শত subsystem tree প্রতিদিন linux-next-এ জোড়া হয়, আর স্বয়ংক্রিয় build ও test-ব্যবস্থা সংঘাত আগেই ধরে।
  4. Linus-এর tree: merge window-এ Linus Torvalds maintainer-দের “pull request” নিয়ে নতুন বৈশিষ্ট্য mainline-এ জোড়েন। তারপর শুধু বাগ-সংশোধন।

এই ছন্দে একটি release-এর জীবন নিচে — দিনের হিসাব সাধারণ নিয়মে (কোনো কোনো release-এ rc8 লাগে, বা ছুটির কারণে merge window একটু লম্বা হয়):

mainline-এর ঘড়ি
দিন
mainline-এ সর্বশেষ
কী ঢোকানো যায়
stable শাখা
  1. Release. Linus v6.N ট্যাগ দিলেন। একই মুহূর্তে পরের সংস্করণের merge window খুলে গেল — maintainer-রা কয়েক সপ্তাহ ধরে নিজেদের tree-তে যা জমিয়েছেন, এখন পাঠাবেন।
  2. Merge window — দুই সপ্তাহ। নতুন বৈশিষ্ট্য শুধু এই সময়ে ঢোকে। যা আগে linux-next-এ পরীক্ষিত হয়নি, তা সাধারণত ফেরত যায়। এর মধ্যেই v6.N-এর প্রথম stable সংশোধন (6.N.1) বেরোতে শুরু করে।
  3. rc1. চৌদ্দতম দিনে window বন্ধ, v6.(N+1)-rc1 (release candidate). এখন থেকে শুধু বাগ-সংশোধন আর regression (আগে চলত, এখন চলে না) দূর করা।
  4. প্রতি রবিবার একটি rc. rc2 থেকে rc7 — সাপ্তাহিক ছন্দ। পরিবর্তন সপ্তাহে সপ্তাহে ছোট হওয়ার কথা; বড় হলে Linus প্রকাশ্যে চিন্তা জানান, আর এক সপ্তাহ বাড়তে পারে।
  5. নতুন release. সাধারণত rc7-এর এক সপ্তাহ পর, ৬৩ দিনে (rc8 লাগলে ৭০ দিনে) v6.(N+1). তাই ছন্দ মোটামুটি ৯–১০ সপ্তাহ — বছরে পাঁচ-ছয়টি release.
  6. Stable আর LTS. Release-এর পর সংশোধন আসে stable শাখায় (Greg Kroah-Hartman-এর নেতৃত্বে): নিয়ম “upstream first” — সংশোধন আগে mainline-এ ঢুকতে হবে, তারপর পেছনের শাখায় কপি। সাধারণত প্রতি বছরের একটি release LTS (long-term support) ঘোষিত হয়, যা কয়েক বছর সংশোধন পায়; ঠিক কত বছর, তা সময়ে সময়ে বদলেছে, তাই kernel.org-এর বর্তমান তালিকা দেখুন।

সংস্করণ নম্বর আর uname -r

Linux 1.0 আসে ১৯৯৪ সালে। পুরোনো যুগে জোড়-বিজোড় নিয়ম ছিল (২.৪ স্থিতিশীল, ২.৫ উন্নয়ন), যা ২.৬-এ (২০০৩) উঠে যায়। তারপর ৩.০ (২০১১), ৪.০ (২০১৫), ৫.০ (২০১৯), ৬.০ (২০২২) — প্রথম সংখ্যার বদল কোনো বড় পরিবর্তনের চিহ্ন নয়; দ্বিতীয় সংখ্যাটি খুব বড় হয়ে গেলে Linus প্রথমটি বাড়ান। তাই 6.12-এর পরে 6.13 — দশমিক সংখ্যা নয়, দুটি আলাদা পূর্ণসংখ্যা। পড়ার নিয়ম:

  • 6.12 — mainline release; 6.12-rc3 — তার আগের তৃতীয় release candidate; 6.12.57 — সেই release-এর ৫৭তম stable সংশোধন।
  • Distro-র kernel নিজের নম্বর জোড়ে। রানারের 6.8.0-137-generic: upstream 6.8-এর ওপর Ubuntu-র kernel, 137 Ubuntu-র নিজের সংশোধন/ABI নম্বর, generic সাধারণ ধরন। ভেতরে অনেক সংশোধন যোগ হয়েছে, তাই নিরাপত্তার অবস্থা জানতে distro-র নিজের তালিকা দেখুন।

Rust for Linux

Kernel-এর নিরাপত্তা-ত্রুটির বড় অংশ মেমরি-ভুল থেকে: buffer-এর বাইরে লেখা, মুক্ত মেমরি ব্যবহার। Rust for Linux প্রকল্পের প্রাথমিক কাঠামো Linux ৬.১-এ (ডিসেম্বর ২০২২) mainline-এ ঢোকে — C ছাড়া দ্বিতীয় কোনো ভাষা প্রথমবার। লক্ষ্য পুরো kernel নতুন করে লেখা নয়; নতুন driver Rust-এ লেখা, যেখানে compiler-ই অনেক মেমরি-ভুল আটকায়। পরে ছোট কিছু Rust driver এসেছে (প্রথমগুলোর একটি ৬.৮-এ একটি network PHY driver বলে জানা যায়), আর বড় driver-এর কাজ চলছে। C আর Rust-এর সংযোগস্থলের দায়িত্ব নিয়ে প্রকাশ্য বিতর্কও হয়েছে (ভাবনার প্রশ্ন ৩)।

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

  • container_of কী করে? — একটি member-এর pointer থেকে offsetof(type, member) বিয়োগ করে পুরো struct-এর pointer বের করে; kernel-এর intrusive list আর tree এর ওপর দাঁড়িয়ে।
  • GFP_KERNEL আর GFP_ATOMIC-এর পার্থক্য? — প্রথমটি ঘুমাতে পারে, শুধু process context-এ; দ্বিতীয়টি ঘুমায় না, interrupt বা spinlock-এর ভেতরে, ব্যর্থ হওয়ার সম্ভাবনা বেশি।
  • Spinlock ধরে বা interrupt handler-এ কেন ঘুমানো যায় না? — পেছনে ঘুমাতে-পারা task নেই, বা lock-ধারী ঘুমালে অন্য CPU ঘুরতেই থাকবে — deadlock বা দীর্ঘ আটকে থাকা।
  • eBPF কেন kernel-এর জন্য নিরাপদ? — লোডের আগে verifier সব পথ যাচাই করে (সীমার ভেতরে মেমরি, NULL পরীক্ষা, শেষ হওয়া লুপ, শুধু অনুমোদিত helper), তারপর JIT; তবু লোড করতে বিশেষ ক্ষমতা লাগে।
  • Tainted kernel কী? — proprietary, out-of-tree বা স্বাক্ষরবিহীন module, oops বা warning-এর মতো ঘটনায় জ্বলা bit; /proc/sys/kernel/tainted-এ যোগফল, bug report-এ অক্ষর।

১০ · AI-এর যুগে kernel-এর কাজ — দ্রুত কোড, কঠোর যাচাই

Module, eBPF প্রোগ্রাম বা sysctl-এর খসড়া AI দ্রুত দেয়, কিন্তু এখানে ভুলের দাম বেশি — ভুল kernel কোড পুরো যন্ত্র ফেলে। আর kernel-এর ভেতরের API স্থিতিশীল নয় (ভাবনার প্রশ্ন ১), তাই পুরোনো লেখায় শেখা AI প্রায়ই এমন ফাংশন দেয় যা আর নেই।

AI-এর সাধারণ পরামর্শসমস্যাভালো পথ
পুরোনো kernel API: create_proc_entry, /proc-এর জন্য file_operations, পুরোনো access_okআপনার kernel-এ compile-ই হয় না, বা ভুলভাবে হয়নিজের kernel version বলুন; elixir.bootlin.com-এ সেই version-এর source দেখুন
Spinlock বা interrupt-এর ভেতরে GFP_KERNEL, mutex_lock, msleepসাধারণ পরীক্ষায় চলে, চাপের মুখে যন্ত্র আটকায়Context চিহ্নিত করুন; debug kernel-এ DEBUG_ATOMIC_SLEEP, lockdep চালু রেখে পরীক্ষা
ধনাত্মক error ফেরত, বা IS_ERR ছাড়া pointer ব্যবহারত্রুটি চাপা পড়ে, পরে oopsরীতি: ঋণাত্মক errno, ERR_PTR/IS_ERR; প্রতিটি পথে cleanup
“Module লোড হচ্ছে না? Secure Boot বন্ধ করুন”, insmod -f, kptr_restrict=0 বা perf_event_paranoid=-1 স্থায়ীভাবেসুরক্ষা উঠে যায়; সার্ভারে ঝুঁকিMOK-এ নিজের চাবি; শুধু পরীক্ষার VM-এ, শুধু দরকারি সময়ের জন্য সেটিং বদল
পুরোনো bpftrace/bcc-এর বাক্যগঠন, ভুল tracepoint-এর নাম“probe not found” বা ভুল তথ্যbpftrace -l 'tracepoint:syscalls:*' দিয়ে আগে hook-এর তালিকা দেখুন
Kernel-এর ইতিহাস বা version নিয়ে আত্মবিশ্বাসী কিন্তু ভুল তথ্যভুল সিদ্ধান্ত (যেমন “এই বৈশিষ্ট্য আপনার kernel-এ আছে”)Documentation/, kernelnewbies.org-এর release নোট, আর নিজের যন্ত্রে uname -r ও /boot/config-*

AI-কে kernel-এর কোড চাওয়ার আগে নিজের পরিবেশ ঠিকঠাক জানান, আর বিপজ্জনক “সমাধান” আগেই বাতিল করে দিন:

আমি Ubuntu 24.04-এর একটি পরীক্ষামূলক VM-এ কাজ করছি, kernel: uname -r = 6.8.0-45-generic, Secure Boot চালু। একটি out-of-tree kernel module লিখতে চাই, যা একটি sysfs ফাইলে একটি counter দেখাবে, আর একটি timer প্রতি সেকেন্ডে counter বাড়াবে। ১. শুধু এই kernel version-এ আছে এমন API ব্যবহার করো; প্রতিটি API কোন header-এ, আর কোনটি অনিশ্চিত, তা বলো। ২. প্রতিটি ফাংশন process context না atomic context-এ চলে, পাশে comment দাও; সেই অনুযায়ী GFP flag আর lock বেছে নাও। ৩. সব error-পথে ঋণাত্মক errno আর ঠিকঠাক cleanup (module_exit-এ timer বন্ধ) দেখাও। ৪. Kbuild Makefile আর module-এ স্বাক্ষর করার ধাপ দাও। Secure Boot বন্ধ করা বা insmod -f চলবে না। ৫. কীভাবে যাচাই করব: dmesg, /sys-এর ফাইল, আর rmmod-এর পর কিছু পড়ে থাকে কি না।

AI-এর kernel-পরামর্শ প্রয়োগের আগে — চেকলিস্ট

AI kernel-এর কোড লিখতে পারে, কিন্তু context, version আর ঝুঁকি
আপনাকেই চিনতে হয় — kernel-এ “আমার যন্ত্রে চলে” কোনো প্রমাণ নয়।

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১Linux user space-এর সাথে চুক্তি কখনো ভাঙে না, অথচ kernel-এর ভেতরের API ইচ্ছেমতো বদলায় — এই দ্বিমুখী নীতি কেন?

Linus Torvalds-এর সবচেয়ে বিখ্যাত নিয়মগুলোর একটি: “we do not break userspace”. বিশ বছর আগে compile করা একটি প্রোগ্রাম আজকের kernel-এ চলতে হবে; syscall-এর নম্বর, struct-এর বিন্যাস, /proc-এর একটি ফাইলের ফরম্যাট — একবার প্রকাশিত হলে তা চুক্তি। কোনো পরিবর্তন একটি পরিচিত প্রোগ্রামও ভাঙলে, পরিবর্তনটি যত যুক্তিসংগতই হোক, সাধারণত ফিরিয়ে নেওয়া হয় (regression). কারণ সরল: kernel-এর ব্যবহারকারী লাখো প্রোগ্রাম, যাদের বেশিরভাগের লেখককে kernel ডেভেলপাররা চেনেনই না।

অথচ kernel-এর ভেতরের ফাংশন আর struct নিয়ে নীতি ঠিক উল্টো। Greg Kroah-Hartman-এর একটি পুরোনো লেখা আজও kernel-এর Documentation/process/stable-api-nonsense.rst-এ আছে, যার মূল কথা: kernel-এর ভেতরে স্থিতিশীল driver API নেই, থাকবেও না। কেন? কারণ ভেতরের নকশা বদলানোর স্বাধীনতাই kernel-কে তিন দশক ধরে আধুনিক রেখেছে। একটি উদাহরণ: memory management-এ struct page-এর জায়গায় “folio” ধারণা (২০২১–২০২২ থেকে ধাপে ধাপে) আনতে হাজারো ফাংশন বদলাতে হয়েছে। কোনো API স্থির রাখার প্রতিশ্রুতি থাকলে এই কাজ অসম্ভব হতো, বা পুরোনো আর নতুন দুই পথ চিরকাল পাশাপাশি রাখতে হতো।

এই নীতির দাম দেন kernel-এর বাইরে থাকা (out-of-tree) driver-এর মালিকেরা: প্রতিটি নতুন kernel-এ তাঁদের কোড ভাঙতে পারে, আর DKMS-এর মতো ব্যবস্থা দিয়ে প্রতিবার নতুন করে build করতে হয়। Kernel সম্প্রদায়ের উত্তর হলো: driver mainline-এ জমা দিন — তখন যিনি API বদলাবেন, তিনিই আপনার driver-ও একসাথে ঠিক করবেন। অর্থাৎ নীতিটি একটি প্রণোদনা: source খোলা রাখুন, ভেতরে আসুন, বিনিময়ে রক্ষণাবেক্ষণের বোঝা ভাগ হবে। এজন্যই অংশ ১-এ দেখা drivers/ এত বড়।

মূল উপলব্ধি: একটি সিস্টেম কোথায় স্থিতিশীলতা দেবে আর কোথায় বদলের স্বাধীনতা রাখবে, তা সচেতনভাবে বেছে নিতে হয়। Linux বাইরের চুক্তি (ABI) পাথরে খোদাই করেছে, আর ভেতরের নকশা নরম রেখেছে।

প্র ০২Monolithic kernel-এ যাচাই-করা ছোট প্রোগ্রাম চালানো — eBPF কি পুরোনো microkernel বিতর্কের একটি তৃতীয় উত্তর?

পর্ব ৬-এ দেখেছেন, ১৯৯২ সালের Tanenbaum–Torvalds বিতর্কের মূল প্রশ্ন ছিল নিরাপত্তা বনাম গতি: microkernel-এ driver আর সেবা আলাদা address space-এ, তাই একটির বাগ বাকিদের ফেলে না, কিন্তু প্রতিটি কাজে বার্তা আদান-প্রদানের খরচ; monolithic kernel-এ সব এক জায়গায়, দ্রুত, কিন্তু একটি ভুল সবার। Loadable module নমনীয়তা দিল, নিরাপত্তা দিল না — একটি .ko kernel-এর পূর্ণ ক্ষমতা পায়।

“বাইরের কোড kernel-এ নিরাপদে চালানো” — এই ধারণাও নতুন নয়। ১৯৯০-এর দশকে গবেষকেরা এ নিয়ে কাজ করেছেন: software fault isolation (১৯৯৩) কোড পুনর্লিখন করে মেমরি-সীমা চাপিয়ে দেয়; University of Washington-এর SPIN OS (১৯৯৫) type-safe ভাষা Modula-3-এ লেখা extension kernel-এ লোড করত; MIT-এর exokernel প্রকল্প প্যাকেট-ছাঁকনিসহ অনেক কিছু অ্যাপের হাতে দিত। এগুলো গবেষণায় সফল হলেও সাধারণ ব্যবহারে আসেনি।

eBPF একই পুরোনো স্বপ্নের একটি ব্যবহারিক রূপ, তবে ভিন্ন কৌশলে: ভাষা বা address space-এর বদলে লোডের আগে একটি প্রমাণকারী (verifier). প্রোগ্রামটি kernel-এর ভেতরে, পূর্ণ গতিতে, JIT-এর পর আসল machine code হিসেবে চলে — কিন্তু শুধু অনুমোদিত helper ডাকতে পারে, শুধু প্রমাণিত সীমার মেমরি ছোঁয়, আর শেষ হতে বাধ্য। ফল হিসেবে নতুন tracing টুল, firewall-এর নিয়ম, এমনকি scheduler-এর নীতি (sched_ext) এখন রিবুট বা নতুন kernel ছাড়াই বদলানো যায়। অনেকে তুলনা করেন, ব্রাউজারে JavaScript যা করেছিল — স্থির পাতাকে প্রোগ্রাম চালানোর জায়গা বানানো — kernel-এর জন্য eBPF সেই কাজ করছে।

তবে এটি নিখুঁত উত্তর নয়। Verifier নিজেই একটি জটিল, ভুল-প্রবণ কোড; তার বাগ সরাসরি নিরাপত্তা-ঝুঁকি, তাই সাধারণ ব্যবহারকারীর জন্য eBPF প্রায় সব জায়গায় বন্ধ। আর verifier যা প্রমাণ করতে পারে না, এমন সঠিক প্রোগ্রামও প্রত্যাখ্যাত হয় — ডেভেলপারদের প্রায়ই verifier-কে “খুশি করতে” কোড ঘুরিয়ে লিখতে হয়।

মূল উপলব্ধি: নিরাপত্তা শুধু আলাদা address space থেকে আসে না; চালানোর আগে প্রমাণ থেকেও আসতে পারে। eBPF monolithic kernel-এর গতি রেখে তার ভেতরে একটি যাচাই-করা, সীমিত প্রোগ্রামযোগ্যতা যোগ করেছে।

প্র ০৩Kernel কেন C++-এর তৈরি container বা সাধারণ linked list না নিয়ে intrusive list_head আর container_of-এর মতো “কৌশল” বেছে নিল — আর Rust-এ এটি কেন কঠিন?

১৯৯০-এর দশকের শুরুতে, প্রচলিত বর্ণনা অনুযায়ী, Linux kernel অল্প সময়ের জন্য C++ compiler দিয়ে build করার চেষ্টা হয়েছিল, আর দ্রুতই ফিরিয়ে আনা হয়। পরে Linus বহুবার লিখেছেন, kernel-এ C++-এর exception, লুকানো মেমরি-বরাদ্দ আর জটিল abstraction তিনি চান না। কিন্তু শুধু রুচির প্রশ্ন নয়; intrusive list-এর পেছনে প্রকৌশলের শক্ত কারণ আছে।

প্রথম কারণ মেমরি-বরাদ্দ। সাধারণ (non-intrusive) list-এ যোগ করা মানে নতুন একটি node-এর জন্য মেমরি চাওয়া — আর kernel-এ সেই চাওয়া interrupt-এর ভেতরে ব্যর্থ হতে পারে বা ঘুমাতে চাইতে পারে। Intrusive list-এ node বস্তুর ভেতরেই, তাই যোগ করা কখনো ব্যর্থ হয় না। দ্বিতীয়, একটি বস্তু একসাথে অনেক তালিকায়: একটি task_struct-এ কয়েকটি list_head, প্রতিটি আলাদা সম্পর্কের জন্য, আর প্রতিটি তালিকা থেকে container_of দিয়ে একই বস্তুতে ফেরা যায়। তৃতীয়, একটি pointer-লাফ কম — node থেকে ডেটা আলাদা জায়গায় থাকলে cache-miss বেশি। চতুর্থ, C-তে template নেই, অথচ একই list_add সব ধরনের বস্তুতে কাজ করে।

Rust-এ এটি কঠিন, কারণ Rust-এর নিরাপত্তা দাঁড়িয়ে আছে মালিকানা আর “একজন লেখক অথবা অনেক পাঠক” নিয়মের ওপর, আর ধরে নেয় যে একটি মান মেমরিতে সরানো (move) যায়। Intrusive list-এ বস্তুর ঠিকানা অন্যরা মনে রাখে — বস্তুটি সরালে সবার pointer ভুল হয়ে যাবে, আর একই বস্তুকে অনেক তালিকা একসাথে “ধরে” থাকে। Rust for Linux প্রকল্পকে তাই ঠিকানা-স্থির (pinned) বস্তু নিরাপদে বানানোর জন্য আলাদা কৌশল আর library তৈরি করতে হয়েছে, আর কিছু জায়গায় unsafe কোডকে নিরাপদ মোড়কে ঢাকতে হয়।

মূল উপলব্ধি: যে কৌশলটি প্রথমে “হ্যাক” মনে হয়, তা প্রায়ই পরিবেশের কঠিন শর্তের (বরাদ্দ ব্যর্থ হতে পারে, বহু তালিকায় সদস্যপদ, গতি) সবচেয়ে সরল উত্তর — আর নতুন ভাষা আনলে সেই শর্তগুলোর দাম নতুনভাবে দিতে হয়।

প্র ০৪EXPORT_SYMBOL_GPL, taint আর proprietary driver — kernel-এর লাইসেন্স কীভাবে বাস্তব প্রকৌশল-সিদ্ধান্তে ঢুকে পড়ে?

Linux kernel GPL সংস্করণ ২-এর অধীনে। প্রশ্ন হলো, একটি আলাদা ফাইলে লেখা, বাইরে বানানো module কি kernel-এর “derivative work”, যার source-ও GPL-এ দিতে হবে? Kernel সম্প্রদায় এর কোনো সর্বজনীন আইনি উত্তর দেয়নি, কিন্তু একটি ব্যবহারিক রেখা টেনেছে: সাধারণ EXPORT_SYMBOL করা ফাংশন যেকোনো module ব্যবহার করতে পারে; EXPORT_SYMBOL_GPL করা ফাংশন শুধু GPL-সামঞ্জস্যপূর্ণ MODULE_LICENSE ঘোষণা করা module. যুক্তি: যে ফাংশন kernel-এর এত ভেতরের যে তাকে ব্যবহার করা মানেই kernel-এর অংশ হয়ে যাওয়া, সেটি GPL-only.

দুটি বাস্তব ঘটনা এই রেখার প্রভাব দেখায়। প্রথমটি NVIDIA-র GPU driver: বহু বছর এটি একটি proprietary module ছিল, লোড হলে kernel-এ P taint জ্বলত, আর bug report-এ kernel ডেভেলপাররা সাধারণত সাহায্য করতে অস্বীকার করতেন। ২০২২ সালে NVIDIA তাদের kernel module-এর একটি open-source সংস্করণ প্রকাশ করে (GPL আর MIT দুই লাইসেন্সে), আর পরে নতুন GPU-র জন্য সেটিকেই প্রধান করে বলে জানা যায় — যদিও user-space অংশ বন্ধই থাকে।

দ্বিতীয়টি অংশ ৩-এর floating point নিয়মের সাথে যুক্ত। ২০১৯ সালে Linux ৫.০-এ kernel-এর ভেতরে FPU/SIMD ব্যবহারের একটি নিম্নস্তরের ফাংশনের export সরিয়ে দেওয়া হয়, আর প্রচলিত পথটি শুধু GPL-only হিসেবে থাকে। এর আঘাত পড়ে ZFS on Linux-এ — CDDL লাইসেন্সের এই file system checksum-এর হিসাবে SIMD ব্যবহার করত; নতুন kernel-এ সেই দ্রুত পথ বন্ধ হয়ে যায়, পরে তাদের নিজস্ব বিকল্প পথ বানাতে হয়। একজন প্রধান maintainer তখন প্রকাশ্যেই জানান, GPL-বহির্ভূত বাইরের কোডের সুবিধার জন্য kernel নিজের ভেতরের API রক্ষা করবে না — প্রশ্ন ০১-এর নীতিরই প্রয়োগ।

মূল উপলব্ধি: Open-source প্রকল্পে লাইসেন্স শুধু আইনি কাগজ নয়, স্থাপত্যের অংশ। কোন symbol কার জন্য খোলা, taint কখন জ্বলে — এগুলো প্রকৌশলের সিদ্ধান্ত, যা বাইরের কোম্পানির ব্যবসা-কৌশলও বদলে দিতে পারে।

প্র ০৫১৯৯১-এর একটি newsgroup-বার্তা থেকে হাজারো ডেভেলপারের প্রকল্প — ইমেইল-নির্ভর এই প্রক্রিয়া আস্থা আর গুণমান কীভাবে রক্ষা করে, আর কোথায় এটি ভঙ্গুর?

২৫ আগস্ট ১৯৯১-এ হেলসিঙ্কির ছাত্র Linus Torvalds comp.os.minix newsgroup-এ লেখেন, তিনি একটি “শখের” free OS বানাচ্ছেন, “বড় আর পেশাদার কিছু হবে না”। শুরু থেকেই পরিবর্তন আসত ইমেইলে patch হয়ে, আর Linus নিজে হাতে বসাতেন। ২০০২ সালে প্রকল্পটি BitKeeper নামের একটি বাণিজ্যিক version-control ব্যবস্থায় যায়; ২০০৫ সালের এপ্রিলে সেই বিনামূল্যের লাইসেন্স প্রত্যাহার হলে Linus কয়েক সপ্তাহের মধ্যে নিজেই একটি নতুন টুল লেখেন — Git. অর্থাৎ আজকের সফটওয়্যার-জগতের সবচেয়ে ব্যবহৃত টুলটির জন্মই kernel-এর প্রক্রিয়ার প্রয়োজনে।

আস্থার ব্যবস্থা স্তরে স্তরে: প্রতিটি patch প্রকাশ্য mailing list-এ, যে কেউ পড়ে আপত্তি জানাতে পারেন; প্রতিটি subsystem-এর maintainer নিজের এলাকার দায় নেন; Linus মূলত maintainer-দের বিশ্বাস করেন, প্রতিটি লাইন নয় — একটি “বিশ্বাসের জাল”। ২০০৩ সালে SCO নামের একটি কোম্পানি দাবি করে Linux-এ তাদের Unix কোড ঢুকেছে; উত্তরে ২০০৪ সালে DCO আর Signed-off-by: আসে, যাতে প্রতিটি লাইনের উৎস আর জমাদাতার দায় লেখা থাকে।

এই ব্যবস্থার দুর্বলতাও দেখা গেছে। ২০২১ সালে University of Minnesota-র একটি গবেষক দল জেনেশুনে ত্রুটিপূর্ণ patch পাঠিয়ে পর্যালোচনার ফাঁক পরীক্ষা করছিল বলে প্রকাশ পায়; Greg Kroah-Hartman ওই বিশ্ববিদ্যালয়ের ঠিকানা থেকে আসা patch সাময়িকভাবে নিষিদ্ধ করেন আর আগের অনেক patch আবার পর্যালোচনার জন্য তালিকাভুক্ত করেন। আরেকটি পুরোনো সমস্যা হলো maintainer-দের ক্লান্তি: অল্প কয়েকজনের ঘাড়ে বিশাল দায়িত্ব, অনেকে স্বেচ্ছাসেবী বা কোম্পানির সীমিত সময় নিয়ে। স্বয়ংক্রিয় পরীক্ষা (linux-next, KernelCI, fuzzing-এর syzbot) এই চাপ কিছুটা কমায়।

মূল উপলব্ধি: বড় open-source প্রকল্পে আস্থা আসে স্বচ্ছতা, দায়িত্বের স্পষ্ট শৃঙ্খল আর স্বয়ংক্রিয় পরীক্ষা থেকে — কিন্তু শেষ পর্যন্ত তা মানুষের ওপর নির্ভর করে, আর মানুষের সময় ও মনোযোগই সবচেয়ে দুর্লভ সম্পদ।

অনুশীলন

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

  1. হিসাব — taint-এর bit: Taint-এর মান: P = ১, W = ৫১২, D = ১২৮, O = ৪০৯৬, E = ৮১৯২. (ক) একটি সার্ভারে /proc/sys/kernel/tainted = 4097. কোন কোন bit জ্বলেছে? (খ) একটি যন্ত্রে একটি proprietary, বাইরে বানানো, স্বাক্ষরবিহীন module লোড হলো, তারপর kernel একটি WARN_ON দিল। মান কত? (গ) এরপর module-টি rmmod করা হলো — মান কি কমবে? (ঘ) তারপর একটি oops ঘটলে নতুন মান কত?

    (ক) ৪০৯৭ = ৪০৯৬ + ১ ⇒ O আর P: একটি বাইরে বানানো proprietary module (স্বাক্ষর ঠিক ছিল, তাই E নেই)। (খ) P + O + E + W = ১ + ৪০৯৬ + ৮১৯২ + ৫১২ = ১২৮০১. (গ) না — taint রিবুট পর্যন্ত থাকে; module চলে গেলেও তার রেখে যাওয়া প্রভাব (বদলানো মেমরি, অবস্থা) থাকতে পারে, তাই bug report-এ এটি জানা জরুরি। (ঘ) ১২৮০১ + ১২৮ = ১২৯২৯. যাচাই: ১২৯২৯ − ৮১৯২ = ৪৭৩৭; − ৪০৯৬ = ৬৪১; − ৫১২ = ১২৯; − ১২৮ = ১; − ১ = ০ ✓.

  2. হাতে trace — container_of: x86-64-এ struct pkt { u32 len; u16 proto; u16 flags; u64 ts; struct list_head node; char data[64]; struct list_head lru; }; (u32 ৪ বাইট, u16 ২, u64 ৮, pointer ৮)। (ক) node আর lru-এর offset আর sizeof(struct pkt) কত? (খ) একটি তালিকায় হাঁটার সময় হাতে এল 0xffff888004a3c250 (একটি node-এর ঠিকানা)। বস্তুটি কোথায় শুরু? (গ) আরেকটি তালিকায় হাতে এল 0xffff888004a3c2a0 (একটি lru). এটি কি একই বস্তু?

    (ক) len ০–৩, proto ৪–৫, flags ৬–৭, ts ৮–১৫ (৮-এর গুণিতকে, কোনো ফাঁক লাগেনি), node ১৬ (0x10) থেকে ৩১, data ৩২–৯৫, lru ৯৬ (0x60) থেকে ১১১ ⇒ sizeof = ১১২ (৮-এর গুণিতক, শেষে ফাঁক নেই)। (খ) 0xffff888004a3c250 − 0x10 = 0xffff888004a3c240. (গ) 0xffff888004a3c2a0 − 0x60 = 0xffff888004a3c240 ⇒ হ্যাঁ, একই বস্তু, যা একসাথে দুটি তালিকায় আছে — intrusive list-এর সুবিধাটি ঠিক এই। (শুরুর 0xffff888… x86-64 Linux-এ kernel-এর direct-map এলাকার পরিচিত চেহারা।)

  3. আউটপুট অনুমান করুন — মাথায় ঢোকানো: অংশ ৪-এর প্রোগ্রামে list_add_tail-এর বদলে kernel-এর list_add(n, head) ব্যবহার করা হলো, যা নতুন node-কে head-এর ঠিক পরে বসায় (stack-এর মতো)। t[0], t[1], t[2] এই ক্রমে যোগ, তারপর list_del(&t[1].run). লুপ কী ছাপবে, আর rq.prev কোন task দেখাবে? আরেকটি প্রশ্ন: কেউ ভুলে list_del(&t[1].run) দুবার ডাকলে kernel-এ কী হওয়ার কথা?

    প্রতিবার head-এর পরে বসানোয় ক্রম দাঁড়ায় t[2], t[1], t[0] (backup, nginx, bash). nginx বাদ দিলে লুপ ছাপবে প্রথমে pid=103 comm=backup, তারপর pid=101 comm=bash; প্রতিটির পাশে আগের মতোই “24 bytes into the task”. rq.prev এখন শেষ node — bash (&t[0]). দুবার list_del: আমাদের user-space নকলে দ্বিতীয়বার পুরোনো প্রতিবেশীদের pointer আবার লেখা হবে — তালিকা চুপচাপ নষ্ট হতে পারে। আসল kernel-এর list_del মোছার পর node-এর next/prev-এ ইচ্ছাকৃত অবৈধ “poison” ঠিকানা (LIST_POISON1/2) বসায়, তাই দ্বিতীয়বার ছুঁলেই একটি স্পষ্ট oops হয়, আর debug kernel-এ (CONFIG_DEBUG_LIST) আরও আগেই সতর্কবার্তা — নীরব নষ্ট হওয়ার চেয়ে জোরে ব্যর্থ হওয়া ভালো।

  4. কমান্ড লিখুন — eBPF-এর জন্য যন্ত্র প্রস্তুত কি না: একটি নতুন Linux সার্ভারে (আপনার root অধিকার আছে) এমন কমান্ড লিখুন যা দেখাবে: (ক) kernel-এ BTF আছে কি না আর eBPF ও JIT-এর config; (খ) JIT চালু কি না আর সাধারণ ব্যবহারকারীর eBPF বন্ধ কি না; (গ) কোন প্রসেস-নাম সবচেয়ে বেশি নতুন প্রোগ্রাম চালাচ্ছে (execve), bpftrace-এ; (ঘ) এখন কোন কোন BPF প্রোগ্রাম আর map লোড করা আছে।
    # (a) BTF and config
    ls -l /sys/kernel/btf/vmlinux
    grep -E 'CONFIG_BPF_SYSCALL=|CONFIG_BPF_JIT=|CONFIG_DEBUG_INFO_BTF=' /boot/config-$(uname -r)
    # (b) JIT and unprivileged BPF
    cat /proc/sys/net/core/bpf_jit_enable /proc/sys/kernel/unprivileged_bpf_disabled
    # (c) who calls execve most (Ctrl+C to print)
    sudo bpftrace -e 'tracepoint:syscalls:sys_enter_execve { @[comm] = count(); }'
    # (d) loaded programs and maps
    sudo bpftool prog show
    sudo bpftool map show

    প্রত্যাশা: (ক) ফাইলটি আছে আর তিনটিই =y. (খ) bpf_jit_enable = 1; unprivileged_bpf_disabled = 1 বা 2 (দুটিতেই সাধারণ ব্যবহারকারী বন্ধ; ২ হলে admin আবার খুলতে পারেন, ১ হলে রিবুট ছাড়া নয়)। (গ) Ctrl+C-তে ছোট থেকে বড় ক্রমে @[bash]: … ধরনের তালিকা; নাম নিশ্চিত না হলে আগে sudo bpftrace -l 'tracepoint:syscalls:sys_enter_exec*'. (ঘ) systemd নিজেও কিছু প্রোগ্রাম লোড করে, তাই তালিকা খালি না-ও হতে পারে; অপরিচিত নাম দেখলে uid আর loaded_at দেখুন।

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

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

ক্রমএকটি eBPF tracing প্রোগ্রাম লেখা থেকে ফল দেখা পর্যন্ত — সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ✍️ সীমিত C-তে প্রোগ্রাম লিখে clang -target bpf দিয়ে bytecode বানানো
  2. 📥 Loader map তৈরি করে, BTF দেখে offset ঠিক করে, bpf(BPF_PROG_LOAD) ডাকে
  3. 🔍 Verifier প্রতিটি পথ যাচাই করে — মেমরির সীমা, NULL পরীক্ষা, লুপ শেষ হয় কি না
  4. ⚙️ JIT bytecode-কে CPU-র machine code-এ অনুবাদ করে
  5. 🪝 প্রোগ্রামটি একটি tracepoint-এ জোড়া হয় আর প্রতিটি ঘটনায় map-এ লেখে
  6. 📊 User-space প্রোগ্রাম map পড়ে ফল ছাপে

Linux kernel-এর source tree-তে কোন ফোল্ডারে সবচেয়ে বেশি কোড থাকে?

  • kernel/
  • mm/
  • drivers/
  • arch/

struct task { int pid; char comm[12]; int nice; struct list_head run; }; (x86-64). একটি তালিকায় হাঁটার সময় p = 0x5000, যা একটি task-এর run member-এর ঠিকানা। container_of(p, struct task, run) কী ফেরত দেবে?

  • 0x4FE8
  • 0x4FEC
  • 0x5000
  • 0x5018

একটি network driver-এর interrupt handler spinlock ধরে থাকা অবস্থায় একটি ছোট struct-এর জন্য মেমরি চায়। কোনটি সঠিক?

  • kmalloc(size, GFP_KERNEL), কারণ এটিই স্বাভাবিক পছন্দ
  • kmalloc(size, GFP_ATOMIC), আর ফল NULL কি না পরীক্ষা
  • malloc(size)
  • আগে mutex_lock() নিয়ে তারপর GFP_KERNEL

একটি kernel ফাংশন pointer ফেরত দেয়, আর মেমরি না পেলে ত্রুটি জানাতে চায়। Linux kernel-এর রীতি কোনটি?

  • global errno = ENOMEM বসিয়ে NULL ফেরত
  • একটি exception ছুড়ে দেওয়া
  • (void *)ENOMEM ফেরত
  • return ERR_PTR(-ENOMEM); — ডাকা ফাংশন IS_ERR() দিয়ে পরীক্ষা করে

eBPF verifier একটি প্রোগ্রাম লোডের সময়ই প্রত্যাখ্যান করল। নিচের কোনটি সবচেয়ে সম্ভাব্য কারণ?

  • প্রোগ্রামটি একটি hash map-এ গণনা লেখে
  • প্রোগ্রামটি C-তে লিখে clang দিয়ে compile করা
  • Map lookup-এর ফল NULL কি না পরীক্ষা না করেই সেই pointer থেকে পড়া
  • প্রোগ্রামটি একটি tracepoint-এ বসানোর জন্য লেখা

একটি সার্ভারে /proc/sys/kernel/tainted-এর মান 12288 (O = ৪০৯৬, E = ৮১৯২)। এর অর্থ কী?

  • একটি বাইরে বানানো, স্বাক্ষরবিহীন module লোড হয়েছিল
  • Kernel একটি oops-এর পর চলছে
  • একটি proprietary লাইসেন্সের module লোড হয়েছে
  • Kernel সম্পূর্ণ স্বাভাবিক, কোনো দাগ নেই

শব্দকোষ

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

মনে রাখুন

  1. Source tree = মানচিত্র: arch/, kernel/, mm/, fs/, block/, net/, drivers/, security/, init/; সবচেয়ে বড় অংশ driver. start_kernel() → rest_init() → PID 1 (kernel_init) আর PID 2 (kthreadd), PID 0 idle.
  2. Kernel-এর নিয়ম: libc নেই, ছোট স্থির stack (x86-64-এ সাধারণত ১৬ KiB), সাধারণত FPU নেই; atomic context-এ ঘুম নিষেধ (GFP_ATOMIC, spinlock), process context-এ GFP_KERNEL, mutex; ত্রুটি = ঋণাত্মক errno বা ERR_PTR; log = printk/dmesg.
  3. list_head বস্তুর ভেতরে, container_of(ptr, type, member) = ptr − offsetof; সাজানো খোঁজায় red-black tree, index থেকে pointer-এ xarray, পরিসরে maple tree.
  4. Module: obj-m + make -C …/build M=…; module_param → /sys/module/*/parameters; EXPORT_SYMBOL(_GPL); taint-এর bit (P ১, O ৪০৯৬, E ৮১৯২…)। চলমান kernel দেখা: /proc, /sys, sysctl; build: Kconfig → .config → make.
  5. eBPF: bytecode → verifier (সব পথ, সীমা, NULL, শেষ হওয়া) → JIT → hook (kprobe, tracepoint, XDP, tc, LSM, sched_ext) → map. BTF/CO-RE দিয়ে বহনযোগ্য; লোড করতে বিশেষ ক্ষমতা লাগে। উন্নয়ন: email-এ patch, Signed-off-by, দুই সপ্তাহের merge window, সাপ্তাহিক rc, ৯–১০ সপ্তাহে release, stable/LTS, ৬.১ থেকে Rust.

পরের পর্বে: একই প্রশ্ন, আরেকটি বড় OS — Windows NT kernel-এর ভেতর: executive আর kernel, object manager, process আর thread, virtual memory আর registry — পর্ব ৫৬-এ।

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

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