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
সঠিকভাবে লিখতে গিয়েই আপনি তিনটা জিনিসের মুখোমুখি হবেন যা
বেশিরভাগ প্রোগ্রামার কখনো ভাবে না:
readযতটা চেয়েছেন তার চেয়ে কম দিতে পারে — এবং সেটা error নয়, স্বাভাবিকwrite-ও আংশিক হতে পারে — বিশেষত pipe আর socket-এ- 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 s | 104,857,600 |
| 64 | ~0.8 s | 1,638,400 |
| 4096 | ~0.09 s | 25,600 |
| 65536 | ~0.05 s | 1,600 |
| 1 MB | ~0.05 s | 100 |
৪০৯৬-এর পর লাভ প্রায় থেমে যায় — কারণ তখন 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-এর stream —
d_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 নেই এমন ফাইল, খুব লম্বা লাইন।
নিজেকে চ্যালেঞ্জ করুন
catএsendfileবাcopy_file_rangeব্যবহার করুন — zero-copy, kernel-এর ভেতরেই ডেটা যায়, userspace ছোঁয় না। Throughput তুলনা করুনwc -wতে UTF-8 সঠিকভাবে হ্যান্ডল করুন (Level 1-এর encoding লেসন)grepএ Boyer–Moore বা KMP লাগান আর naive-এর সাথে তুলনা করুন (Level 6-এর string algorithm)ls -lতে column alignment, আর-hhuman-readable sizetail -f—inotifyদিয়ে ফাইল বদলানোর জন্য অপেক্ষা করুন- প্রতিটা tool-এ
--helpআর সঠিক exit code (0 সফল, 1 কিছু পাওয়া যায়নি, 2 error — grep-এর convention)