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

ক্যাপস্টোন — নিজের shell, scheduler আর memory allocator

OS Capstone — Build a Shell with Pipes and Signals, an Event-Driven Scheduler Simulator, and malloc/free with Boundary Tags and a Buddy Allocator

Taught in Bangla, this capstone builds three OS projects in runnable C: a shell with parsing, builtins, redirection, pipes, signals and background jobs; an event-driven CPU scheduler simulator checked against earlier worked answers; and a malloc/free allocator with boundary tags, alignment, invariants, random traces, fragmentation and a buddy allocator. It also maps each project to real kernels and shows how to present the work in a portfolio and interviews.

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

  • পর্ব ১১-এর ৩৫ লাইনের mysh-কে builtin, pipe, redirection, background job, SIGCHLD আর exit status-সহ পূর্ণ shell-এ বাড়ানো — আর তার পরীক্ষার পরিকল্পনা
  • FCFS, SJF, SRTF, priority, RR আর MLFQ এক কাঠামোয় চালানো event-driven simulator — আর পাঠ্যবইয়ের উত্তর দিয়ে তার যাচাই
  • নির্দিষ্ট arena-র ওপর নিজের malloc/free: explicit free list, first fit, split, boundary tag দিয়ে coalescing, ১৬-byte alignment — আর buddy রূপান্তর
  • Invariant, এলোমেলো trace আর fragmentation মাপা — “চলছে” থেকে “ঠিক আছে” প্রমাণের পথ
  • তিনটি প্রজেক্ট বাস্তব kernel-এ কোথায় আছে, code review-এ কী প্রশ্ন আসে, আর portfolio-তে কীভাবে উপস্থাপন করবেন

১ · ক্যাপস্টোনের মানচিত্র — তিন প্রজেক্ট, একটি ছোট OS

৫৮টি পর্বে আপনি অপারেটিং সিস্টেমকে টুকরো টুকরো করে দেখেছেন। এই পর্বে উল্টো কাজ — টুকরোগুলো জোড়া লাগিয়ে নিজের হাতে তিনটি ছোট কিন্তু সত্যিকারের সিস্টেম-প্রোগ্রাম লেখা। প্রতিটি kernel-এর একটি মূল দায়িত্বের প্রতিনিধি:

প্রজেক্ট A · Shell

process তৈরি, প্রোগ্রাম বসানো, fd জোড়া লাগানো, signal সামলানো। ভিত্তি: পর্ব ১০, পর্ব ১১, ১২, ১৩.

fork · exec · pipe · dup2 · waitpid

প্রজেক্ট B · Scheduler simulator

CPU কে, কখন, কতক্ষণ পাবে — নীতি বদলালে ফল কীভাবে বদলায়। ভিত্তি: পর্ব ১৭, ১৮, ১৯.

event loop · pick_next() · Gantt

প্রজেক্ট C · Memory allocator

এক টানা মেমরিকে ব্লকে ভাগ, ফেরত নেওয়া, জোড়া লাগানো। ভিত্তি: পর্ব ৩১, পর্ব ৩৮.

header/footer · split · coalesce · buddy

একটি ব্যস্ত রেস্তোরাঁর কথা ভাবুন। ওয়েটার অর্ডার নিয়ে বোঝেন কী চাই, তারপর রান্নাঘরের নির্দিষ্ট লোকের হাতে কাজ তুলে দেন, আর রান্না শেষ হলে খবর নেন — এটি shell. রান্নাঘরে চুলা একটি, অর্ডার অনেক; কোনটি আগে চুলায় উঠবে, কতক্ষণ থাকবে — এটি scheduler. আর ভাঁড়ারের তাক সীমিত; কোন জিনিস কোন তাকে কতটা জায়গা পাবে, খালি হলে পাশের খালি জায়গার সাথে মিলিয়ে বড় তাক বানানো — এটি allocator. তিনজনের কাজ আলাদা, কিন্তু রেস্তোরাঁ চলে তিনজন মিলেই।

আগের পর্বের মৌলিক কথা এখানে আবার বলা হবে না — লিংক দেওয়া আছে। মনোযোগ তিনটি জিনিসে: নকশার সিদ্ধান্ত, ঠিকতার প্রমাণ (পরীক্ষা আর invariant), আর বাস্তবের সাথে তুলনা (bash, Linux scheduler, glibc malloc). প্রতিটি প্রজেক্টের মূল অংশ পাতাতেই চালানো যায়; পূর্ণ সংস্করণ আলাদা তালিকায় — সেগুলোও Linux-এ gcc দিয়ে চালিয়ে পরীক্ষা করা, আসল আউটপুট পাশেই।

কাজের পদ্ধতি — যেভাবে পেশাদাররা এগোন

capstone/
├── shell/      mysh2.c      tests/shell_cases.txt   tests/expected.txt
├── sched/      schedsim.c   tests/ch17.txt ch18a.txt ch18b.txt ch19.txt  golden/
├── alloc/      myalloc.c    buddy.c   bench.c
├── Makefile    (make test = সব পরীক্ষা, make bench = মাপজোখ)
└── README.md   (কী, কেন, কীভাবে চালাবেন, ফলাফল, সীমাবদ্ধতা)
  1. আগে পরীক্ষা লিখুন: কোড লেখার আগে “ঠিক হলে কী দেখব” লিখে ফেলুন — shell-এর প্রতিটি কমান্ডের প্রত্যাশিত আউটপুট, scheduler-এর পাঠ্যবইয়ের উত্তর, allocator-এর invariant.
  2. ছোট ধাপে বাড়ান: প্রতিটি ফিচার (একটি pipe, একটি redirection, একটি নীতি) যোগ করে সব পুরোনো পরীক্ষা আবার চালান — একে বলে regression test.
  3. সতর্কবার্তা চালু রাখুন: gcc -Wall -Wextra -g, আর মাঝে মাঝে -fsanitize=address,undefined; থ্রেড যোগ করলে ThreadSanitizer.
  4. প্রতিটি সিদ্ধান্ত লিখে রাখুন: tie-নিয়ম, ন্যূনতম ব্লক, কোন signal কে উপেক্ষা করে — README-তে এক লাইনে। পরে ইন্টারভিউতে ঠিক এগুলোই জিজ্ঞেস করা হয়।

২ · প্রজেক্ট A — mysh থেকে পূর্ণ shell: parse, builtin, redirection আর pipe

পর্ব ১১-এর mysh একটি লাইন পড়ত, শব্দে ভাঙত, cd আর exit নিজে করত, বাকিটা fork + exec + waitpid. এবার তাতে যা যোগ হবে, আর প্রতিটির পেছনের system call ও ফাঁদ:

ফিচারযেভাবে হয়সবচেয়ে প্রচলিত ভুল
Builtin: cd, exit, exportshell-এর নিজের process-এ chdir, exit, setenvpipeline-এর ভেতরের cd-কেও shell-এ চালানো (bash-এ সেটি subshell-এ চলে, প্রভাব থাকে না)
Redirection < >child-এ open, তারপর dup2 দিয়ে fd ০ বা ১-এ বসানো, মূলটি closeparent-এ open করে ফেলা — shell-এর নিজের stdout বদলে যায়
একটি pipe |pipe(fd), দুটি fork, লেখক child-এ dup2(fd[1], 1), পাঠক child-এ dup2(fd[0], 0)কোনো process-এ লেখার মুখ খোলা রেখে দেওয়া — fd leak, পাঠক EOF পায় না
Background &fork করে অপেক্ষা না করা; SIGCHLD-এ reapreap না করা — zombie জমা; বা handler-এ foreground-এর status কেড়ে নেওয়া
Exit status দেখানোWIFEXITED/WEXITSTATUS, WIFSIGNALED/WTERMSIGpipeline-এর status হিসেবে প্রথম কমান্ডেরটি দেখানো (POSIX-এ শেষ কমান্ডেরটি)

Parsing-এর নকশা

পূর্ণ shell-এর ব্যাকরণ বিশাল — উদ্ধৃতিচিহ্ন, variable, wildcard, &&, ;, subshell. ক্যাপস্টোনের জন্য একটি ছোট, স্পষ্ট ব্যাকরণ বেছে নিন আর README-তে লিখে দিন:

line := cmd [ "|" cmd ] [ "&" ]  ·  cmd := word { word } [ "<" file ] [ ">" file ]
শব্দ আলাদা হয় space বা tab দিয়ে · উদ্ধৃতিচিহ্ন নেই · & থাকলে শুধু শেষে

ভুল ইনপুটে shell মরবে না — একটি স্পষ্ট বার্তা দিয়ে পরের লাইনে যাবে: ls | (pipe-এর পরে কমান্ড নেই), echo a > (ফাইলের নাম নেই), | wc (শুরুতেই pipe). নকশার আরেকটি নীতি: parse করা শেষ হওয়ার আগে কোনো fork নয় — অর্ধেক বোঝা লাইনের জন্য process তৈরি করলে ত্রুটির সময় সেগুলো পরিষ্কার করার ঝামেলা হয়।

মূল অংশ — চালিয়ে দেখুন

নিচের ব্লকটি পূর্ণ shell-এর হৃৎপিণ্ড: একটি |, একটি < বা >, builtin exit, আর প্রতিটি কমান্ডের পর exit status. stdin-এ আগে থেকে নয়টি লাইন দেওয়া আছে — যেন কেউ টাইপ করছেন। ষষ্ঠ লাইনটি একটি চালাক পরীক্ষা: ls /proc/self/fd দেখায় ls process-এর নিজের খোলা fd-গুলো — কোনো pipe-এর মুখ ফাঁস হলে সংখ্যা বেড়ে যাবে।

#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
static void child(char **av, int in, int out) {          /* runs in the child */
    for (int i = 0; av[i]; i++)                          /* "< file", "> file" */
        if (!strcmp(av[i], "<") || !strcmp(av[i], ">")) {
            if (av[i][0] == '<') in = open(av[i + 1], O_RDONLY);
            else out = open(av[i + 1], O_WRONLY | O_CREAT | O_TRUNC, 0644);
            if (in < 0 || out < 0) { perror(av[i + 1]); _exit(1); }
            av[i] = NULL;
        }
    if (in != 0)  { dup2(in, 0);  close(in); }           /* wire stdin  */
    if (out != 1) { dup2(out, 1); close(out); }          /* wire stdout */
    execvp(av[0], av); perror(av[0]); _exit(127);        /* only on failure */
}
int main(void) {
    char line[256], *cmd[2][16];  dup2(1, 2);     /* stderr -> same output */
    while (fgets(line, sizeof line, stdin)) {
        printf("$ %s", line); fflush(stdout);    /* flush BEFORE fork */
        int n[2] = {0, 0}, k = 0, fd[2] = {0, 1}, st = 0;
        for (char *t = strtok(line, " \n"); t; t = strtok(NULL, " \n"))
            if (!strcmp(t, "|")) k = 1; else if (n[k] < 15) cmd[k][n[k]++] = t;
        cmd[0][n[0]] = cmd[1][n[1]] = NULL;
        if (n[0] == 0 || (k && n[1] == 0)) continue;
        if (!strcmp(cmd[0][0], "exit")) break;               /* a builtin */
        if (k && pipe(fd) < 0) { perror("pipe"); continue; }
        pid_t a = fork(), b = -1;
        if (a == 0) { if (k) close(fd[0]); child(cmd[0], 0, k ? fd[1] : 1); }
        if (k && (b = fork()) == 0) { close(fd[1]); child(cmd[1], fd[0], 1); }
        if (k) { close(fd[0]); close(fd[1]); }   /* parent keeps NO pipe end */
        waitpid(a, &st, 0); if (k) waitpid(b, &st, 0);   /* status = last cmd */
        printf("[exit %d]\n", WIFEXITED(st) ? WEXITSTATUS(st) : 128 + WTERMSIG(st));
    }
}

আসল রানের আউটপুট:

$ echo hello world | tr a-z A-Z
HELLO WORLD
[exit 0]
$ ls /no/such
ls: cannot access '/no/such': No such file or directory
[exit 2]
$ echo done > out.txt
[exit 0]
$ cat out.txt
done
[exit 0]
$ cat < out.txt | wc -c
5
[exit 0]
$ ls /proc/self/fd | wc -l
4
[exit 0]
$ nosuchcmd -x
nosuchcmd: No such file or directory
[exit 127]
$ exit

প্রথম লাইনে echo-র আউটপুট pipe দিয়ে tr-এ গিয়ে বড় হাতের অক্ষর হলো। ls /no/such-এ ls নিজেই ত্রুটি ছাপিয়ে ২ দিয়ে বেরিয়েছে — shell শুধু সেই সংখ্যাটি দেখাচ্ছে। echo done > out.txt পর্দায় কিছু ছাপায়নি, কারণ child-এর fd ১ এখন ফাইল; পরের cat তা প্রমাণ করল। cat < out.txt | wc -c একই লাইনে redirection আর pipe — done আর newline মিলে ৫ byte. ls /proc/self/fd | wc -l = ৪: ls-এর fd ০, ১, ২ আর ফোল্ডারটি পড়ার জন্য খোলা একটি — pipe-এর কোনো বাড়তি মুখ নেই। শেষে execvp ব্যর্থ হয়ে perror আর ১২৭, তারপর builtin exit-এ লুপ ভাঙল, echo never আর পড়াই হলো না।

কোডের তিনটি সিদ্ধান্ত খেয়াল করুন। (১) parent দুই মুখই বন্ধ করে (/* parent keeps NO pipe end */) — এটি বাদ দিলে wc চিরকাল অপেক্ষা করত, নিচের stepper-এ দেখবেন কেন। (২) pipeline-এর status শেষ কমান্ডের: দুটি waitpid-ই একই st-এ লেখে, শেষেরটি জেতে। (৩) dup2(1, 2) শুধু এই পাতার জন্য — stderr-কে stdout-এর সাথে মিলিয়ে দেয়, যাতে ত্রুটির বার্তা সঠিক জায়গায় দেখা যায়।

ধাপে ধাপে: ls | wc -l

উপরের কোড ঠিক কোন ক্রমে fd খোলে-বন্ধ করে, নিচে প্রতিটি ধাপে তিনটি process-এর fd table আর pipe-এর দুই মুখ কয়জনের কাছে খোলা — তা দেখুন। ধরা যাক ফোল্ডারে ৫টি ফাইল আছে।

1parse: cmd0 = ls, cmd1 = wc -l2pipe(fd)      // fd[0]=3 read, fd[1]=4 write3a = fork()4  [A] close(3); dup2(4,1); close(4); exec ls5b = fork()6  [B] close(4); dup2(3,0); close(3); exec wc7close(3); close(4)        // parent8waitpid(a); waitpid(b)
fd table আর pipe (ধাপের শেষে)
shell
A (ls)
B (wc -l)
W খোলা আছে কার কাছে
R খোলা আছে কার কাছে
  1. Parse — এখনো কোনো process নেই। লাইন ভেঙে দুটি কমান্ড পাওয়া গেল। shell-এর fd ০, ১, ২ terminal-এ। নিয়ম মেনে parse পুরো শেষ, তারপরই system call.
  2. pipe(fd). kernel একটি pipe বানিয়ে shell-কে দুটি নতুন fd দিল — সবচেয়ে ছোট খালি নম্বর ৩ (পড়ার মুখ, R) আর ৪ (লেখার মুখ, W). এখন দুই মুখই শুধু shell-এর কাছে।
  3. প্রথম fork — child A. fork পুরো fd table কপি করে, তাই A-র কাছেও ৩ আর ৪ — একই pipe-এর দিকে আরেক জোড়া তীর। এখন W আর R দুটিই খোলা দুজনের কাছে।
  4. A নিজেকে সাজায়, তারপর exec. A পড়বে না: close(3). dup2(4, 1) — fd ১ এখন pipe-এর W; বাড়তি close(4). তারপর exec ls: নতুন প্রোগ্রাম fd table অক্ষত পায়, জানেই না তার stdout আসলে pipe. ls এখনই লিখতে পারে — pipe-এর buffer (Linux-এ সাধারণত ৬৪ KiB) লেখা ধরে রাখে।
  5. দ্বিতীয় fork — child B. B কপি পায় shell-এর বর্তমান table: ৩ আর ৪ দুটিই। এই মুহূর্তে W খোলা তিনজনের কাছে — তাই প্রত্যেককে নিজের অব্যবহৃত মুখ বন্ধ করতে হয়।
  6. B নিজেকে সাজায়, তারপর exec. close(4); dup2(3, 0) — fd ০ এখন pipe-এর R; বাড়তি close(3); exec wc -l. নিজের কাছে W খোলা থাকলে wc নিজেকেই আটকে রাখত।
  7. Parent দুই মুখই বন্ধ করে। এখন W শুধু A-র কাছে, R শুধু B-র কাছে — ঠিক এটাই চাই। এই লাইন ভুলে গেলে shell-এর কাছে W খোলা থাকত, wc কখনো EOF পেত না, আর terminal ঝুলে থাকত।
  8. ডেটা চলে, তারপর EOF. ls ৫টি নাম লিখে বেরোয়; kernel তার fd বন্ধ করে, W-এর আর কোনো কপি নেই। wc-এর read তখন ০ ফেরত দেয় — এটাই EOF. wc 5 ছাপিয়ে বেরোয়; দুজনেই zombie, যতক্ষণ না shell status নেয়।
  9. waitpid দুবার, status শেষ কমান্ডের। shell দুই child-কে reap করে; pipeline-এর status = wc-এর, অর্থাৎ ০. pipe-এর সব মুখ বন্ধ, তাই kernel সেটি মুছে দিয়েছে। নতুন prompt.

৩ · Shell-এর signal, background job আর পরীক্ষার পরিকল্পনা

Ctrl+C: shell বাঁচবে, foreground কমান্ড থামবে

পর্ব ১১-এ দেখেছেন, Ctrl+C চাপলে terminal driver foreground process group-কে SIGINT পাঠায়। আমাদের shell-এ job control নেই, তাই shell আর তার child-রা একই group-এ — Ctrl+C সবাইকে পৌঁছায়। সমাধান দুই ধাপের:

  • Shell নিজে উপেক্ষা করে: signal(SIGINT, SIG_IGN). তাহলে Ctrl+C-তে shell মরে না।
  • Foreground child exec-এর আগে ডিফল্টে ফেরে: signal(SIGINT, SIG_DFL). এটি বাধ্যতামূলক, কারণ exec-এর নিয়ম অসম: ধরা signal-এর handler exec-এ ডিফল্টে ফিরে যায় (handler-এর কোড তো আর নেই), কিন্তু SIG_IGN থেকে যায়. ভুলে গেলে আপনার shell-এ চালানো প্রতিটি প্রোগ্রাম Ctrl+C উপেক্ষা করবে। একইভাবে blocked signal mask-ও exec পেরিয়ে যায় — child-এ সেটিও ফিরিয়ে দিতে হয়।
  • Background child উপেক্ষাই রাখে: job control না থাকলে POSIX নিয়মে &-এ চালানো কমান্ড SIGINT আর SIGQUIT উপেক্ষা অবস্থায় পায় — নইলে একটি Ctrl+C পেছনের সব কাজও মেরে ফেলত।

আসল job control যেভাবে করে (এখানে শুধু বর্ণনা): bash-এর মতো interactive shell প্রতিটি job-কে নিজের process group-এ রাখে (parent আর child দুজনেই setpgid ডাকে, দৌড়-প্রতিযোগিতা এড়াতে), আর foreground job চালানোর আগে tcsetpgrp() দিয়ে terminal-কে সেই group দেয়; শেষ হলে terminal ফেরত নেয়। তখন Ctrl+C শুধু foreground group-এ যায়, shell-এর কাছে পৌঁছায়ই না। তবু interactive bash নিজে SIGINT, SIGTSTP, SIGTTOU-কে বিশেষভাবে সামলায় — terminal ফেরত নেওয়ার সময় যেন নিজেই থেমে না যায়। এটি “বাড়তি কাজ” অংশে (অংশ ৮) প্রথম এক্সটেনশন।

Background আর SIGCHLD — race ছাড়া reap

&-এর জন্য shell অপেক্ষা করে না; child শেষ হলে kernel SIGCHLD পাঠায়। সরল উপায় — handler-এ waitpid(-1, &st, WNOHANG) লুপ — একটি সূক্ষ্ম বাগ আনে: handler foreground child-কেও reap করে ফেলতে পারে, তখন main-এর waitpid(pid) ব্যর্থ হয়। পূর্ণ সংস্করণ এই নকশা নেয়:

  1. handler সব child reap করে, কিন্তু শুধু ফল লিখে রাখে: foreground হলে fg_left-- আর status, background হলে job table-এ “done”. কোনো printf নয় — handler-এ শুধু async-signal-safe কাজ।
  2. fork-এর আগে sigprocmask দিয়ে SIGCHLD আটকে রাখা হয়, PID লেখা শেষ হলে তবেই ছাড়া — নইলে child এত দ্রুত মরতে পারে যে handler তার PID চেনার আগেই চলে যায়।
  3. foreground-এর অপেক্ষা while (fg_left > 0) sigsuspend(&old); — mask ছাড়া আর ঘুম এক ধাপে, তাই মাঝখানে signal হারায় না।
  4. background-এর “done” বার্তা main লুপ ছাপায়, পরের prompt-এর আগে — bash-ও ঠিক এভাবে [1]+ Done দেখায়।

পূর্ণ সংস্করণ নিচে (চালানোর বোতাম নেই, কারণ এটি ৩৫ লাইনের সীমার অনেক বড়; নিজের Linux, WSL বা অনলাইন compiler-এ চালান)। দ্বিতীয় ট্যাবে একটি আসল রানের আউটপুট — Linux-এ gcc দিয়ে, stdin-এ একটি পরীক্ষার ফাইল দিয়ে।

/* mysh2.c — capstone shell: builtins (cd, exit, export), one pipe, < and >,
 * background with &, SIGCHLD reaping, exit-status display, SIGINT handling.
 * Build: gcc -Wall -Wextra -O2 -o mysh2 mysh2.c */
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

#define MAXARGS 32
#define MAXBG   16
typedef struct { char *argv[MAXARGS]; char *in, *out; } Cmd;

/* ---- state shared with the SIGCHLD handler (only sig_atomic_t / pid_t) ---- */
static volatile pid_t fg_pid[2];          /* foreground children, 0 = none   */
static volatile sig_atomic_t fg_left;     /* how many are still running      */
static volatile sig_atomic_t fg_status;   /* raw status of the LAST command  */
static volatile pid_t bg_pid[MAXBG];      /* job table: slot i = job [i+1]   */
static volatile sig_atomic_t bg_done[MAXBG], bg_status[MAXBG];

static void on_sigchld(int sig) {         /* async-signal-safe: no printf! */
    (void)sig;
    int saved = errno, st;
    pid_t p;
    while ((p = waitpid(-1, &st, WNOHANG)) > 0) {
        if (p == fg_pid[0] || p == fg_pid[1]) {
            if (p == fg_pid[1] || fg_pid[1] == 0) fg_status = st;
            fg_left--;
            continue;
        }
        for (int i = 0; i < MAXBG; i++)
            if (bg_pid[i] == p) { bg_status[i] = st; bg_done[i] = 1; }
    }
    errno = saved;
}

static void report(const char *who, int st) {        /* exit-status display */
    if (WIFEXITED(st) && WEXITSTATUS(st) != 0) printf("[%s exit %d]\n", who, WEXITSTATUS(st));
    else if (WIFSIGNALED(st)) printf("[%s killed by signal %d]\n", who, WTERMSIG(st));
}

/* split "a b < f | c > g &" into at most two commands; returns count, -1 on error */
static int parse(char *line, Cmd c[2], int *bg) {
    int n = 0, k = 0;
    memset(c, 0, 2 * sizeof *c);
    *bg = 0;
    for (char *t = strtok(line, " \t\n"); t; t = strtok(NULL, " \t\n")) {
        if (*bg) { fprintf(stderr, "mysh: '&' must be last\n"); return -1; }
        if (!strcmp(t, "&")) { *bg = 1; continue; }
        if (!strcmp(t, "|")) {
            if (n == 0 || k == 1) { fprintf(stderr, "mysh: syntax error near '|'\n"); return -1; }
            k = 1; n = 0; continue;
        }
        if (!strcmp(t, "<") || !strcmp(t, ">")) {
            char *f = strtok(NULL, " \t\n");
            if (!f) { fprintf(stderr, "mysh: missing file after '%s'\n", t); return -1; }
            if (*t == '<') c[k].in = f; else c[k].out = f;
            continue;
        }
        if (n < MAXARGS - 1) c[k].argv[n++] = t;
    }
    if (k == 1 && n == 0) { fprintf(stderr, "mysh: syntax error near '|'\n"); return -1; }
    return c[0].argv[0] ? k + 1 : 0;
}

/* in the child: restore signals, wire fds, exec — never returns */
static void exec_child(Cmd *c, int in, int out, int bg, const sigset_t *old) {
    if (!bg) signal(SIGINT, SIG_DFL);    /* foreground gets Ctrl+C; background keeps SIG_IGN */
    signal(SIGQUIT, bg ? SIG_IGN : SIG_DFL);
    sigprocmask(SIG_SETMASK, old, NULL); /* the blocked mask survives exec — undo it */
    if (c->in  && (in  = open(c->in, O_RDONLY)) < 0) { perror(c->in); _exit(1); }
    if (c->out && (out = open(c->out, O_WRONLY | O_CREAT | O_TRUNC, 0644)) < 0) { perror(c->out); _exit(1); }
    if (in  != STDIN_FILENO)  { dup2(in, STDIN_FILENO);   close(in); }
    if (out != STDOUT_FILENO) { dup2(out, STDOUT_FILENO); close(out); }
    execvp(c->argv[0], c->argv);
    fprintf(stderr, "mysh: %s: %s\n", c->argv[0], strerror(errno));
    _exit(errno == ENOENT ? 127 : 126);
}

static int builtin(Cmd *c, int ncmd) {   /* 1 = handled here, in the shell itself */
    char **a = c->argv;
    if (ncmd != 1) return 0;
    if (!strcmp(a[0], "exit")) exit(a[1] ? atoi(a[1]) : 0);
    if (!strcmp(a[0], "cd")) {
        const char *d = a[1] ? a[1] : getenv("HOME");
        if (!d || chdir(d) != 0) perror("cd");
        return 1;
    }
    if (!strcmp(a[0], "export")) {
        char *eq = a[1] ? strchr(a[1], '=') : NULL;
        if (!eq) { fprintf(stderr, "usage: export NAME=VALUE\n"); return 1; }
        *eq = '\0';
        if (setenv(a[1], eq + 1, 1) != 0) perror("export");
        return 1;
    }
    return 0;
}

int main(void) {
    struct sigaction sa;
    memset(&sa, 0, sizeof sa);
    sa.sa_handler = on_sigchld;
    sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;   /* restart read(); ignore stops */
    sigemptyset(&sa.sa_mask);
    sigaction(SIGCHLD, &sa, NULL);
    signal(SIGINT, SIG_IGN);                   /* Ctrl+C must not kill the shell */
    signal(SIGQUIT, SIG_IGN);

    sigset_t chld, old;
    sigemptyset(&chld);
    sigaddset(&chld, SIGCHLD);
    char line[1024];
    int interactive = isatty(STDIN_FILENO);
    for (;;) {
        for (int i = 0; i < MAXBG; i++)        /* report finished background jobs */
            if (bg_done[i]) {
                printf("[%d] done", i + 1);
                if (WIFEXITED(bg_status[i])) printf(", exit %d\n", WEXITSTATUS(bg_status[i]));
                else printf(", signal %d\n", WTERMSIG(bg_status[i]));
                bg_done[i] = 0; bg_pid[i] = 0;
            }
        if (interactive) printf("mysh> ");
        fflush(stdout);
        if (!fgets(line, sizeof line, stdin)) break;      /* EOF (Ctrl+D) */
        if (!interactive) { printf("$ %s", line); fflush(stdout); }

        Cmd c[2];
        int bg, n = parse(line, c, &bg);
        if (n <= 0 || builtin(&c[0], n)) continue;

        int fd[2] = { -1, -1 };
        if (n == 2 && pipe(fd) < 0) { perror("pipe"); continue; }
        sigprocmask(SIG_BLOCK, &chld, &old);   /* no SIGCHLD until pids are recorded */
        pid_t pid[2] = { 0, 0 };
        for (int i = 0; i < n; i++) {
            pid[i] = fork();
            if (pid[i] < 0) { perror("fork"); break; }
            if (pid[i] == 0) {
                if (n == 2) {                  /* the child closes the end it does not use */
                    if (i == 0) close(fd[0]); else close(fd[1]);
                }
                exec_child(&c[i], n == 2 && i == 1 ? fd[0] : STDIN_FILENO,
                           n == 2 && i == 0 ? fd[1] : STDOUT_FILENO, bg, &old);
            }
        }
        if (n == 2) { close(fd[0]); close(fd[1]); }      /* else the reader never sees EOF */
        if (bg) {
            int slot = 0;
            while (slot < MAXBG && bg_pid[slot]) slot++;
            if (slot < MAXBG) { bg_pid[slot] = pid[n - 1]; printf("[%d] started\n", slot + 1); }
        } else {
            fg_pid[0] = pid[0]; fg_pid[1] = n == 2 ? pid[1] : 0;
            fg_left = (pid[0] > 0) + (n == 2 && pid[1] > 0);
            while (fg_left > 0) sigsuspend(&old);              /* sleep until SIGCHLD */
            fg_pid[0] = fg_pid[1] = 0;
            report(n == 2 ? "pipeline" : c[0].argv[0], fg_status);
        }
        sigprocmask(SIG_SETMASK, &old, NULL);
    }
    return 0;
}
$ ./mysh2 < tests.txt          # stdin is a file, so mysh2 echoes each line
$ echo hello world | tr a-z A-Z
HELLO WORLD
$ cd /tmp
$ pwd
/tmp
$ export GREETING=salam
$ printenv GREETING
salam
$ ls /no/such
ls: cannot access '/no/such': No such file or directory
[ls exit 2]
$ echo done > out.txt
$ cat < out.txt | wc -c
5
$ sleep 0.3 | cat &
[1] started
$ sleep 0.2 &
[2] started
$ echo started-bg
started-bg
$ sleep 1
[1] done, exit 0
[2] done, exit 0
$ false | true
$ true | false
[pipeline exit 1]
$ nosuchcmd -x
mysh: nosuchcmd: No such file or directory
[nosuchcmd exit 127]
$ ls |
mysh: syntax error near '|'
$ echo a >
mysh: missing file after '>'
$ bash -c kill${IFS}-9${IFS}$$
[bash killed by signal 9]
$ exit 3
$ echo $?                      # back in the real bash
3

[1] আর [2]-এর “done” বার্তা এসেছে sleep 1 শেষ হওয়ার পর, পরের লাইন পড়ার আগে — handler শুধু লিখে রেখেছিল, ছাপিয়েছে main লুপ। false | true কিছু দেখায়নি (শেষ কমান্ড সফল), true | false দেখিয়েছে [pipeline exit 1]. bash -c kill${IFS}-9${IFS}$$ একটি কৌশল: আমাদের parser উদ্ধৃতিচিহ্ন বোঝে না, তাই space-এর বদলে bash-এর ${IFS}; bash নিজেকে SIGKILL পাঠায়, আর shell দেখায় [bash killed by signal 9].

পরীক্ষার পরিকল্পনা

প্রতিটি সারি একটি পরীক্ষা-ফাইলের এক লাইন; প্রত্যাশিত আউটপুট আলাদা ফাইলে রেখে diff করুন। যে পরীক্ষা সময়ের ওপর নির্ভর করে (background), তাতে শুধু “done” এসেছে কি না দেখুন, ক্রম নয়।

কী যাচাইইনপুটপ্রত্যাশা
সাধারণ কমান্ড, statusfalse; nosuchcmd[exit 1]; বার্তা + [exit 127]
builtin shell-এই চলেcd /tmp তারপর pwd; export X=1 তারপর printenv X/tmp; 1
redirectionecho a > f, wc -c < f; cat < /no/file2; ত্রুটি + [exit 1], shell বেঁচে
pipe, EOF, fd leakls /proc/self/fd | wc -l; yes | head -14; y আর prompt ফেরে (yes SIGPIPE-এ মরে)
pipeline-এর statustrue | false; false | true1; 0
background, zombie নেইsleep 0.2 &, পরে sleep 1, তারপর ps -o stat=[1] done; কোনো Z নেই
ভুল ইনপুটls |; | wc; echo >; ফাঁকা লাইনবার্তা, কোনো fork নয়, shell বেঁচে
Ctrl+C (হাতে, আসল terminal-এ)sleep 10 চালিয়ে Ctrl+Csleep মরে, [sleep killed by signal 2], shell বেঁচে

৪ · প্রজেক্ট B — scheduler simulator: event-driven লুপ আর pick_next()

পর্ব ১৭–১৯-এ প্রতিটি নীতির জন্য আলাদা JavaScript simulator দেখেছেন, বেশিরভাগই সময়কে ১ ms করে এগোয়। ক্যাপস্টোনে লক্ষ্য একটি CLI প্রোগ্রাম: একটি process table পড়বে, একটি নীতি চালাবে, Gantt chart আর মাপকাঠি ছাপবে — সব নীতির জন্য একই লুপ।

$ cat ch18b.txt                 $ ./schedsim rr 2 < ch18b.txt
P1 0 5                          Gantt: P1 0-2 | P2 2-4 | P3 4-5 | ...
P2 1 3                          proc   AT  BT  PR   CT  TAT   WT   RT
...                             ...
                                avg TAT 8.60  avg WT 5.80  avg RT 2.40  dispatches 9

নকশা — তিনটি সিদ্ধান্ত

  1. Event-driven, tick নয়। ঘড়ি প্রতি একক করে না এগিয়ে সরাসরি পরের ঘটনায় লাফ দেয়। ঘটনা মাত্র তিন রকম: নতুন আগমন, চলতি প্রসেসের শেষ, quantum ফুরানো। তাই চলার সময় run = min(বাকি burst, quantum-এর বাকি, পরের আগমন − t). burst যদি ১০ লাখ ms হয়, tick-লুপ ১০ লাখবার ঘোরে; event-লুপ ঘোরে ঘটনার সংখ্যার সমান।
  2. সব নীতির সিদ্ধান্ত এক জায়গায় — pick_next(). ready প্রসেসদের মধ্যে যার key() সবচেয়ে ছোট সে জেতে: FCFS-এ arrival, SJF-এ burst, SRTF-এ বাকি সময়, priority-তে priority নম্বর, RR-এ queue-তে ঢোকার ক্রম (stamp), MLFQ-এ (স্তর, stamp). নতুন নীতি যোগ করা মানে শুধু একটি key — যেমন অনুশীলনে HRRN.
  3. Tie-নিয়ম কোডে আর README-তে লেখা। (১) key সমান হলে চলতি প্রসেস CPU রাখে; (২) তারপর আগে আসা, তারপর টেবিলে আগের লাইন; (৩) RR/MLFQ-এ একই মুহূর্তে আগমন আর quantum ফুরালে নতুনটি আগে সারিতে, তারপর সময়-ফুরানোটি; ওপরের স্তরের আগমনে থামানো MLFQ প্রসেস নিজের স্তরের সামনেই থাকে, quantum-এর বাকিটুকু নিয়ে। পর্ব ১৮-এ দেখেছেন, এই একটি নিয়ম বদলালে RR-এর উত্তর বদলে যায়।

Non-preemptive নীতিতে (FCFS, SJF, non-preemptive priority) pick_next() চলতি প্রসেস থাকলে তাকেই ফেরত দেয়; preemptive নীতিতে প্রতিটি ঘটনায় আবার সবাইকে তুলনা করে। নিচের stepper-এ পর্ব ১৭-এর ছয়-প্রসেসের সমস্যাটি SRTF-এ চলছে — প্রতিটি ধাপ লুপের একটি পাক।

1P1 1/3  P2 3/6  P3 5/4      (AT/BT)2P4 7/5  P5 9/2  P6 23/33loop: admit arrivals at t4      cur = pick_next()   // min left5      run = min(left, next arrival - t)6      none ready: jump to next arrival7done: print Gantt + metrics
লুপের এই পাকে
t
ready (বাকি)
pick_next()
পরের ঘটনা
  1. t = ০, কেউ আসেনি। pick_next() কাউকে পায় না। লুপ প্রতি ms-এ অপেক্ষা না করে সরাসরি পরের আগমনে (t = ১) লাফ দেয়; Gantt-এ idle 0-1.
  2. t = ১: P1 এল, একাই বাছাই। P1-এর বাকি ৩, কিন্তু পরের আগমন t = ৩-এ, তাই এই পাকে চলে মাত্র ২ একক (১–৩) — preemptive নীতিতে প্রতিটি আগমন একটি সিদ্ধান্তের মুহূর্ত।
  3. t = ৩: P2 এল (৬)। P1-এর বাকি ১ < ৬, তাই P1-ই থাকে — dispatch বাড়ে না, Gantt-এর খোপও একই থাকে। পরের ঘটনা P1-এর শেষ, t = ৪.
  4. t = ৪: P1 শেষ (CT ৪)। ready-তে শুধু P2; সে চলবে পরের আগমন t = ৫ পর্যন্ত।
  5. t = ৫: P3 এল (৪) — preemption. P2-এর বাকি ৫, P3-এর ৪ → pick_next() P3-কে দেয়। এটিই SRTF-এর বাড়তি context switch.
  6. t = ৭: P4 এল (৫)। প্রার্থী P3 (২), P2 (৫), P4 (৫) → P3 থাকে। পরের ঘটনা দুটি একসাথে: t = ৯-এ P3 শেষ আর P5-এর আগমন।
  7. t = ৯: P3 শেষ (CT ৯), P5 এল (২)। লুপ আগে আগমন ভর্তি করে, তারপর বাছে: P5 (২) সবচেয়ে ছোট। পরের আগমন অনেক পরে (২৩), তাই P5 শেষ পর্যন্ত চলে।
  8. t = ১১: tie. P2 আর P4 দুজনেরই বাকি ৫. নিয়ম ২ — আগে আসা P2 জেতে। এই নিয়ম কোডে না থাকলে অ্যারের ক্রমে যে আগে, সে জিতত — কাকতালীয়ভাবে ঠিক, কিন্তু অন্য টেবিলে ভুল।
  9. t = ১৬: P2 শেষ (CT ১৬)। P4 একা, চলে ১৬–২১.
  10. t = ২১: P4 শেষ, কেউ নেই। আবার idle-লাফ, t = ২৩-এ। idle খোপ বাদ পড়লে সব CT ভুল হতো — পর্ব ১৭-এর সতর্কবার্তা।
  11. t = ২৩: P6 এল। একা, আর কোনো আগমন নেই, তাই এক পাকেই ২৩–২৬.
  12. t = ২৬: সব শেষ। মোট ১১টি পাক — tick-লুপে লাগত ২৬টি। ফল পর্ব ১৭-এর হাতের হিসাবের সাথে হুবহু: গড় TAT ৬.৫০, গড় WT ২.৬৭, ৭ বার dispatch.

৫ · Simulator যাচাই — পর্ব ১৭–১৯-এর উত্তরের সাথে মিলিয়ে দেখা

প্রথমে মূল অংশটি চালান — শুধু Round Robin, একটি FIFO ready queue, আর সেই একই tie-নিয়ম। ডেটা পর্ব ১৮-এর উদাহরণ ৩: পাঁচটি প্রসেস, q = ২.

#include <stdio.h>
#define N 5
const char *name[N] = {"P1", "P2", "P3", "P4", "P5"};      /* sorted by arrival */
int at[N] = {0, 1, 2, 3, 4}, bt[N] = {5, 3, 1, 2, 3};
int left[N], start[N], ct[N], q[64], head, tail, next, t;
static void admit(void) {                 /* every arrival with at <= t joins */
    while (next < N && at[next] <= t) q[tail++] = next++;
}
static int pick_next(void) { return q[head++]; }      /* RR: front of FIFO */
int main(void) {
    const int Q = 2;
    for (int i = 0; i < N; i++) { left[i] = bt[i]; start[i] = -1; }
    admit();
    printf("Gantt:");
    while (head < tail || next < N) {
        if (head == tail) { t = at[next]; admit(); continue; }  /* CPU idle */
        int i = pick_next();
        if (start[i] < 0) start[i] = t;
        int run = left[i] < Q ? left[i] : Q;     /* next event: done or expiry */
        printf(" %s %d-%d |", name[i], t, t + run);
        t += run; left[i] -= run;
        admit();                       /* tie rule: NEW arrivals enter first, */
        if (left[i] > 0) q[tail++] = i;          /* then the expired process */
        else ct[i] = t;
    }
    int tat = 0, wt = 0, rt = 0;
    printf("\nproc AT BT  CT TAT WT RT\n");
    for (int i = 0; i < N; i++) {
        int a = ct[i] - at[i], w = a - bt[i], r = start[i] - at[i];
        tat += a; wt += w; rt += r;
        printf("%-4s %2d %2d %3d %3d %2d %2d\n", name[i], at[i], bt[i], ct[i], a, w, r);
    }
    printf("avg TAT = %.2f, WT = %.2f, RT = %.2f\n", tat / 5.0, wt / 5.0, rt / 5.0);
    return 0;
}

আউটপুট:

Gantt: P1 0-2 | P2 2-4 | P3 4-5 | P1 5-7 | P4 7-9 | P5 9-11 | P2 11-12 | P1 12-13 | P5 13-14 |
proc AT BT  CT TAT WT RT
P1    0  5  13  13  8  0
P2    1  3  12  11  8  1
P3    2  1   5   3  2  2
P4    3  2   9   6  4  4
P5    4  3  14  10  7  5
avg TAT = 8.60, WT = 5.80, RT = 2.40

পর্ব ১৮-এর হাতে আঁকা চার্ট আর টেবিলের প্রতিটি সংখ্যা মেলে: CT ১৩, ১২, ৫, ৯, ১৪; গড় TAT ৮.৬, WT ৫.৮, RT ২.৪. admit(); আগে, তারপর if (left[i] > 0) q[tail++] = i; — এই ক্রমই tie-নিয়ম ৩. পর্ব ১৮-এর “উল্টো প্রথা” চাইলে শুধু লাইন দুটি অদলবদল করলে হবে না — তাতে t = ২-এর আগে আসা P2-ও P1-এর পেছনে পড়ে। ঠিক রূপ: আগে at < t আগমনগুলো ভর্তি, তারপর সময়-ফুরানো প্রসেস, তারপর at == t-এর আগমন। তখন t = ২-এর সারি হয় [P2 P1 P3], আর পর্ব ১৮-এর উল্টো-প্রথার চার্ট আসে (গড় RT ৩.০)। pick_next() এখানে FIFO-র সামনে থেকে নেওয়া; পূর্ণ সংস্করণে এটিই key-ভিত্তিক তুলনা।

পূর্ণ সংস্করণ সব নীতি চালায়, stdin থেকে টেবিল পড়ে, আর command line থেকে নীতি নেয়:

/* schedsim.c — event-driven CPU-scheduling simulator.
 *   usage: ./schedsim fcfs|sjf|srtf|prio|npprio|rr Q|mlfq [Q0 Q1]  < table.txt
 *   table: one process per line: NAME ARRIVAL BURST [PRIORITY]  (smaller = higher)
 * Tie rules (documented, and the same as the hand-worked lessons):
 *   1. equal key -> the running process keeps the CPU (preemptive policies);
 *   2. then earlier arrival, then earlier line in the table;
 *   3. RR / MLFQ: arrivals at time t join the queue BEFORE a process whose
 *      quantum expired at t; a process preempted by a higher MLFQ level stays
 *      at the head of its own level with the rest of its quantum.            */
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAXP 64
typedef struct {
    char name[16];
    int at, bt, pr;              /* input                                     */
    int left, start, ct;         /* simulation state; start = first dispatch  */
    int level, used;             /* MLFQ level, time used of current quantum  */
    long stamp;                  /* FIFO order inside a queue (smaller = older) */
    int ready;                   /* arrived and not finished                  */
} Proc;

typedef enum { FCFS, SJF, SRTF, PRIO, NPPRIO, RR, MLFQ } Policy;
static Proc p[MAXP];
static int np, quantum[3] = { 0, 0, INT_MAX };
static long clock_stamp;

static int preemptive(Policy pol) { return pol == SRTF || pol == PRIO || pol == RR || pol == MLFQ; }

/* key of a ready process — smaller wins. This is the ONLY place a policy lives. */
static long key(Policy pol, const Proc *x) {
    switch (pol) {
    case FCFS:   return x->at;
    case SJF:    return x->bt;
    case SRTF:   return x->left;
    case PRIO: case NPPRIO: return x->pr;
    case RR:     return x->stamp;
    case MLFQ:   return (long)x->level * 1000000000L + x->stamp;
    }
    return 0;
}

/* pick_next(): the scheduler's whole decision, given who is running now */
static int pick_next(Policy pol, int running) {
    if (running >= 0 && !preemptive(pol)) return running;      /* never interrupt */
    int best = -1;
    for (int i = 0; i < np; i++) {
        if (!p[i].ready) continue;
        if (best < 0) { best = i; continue; }
        long a = key(pol, &p[i]), b = key(pol, &p[best]);
        if (a < b || (a == b && (i == running ||
            (best != running && p[i].at < p[best].at)))) best = i;   /* tie rules 1-2 */
    }
    return best;
}

static int next_arrival(int t) {                       /* earliest arrival > t */
    int na = INT_MAX;
    for (int i = 0; i < np; i++) if (p[i].at > t && p[i].at < na) na = p[i].at;
    return na;
}

static void admit(int t) {                             /* arrivals at time t   */
    for (int i = 0; i < np; i++)
        if (p[i].at == t) { p[i].ready = 1; p[i].stamp = ++clock_stamp; }
}

int main(int argc, char **argv) {
    const char *names[] = { "fcfs", "sjf", "srtf", "prio", "npprio", "rr", "mlfq" };
    Policy pol = (Policy)-1;
    for (int i = 0; argc > 1 && i < 7; i++) if (!strcmp(argv[1], names[i])) pol = (Policy)i;
    if ((int)pol < 0 || (pol == RR && (argc < 3 || atoi(argv[2]) <= 0))) {
        fprintf(stderr, "usage: %s fcfs|sjf|srtf|prio|npprio|rr Q|mlfq [Q0 Q1] < table\n", argv[0]);
        return 2;
    }
    if (pol == RR) quantum[0] = atoi(argv[2]);
    if (pol == MLFQ) { quantum[0] = argc > 2 ? atoi(argv[2]) : 8; quantum[1] = argc > 3 ? atoi(argv[3]) : 16; }

    char line[128];
    while (np < MAXP && fgets(line, sizeof line, stdin)) {
        Proc *x = &p[np];
        x->pr = 0;
        if (sscanf(line, "%15s %d %d %d", x->name, &x->at, &x->bt, &x->pr) < 3 || x->bt <= 0) continue;
        x->left = x->bt; x->start = -1; np++;
    }

    int t = 0, cur = -1, done = 0, dispatches = 0, nseg = 0;
    static char seg[512][24]; static int seg_s[512], seg_e[512];
    admit(0);
    while (done < np) {                     /* one iteration = one event */
        int nxt = pick_next(pol, cur);
        char label[24];
        int run;
        if (nxt < 0) {                                        /* CPU idle */
            run = next_arrival(t) - t;
            strcpy(label, "idle");
        } else {
            if (nxt != cur) dispatches++;
            cur = nxt;
            Proc *x = &p[cur];
            if (x->start < 0) x->start = t;
            if (pol == MLFQ) snprintf(label, sizeof label, "%s@Q%d", x->name, x->level);
            else snprintf(label, sizeof label, "%s", x->name);
            /* next event = completion, quantum expiry or next arrival */
            int q = (pol == RR) ? quantum[0] : (pol == MLFQ) ? quantum[x->level] : INT_MAX;
            run = x->left;
            if (q != INT_MAX && q - x->used < run) run = q - x->used;
            int na = next_arrival(t);
            if (na != INT_MAX && na - t < run) run = na - t;
            x->left -= run; x->used += run;
        }
        if (nseg && !strcmp(seg[nseg - 1], label) && seg_e[nseg - 1] == t) seg_e[nseg - 1] = t + run;
        else if (nseg < 512) { strcpy(seg[nseg], label); seg_s[nseg] = t; seg_e[nseg++] = t + run; }
        t += run;
        admit(t);                                   /* tie rule 3: arrivals first */
        if (cur < 0) continue;
        Proc *x = &p[cur];
        int q = (pol == RR) ? quantum[0] : (pol == MLFQ) ? quantum[x->level] : INT_MAX;
        if (x->left == 0) {
            x->ct = t; x->ready = 0; done++; cur = -1;
        } else if (q != INT_MAX && x->used == q) {  /* quantum expired */
            x->used = 0;
            if (pol == MLFQ && x->level < 2) x->level++;
            x->stamp = ++clock_stamp;               /* to the tail of its queue */
            cur = -1;
        }
    }
    printf("Gantt:");
    for (int i = 0; i < nseg; i++) printf(" %s %d-%d |", seg[i], seg_s[i], seg_e[i]);
    printf("\n%-5s %3s %3s %3s %4s %4s %4s %4s\n", "proc", "AT", "BT", "PR", "CT", "TAT", "WT", "RT");
    double s_tat = 0, s_wt = 0, s_rt = 0;
    for (int i = 0; i < np; i++) {
        int tat = p[i].ct - p[i].at, wt = tat - p[i].bt, rt = p[i].start - p[i].at;
        s_tat += tat; s_wt += wt; s_rt += rt;
        printf("%-5s %3d %3d %3d %4d %4d %4d %4d\n", p[i].name, p[i].at, p[i].bt, p[i].pr,
               p[i].ct, tat, wt, rt);
    }
    printf("avg TAT %.2f  avg WT %.2f  avg RT %.2f  dispatches %d\n",
           s_tat / np, s_wt / np, s_rt / np, dispatches);
    return 0;
}

যাচাইয়ের টেবিল — পাঠ্যবইয়ের উত্তর বনাম simulator

প্রতিটি সারি একটি আসল রান (Linux, gcc, -Wall -Wextra-এ কোনো warning নেই)। “প্রত্যাশিত” কলাম আগের পর্বগুলোর হাতে করা হিসাব।

ডেটাকমান্ডপ্রত্যাশিত (গড় TAT / WT)schedsim
পর্ব ১৭, ছয় প্রসেসfcfs৭.৬৭ / ৩.৮৩7.67 / 3.83 ✓
পর্ব ১৭sjf৬.৮৩ / ৩.০০6.83 / 3.00 ✓
পর্ব ১৭srtf৬.৫০ / ২.৬৭ (RT ১.৬৭, ৭ dispatch)6.50 / 2.67, RT 1.67, 7 ✓
পর্ব ১৮, উদাহরণ ১prio৬.৭৫ / ৩.২৫ (RT ২)6.75 / 3.25, RT 2.00 ✓
পর্ব ১৮, উদাহরণ ১npprio৭ / ৩.৫7.00 / 3.50 ✓
পর্ব ১৮, উদাহরণ ৩rr 2৮.৬ / ৫.৮ (RT ২.৪)8.60 / 5.80, RT 2.40 ✓
পর্ব ১৮, উদাহরণ ৩rr 4৮.৪ / ৫.৬ (RT ৩.৮)8.40 / 5.60, RT 3.80 ✓
পর্ব ১৯, চার প্রসেসmlfq (৮, ১৬, FCFS)২৮.৫ / ১৩.২৫28.50 / 13.25 ✓
পর্ব ১৯fcfs; srtf৩৮.২৫ / ২৩; ২১.৫ / ৬.২৫38.25 / 23.00; 21.50 / 6.25 ✓

MLFQ-এর Gantt লাইনটিও পর্ব ১৯-এর stepper-এর সাথে খোপে খোপে মেলে: P1@Q0 0-8 | P2@Q0 8-14 | P3@Q0 14-22 | P1@Q1 22-38 | P3@Q1 38-40 | P1@Q2 40-44 | P4@Q0 44-49 | P1@Q2 49-61 — ৪৪-এ P4-এর আগমনে Q2-র P1 থেমে আবার নিজের স্তরের সামনে ফিরেছে। এই নয়টি রান golden/ ফোল্ডারে রেখে make test-এ প্রতিবার diff করুন — scheduler-এর কোড বদলালে কোনো পুরোনো উত্তর ভাঙল কি না সঙ্গে সঙ্গে ধরা পড়বে।

একটি সৎ সীমাবদ্ধতা: simulator context switch-এর খরচ শূন্য ধরে, I/O burst নেই, আর সব সময় পূর্ণসংখ্যা। পাঠ্যবইয়ের অঙ্কের জন্য এটাই ঠিক মডেল, কিন্তু বাস্তব যন্ত্রের পূর্বাভাস নয়। README-তে লিখুন, আর এক্সটেনশন হিসেবে CPU–I/O burst-এর পালা আর প্রতি switch-এ s একক খরচ যোগ করুন — তখন পর্ব ৯-এর q/(q + s) দক্ষতা নিজেই মাপতে পারবেন।

৬ · প্রজেক্ট C — নিজের malloc: block, boundary tag আর alignment

পর্ব ৩১-এ fit কৌশল আর fragmentation কাগজে দেখেছেন; পর্ব ৩৮-এ দেখেছেন glibc-র malloc কীভাবে brk আর mmap দিয়ে kernel থেকে মেমরি আনে। এবার মাঝের অংশটি নিজে লিখবেন: kernel থেকে একবার পাওয়া একটি টানা arena-র ভেতরে ব্লক ভাগ করা। (C কোর্সের memory allocator পর্ব-এ একটি সরল রূপ আছে; এখানে লক্ষ্য boundary tag, explicit list, পরীক্ষা আর buddy.)

ব্লকের গঠন আর মাপের হিসাব

  ঠিকানা b                 b+8 (১৬-এর গুণিতক)               b+size-8
  ┌──────────────────┬──────────────────────────────────┬──────────────────┐
  │ header: size│used │ payload (ব্যবহারকারী পায় এটুকু)    │ footer: size│used │
  └──────────────────┴──────────────────────────────────┴──────────────────┘
  খালি ব্লকে payload-এর শুরুতে থাকে next আর prev pointer (explicit free list)
  • মাপ সবসময় ১৬-এর গুণিতক, তাই নিচের ৪টি bit সবসময় ০ — তার সবচেয়ে নিচের bit-এ “ব্যবহৃত কি না” রাখা যায়, আলাদা জায়গা লাগে না।
  • প্রয়োজনীয় মাপ = n + ৮ (header) + ৮ (footer), ১৬-এর পরের গুণিতকে তোলা: need = (n + 16 + 15) & ~15. যেমন n = ২০ → ৩৬ → ৪৮. খালি ব্লকে দুটি pointer আর দুটি tag রাখতে হয়, তাই ন্যূনতম ব্লক ৩২.
  • Alignment: x86-64 Linux-এ malloc-এর ঠিকানা ১৬-এর গুণিতক হয় (alignof(max_align_t) = ১৬), যাতে long double বা SSE-র ১৬-byte নির্দেশ নিরাপদে চলে। কৌশল: arena নিজে ১৬-aligned, প্রথম header বসে ৮ byte পরে, আর সব মাপ ১৬-এর গুণিতক — তাই প্রতিটি payload ১৬-এর গুণিতকে পড়ে।
  • Boundary tag কেন: header দিয়ে পরের ব্লক পাওয়া সহজ (b + size). আগের ব্লক খুঁজতে হলে তার footer পড়ুন — ঠিক b − ৮-এ। ফলে free-এর সময় দুই প্রতিবেশীই O(১)-এ দেখা যায়।
free(b)-এর সময়আগের ব্লকপরের ব্লকফল
ক্ষেত্র ১ব্যবহৃতব্যবহৃতশুধু b খালি হয়
ক্ষেত্র ২ব্যবহৃতখালিb + পরের = এক ব্লক, শুরু b-তে
ক্ষেত্র ৩খালিব্যবহৃতআগের + b, শুরু আগেরটিতে
ক্ষেত্র ৪খালিখালিতিনটি মিলে এক ব্লক

নিচের মূল অংশে ২৫৬ byte-এর arena, first fit, split (বাকি অন্তত ৩২ হলে) আর দুই দিকের coalescing. খোঁজা হয় implicit list-এ — header ধরে ধরে পুরো arena হেঁটে। প্রতিটি অপারেশনের পর show() ব্লকের তালিকা ছাপে আর invariant যাচাই করে।

#include <stdio.h>
#define ARENA 256                     /* bytes we manage; header = footer = 8 */
static _Alignas(16) unsigned char heap[8 + ARENA];
static unsigned char *const lo = heap + 8, *const hi = heap + 8 + ARENA;
#define TAG(p) (*(size_t *)(p))                        /* size | used-bit */
#define SIZE(b) (TAG(b) & ~(size_t)15)
static void mark(unsigned char *b, size_t sz, int used) { TAG(b) = TAG(b + sz - 8) = sz | used; }
static void *my_malloc(size_t n) {
    size_t need = (n + 16 + 15) & ~(size_t)15;   /* + tags, round up to 16 */
    for (unsigned char *b = lo; b < hi; b += SIZE(b))            /* first fit */
        if (!(TAG(b) & 1) && SIZE(b) >= need) {
            size_t rest = SIZE(b) - need;              /* split if >= 32 left */
            if (rest < 32) need = SIZE(b); else mark(b + need, rest, 0);
            mark(b, need, 1); return b + 8;            /* payload after header */
        }
    return NULL;
}
static void my_free(void *p) {
    unsigned char *b = (unsigned char *)p - 8; size_t sz = SIZE(b);
    if (b + sz < hi && !(TAG(b + sz) & 1)) sz += SIZE(b + sz);   /* next free? */
    if (b > lo && !(TAG(b - 8) & 1)) { b -= SIZE(b - 8); sz += SIZE(b); } /* prev */
    mark(b, sz, 0);                              /* one big free block */
}
static void show(const char *op) {              /* block list + invariants */
    size_t total = 0; int ok = 1, prev_free = 0; printf("%-18s", op);
    for (unsigned char *b = lo; b < hi; b += SIZE(b)) {
        int used = TAG(b) & 1; total += SIZE(b);
        printf(" [%3d %3zu %c]", (int)(b - lo), SIZE(b), used ? 'U' : 'F');
        ok &= TAG(b + SIZE(b) - 8) == TAG(b) && !(prev_free && !used)  /* tags, coalesced */
              && (size_t)(b + 8) % 16 == 0;  prev_free = !used;  /* aligned */
    }
    printf("  %s\n", ok && total == ARENA ? "OK" : "BROKEN");
}
#define DO(op) do { op; show(#op); } while (0)
int main(void) {
    char *a, *b, *c; mark(lo, ARENA, 0); show("init");
    DO(a = my_malloc(20)); DO(b = my_malloc(40)); DO(c = my_malloc(10));
    DO(my_free(b)); DO(my_free(a)); DO(a = my_malloc(60)); DO(my_free(c));
}

আউটপুট (প্রতিটি [offset size U/F] একটি ব্লক; offset arena-র শুরু থেকে):

init               [  0 256 F]  OK
a = my_malloc(20)  [  0  48 U] [ 48 208 F]  OK
b = my_malloc(40)  [  0  48 U] [ 48  64 U] [112 144 F]  OK
c = my_malloc(10)  [  0  48 U] [ 48  64 U] [112  32 U] [144 112 F]  OK
my_free(b)         [  0  48 U] [ 48  64 F] [112  32 U] [144 112 F]  OK
my_free(a)         [  0 112 F] [112  32 U] [144 112 F]  OK
a = my_malloc(60)  [  0  80 U] [ 80  32 F] [112  32 U] [144 112 F]  OK
my_free(c)         [  0  80 U] [ 80 176 F]  OK

মাপগুলো সূত্র মেনে: ২০ → ৪৮, ৪০ → ৬৪, ১০ → ৩২, ৬০ → ৮০. my_free(b) ক্ষেত্র ১, my_free(a) ক্ষেত্র ২ (মিলে ১১২), a = my_malloc(60) সেই ১১২-তে বসে ৩২ বাকি রাখল, আর শেষ my_free(c) ক্ষেত্র ৪ — তিনটি মিলে ১৭৬. প্রতিটি OK মানে: header = footer, payload ১৬-এর গুণিতকে, পাশাপাশি দুটি খালি ব্লক নেই, আর যোগফল ২৫৬ = used + free. DO(op) macro অপারেশনের লেখাটিই লেবেল হিসেবে ছাপে (#op).

২৫৬ byte-এর arena (offset arena-র শুরু থেকে; tag = size আর F/U)
অপারেশন
ব্লক-মানচিত্র
যে tag লেখা হলো
প্রতিবেশী যাচাই
invariant
  1. init. পুরো arena একটি খালি ব্লক: offset ০-তে header 256 F, শেষ ৮ byte-এ (offset ২৪৮) footer 256 F. ছবিতে F মানে নিচের bit ০ (খালি), U মানে ১ (ব্যবহৃত) — মেমরিতে আসলে লেখা থাকে 256 বা 49 (48 + 1).
  2. a = malloc(20) → ৪৮, split. first fit-এ প্রথম ব্লকটিই যথেষ্ট। আগে বাকি অংশ চিহ্নিত হয় (offset ৪৮-এ header 208 F, পুরোনো footer-এর জায়গায় 208 F), তারপর নিজের অংশ (48 U, footer offset ৪০-এ)। ব্যবহারকারী পায় offset ৮ — ১৬-এর গুণিতক ঠিকানা।
  3. b = malloc(40) → ৬৪. offset ০ ব্যবহৃত, header-এর মাপ দেখে লাফ দিয়ে ৪৮-এ: ২০৮ ≥ ৬৪, ভাগ করে ১৪৪ বাকি থাকে offset ১১২-তে।
  4. c = malloc(10) → ৩২. ১০ byte চাইলেও ৩২ byte-এর ব্লক — এর ১৬ byte tag, আর ৬ byte ভরাট; এটাই internal fragmentation. বাকি ১১২ খালি থাকে offset ১৪৪-এ।
  5. free(b) — ক্ষেত্র ১. আগের footer (offset ৪০) আর পরের header (offset ১১২) দুটিই বলে ব্যবহৃত। শুধু b-র দুই tag-এর used-bit ০ হয়। দুটি খালি ব্লক আছে, কিন্তু পাশাপাশি নয় — invariant ঠিক।
  6. free(a) — ক্ষেত্র ২. আগে কেউ নেই; পরের header (offset ৪৮) খালি → ৪৮ + ৬৪ = ১১২, header offset ০-তে, footer ১০৪-এ। পুরোনো ftr@40 আর hdr@48 এখন খালি জায়গার ভেতরের আবর্জনা — কেউ আর পড়বে না।
  7. a = malloc(60) → ৮০. first fit offset ০-এর ১১২-তে; ১১২ − ৮০ = ৩২, তাই ভাগ — offset ৮০-তে ৩২ byte-এর খালি ব্লক। ৩২-এর কম বাকি থাকলে পুরোটাই দেওয়া হতো, কারণ ১৬ byte-এ tag আর pointer আঁটে না।
  8. free(c) — ক্ষেত্র ৪. c-র ঠিক আগের ৮ byte (offset ১০৪) বাঁয়ের ব্লকের footer: 32 F, তাই তার শুরু ১১২ − ৩২ = ৮০. ডানের header (১৪৪) খালি, ১১২. তিনটি মিলে ৩২ + ৩২ + ১১২ = ১৭৬. footer না থাকলে বাঁয়ের ব্লক খুঁজতে শুরু থেকে হাঁটতে হতো।

Explicit free list — খোঁজা শুধু খালি ব্লকে

Implicit list-এ malloc প্রতিবার ব্যবহৃত ব্লকগুলোও পার হয় — heap-এ হাজার ব্লক থাকলে ধীর। Explicit free list-এ খালি ব্লকের payload-এর ভেতরেই next আর prev রাখা হয় — খালি জায়গা তো কেউ ব্যবহার করছে না। malloc শুধু এই তালিকা হাঁটে; free coalescing-এর সময় প্রতিবেশীকে তালিকা থেকে O(১)-এ সরায় (doubly linked বলেই সম্ভব), তারপর জোড়া লাগানো ব্লকটি সামনে বসায় (LIFO). নিচের পূর্ণ সংস্করণে এটি, সাথে একটি heap_check() আর এলোমেলো পরীক্ষা — অংশ ৭-এ তার ফল।

/* myalloc.c — malloc/free over a fixed arena: explicit (doubly linked) free list,
 * first fit, splitting, coalescing with boundary tags, 16-byte alignment,
 * a heap checker and a randomised test with a fixed seed.
 * Build: gcc -Wall -Wextra -O2 -o myalloc myalloc.c
 *
 * Block layout (sizes are multiples of 16, minimum 32):
 *   [ header: size|used ][ payload ...                          ][ footer: size|used ]
 *   a FREE block keeps two pointers at the start of its payload: next, prev   */
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define ARENA   (64 * 1024)
#define TAGSZ   8
#define MINBLK  32
static _Alignas(16) unsigned char heap[TAGSZ + ARENA];   /* +8: payloads land on 16 */
static unsigned char *const lo = heap + TAGSZ, *const hi = heap + TAGSZ + ARENA;

typedef struct Free { struct Free *next, *prev; } Free;
static Free *free_head;                                   /* LIFO explicit list */

static size_t tag(const unsigned char *b)      { size_t v; memcpy(&v, b, sizeof v); return v; }
static void   set_tag(unsigned char *b, size_t v) { memcpy(b, &v, sizeof v); }
static size_t bsize(const unsigned char *b)    { return tag(b) & ~(size_t)15; }
static int    used(const unsigned char *b)     { return (int)(tag(b) & 1); }
static void   mark(unsigned char *b, size_t sz, int u) { set_tag(b, sz | (size_t)u); set_tag(b + sz - TAGSZ, sz | (size_t)u); }
static Free  *node(unsigned char *b)           { return (Free *)(void *)(b + TAGSZ); }
static unsigned char *blk(Free *f)             { return (unsigned char *)f - TAGSZ; }

static void list_push(unsigned char *b) {
    Free *f = node(b);
    f->prev = NULL; f->next = free_head;
    if (free_head) free_head->prev = f;
    free_head = f;
}
static void list_remove(unsigned char *b) {
    Free *f = node(b);
    if (f->prev) f->prev->next = f->next; else free_head = f->next;
    if (f->next) f->next->prev = f->prev;
}

void my_init(void) { mark(lo, ARENA, 0); free_head = NULL; list_push(lo); }

void *my_malloc(size_t n) {
    if (n == 0 || n > ARENA) return NULL;
    size_t need = (n + 2 * TAGSZ + 15) & ~(size_t)15;
    if (need < MINBLK) need = MINBLK;
    for (Free *f = free_head; f; f = f->next) {             /* first fit on the list */
        unsigned char *b = blk(f);
        size_t sz = bsize(b);
        if (sz < need) continue;
        list_remove(b);
        if (sz - need >= MINBLK) {                          /* split: tail stays free */
            mark(b + need, sz - need, 0);
            list_push(b + need);
            sz = need;
        }
        mark(b, sz, 1);
        return b + TAGSZ;
    }
    return NULL;                                            /* out of memory */
}

void my_free(void *p) {
    if (!p) return;
    unsigned char *b = (unsigned char *)p - TAGSZ;
    if (b < lo || b >= hi || ((uintptr_t)p & 15) || !used(b)) {
        fprintf(stderr, "my_free: invalid or double free %p\n", p);
        abort();
    }
    size_t sz = bsize(b);
    unsigned char *next = b + sz;
    if (next < hi && !used(next)) { list_remove(next); sz += bsize(next); }        /* case 2 */
    if (b > lo && !used(b - TAGSZ)) {                                               /* case 3 */
        unsigned char *prev = b - bsize(b - TAGSZ);
        list_remove(prev); sz += bsize(prev); b = prev;
    }
    mark(b, sz, 0);                                         /* case 1/4: one block */
    list_push(b);
}

/* invariants: tags agree, sizes sane, aligned, used + free = arena,
 * no two adjacent free blocks, the free list holds exactly the free blocks  */
int heap_check(size_t *used_bytes, size_t *free_bytes, size_t *largest) {
    size_t u = 0, fr = 0, big = 0; int nfree = 0, prev_free = 0;
    for (unsigned char *b = lo; b < hi; b += bsize(b)) {
        size_t sz = bsize(b);
        if (sz < MINBLK || sz % 16 || b + sz > hi || tag(b + sz - TAGSZ) != tag(b)) return 0;
        if ((uintptr_t)(b + TAGSZ) % 16) return 0;
        if (!used(b) && prev_free) return 0;               /* missed coalescing */
        prev_free = !used(b);
        if (used(b)) u += sz; else { fr += sz; nfree++; if (sz > big) big = sz; }
    }
    if (u + fr != ARENA) return 0;
    int nlist = 0;
    for (Free *f = free_head; f; f = f->next, nlist++) {
        unsigned char *b = blk(f);
        if (b < lo || b >= hi || used(b) || (f->next && f->next->prev != f) || nlist > nfree) return 0;
    }
    if (nlist != nfree) return 0;
    *used_bytes = u; *free_bytes = fr; *largest = big;
    return 1;
}

/* ---------------- randomised test with a fixed seed ---------------- */
static uint32_t rng = 2026;
static uint32_t xorshift(void) { rng ^= rng << 13; rng ^= rng >> 17; rng ^= rng << 5; return rng; }

int main(void) {
    enum { SLOTS = 400, OPS = 100000 };
    static unsigned char *ptr[SLOTS]; static size_t req[SLOTS];
    size_t live_req = 0, ok_allocs = 0, oom = 0;
    my_init();
    for (int op = 1; op <= OPS; op++) {
        int s = (int)(xorshift() % SLOTS);
        if (ptr[s]) {                                       /* free: check the pattern first */
            for (size_t i = 0; i < req[s]; i++)
                if (ptr[s][i] != (unsigned char)s) { printf("OVERLAP detected at op %d\n", op); return 1; }
            my_free(ptr[s]); ptr[s] = NULL; live_req -= req[s];
        } else {
            size_t n = 1 + xorshift() % (xorshift() % 8 == 0 ? 2048 : 128);   /* mostly small */
            if ((ptr[s] = my_malloc(n))) { memset(ptr[s], s, n); req[s] = n; live_req += n; ok_allocs++; }
            else oom++;
        }
        size_t u, f, big;
        if (!heap_check(&u, &f, &big)) { printf("INVARIANT broken at op %d\n", op); return 1; }
        if (op % 25000 == 0)
            printf("op %6d: used %5zu B (requested %5zu), free %5zu B, largest free %5zu, ext-frag %4.1f%%, internal %4.1f%%\n",
                   op, u, live_req, f, big, f ? 100.0 * (1.0 - (double)big / f) : 0.0,
                   u ? 100.0 * (double)(u - live_req) / u : 0.0);
    }
    for (int s = 0; s < SLOTS; s++) my_free(ptr[s]);     /* free everything */
    size_t u, f, big;
    int ok = heap_check(&u, &f, &big);
    printf("%zu successful mallocs, %zu out-of-memory; after freeing all: %s, one block of %zu B\n",
           ok_allocs, oom, ok ? "invariants OK" : "BROKEN", big);
    return 0;
}

দুটি খুঁটিনাটি: tag পড়া-লেখায় memcpy — compiler এটিকে একটি সাধারণ load/store বানায়, কিন্তু C-র type-নিয়ম (strict aliasing) ভাঙে না; আর my_free ভুল ঠিকানা বা দুবার free ধরলে abort() করে — নিঃশব্দে heap নষ্ট হওয়ার চেয়ে জোরে ভেঙে পড়া অনেক ভালো। glibc-ও “double free detected” বলে ঠিক এটাই করে।

৭ · Allocator পরীক্ষা — invariant, এলোমেলো trace, fragmentation আর buddy

Allocator-এর বাগ প্রায়ই সঙ্গে সঙ্গে crash করে না; ভুল একটি tag হাজার অপারেশন পরে অন্য কারও ডেটা নষ্ট করে। তাই “আমার উদাহরণে চলেছে” যথেষ্ট নয় — দরকার এমন শর্ত যা প্রতিটি অপারেশনের পর সত্য থাকবে, আর এমন ইনপুট যা আপনি নিজে ভেবে বের করতেন না। এটিই invariant আর randomised testing-এর জুটি।

Allocator-এর invariant (heap_check যা যাচাই করে)
  • সীমা আর মাপ: প্রতিটি ব্লক arena-র ভেতরে, মাপ ১৬-এর গুণিতক আর ≥ ৩২, header = footer.
  • Alignment: প্রতিটি payload-এর ঠিকানা ১৬-এর গুণিতক।
  • হিসাব মেলে: সব ব্লক পরপর হেঁটে arena-র শেষে পৌঁছানো যায়, আর used + free = arena — অর্থাৎ ফাঁক বা দ্বৈত গণনা নেই।
  • Coalescing সম্পূর্ণ: পাশাপাশি দুটি খালি ব্লক নেই।
  • তালিকা আর heap একমত: free list-এর প্রতিটি নোড খালি ব্লক, next->prev ফিরে আসে, আর তালিকার দৈর্ঘ্য = খালি ব্লকের সংখ্যা।
  • কোনো overlap নেই: প্রতিটি বরাদ্দের payload তার নিজস্ব byte দিয়ে ভরা হয় (slot নম্বর), free-এর আগে যাচাই করা হয় নকশাটি অক্ষত কি না — দুটি ব্লক একই জায়গা পেলে একজনের লেখা অন্যজনেরটি মুছবে।

myalloc.c-এর main ঠিক এটাই করে: ৬৪ KiB arena, ৪০০টি slot, ১ লাখ অপারেশন, xorshift নামের ছোট pseudo-random generator, seed ২০২৬. প্রতিটি অপারেশনের পর heap_check(). একটি আসল রান (Linux, gcc):

op  25000: used 30448 B (requested 25976), free 35088 B, largest free  2080, ext-frag 94.1%, internal 14.7%
op  50000: used 39504 B (requested 34850), free 26032 B, largest free  1360, ext-frag 94.8%, internal 11.8%
op  75000: used 36656 B (requested 32072), free 28880 B, largest free  1440, ext-frag 95.0%, internal 12.5%
op 100000: used 35888 B (requested 30935), free 29648 B, largest free  1456, ext-frag 95.1%, internal 13.8%
49526 successful mallocs, 1155 out-of-memory; after freeing all: invariants OK, one block of 65536 B

ঠিকতার খবর ভালো: ১ লাখবার যাচাইয়ে কোনো invariant ভাঙেনি, overlap নেই, আর সব free করার পর arena আবার একটি অখণ্ড ৬৫৫৩৬ byte-এর ব্লক। কিন্তু মানের দিকে সতর্কসংকেত: প্রায় ২৯ KB খালি থাকলেও সবচেয়ে বড় খালি ব্লক দেড় KB-এর মতো; external fragmentation (১ − সবচেয়ে বড় খালি ÷ মোট খালি) প্রায় ৯৫%, ১১৫৫টি অনুরোধ ব্যর্থ। Internal fragmentation (tag আর ১৬-এ তোলার অপচয়) ১২–১৫%. সম্ভাব্য কারণ: LIFO তালিকা সদ্য-মুক্ত ছোট ব্লক সামনে রাখে, আর ২ KB পর্যন্ত অনুরোধ তাদের মাঝে জায়গা পায় না। এটিই পরের benchmark-এর প্রশ্ন — address-ordered তালিকা বা best fit-এ সংখ্যাটি কমে কি?

Fragmentation চোখে দেখা — তিন fit কৌশল, একই trace

নিচের simulator একই ধরনের allocator-কে মডেল করে (ব্লকের তালিকা হিসেবে, byte হিসেবে নয়), যাতে কৌশল বদলে তুলনা সহজ হয়। ১০২৪ byte-এর arena, নির্দিষ্ট seed-এর ৩০০০ অপারেশন; প্রতিটির পর invariant যাচাই; first fit-এর arena-মানচিত্র প্রতি ১০০০ অপারেশনে।

// Randomised trace (fixed seed) on a 1024-byte arena: first / next / best fit.
// After EVERY operation the invariants are checked. Same trace for each policy.
function run(policy, show) {
  let seed = 2026, rover = 0, fails = 0, fragFails = 0, fragSum = 0, bad = 0;
  const rand = () => (seed = (seed * 16807) % 2147483647) / 2147483647;
  let blocks = [{ off: 0, size: 1024, used: false }]; const live = [];
  const freeSizes = () => blocks.filter(b => !b.used).map(b => b.size);
  for (let op = 1; op <= 3000; op++) {
    if (live.length && rand() < 0.45) {                       // free a random block
      live.splice(Math.floor(rand() * live.length), 1)[0].used = false;
      blocks = blocks.reduce((acc, x) => { const l = acc[acc.length - 1];   // coalesce
        if (l && !l.used && !x.used) l.size += x.size; else acc.push(x); return acc; }, []);
    } else {
      const need = 16 * Math.ceil((8 + rand() * 112 + 16) / 16);   // payload + tags
      const fit = blocks.filter(b => !b.used && b.size >= need);
      const b = policy === "best" ? fit.sort((x, y) => x.size - y.size)[0]
              : policy === "next" ? (fit.find(x => x.off >= rover) || fit[0]) : fit[0];
      if (!b) { fails++; if (freeSizes().reduce((s, x) => s + x, 0) >= need) fragFails++; }
      else { const rest = b.size - need;                       // split if >= 32 left
        if (rest >= 32) { blocks.splice(blocks.indexOf(b) + 1, 0, { off: b.off + need, size: rest, used: false }); b.size = need; }
        b.used = true; live.push(b); rover = b.off + b.size; }
    }
    let off = 0;                                               // invariants
    blocks.forEach((b, i) => { if (b.off !== off || (i && !b.used && !blocks[i - 1].used)) bad++; off += b.size; });
    if (off !== 1024) bad++;                                   // used + free = arena
    const free = freeSizes(), tot = free.reduce((s, x) => s + x, 0);
    fragSum += tot ? 1 - Math.max(...free) / tot : 0;           // external fragmentation
    if (show && op % 1000 === 0)
      console.log(String(op).padStart(4) + " " + blocks.map(b => (b.used ? "#" : ".").repeat(b.size / 16)).join(""));
  }
  console.log(`${policy.padEnd(5)} failed ${String(fails).padStart(3)} (enough total free: ${String(fragFails).padStart(3)})` +
              `  avg ext-frag ${(fragSum / 30).toFixed(1)}%  invariant violations ${bad}`);
}
console.log("first-fit map, one char = 16 bytes (# used, . free):");
run("first", true); run("next", false); run("best", false);

আউটপুট (নির্ধারিত — প্রতিবার একই):

first-fit map, one char = 16 bytes (# used, . free):
1000 ##############..................................................
2000 ##############....#########.....................................
3000 #####..########...........####.......#############........###...
first failed 306 (enough total free: 219)  avg ext-frag 36.2%  invariant violations 0
next  failed 279 (enough total free: 152)  avg ext-frag 37.5%  invariant violations 0
best  failed 299 (enough total free: 150)  avg ext-frag 29.4%  invariant violations 0

৩০০০-এর লাইনে খালি জায়গা (.) পাঁচ টুকরোয় ছড়ানো — external fragmentation-এর ছবি। “enough total free” কলামটি সবচেয়ে কাজের: কতবার অনুরোধ ব্যর্থ হয়েছে যদিও মোট খালি জায়গা যথেষ্ট ছিল — first fit-এ ২১৯, best fit-এ ১৫০ বার। এই trace-এ best fit-এর গড় fragmentation সবচেয়ে কম (২৯.৪%). কিন্তু এটি একটি মাত্র কৃত্রিম trace — seed বদলে কয়েকবার চালান, ফল দোলে; পর্ব ৩১-এর শিক্ষাই ফেরে: দাবি করার আগে অনেক trace-এ মাপুন। invariant violations 0 তিনবারই — মডেলটি নিজেও পরীক্ষিত।

Buddy রূপান্তর

পর্ব ৩১-এর buddy system একই সমস্যার আরেক উত্তর: সব ব্লক ২-এর ঘাত, আর buddy-র ঠিকানা = offset XOR size — boundary tag ছাড়াই প্রতিবেশী পাওয়া যায়। নিচের সংস্করণ Linux-এর page allocator-এর মতো প্রতিটি ব্লকের order ব্লকের বাইরে একটি অ্যারেতে রাখে, তাই ৬৪ KiB চাইলে সত্যিই ৬৪ KiB-এর ব্লক — ব্লকের ভেতরে header রাখলে ৬৪ KiB + header-এর জন্য ১২৮ KiB লাগত।

/* buddy.c — binary buddy allocator over a 1 MiB arena, 4 KiB minimum block.
 * The order of every block is kept OUTSIDE the block (like Linux's struct page),
 * so a 64 KiB request really gets 64 KiB.  buddy(offset, size) = offset XOR size. */
#include <stdio.h>
#include <string.h>

#define MIN_ORDER 12                      /* 4 KiB  */
#define MAX_ORDER 20                      /* 1 MiB  */
#define NBLK (1 << (MAX_ORDER - MIN_ORDER))
static _Alignas(4096) unsigned char arena[1 << MAX_ORDER];
static signed char free_order[NBLK];      /* order of a FREE block starting here, else -1 */
static signed char used_order[NBLK];      /* order of a USED block starting here, else -1 */
static int head[MAX_ORDER + 1];           /* free list per order: block index, -1 = empty */
static int nxt[NBLK], prv[NBLK];          /* doubly linked, so a buddy is removed in O(1) */

static void push(int i, int k) {
    free_order[i] = (signed char)k; prv[i] = -1; nxt[i] = head[k];
    if (head[k] >= 0) prv[head[k]] = i;
    head[k] = i;
}
static void unlink_blk(int i, int k) {
    free_order[i] = -1;
    if (prv[i] >= 0) nxt[prv[i]] = nxt[i]; else head[k] = nxt[i];
    if (nxt[i] >= 0) prv[nxt[i]] = prv[i];
}

void buddy_init(void) {
    memset(free_order, -1, sizeof free_order); memset(used_order, -1, sizeof used_order);
    for (int k = 0; k <= MAX_ORDER; k++) head[k] = -1;
    push(0, MAX_ORDER);
}
void *buddy_alloc(size_t n) {
    int need = MIN_ORDER;
    while (need <= MAX_ORDER && ((size_t)1 << need) < n) need++;   /* round up to 2^k */
    int k = need;
    while (k <= MAX_ORDER && head[k] < 0) k++;                      /* smallest big-enough */
    if (k > MAX_ORDER) return NULL;
    int i = head[k];
    unlink_blk(i, k);
    while (k > need) { k--; push(i + (1 << (k - MIN_ORDER)), k); }  /* split; keep low half */
    used_order[i] = (signed char)need;
    return arena + ((size_t)i << MIN_ORDER);
}
void buddy_free(void *p) {
    int i = (int)(((unsigned char *)p - arena) >> MIN_ORDER), k = used_order[i];
    used_order[i] = -1;
    while (k < MAX_ORDER) {
        int b = i ^ (1 << (k - MIN_ORDER));                         /* the buddy's index */
        if (free_order[b] != k) break;                              /* busy or split: stop */
        unlink_blk(b, k);
        if (b < i) i = b;                                           /* merged block starts low */
        k++;
    }
    push(i, k);
}
static void show(const char *what) {          /* block map in KiB: A=used, .=free */
    printf("%-14s", what);
    for (int i = 0; i < NBLK; ) {
        int k = free_order[i] >= 0 ? free_order[i] : used_order[i];
        printf(" %s%d", free_order[i] >= 0 ? "." : "U", 1 << (k - 10));
        i += 1 << (k - MIN_ORDER);
    }
    printf("\n");
}
int main(void) {
    buddy_init(); show("start");
    void *A = buddy_alloc(100 * 1024); show("A = 100K");
    void *B = buddy_alloc(240 * 1024); show("B = 240K");
    void *C = buddy_alloc(64 * 1024);  show("C = 64K");
    void *D = buddy_alloc(256 * 1024); show("D = 256K");
    buddy_free(B); show("free B");
    buddy_free(A); show("free A");
    buddy_free(C); show("free C");
    buddy_free(D); show("free D");
    return 0;
}
start          .1024
A = 100K       U128 .128 .256 .512
B = 240K       U128 .128 U256 .512
C = 64K        U128 U64 .64 U256 .512
D = 256K       U128 U64 .64 U256 U256 .256
free B         U128 U64 .64 .256 U256 .256
free A         .128 U64 .64 .256 U256 .256
free C         .512 U256 .256
free D         .1024

প্রতিটি লাইন পর্ব ৩১-এর stepper-এর ব্লক-মানচিত্রের সাথে হুবহু মেলে (U = ব্যবহৃত, . = খালি, সংখ্যা KiB-এ)। free C-তে একটি মুক্তি থেকে তিন ধাপের জোড়া: ৬৪ + ৬৪ → ১২৮, + ১২৮ → ২৫৬, + ২৫৬ → ৫১২; তারপর ৫১২-এর buddy (offset ৫১২ KiB থেকে) আস্ত খালি নয় — ভাগ করা, তাতে D ব্যবহৃত — তাই থামা।

তুলনা: buddy-তে free আর coalescing খুব দ্রুত ও সরল, external fragmentation সীমিত; দাম internal fragmentation — ১০০K চাইলে ১২৮K. Boundary-tag allocator যেকোনো মাপ দেয়, কিন্তু খোঁজা আর fragmentation-এর হিসাব কঠিন। এজন্যই বাস্তবে দুটি স্তর: Linux kernel page-এর জন্য buddy, তার ওপর ছোট বস্তুর জন্য slab (পর্ব ৩৮)।

৮ · Mini-OS — তিন প্রজেক্ট বাস্তব kernel-এ কোথায়, আর code review-এর প্রশ্ন

আপনার প্রজেক্টবাস্তবেবাস্তবটি আর কী কী করে
mysh2 (~১৬৫ লাইন)bash, zsh, dash; Windows-এ cmd.exe আর PowerShellপূর্ণ ব্যাকরণ (quote, variable, glob, &&, function), job control, history, line editing, script. Windows-এ fork নেই — CreateProcess নতুন process বানায় আর handle উত্তরাধিকার আলাদা নিয়মে।
schedsim (~১৪৩ লাইন)Linux-এর fair scheduler — CFS, ৬.৬ থেকে EEVDF (পর্ব ২১); Windows-এর priority-ভিত্তিক schedulerপ্রতিটি CPU-র আলাদা queue, load balancing, nice/weight, cgroup, real-time class, power আর cache-এর হিসাব। “ঘটনা” আসে timer interrupt, wakeup আর I/O থেকে।
myalloc (~১৩৯ লাইন)glibc malloc (ptmalloc), jemalloc, tcmalloc; Windows-এ heap managerমাপ অনুযায়ী আলাদা তালিকা (bins, segregated list), থ্রেড-প্রতি cache (tcache), একাধিক arena, বড় অনুরোধে সরাসরি mmap, নিরাপত্তা-যাচাই।
buddy.c (~৭৬ লাইন)Linux-এর page allocator; তার ওপর kmalloc আর slab (SLUB)প্রতিটি NUMA node আর zone-এর আলাদা free area, CPU-প্রতি page তালিকা, migrate type, compaction, watermark.

একজন reviewer যা জিজ্ঞেস করবেন

অভিজ্ঞ প্রকৌশলীরা code review-এ প্রথমে “চলে কি না” নয়, “ভাঙে কোথায়” খোঁজেন। নিজের কোডে এই প্রশ্নগুলো নিজেই আগে জিজ্ঞেস করুন:

  • Shell: exec ব্যর্থ হলে child কি _exit করে, নাকি লুপে ফিরে দ্বিতীয় shell হয়? echo hi > f | wc-এ লেখক child-এর কাছে pipe-এর মূল fd খোলা থেকে যায় কি? (মূল অংশে থাকে — O_CLOEXEC/pipe2 বা স্পষ্ট close সমাধান।) handler-এ printf আছে কি? fork ব্যর্থ হলে আগের child-কে কে reap করবে?
  • Scheduler: tie-নিয়ম কোথায় লেখা, পরীক্ষা কোথায়? সবাই একসাথে এলে, burst ০ হলে, ইনপুট ফাঁকা হলে কী হয়? key()-এ overflow হতে পারে কি?
  • Allocator: malloc(0), বিশাল n (size_t overflow), ভুল pointer বা দুবার free — আচরণ কী? thread-safe কি? সবচেয়ে খারাপ ক্ষেত্রে malloc কত ধীর? benchmark কোন যন্ত্রে, কোন option-এ?

এক্সটেনশন — যেখান থেকে প্রজেক্ট আলাদা হয়ে ওঠে

Job control

প্রতিটি job-কে setpgid-এ নিজের group, tcsetpgrp-এ terminal দেওয়া-ফেরত নেওয়া, WUNTRACED-এ Ctrl+Z ধরা, jobs/fg/bg builtin. আসল terminal-এ পরীক্ষা করতে হবে।

MLFQ boost tuning

পর্ব ১৯-এর Rule 5: প্রতি S সময়ে সবাইকে Q0-তে তোলা। S বদলে একটি লম্বা CPU-কাজ আর অনেক ছোট কাজের মিশ্রণে starvation আর response time-এর টানাপোড়েন মাপুন; ফল একটি গ্রাফে।

Thread-safe allocator

প্রথমে একটি global mutex; তারপর থ্রেড-প্রতি ছোট cache (একই মাপের কয়েকটি ব্লক, lock ছাড়া), খালি হলে বা উপচে পড়লে কেন্দ্রীয় তালিকায় — tcache-এর ধারণা। পর্ব ১৫-এর থ্রেড দিয়ে ৮ থ্রেডে benchmark, ThreadSanitizer-এ যাচাই।

আরও গভীরে: glibc-র malloc প্রতিটি থ্রেডের জন্য মাপভিত্তিক ছোট tcache তালিকা রাখে (২.২৬ থেকে), না পেলে arena-র fast/small/large bin-এ যায়, আর বড় অনুরোধ (ডিফল্টে মোটামুটি ১২৮ KiB-এর ওপরে, গতিশীলভাবে বদলায়) সরাসরি mmap করে। শুরুর দিকে tcache-এ প্রায় কোনো যাচাই ছিল না বলে heap-বাগ বেশি বিপজ্জনক হয়েছিল; পরে double-free যাচাই আর ২.৩২-এ “safe-linking” (তালিকার pointer একটি গোপন মান দিয়ে mangle করা) যোগ হয়। Linux kernel-এর page allocator-এর সর্বোচ্চ order সাধারণ কনফিগারেশনে ১০ — ৪ KiB page-এ ৪ MiB-এর ব্লক।

নিজেকে মূল্যায়নের রুব্রিক

প্রতিটি প্রজেক্ট ১০০ নম্বরে। ৭০-এর নিচে হলে প্রথমে “ঠিকতা” আর “পরীক্ষা” সারি পূর্ণ করুন — ফিচার পরে।

মাপকাঠিনম্বরপূর্ণ নম্বরের শর্ত
ঠিকতা৩০Shell: পরীক্ষা-পরিকল্পনার সব সারি পাস। Scheduler: যাচাই-টেবিলের নয়টি রান হুবহু। Allocator: ১ লাখ এলোমেলো অপারেশনে ০ invariant-ভঙ্গ।
ত্রুটি সামলানো১৫প্রতিটি system call-এর ফেরত মান দেখা; ভুল ইনপুটে বার্তা, crash নয়; double free ধরা
পরীক্ষা আর পুনরাবৃত্তি২০make test এক কমান্ডে সব চালায়; golden ফাইল; নির্দিষ্ট seed; sanitizer-এ পরিষ্কার
নকশা আর পড়ার সহজতা১৫একটি নীতি = একটি key; এক ফাংশন এক কাজ; tie-নিয়ম আর block layout মন্তব্যে
মাপজোখ১০fragmentation বা গতির অন্তত একটি benchmark, যন্ত্র ও option-সহ, দুটি নকশার তুলনা
ডকুমেন্টেশন১০README: কী, কীভাবে build ও test, নকশার সিদ্ধান্ত, সীমাবদ্ধতা, নমুনা আউটপুট

৯ · Portfolio আর ইন্টারভিউ — কাজটি কীভাবে দেখাবেন

বাংলাদেশের সফটওয়্যার কোম্পানি, বিদেশি কোম্পানির remote চাকরি বা উচ্চশিক্ষার আবেদন — অনেক ক্ষেত্রে নিয়োগকারী বা শিক্ষক আপনার GitHub খুলে দেখতে পারেন। সবাই দেখেন না, কিন্তু যিনি দেখেন তাঁর চোখে পরিচ্ছন্ন একটি সিস্টেম-প্রজেক্ট সাধারণ “to-do অ্যাপ” থেকে আলাদা পড়ে — এখানে বোঝার গভীরতা লুকানো যায় না।

README-র কাঠামো

# mini-os-capstone — shell, CPU-scheduler simulator, malloc in C
এক লাইনে: কী, কোন OS-এ (Linux/WSL), কোন compiler.
## Build & test      make && make test   (সব পরীক্ষা কয়েক সেকেন্ডে)
## Shell              ব্যাকরণ, builtin, signal-নীতি, একটি নমুনা সেশন
## Scheduler          নীতি, tie-নিয়ম, যাচাই-টেবিল (পাঠ্যবইয়ের উত্তর বনাম ফল)
## Allocator          block layout ছবি, invariant, fragmentation-এর মাপ ও গ্রাফ
## Design decisions   কেন event-driven, কেন LIFO, কেন SIGCHLD-এ শুধু লিখে রাখা
## Limitations        যা নেই (job control, quote, thread-safety) — সৎভাবে
## What I learned     তিন-চার লাইন, নিজের ভাষায়

README-র “কী পারে না” অংশটি অনেকে বাদ দেন, অথচ অভিজ্ঞ পাঠকের কাছে এটাই সবচেয়ে বিশ্বাসযোগ্য — দেখায় আপনি নিজের কাজের সীমা জানেন। commit-ইতিহাসও গল্প বলে: “add pipe”, “fix fd leak in redirect”, “add heap_check”; একটি বিশাল “final” commit সেই গল্প মুছে দেয়।

ইন্টারভিউতে উপস্থাপন

  • দুই মিনিটের ব্যাখ্যা তৈরি রাখুন — বাংলায় আর ইংরেজিতে: সমস্যা, নকশা, একটি কঠিন বাগ আর কীভাবে ধরলেন (যেমন “wc ঝুলে থাকত, কারণ parent লেখার মুখ বন্ধ করত না; /proc/self/fd দিয়ে ধরেছি”)। বাগের গল্প প্রায়ই সবচেয়ে বেশি মনে থাকে।
  • সংখ্যা দিয়ে কথা বলুন, কিন্তু নিজের মাপা সংখ্যা: “১ লাখ এলোমেলো অপারেশনে ০ invariant-ভঙ্গ”, “LIFO-তে external fragmentation প্রায় ৯৫%, address-ordered-এ এত” — যন্ত্র আর শর্তসহ।
  • প্রশ্ন আন্দাজ করুন: “Windows-এ কীভাবে করতেন?” “এক কোটি প্রসেসে simulator কত দ্রুত?” “malloc-কে thread-safe করবেন কীভাবে?” — অংশ ৮-এর review-প্রশ্নগুলোই সাধারণত আসে।
  • AI-এর ব্যবহার নিয়ে সৎ থাকুন: কোন অংশে AI সাহায্য করেছে, আর আপনি কীভাবে যাচাই করেছেন — বললে ক্ষতি নেই, কিন্তু নিজে ব্যাখ্যা করতে না পারা কোড portfolio-তে রাখা ঝুঁকিপূর্ণ।

পরীক্ষা ও ইন্টারভিউতে:

  • pipe-এর পর parent লেখার মুখ বন্ধ না করলে কী হয়? — পাঠক কখনো EOF পায় না, কারণ একটি লেখক তখনো আছে; wc-এর মতো প্রোগ্রাম ঝুলে থাকে।
  • shell SIGINT উপেক্ষা করলে child-ও কেন উপেক্ষা করবে? — exec-এ SIG_IGN টিকে থাকে (handler থাকে না), তাই exec-এর আগে child-কে SIG_DFL-এ ফেরাতে হয়।
  • SIGCHLD handler-এ printf কেন নয়? — printf async-signal-safe নয়; main-এর printf-এর মাঝখানে handler চললে buffer-এর lock বা অবস্থা নষ্ট হতে পারে।
  • Boundary tag-এর footer কেন লাগে? — free-এর সময় আগের ব্লকের মাপ আর অবস্থা O(১)-এ জানতে, যাতে শুরু থেকে না হেঁটেই coalesce করা যায়।
  • Event-driven simulation-এ পরের ঘটনার সময় কী? — min(চলতিটির শেষ, quantum ফুরানো, পরের আগমন); কেউ ready না থাকলে সরাসরি পরের আগমনে লাফ।

১০ · AI-এর যুগে ক্যাপস্টোন — AI সহকারী, লেখক আপনি

“C-তে pipe-সহ একটি shell লিখে দাও” — AI এক মিনিটে ২০০ লাইন দেবে, প্রথম দেখায় চলবেও। কিন্তু এই তিন প্রজেক্টের বাগ একবার চালিয়ে ধরা পড়ে না: fd leak শুধু নির্দিষ্ট pipeline-এ ঝোলে, signal race হাজারে একবার ঘটে, ভুল tag হাজার অপারেশন পরে ডেটা নষ্ট করে। বারবার দেখা ভুল:

AI-এর ভুলযা ঘটেকীভাবে ধরবেন
parent-এ বা অন্য child-এ pipe-এর মুখ খোলা রাখাpipeline ঝুলে থাকে, বিশেষ করে wc, sort-এর মতো EOF-নির্ভর প্রোগ্রামেls /proc/self/fd pipeline-এর শেষে; sort দিয়ে পরীক্ষা
SIGCHLD handler-এ printf, malloc, বা foreground-এর status কেড়ে নেওয়াকদাচিৎ deadlock বা হারানো exit status — “আমার যন্ত্রে চলে”man 7 signal-safety-র তালিকা মিলিয়ে দেখা; লুপে ১০০০ বার ছোট কমান্ড চালানো
exec-এর আগে SIGINT-কে SIG_DFL-এ না ফেরানোshell-এ চালানো কোনো প্রোগ্রাম Ctrl+C-তে থামে নাআসল terminal-এ sleep 10 + Ctrl+C
scheduler-এ tie-নিয়ম অস্পষ্ট বা পাঠ্যবইয়ের উল্টো; গড় ভুল হিসাবসংখ্যা “কাছাকাছি” কিন্তু ভুলপর্ব ১৭–১৯-এর উত্তর golden ফাইলে; হাতে একটি ছোট টেবিল
malloc-এ alignment ৮, overflow যাচাই নেই, coalescing শুধু এক দিকেSSE কোডে crash, বিশাল n-এ ছোট ব্লক, ধীরে ধীরে fragmentationheap_check, পাশাপাশি-খালি invariant, malloc(SIZE_MAX) পরীক্ষা
Linux-এর কোড macOS-এ বা Windows-এ চলবে বলে দাবিmacOS-এ /proc নেই; Windows-এ fork নেইপ্রশ্নে OS বলুন; WSL বা Linux VM-এ চালান

AI-কে লেখক না বানিয়ে reviewer আর পরীক্ষক বানান — নিজের কোড দিয়ে নির্দিষ্ট প্রশ্ন করুন:

আমি Ubuntu 24.04-এ C-তে (gcc, -Wall -Wextra) একটি ছোট shell লিখেছি: একটি pipe, < আর >, & দিয়ে background, SIGCHLD handler. নিচে পুরো কোড দিলাম। ১. প্রতিটি process-এ (shell, দুই child) fork, dup2 আর close-এর পর কোন fd খোলা থাকে, একটি টেবিলে দেখাও; কোথাও pipe-এর মুখ ফাঁস হলে লাইন নম্বরসহ বলো। ২. SIGCHLD handler-এ যা যা ডাকা হয়েছে, তার প্রতিটি async-signal-safe কি না man 7 signal-safety অনুযায়ী বলো; fork আর handler-এর মধ্যে কোনো race আছে কি? ৩. কোড বদলে দিও না — আগে ১০টি পরীক্ষার কমান্ড আর প্রত্যাশিত আউটপুট দাও, যেগুলো এই বাগগুলো ধরবে। ৪. তোমার কোনো দাবি Linux-নির্দিষ্ট হলে আলাদা করে চিহ্নিত করো।

AI-এর দেওয়া ক্যাপস্টোন-কোড রাখার আগে — চেকলিস্ট

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

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

প্র ০১২০১৪ সালের Shellshock বাগ bash-এ প্রায় ২৫ বছর লুকিয়ে ছিল। shell-এর মতো “সরল” প্রোগ্রামে এত পুরোনো বাগ থাকে কীভাবে, আর নিজের shell-এর নকশায় এ থেকে কী শেখার আছে?

২০১৪ সালের সেপ্টেম্বরে Stéphane Chazelas নামের একজন গবেষক দেখান যে bash environment variable-এর ভেতরে রাখা function-এর সংজ্ঞা পড়ার সময় সংজ্ঞার পরের লেখাও কমান্ড হিসেবে চালিয়ে দেয়। bash function-কে child process-এ পাঠাত একটি variable হিসেবে, যার মান () { দিয়ে শুরু; নতুন bash শুরু হওয়ার সময় সেটি parse করত — আর parser থামত না। ফলে যেকোনো প্রোগ্রাম যে বাইরের ইনপুট environment-এ রেখে bash চালায় (যেমন পুরোনো CGI ওয়েব-স্ক্রিপ্ট, কিছু DHCP client), সে আক্রমণকারীর কমান্ড চালাতে পারত। প্রথম patch-এর পর আরও কয়েকটি সংশ্লিষ্ট বাগ বেরোয়; শেষে bash function-এর variable-এ আলাদা নামের নকশা (prefix আর suffix) চালু করে। বৈশিষ্ট্যটি ১৯৮৯-এর কাছাকাছি সময়ে যোগ হয়েছিল বলে ধরা হয়।

কেন এত দিন লুকিয়ে? প্রথমত, shell-এর ব্যাকরণ বিশাল আর ঐতিহাসিক — quote, expansion, here-document, function, arithmetic — প্রতিটি স্তর আলাদা সময়ে আলাদা লোকের হাতে যোগ হয়েছে। দ্বিতীয়ত, “environment থেকে function import” এমন একটি কোণ যা সাধারণ ব্যবহারকারী কখনো ছোঁয় না, তাই কেউ পরীক্ষাও করত না। তৃতীয়ত, ঝুঁকি তৈরি হয় অন্য প্রোগ্রামের সাথে মিলে — bash একা নিরীহ, কিন্তু ওয়েব সার্ভার যখন বাইরের header environment-এ রাখে, তখন বিশ্বাসের সীমানা বদলে যায়।

নিজের shell-এর জন্য শিক্ষা তিনটি। ব্যাকরণ ছোট আর লিখিত রাখুন — যা বোঝেন না, তাতে স্পষ্ট ত্রুটি দিন। Parse শেষ হওয়ার আগে কিছু চালাবেন না। আর parser-কে fuzz করুন: এলোমেলো লাইন দিয়ে লাখবার চালিয়ে দেখুন shell crash করে কি না, বা লাইনে নেই এমন কিছু চালায় কি না।

মূল উপলব্ধি: parser হলো বিশ্বাসের সীমানা — বাইরের লেখা সেখানে ঢুকে কমান্ড হয়ে বেরোয়। যত ছোট আর স্পষ্ট ব্যাকরণ, তত কম লুকানো দরজা।

প্র ০২পাঠ্যবইয়ের উত্তর মেলানো simulator কি বলতে পারে বাস্তব Linux-এ কোন scheduler ভালো হবে? Linux নিজে নতুন scheduler বাছাইয়ের সময় কীভাবে যাচাই করে?

পারে না, আর এটাই সৎ উত্তর। আমাদের simulator একটি মডেল: burst আগে থেকে জানা, context switch বিনামূল্যে, এক CPU, কোনো I/O নেই, cache বা memory-র প্রভাব নেই। পাঠ্যবইয়ের অঙ্ক শেখার জন্য এই সরলীকরণই ঠিক, কারণ নীতির যুক্তি আলাদা করে দেখা যায়। কিন্তু বাস্তবে একটি প্রসেস CPU থেকে সরলে তার cache ঠান্ডা হয়ে যায়, অন্য CPU-তে গেলে আরও বেশি; ছোট quantum-এর আসল খরচ এখানেই। SRTF “সেরা” — কিন্তু burst জানা যায় না, শুধু অনুমান করা যায় (পর্ব ১৭-এর exponential averaging).

Simulation-এর দুই ধরন আছে। Synthetic: আমাদের মতো হাতে বা এলোমেলোভাবে বানানো ভার। Trace-driven: আসল যন্ত্র থেকে রেকর্ড করা ঘটনা (কখন কে জাগল, কতক্ষণ চলল) simulator-এ আবার চালানো — বাস্তবের অনেক কাছাকাছি, কিন্তু একটি সমস্যা থেকে যায়: নীতি বদলালে প্রোগ্রামের আচরণও বদলায় (যে আগে CPU পেত সে আগে I/O করত), আর trace তা ধরতে পারে না। Linux-এর ইতিহাসে kernel-এর scheduler কোড user space-এ এনে simulate করার চেষ্টাও হয়েছে (LinSched-এর মতো প্রকল্প), কিন্তু শেষ পর্যন্ত মূল যাচাই হয় আসল kernel-এ benchmark দিয়ে।

২০০৭ সালে Ingo Molnár-এর CFS যখন ২.৬.২৩-এ আসে, আর ২০২৩ সালে Peter Zijlstra-র EEVDF-ভিত্তিক পরিবর্তন যখন ৬.৬-এ মেশে (পর্ব ২১), দুবারই mailing list-এ মাসের পর মাস নানা ভারের benchmark — ডেস্কটপের সাড়া, ডেটাবেস, compile, latency-মাপার টুল — নিয়ে আলোচনা হয়েছে, আর কিছু ভারে ফল খারাপ হওয়ার রিপোর্টে আরও সংশোধন এসেছে। অর্থাৎ বাস্তবের যাচাই হলো অনেক রকম আসল ভারে মাপ, আর regression ধরার জন্য নিয়মিত পরীক্ষা।

তাহলে simulator-এর মূল্য কী? নীতির সংজ্ঞার নির্ভুল বাস্তবায়ন (উত্তর মেলে বলেই জানি tie-নিয়ম ঠিক), আর একটি নিয়ন্ত্রিত পরীক্ষাগার — একটি জিনিস বদলে প্রভাব দেখা, যেমন MLFQ-এর boost-এর সময়।

মূল উপলব্ধি: মডেল প্রশ্নের উত্তর দেয় মডেলের অনুমানের ভেতরে। কোন অনুমান করেছেন তা লিখে রাখুন — তাহলেই ফলাফলের সীমাও স্পষ্ট হয়।

প্র ০৩আমাদের allocator-এ এলোমেলো পরীক্ষায় external fragmentation প্রায় ৯৫%. fragmentation কি অনিবার্য? গবেষণা আর বাস্তব allocator এ নিয়ে কী বলে?

তাত্ত্বিকভাবে, হ্যাঁ — কোনো allocator সব ক্রমে ভালো করতে পারে না। ১৯৭০-এর দশকে J. M. Robson দেখান: যে allocator ব্লক সরাতে পারে না (C-র malloc সরাতে পারে না, কারণ প্রোগ্রামের pointer তার ঠিকানা ধরে রেখেছে), তার জন্য এমন প্রতিকূল অনুরোধ-ক্রম তৈরি করা যায় যে প্রয়োজনীয় মোট মেমরি সবচেয়ে বেশি live মেমরির একটি গুণিতকে পৌঁছায় — আর সেই গুণিতক মোটামুটি সবচেয়ে বড় আর সবচেয়ে ছোট ব্লকের অনুপাতের লগারিদমের সমানুপাতিক। অর্থাৎ সবচেয়ে খারাপ ক্ষেত্র শুধু নীতি বদলে এড়ানো যায় না।

কিন্তু বাস্তবের প্রোগ্রাম প্রতিকূল নয়। ১৯৯৫ সালে Paul Wilson ও সহকর্মীরা “Dynamic Storage Allocation: A Survey and Critical Review”-এ দেখান যে কৃত্রিম, এলোমেলো trace দিয়ে allocator বিচার করা বিভ্রান্তিকর: আসল প্রোগ্রাম একসাথে অনেক একই মাপের বস্তু বানায়, আর একসাথে ছাড়ে (একটি request শেষে তার সব ডেটা)। আসল trace-এ address-ordered first fit আর best fit-এর মতো ভালো নীতির fragmentation অনেক কম পাওয়া গেছে। আমাদের পরীক্ষা ইচ্ছা করেই কঠিন: সম্পূর্ণ এলোমেলো মাপ, এলোমেলো ক্রমে free, LIFO তালিকা — এখানে ৯৫% আসলে বলে “এই ভারে এই নীতি খারাপ”, “allocator ভুল” নয়।

বাস্তব allocator তাই মাপভিত্তিক কৌশল নেয়। jemalloc আর tcmalloc ছোট বস্তুকে মাপের শ্রেণিতে (size class) ভাগ করে, প্রতিটি শ্রেণির জন্য আলাদা page — একই page-এ শুধু একই মাপ, তাই সেখানে external fragmentation প্রায় নেই, বদলে সামান্য internal fragmentation (শ্রেণির মাপে তোলা)। Linux kernel-এর slab-ও একই ধারণা, প্রতিটি kernel বস্তুর ধরনের জন্য আলাদা cache. আর যেখানে বস্তু সরানো সম্ভব — Java বা .NET-এর garbage collector — সেখানে compaction দিয়ে fragmentation সরাসরি মুছে ফেলা যায়, যেমন পর্ব ৩১-এর compaction.

মূল উপলব্ধি: fragmentation-এর বিরুদ্ধে সবচেয়ে বড় অস্ত্র হলো ভারের গঠন জানা। একটি সংখ্যা দেখানোর সময় সবসময় বলুন সেটি কোন trace-এ মাপা।

প্র ০৪আমাদের allocator একটি global তালিকা দিয়ে চলে। ৩২টি থ্রেড একসাথে malloc ডাকলে কী হবে, আর আধুনিক allocator এই সমস্যা কীভাবে সমাধান করেছে?

প্রথমে ঠিকতার সমস্যা: দুটি থ্রেড একসাথে একই খালি ব্লক তালিকা থেকে সরালে দুজনেই একই জায়গা পাবে — overlap, অর্থাৎ ডেটা নষ্ট (পর্ব ২২-এর race condition). সহজ সমাধান একটি global mutex — malloc আর free-এর পুরোটা lock-এর ভেতরে। এবার ঠিক, কিন্তু ধীর: ৩২টি থ্রেড একটি lock-এর জন্য লাইনে দাঁড়ায়, আর lock-এর cache line এক CPU থেকে আরেকটিতে ছোটাছুটি করে। বহু-থ্রেড সার্ভারে malloc তখন সবচেয়ে বড় বাধা হয়ে উঠতে পারে।

২০০০ সালে Emery Berger ও সহকর্মীদের Hoard allocator এই সমস্যার একটি পরিচিত সমাধান দেখায়: প্রতিটি থ্রেড (বা CPU)-এর নিজের heap, আর একটি global heap শুধু ভারসাম্যের জন্য। তারা আরেকটি সূক্ষ্ম বিপদও চিহ্নিত করে — “blowup”: এক থ্রেড malloc করে, আরেক থ্রেড free করে (producer-consumer), আর মুক্ত মেমরি ভুল থ্রেডের heap-এ জমতে থাকে, যা কেউ আর ব্যবহার করে না। থ্রেড-প্রতি নকশা তাই দুটি জিনিস একসাথে চায়: lock ছাড়া দ্রুত পথ, আর মাঝে মাঝে মেমরি কেন্দ্রে ফেরত।

আজকের allocator-গুলো এর রূপভেদ। Google-এর tcmalloc নাম থেকেই thread-caching; নতুন সংস্করণে CPU-প্রতি cache, Linux-এর restartable sequences দিয়ে lock ছাড়া। Jason Evans-এর jemalloc (প্রথমে FreeBSD-র জন্য, পরে Firefox ও বহু সার্ভারে) একাধিক arena-তে থ্রেড ভাগ করে, সাথে থ্রেড-cache. glibc ২.২৬ (২০১৭) থেকে tcache যোগ করে — প্রতিটি থ্রেডের ছোট মাপের তালিকা, প্রতিটিতে ডিফল্টে সর্বোচ্চ ৭টি ব্লক, lock ছাড়া; না পেলে arena, যেখানে lock আছে।

এর একটি নিরাপত্তার দিকও আছে: দ্রুত পথে যাচাই কম থাকলে heap-বাগ (use-after-free, double free) আক্রমণে কাজে লাগানো সহজ হয়। তাই glibc পরে tcache-এ double-free শনাক্তকরণ আর তালিকার pointer লুকানোর (safe-linking) সুরক্ষা যোগ করে। প্রতিরক্ষার মূল নীতি একই থাকে: বাগ নিজের কোডে খুঁজে বের করুন — ASan আর heap checker দিয়ে।

মূল উপলব্ধি: concurrency-তে “ঠিক” আর “দ্রুত” আলাদা ধাপ — আগে একটি lock দিয়ে ঠিক করুন, মাপুন, তারপর যেখানে বাধা সেখানে থ্রেড-প্রতি cache.

প্র ০৫Linux শুরু হয়েছিল একজন ছাত্রের “শখের প্রজেক্ট” হিসেবে, আর xv6-এর মতো শিক্ষণ-OS আজ বহু বিশ্ববিদ্যালয়ে পড়ানো হয়। শেখার জন্য বানানো ছোট সিস্টেম-প্রজেক্টের আসল মূল্য কোথায়?

১৯৯১ সালের ২৫ আগস্ট ফিনল্যান্ডের ছাত্র Linus Torvalds comp.os.minix নিউজগ্রুপে লেখেন, তিনি ৩৮৬ (৪৮৬) AT ক্লোনের জন্য একটি ফ্রি অপারেটিং সিস্টেম বানাচ্ছেন — “just a hobby, won't be big and professional like gnu”. তার আগে তিনি শিখছিলেন Andrew Tanenbaum-এর MINIX থেকে, যা নিজেই অপারেটিং সিস্টেম পড়ানোর জন্য বানানো (১৯৮৭)। অর্থাৎ পৃথিবীর সবচেয়ে বড় OS-প্রকল্পটি শুরু হয়েছিল একটি শিক্ষণ-OS পড়ে আর নিজের হাতে একটি ছোট সংস্করণ লিখে — ঠিক ক্যাপস্টোনের ধাঁচে।

শিক্ষণ-সিস্টেমের ধারা লম্বা। MIT ২০০৬ সালে xv6 বানায় — Sixth Edition Unix-এর আধুনিক, কয়েক হাজার লাইনের রূপ, যা পুরোটা একজন মানুষ এক মৌসুমে পড়ে ফেলতে পারেন (পর্ব ৫৮)। Carnegie Mellon-এর পাঠ্যবই “Computer Systems: A Programmer's Perspective”-এর সাথে যুক্ত ল্যাবে ছাত্ররা নিজের shell আর malloc লেখেন; উইসকনসিনের Arpaci-Dusseau দম্পতির বিনামূল্যের বই OSTEP-এর প্রজেক্টেও shell আর scheduler আছে। আমাদের তিন প্রজেক্ট এই পরিচিত ঐতিহ্যেরই অংশ — কারণ এগুলো ছোট হলেও আসল সমস্যার পুরো স্বাদ দেয়।

মূল্য তিন জায়গায়। প্রথম, বোঝা যাচাই হয়: “pipe বুঝি” আর “pipe দিয়ে ঝুলে না থাকা shell লিখতে পারি” — দুটি আলাদা দক্ষতা; দ্বিতীয়টি প্রথমটির ফাঁক বের করে দেয়। দ্বিতীয়, debugging-এর অভিজ্ঞতা: ঝুলে থাকা wc, হারানো exit status, নষ্ট heap — এগুলো ধরতে গিয়ে যে পদ্ধতি শেখেন (ছোট পুনরুৎপাদন, invariant, /proc, sanitizer), তা যেকোনো বড় সিস্টেমে কাজে লাগে। তৃতীয়, প্রমাণ: একটি পরিচ্ছন্ন repository দেখায় আপনি শুধু পরীক্ষার জন্য পড়েননি — এটি চাকরির আবেদন বা উচ্চশিক্ষার আলোচনায় কথা বলার মতো কিছু দেয়।

open-source-এ অবদান রাখতে চাইলে ছোট থেকে শুরু করুন — কোনো প্রকল্পে একটি পরীক্ষা বা ডকুমেন্টেশন যোগ করা; বড় প্রকল্পে প্রথম patch প্রায়ই এমনই হয়।

মূল উপলব্ধি: ছোট সিস্টেম নিজে লেখার উদ্দেশ্য বড়টিকে প্রতিস্থাপন নয় — বড়টিকে পড়ার, প্রশ্ন করার আর একদিন তাতে অবদান রাখার যোগ্যতা অর্জন।

অনুশীলন

প্রথম দুটি কাগজ-কলমে, তৃতীয়টি আউটপুট অনুমান, চতুর্থটি কোড — পূর্ণ schedsim.c-তে যোগ করে চালান।

  1. RR হাতে (numerical): চারটি প্রসেস — P1 (AT ০, BT ৭), P2 (২, ৪), P3 (৪, ১), P4 (৫, ৪); q = ৩; একই মুহূর্তে আগমন আর quantum ফুরালে নতুনটি আগে সারিতে। Gantt chart, প্রতিটির CT, TAT, WT, RT আর গড় বের করুন। তারপর ./schedsim rr 3 দিয়ে মেলান।

    t = ০: [P1] → P1 ০–৩ (বাকি ৪); এর মধ্যে t = ২-এ P2 আসে। t = ৩: সারি [P2 P1] → P2 ৩–৬ (বাকি ১); t = ৪-এ P3, t = ৫-এ P4 আসে, সারি [P1 P3 P4], তারপর P2 পেছনে: [P1 P3 P4 P2]. P1 ৬–৯ (বাকি ১) → [P3 P4 P2 P1]. P3 ৯–১০ শেষ। P4 ১০–১৩ (বাকি ১) → [P2 P1 P4]. P2 ১৩–১৪ শেষ, P1 ১৪–১৫ শেষ, P4 ১৫–১৬ শেষ।

    │P1 0-3│P2 3-6│P1 6-9│P3 9-10│P4 10-13│P2 13-14│P1 14-15│P4 15-16│

    CT = ১৫, ১৪, ১০, ১৬. TAT = ১৫, ১২, ৬, ১১ → গড় ১১.০০. WT = ৮, ৮, ৫, ৭ → গড় ৭.০০. RT = ০, ১, ৫, ৫ → গড় ২.৭৫. schedsim-এর আসল আউটপুট: avg TAT 11.00 avg WT 7.00 avg RT 2.75 dispatches 8 — হুবহু মেলে।

  2. Allocator হাতে (trace): ২৫৬ byte-এর arena, উপরের মূল অংশের নিয়ম (header + footer ৮ + ৮, মাপ ১৬-এর গুণিতক, বাকি ≥ ৩২ হলে split, first fit, দুই দিকে coalesce). অপারেশন: x = malloc(30), y = malloc(50), z = malloc(8), free(x), w = malloc(40), free(y), free(z), v = malloc(170). প্রতিটির পর ব্লক-তালিকা লিখুন। v কি পাবে? শেষে external fragmentation কত?

    মাপ: ৩০ → ৪৮, ৫০ → ৮০, ৮ → ৩২, ৪০ → ৬৪, ১৭০ → ১৯২. (মূল অংশের main বদলে চালানো আসল আউটপুট:)

    x = my_malloc(30)  [  0  48 U] [ 48 208 F]  OK
    y = my_malloc(50)  [  0  48 U] [ 48  80 U] [128 128 F]  OK
    z = my_malloc(8)   [  0  48 U] [ 48  80 U] [128  32 U] [160  96 F]  OK
    my_free(x)         [  0  48 F] [ 48  80 U] [128  32 U] [160  96 F]  OK
    w = my_malloc(40)  [  0  48 F] [ 48  80 U] [128  32 U] [160  64 U] [224  32 F]  OK
    my_free(y)         [  0 128 F] [128  32 U] [160  64 U] [224  32 F]  OK
    my_free(z)         [  0 160 F] [160  64 U] [224  32 F]  OK
    v = my_malloc(170) [  0 160 F] [160  64 U] [224  32 F]  OK
    v = NULL

    w-এর ৬৪ প্রথম খালি ব্লকে (৪৮) আঁটেনি, তাই first fit পরেরটিতে (৯৬) গেছে আর ৩২ বাকি রেখেছে। free(y) ক্ষেত্র ৩ (আগেরটি খালি), free(z)-ও ক্ষেত্র ৩. শেষে মোট খালি ১৬০ + ৩২ = ১৯২ — ঠিক v-এর দরকারের সমান — কিন্তু সবচেয়ে বড় খালি ব্লক ১৬০, তাই v = NULL. External fragmentation = ১ − ১৬০/১৯২ ≈ ১৬.৭%, আর এই একটি ব্যর্থতাই তার বাস্তব ফল।

  3. আউটপুট অনুমান করুন: উপরের মূল shell-এ (minishell_core.c) stdin-এ এই লাইনটি দিলে কী ছাপা হবে, আর a.txt-এ কী থাকবে? echo hi > a.txt | wc -c. bash-এ একই লাইন কী দেখায়? কোডে কোনো fd-সংক্রান্ত দুর্বলতা চোখে পড়ে কি?

    আসল রান: $ echo hi > a.txt | wc -c, তারপর 0 আর [exit 0]; পরে cat a.txt দেখায় hi. কারণ: লেখক child-এর > a.txt তার stdout-কে ফাইলে পাঠায় — redirection pipe-এর চেয়ে পরে প্রয়োগ হয়, তাই জেতে। pipe-এ কিছুই যায় না, echo শেষ হলে লেখার মুখ বন্ধ, wc EOF পেয়ে ০ ছাপায়। bash-ও একই আচরণ করে: 0.

    দুর্বলতা: child()-এ out ভেরিয়েবল ফাইলের fd দিয়ে বদলে যায়, কিন্তু pipe-এর মূল লেখার fd (৪) বন্ধ হয় না — echo exec হয় সেটি খোলা রেখে। এখানে ক্ষতি নেই কারণ echo দ্রুত শেষ হয়, কিন্তু লেখক যদি দীর্ঘক্ষণ চলত (যেমন sleep 100 > f | wc), wc ততক্ষণ EOF পেত না। সমাধান: redirection বসানোর আগে পুরোনো fd বন্ধ করা, বা pipe তৈরির সময় pipe2(fd, O_CLOEXEC) — তাহলে dup2 করা কপিটি ছাড়া বাকি সব exec-এ নিজে থেকেই বন্ধ হয়।

  4. কোড লিখুন — schedsim-এ HRRN: পর্ব ১৭-এর HRRN (non-preemptive; R = (অপেক্ষা + burst) ÷ burst, বড় R আগে) পূর্ণ schedsim.c-তে যোগ করুন। শর্ত: শুধু Policy, key() আর নামের তালিকা বদলাবে, লুপ নয়। পর্ব ১৭-এর ছয়-প্রসেসের টেবিলে উত্তর কী আসা উচিত?

    সমস্যা একটাই: R বর্তমান সময় t-এর ওপর নির্ভর করে, অথচ key() t জানে না। সবচেয়ে ছোট পরিবর্তন — একটি global now, প্রতিটি পাকে pick_next-এর আগে now = t;. ছোট key জেতে, তাই R-এর ঋণাত্মক; ভগ্নাংশ এড়াতে ১০৬ দিয়ে গুণ:

    typedef enum { FCFS, SJF, SRTF, PRIO, NPPRIO, RR, MLFQ, HRRN } Policy;
    static int now;                          /* set in the loop: now = t; */
    ...
    case HRRN: return -(long)(now - x->at + x->bt) * 1000000L / x->bt;
    ...
    const char *names[] = { ..., "mlfq", "hrrn" };   /* and i < 8 */

    preemptive()-এ HRRN নেই, তাই লুপ নিজে থেকেই non-preemptive আচরণ করে — নকশার সুফল। আসল রান (./schedsim hrrn < ch17.txt): Gantt: idle 0-1 | P1 1-4 | P2 4-10 | P3 10-14 | P5 14-16 | P4 16-21 | idle 21-23 | P6 23-26 আর avg TAT 7.17 avg WT 3.33 — পর্ব ১৭-এর টেবিলের ৭.১৭ আর ৩.৩৩-এর সাথে মেলে। t = ১০-এ P3-এর R = (৫ + ৪)/৪ = ২.২৫, P4-এর (৩ + ৫)/৫ = ১.৬, P5-এর (১ + ২)/২ = ১.৫ → P3. এটি golden ফাইলে যোগ করুন।

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

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

ক্রমএকটি shell ls | wc -l চালাতে যা যা করে, সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ✂️ পুরো লাইন parse করে দুটি কমান্ড আলাদা করা
  2. 🔗 pipe() দিয়ে পড়া আর লেখার দুই মুখ তৈরি
  3. 🍴 প্রথম fork; child-এ dup2 দিয়ে লেখার মুখ stdout-এ, তারপর exec ls
  4. 🍴 দ্বিতীয় fork; child-এ dup2 দিয়ে পড়ার মুখ stdin-এ, তারপর exec wc
  5. 🚪 parent-এ pipe-এর দুই মুখই বন্ধ
  6. ⏳ দুই child-এর জন্য waitpid; শেষ কমান্ডের status দেখানো

একটি shell sort data.txt | wc -l চালানোর সময় দুটি child ঠিকমতো fd সাজায়, কিন্তু parent pipe-এর লেখার মুখ বন্ধ করতে ভুলে যায়। কী ঘটবে?

  • sort কিছুই লিখতে পারবে না
  • wc কখনো EOF পায় না, তাই গোনা শেষ করে না — pipeline ঝুলে থাকে
  • sort SIGPIPE পেয়ে মারা যায়
  • wc ভুল সংখ্যা (দ্বিগুণ) ছাপায়

একটি shell শুরুতেই signal(SIGINT, SIG_IGN) ডাকে, আর child-এ কিছু না বদলে সরাসরি execvp("sleep", ...) করে। terminal-এ sleep চলার সময় Ctrl+C চাপলে কী হয়?

  • shell আর sleep দুজনেই মারা যায়
  • sleep মারা যায়, কারণ exec সব signal ডিফল্টে ফিরিয়ে দেয়
  • কেউ মরে না — sleep-ও SIGINT উপেক্ষা করে, কারণ SIG_IGN exec-এর পরেও থেকে যায়
  • shell sleep-কে SIGKILL পাঠায়

Round Robin, q = ২; প্রসেস (AT, BT): P1 (০, ৫), P2 (১, ৩), P3 (২, ১), P4 (৩, ২), P5 (৪, ৩). একই মুহূর্তে নতুন আগমন সময়-ফুরানো প্রসেসের আগে সারিতে ঢোকে। গড় waiting time কত?

  • ৫.৮
  • ৮.৬
  • ২.৪
  • ৪.৬

একটি allocator-এ প্রতিটি ব্লকে ৮ byte header আর ৮ byte footer, আর ব্লকের মাপ ১৬-এর গুণিতকে তোলা হয়। malloc(20)-এর জন্য ব্লক কত byte?

  • ৩২
  • ৩৬
  • ৪০
  • ৪৮

একটি buddy allocator-এ pool-এর শুরু থেকে offset ৩৮৪-এ ১২৮ byte-এর একটি ব্লক মুক্ত হলো। কোন offset-এর ব্লকটি তার buddy?

  • ৫১২
  • ০
  • ২৫৬
  • ৪৪৮

Event-driven CPU-scheduling simulator-এ (preemptive নীতি) একটি প্রসেস CPU পাওয়ার পর লুপ পরের ঘটনার সময় কীভাবে ঠিক করে?

  • চলতিটির বাকি burst, quantum-এর বাকি আর পরের আগমন পর্যন্ত সময় — এই তিনটির সবচেয়ে ছোটটি
  • সবসময় ১ একক পর
  • চলতিটির পুরো burst শেষ হলে
  • সব প্রসেসের গড় burst পরে

শব্দকোষ

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

মনে রাখুন

  1. Pipeline = pipe → fork → (child) dup2 + অব্যবহৃত মুখ close → exec, আর parent-এ দুই মুখই close; একটি লেখার মুখ খোলা থাকলেই পাঠক EOF পায় না। status শেষ কমান্ডের।
  2. Shell SIGINT উপেক্ষা করে, foreground child exec-এর আগে SIG_DFL-এ ফেরে (SIG_IGN আর blocked mask exec পেরিয়ে যায়); SIGCHLD handler শুধু reap করে লিখে রাখে, fork-এর আগে SIGCHLD block, অপেক্ষা sigsuspend-এ।
  3. Scheduler simulator = event-driven লুপ + একটি pick_next() + লিখিত tie-নিয়ম; পরের ঘটনা = min(শেষ, quantum, আগমন)। পাঠ্যবইয়ের উত্তর golden ফাইলে — regression test.
  4. Allocator: মাপ = (n + ১৬ + ১৫) & ~১৫, header/footer-এ size|used, first fit, বাকি ≥ ৩২ হলে split, footer দিয়ে বাঁয়ে আর header দিয়ে ডানে coalesce; payload ১৬-aligned.
  5. ঠিকতা প্রমাণ হয় invariant + নির্দিষ্ট seed-এর এলোমেলো trace দিয়ে; fragmentation মাপুন আর সবসময় বলুন কোন trace-এ। README, পরীক্ষা আর benchmark — প্রজেক্টের অর্ধেক মূল্য।

পরের পর্বে: কোর্সের শেষ মডিউল — পরীক্ষা ও ইন্টারভিউ প্রস্তুতি। প্রথমে numerical সমস্যার পূর্ণ সংকলন: scheduling, page replacement, Banker's আর disk scheduling, ধাপে ধাপে সমাধানসহ।

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

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