Foundationপ্রথম নীতি থেকে
LEVEL 4মাঝারি~১০ ঘণ্টাC

Unix Tools from Scratch

Unix Tools from Scratch

cat, wc, head, ls, grep — libc-র সুবিধা বাদ দিয়ে সরাসরি read/write/openat/getdents64 syscall দিয়ে। ছোট প্রজেক্ট, কিন্তু এখানেই buffering, partial read আর EINTR-এর মতো বাস্তব বিষয়গুলো প্রথম সামনে আসে।

মাইলস্টোন

আগে যা পড়া দরকার

কেন এই প্রজেক্ট

এগুলো এত সরল tool যে মনে হয় শেখার কিছু নেই। কিন্তু cat সঠিকভাবে লিখতে গিয়েই আপনি তিনটা জিনিসের মুখোমুখি হবেন যা বেশিরভাগ প্রোগ্রামার কখনো ভাবে না:

  1. read যতটা চেয়েছেন তার চেয়ে কম দিতে পারে — এবং সেটা error নয়, স্বাভাবিক
  2. write-ও আংশিক হতে পারে — বিশেষত pipe আর socket-এ
  3. Signal এলে syscall EINTR দিয়ে ফিরে আসতে পারে — retry করতে হবে

এই তিনটা না জানলে আপনার কোড ছোট ফাইলে কাজ করবে আর production-এ মাঝে মাঝে ডেটা হারাবে।

আর দ্বিতীয় শিক্ষা: buffering কেন আছে। ১ byte করে পড়লে ১ MB ফাইলে ১০ লক্ষ syscall — আপনি নিজে মেপে দেখবেন সেটা কত ধীর।

১. cat — সঠিকভাবে

#include \<unistd.h\>
#include \<errno.h\>

#define BUFSZ 65536

static ssize_t write_all(int fd, const char *buf, size_t n) {
    size_t off = 0;
    while (off \< n) {
        ssize_t w = write(fd, buf + off, n - off);
        if (w \< 0) {
            if (errno == EINTR) continue;      /* signal — আবার চেষ্টা */
            return -1;
        }
        off += (size_t)w;                      /* আংশিক write */
    }
    return (ssize_t)off;
}

static int cat_fd(int fd) {
    char buf[BUFSZ];
    for (;;) {
        ssize_t r = read(fd, buf, sizeof buf);
        if (r == 0) return 0;                  /* EOF */
        if (r \< 0) {
            if (errno == EINTR) continue;
            return -1;
        }
        if (write_all(STDOUT_FILENO, buf, (size_t)r) \< 0) return -1;
    }
}

write_all-এর loop-টা লক্ষ্য করুন। এটা ছাড়া আপনার cat একটা ধীর pipe-এ নীরবে ডেটা হারাবে।

২. Buffer size — নিজে মাপুন

dd if=/dev/urandom of=/tmp/test.bin bs=1M count=100

for sz in 1 64 512 4096 65536 1048576; do
    gcc -DBUFSZ=$sz -O2 -o mycat mycat.c
    /usr/bin/time -f "bufsz=$sz  %e s" ./mycat /tmp/test.bin > /dev/null
    strace -c -e trace=read,write ./mycat /tmp/test.bin > /dev/null 2>/tmp/sc
    grep -E "read|write" /tmp/sc
done

সাধারণ ফল:

bufszসময়read syscall
1~45 s104,857,600
64~0.8 s1,638,400
4096~0.09 s25,600
65536~0.05 s1,600
1 MB~0.05 s100

৪০৯৬-এর পর লাভ প্রায় থেমে যায় — কারণ তখন syscall আর bottleneck নয়, memory bandwidth। এই কারণেই stdio-র default buffer সাধারণত ৪ বা ৮ KB।

৩. ls — getdents64 সরাসরি

readdir একটা libc wrapper। নিচের syscall-টা দেখুন:

#include \<sys/syscall.h\>

struct linux_dirent64 {
    ino64_t        d_ino;
    off64_t        d_off;
    unsigned short d_reclen;
    unsigned char  d_type;
    char           d_name[];
};

char buf[32768];
int fd = open(path, O_RDONLY | O_DIRECTORY);
for (;;) {
    long n = syscall(SYS_getdents64, fd, buf, sizeof buf);
    if (n \<= 0) break;
    for (long off = 0; off \< n; ) {
        struct linux_dirent64 *d = (void *)(buf + off);
        write_all(1, d->d_name, strlen(d->d_name));
        write_all(1, "\n", 1);
        off += d->d_reclen;
    }
}

লক্ষ্য করুন directory একটা variable-length record-এর streamd_reclen দিয়ে পরেরটায় যেতে হয়। আর d_ino সেই inode number যা আপনি filesystem লেসনে দেখেছেন।

d_type কখনো DT_UNKNOWN হতে পারে (কিছু filesystem-এ), তখন lstat করতে হবে — একটা বাস্তব portability ফাঁদ।

৪. যাচাই

for f in /etc/passwd /usr/bin/ls /dev/null; do
    ./mycat "$f" | cmp - "$f" && echo "cat $f ok"
done

./mywc /etc/passwd | diff - \<(wc /etc/passwd) && echo "wc ok"
./myls /tmp | sort | diff - \<(ls -A /tmp | sort) && echo "ls ok"

Output byte-for-byte মিলতে হবে। না মিললে edge case খুঁজুন — খালি ফাইল, শেষে newline নেই এমন ফাইল, খুব লম্বা লাইন।

নিজেকে চ্যালেঞ্জ করুন

  1. catsendfile বা copy_file_range ব্যবহার করুন — zero-copy, kernel-এর ভেতরেই ডেটা যায়, userspace ছোঁয় না। Throughput তুলনা করুন
  2. wc -w তে UTF-8 সঠিকভাবে হ্যান্ডল করুন (Level 1-এর encoding লেসন)
  3. grep এ Boyer–Moore বা KMP লাগান আর naive-এর সাথে তুলনা করুন (Level 6-এর string algorithm)
  4. ls -l তে column alignment, আর -h human-readable size
  5. tail -finotify দিয়ে ফাইল বদলানোর জন্য অপেক্ষা করুন
  6. প্রতিটা tool-এ --help আর সঠিক exit code (0 সফল, 1 কিছু পাওয়া যায়নি, 2 error — grep-এর convention)