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

ফাইল — file descriptor, open/read/write আর access method

The File Interface — Attributes, Open-File Tables, Offsets, Locking, Access Methods and Durability

Taught in Bangla, this lesson defines a file and its attributes, inodes and timestamps, file types, operations from create to delete, the three-level open-file tables of descriptors, open file descriptions and inodes, sequential, direct, indexed and ISAM access methods, advisory and mandatory file locking, buffering, fsync and mmap durability, and the Windows view with handles and alternate data streams.

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

  • ফাইলের ধারণা, তার attribute, আর atime/mtime/ctime/birth time ঠিক কখন বদলায় — stat চালিয়ে নিজের চোখে
  • OS ফাইলের ধরন কীভাবে চেনে — Windows-এর extension বনাম Unix-এর magic number
  • ফাইলের মৌলিক কাজগুলো, আর open-file table-এর তিন স্তর — open, dup আর fork-এর পরে কে কী ভাগ করে
  • Sequential, direct, indexed আর ISAM access — রেকর্ডের offset, ব্লক নম্বর আর ডিস্ক-পড়ার সংখ্যার পূর্ণ হিসাব
  • File locking, buffered বনাম raw I/O, fsync, sparse file, আর Windows-এর handle ও alternate data stream

১ · ফাইল কী — নাম দেওয়া তথ্যের একটি বাক্স

আপনার ফোনে একটি ছবি তুললেন। গ্যালারিতে দেখাচ্ছে IMG_2041.jpg, আকার ৩ MB. কিন্তু ফোনের flash মেমরিতে “IMG_2041.jpg” নামে কোনো এক টুকরো জায়গা নেই। আছে হাজার হাজার ছোট ব্লক (সাধারণত ৪ KiB করে), যার কিছু হয়তো পাশাপাশি, কিছু ছড়ানো। কোন ব্লকগুলো মিলে এই ছবি, কোন ক্রমে — সেই হিসাব রাখে অপারেটিং সিস্টেম। আপনাকে দেখায় শুধু একটি নাম আর একটি টানা বাইটের সারি।

এটিই ফাইল-এর সংজ্ঞা: সেকেন্ডারি স্টোরেজে (ডিস্ক, SSD, পেনড্রাইভ) রাখা, একটি নাম দেওয়া, সম্পর্কিত তথ্যের সংগ্রহ। ব্যবহারকারীর দিক থেকে এটি স্টোরেজের সবচেয়ে ছোট একক — ডিস্কে কিছু রাখতে হলে কোনো না কোনো ফাইলের ভেতরেই রাখতে হয়। আর পর্ব ১-এ যে কথা বলেছিলাম, OS-এর বড় কাজ হলো বিমূর্ত রূপ (abstraction) দেওয়া: process যেমন CPU-র বিমূর্ত রূপ, virtual memory যেমন RAM-এর, ফাইল তেমনি ডিস্কের।

একটি সরকারি অফিসের ফাইল ক্যাবিনেটের কথা ভাবুন। প্রতিটি ফাইলের মলাটে একটি নাম আর নম্বর, ভেতরে কাগজ। কিন্তু অফিসে জায়গা কম, তাই একটি মোটা ফাইলের কাগজ হয়তো তিনটি আলাদা ড্রয়ারে রাখা। রেজিস্টার খাতায় লেখা আছে কোন কাগজ কোন ড্রয়ারে। আপনি কেরানিকে শুধু বলেন “অমুক নম্বর ফাইলের পাঁচ নম্বর পাতাটা দিন” — ড্রয়ার খোঁজার ঝামেলা তাঁর। এখানে কেরানি হলো OS, ড্রয়ার হলো ডিস্কের ব্লক, রেজিস্টার খাতা হলো inode (পর্ব ৪৩-এ ভেতরটা দেখবেন), আর “পাঁচ নম্বর পাতা” হলো একটি offset.

দুটি দৃষ্টিভঙ্গি — logical আর physical

Logical view — প্রোগ্রাম যা দেখে

বাইট ০ থেকে শুরু করে একটি টানা সারি। “বাইট ৪৭৩৬ থেকে ১২৮ বাইট পড়ো” — এভাবে কথা বলা যায়। কোনো ফাঁক নেই, কোনো ব্লক-সীমা নেই।

এই পর্বের বিষয়: file-system interface

Physical view — ডিস্কে যা আছে

নির্দিষ্ট আকারের ব্লক, প্রতিটির একটি নম্বর (LBA). ফাইলের ব্লক টানা না-ও হতে পারে; শেষ ব্লক আংশিক ভরা থাকতে পারে।

পর্ব ৪৩: allocation, inode, FAT

এই দুইয়ের মাঝখানে অনুবাদ করে file system — ext4, NTFS, APFS, FAT32. এই পর্বে আমরা অনুবাদের ওপরের দিকটা দেখব: প্রোগ্রাম কোন system call দিয়ে ফাইলের সাথে কথা বলে, kernel খোলা ফাইলের কী হিসাব রাখে, আর কীভাবে ফাইলের ভেতরে ঘোরাফেরা করা যায়। পর্ব ৪২-এ directory, link আর VFS, আর পর্ব ৪৩-এ ডিস্কে আসল বিন্যাস।

১৯৬০-এর দশকের IBM mainframe-এ ফাইল ছিল “dataset” — প্রতিটির একটি নির্দিষ্ট রেকর্ড-বিন্যাস (যেমন ৮০ বাইটের নির্দিষ্ট দৈর্ঘ্যের রেকর্ড, পাঞ্চ কার্ডের মাপে), আর তৈরির সময়ই আকার আর বিন্যাস ঘোষণা করতে হতো। ১৯৬৯–৭৪ সালে Bell Labs-এ Ken Thompson আর Dennis Ritchie Unix-এ একটি সাহসী সরলীকরণ করেন: ফাইল মানে শুধু বাইটের সারি, ভেতরে কী বিন্যাস তা OS জানে না, জানে প্রোগ্রাম। সঙ্গে আরেকটি ধারণা — টার্মিনাল, ডিস্ক, pipe সবই একই open/read/write/close দিয়ে ব্যবহার করা যায়। এই “প্রায় সবকিছুই ফাইল” দর্শন আজ Linux, macOS, Android-এর ভিত্তি; Windows-ও ফাইলকে বাইটের সারি হিসেবেই দেখে।

২ · ফাইলের পরিচয়পত্র — attribute, inode আর সময়ের হিসাব

ফাইলের ভেতরের ডেটা ছাড়াও OS প্রতিটি ফাইলের কিছু তথ্য রাখে — এগুলো ফাইলের attribute বা metadata. প্রায় সব OS-এর পাঠ্যবইয়ে একই তালিকা আসে:

AttributeমানেLinux-এ কোথায়
নাম (name)মানুষের পড়ার মতো নামinode-এ নয়, directory-তে (পর্ব ৪২)
Identifierfile system-এর ভেতরে ফাইলের অনন্য নম্বরinode নম্বর (ls -i)
Typeসাধারণ ফাইল, directory, symlink, device, pipe, socketmode-এর ওপরের বিট (ls -l-এর প্রথম অক্ষর)
Locationডেটা ডিস্কের কোন ব্লকেinode-এর block pointer বা extent (পর্ব ৪৩)
Sizeবাইটে আকার, আর আসলে কতটি ব্লক খরচst_size, st_blocks
Protectionকে পড়বে, লিখবে, চালাবে; মালিক কেmode-এর rwx বিট, uid, gid (বিস্তারিত পর্ব ৪৭)
সময় (timestamps)শেষ পড়া, শেষ লেখা, শেষ metadata বদল, তৈরিatime, mtime, ctime, btime

একটি বিষয় অনেকে খেয়াল করেন না: Unix-ধারার file system-এ ফাইলের নাম inode-এর অংশ নয়। Directory আসলে “নাম → inode নম্বর” জোড়ার একটি তালিকা। তাই একই inode-এর একাধিক নাম থাকতে পারে (hard link), আর inode-এ থাকে কতগুলো নাম তাকে দেখাচ্ছে তার গণনা — link count. এই কারণেই ফাইল “মোছা”-র system call-এর নাম unlink: এটি একটি নাম সরায়, ডেটা মুছে যায় শুধু তখন, যখন link count শূন্য আর কেউ ফাইলটি খুলে রাখেনি। Link-এর পুরো গল্প পর্ব ৪২-এ।

চারটি সময় — কোনটি ঠিক কী মাপে

এই অংশটি পরীক্ষা আর বাস্তব কাজ — দুই জায়গাতেই ভুল বোঝা হয় সবচেয়ে বেশি:

atime — access time

শেষ কখন ফাইলের ডেটা পড়া হয়েছে (read, cat, exec). কিন্তু mount-এর বিকল্পে আপডেটটি সীমিত বা বন্ধ থাকতে পারে — নিচে দেখুন।

ls -lu

mtime — modification time

শেষ কখন ফাইলের ডেটা বদলেছে (write, truncate). ls -l যে সময় দেখায়, make আর backup টুল যেটি দেখে — সেটি এটিই।

ls -l

ctime — change time

শেষ কখন inode বদলেছে: অনুমতি (chmod), মালিক, link count, নাম বদল (অধিকাংশ file system-এ) — আর ডেটা লিখলেও, কারণ তখন mtime আর size বদলায়। ctime তৈরির সময় নয়।

ls -lc

Birth time (btime/crtime)

ফাইল কখন তৈরি হয়েছিল। ext4, Btrfs, XFS (নতুন বিন্যাসে), NTFS, APFS রাখে; Linux-এ এটি পড়া যায় statx() দিয়ে (kernel ৪.১১ থেকে)। File system না রাখলে stat দেখায় -.

stat-এর Birth লাইন

আরেকটি গুরুত্বপূর্ণ পার্থক্য: atime আর mtime ব্যবহারকারী নিজেই যেকোনো মানে বসাতে পারেন (touch -d, utimensat()) — তাই ফাইলের “তারিখ” প্রমাণ হিসেবে দুর্বল। কিন্তু ctime সাধারণ ব্যবহারকারী ইচ্ছেমতো বসাতে পারেন না; atime/mtime বদলালেও ctime “এখন”-এ চলে যায়।

atime-এর খরচ — relatime আর noatime

নিয়ম মানলে প্রতিটি পড়া-র পর inode-এ একটি লেখা হতো — একটি ওয়েব সার্ভার সেকেন্ডে হাজার ফাইল পড়লে হাজারটি অপ্রয়োজনীয় ডিস্ক-লেখা। তাই Linux ২.৬.৩০ (২০০৯) থেকে ডিফল্ট mount বিকল্প relatime: atime আপডেট হয় শুধু তখন, যখন পুরোনো atime mtime বা ctime-এর চেয়ে আগের, অথবা ২৪ ঘণ্টার বেশি পুরোনো। noatime দিলে পড়ায় atime কখনো বদলায় না; strictatime দিলে প্রতিবার বদলায়। Windows-এর NTFS-এও last-access আপডেট অনেক সংস্করণে বন্ধ বা সীমিত থাকে (fsutil behavior query disablelastaccess দিয়ে দেখা যায়)। অর্থাৎ “atime বলছে ফাইলটি কেউ পড়েনি” — এটি নির্ভরযোগ্য সিদ্ধান্ত নয়।

এবার আসল Linux-এ দেখুন। স্ক্রিপ্টটি একটি ফাইল বানিয়ে পুরো stat দেখায়, তারপর ইচ্ছে করে atime আর mtime ২০২০ সালে পিছিয়ে দেয়, আর প্রতিটি কমান্ডের পর কোন সময় বদলাল তা ছাপে। শেষ অংশে তিনটি ফাইলের প্রথম চার বাইট (পরের অংশে কাজে লাগবে):

echo "hello, dhaka" > notice.txt                 # 13 bytes with the newline
stat notice.txt                                  # every attribute the kernel keeps

step() {                         # run one command, report which timestamps changed
  local before after a0 m0 c0 a1 m1 c1
  before=$(stat -c '%x|%y|%z' notice.txt); sleep 0.15
  eval "$1"
  after=$(stat -c '%x|%y|%z' notice.txt)
  IFS='|' read -r a0 m0 c0 <<< "$before"; IFS='|' read -r a1 m1 c1 <<< "$after"
  ch() { [ "$1" = "$2" ] && echo same || echo CHANGED; }
  printf '%-27s atime:%-8s mtime:%-8s ctime:%s\n' "$1" \
    "$(ch "$a0" "$a1")" "$(ch "$m0" "$m1")" "$(ch "$c0" "$c1")"
}
echo; echo "== which timestamp changes? =="
touch -d '2020-01-01 00:00' notice.txt           # atime and mtime pushed back to 2020
step 'cat notice.txt > /dev/null'                 # 1st read
step 'cat notice.txt > /dev/null'                 # 2nd read
step 'chmod 600 notice.txt'                      # metadata only
step 'echo more >> notice.txt'                    # data
step 'touch notice.txt'                          # set atime+mtime to now

echo; echo "== atime option of this mount =="
awk -v d="$PWD/" 'index(d, ($2 == "/" ? "/" : $2 "/")) == 1 && length($2) >= n \
  { n = length($2); o = $4 } END { print o }' /proc/self/mounts \
  | tr ',' '\n' | grep -E 'atime' || echo "(none listed)"

echo; echo "== first 4 bytes: the content, not the name =="
echo "just some text" > photo.jpg                # the extension lies
for f in /bin/ls photo.jpg notice.txt; do
  printf '%-11s' "$f"; head -c 4 "$f" | od -An -c
done
rm -f notice.txt photo.jpg

আসল রানের আউটপুট (inode নম্বর, device আর সময় আপনার রানে আলাদা হবে):

  File: notice.txt
  Size: 13            Blocks: 8          IO Block: 4096   regular file
Device: 253,2    Inode: 4706218     Links: 1
Access: (0644/-rw-r--r--)  Uid: (  999/ wandbox)   Gid: (  999/ wandbox)
Access: 2026-10-25 19:47:30.221482636 +0000
Modify: 2026-10-25 19:47:30.222482658 +0000
Change: 2026-10-25 19:47:30.222482658 +0000
 Birth: 2026-10-25 19:47:30.221482636 +0000

== which timestamp changes? ==
cat notice.txt > /dev/null  atime:CHANGED  mtime:same     ctime:same
cat notice.txt > /dev/null  atime:same     mtime:same     ctime:same
chmod 600 notice.txt        atime:same     mtime:same     ctime:CHANGED
echo more >> notice.txt     atime:same     mtime:CHANGED  ctime:CHANGED
touch notice.txt            atime:CHANGED  mtime:CHANGED  ctime:CHANGED

== atime option of this mount ==
relatime

== first 4 bytes: the content, not the name ==
/bin/ls     177   E   L   F
photo.jpg     j   u   s   t
notice.txt    h   e   l   l

stat-এর অংশগুলো: Size 13 বাইট, কিন্তু Blocks 8 — এগুলো ৫১২ বাইটের একক, অর্থাৎ 8 × 512 = 4096 বাইট, একটি পুরো ব্লক। IO Block হলো kernel-এর পছন্দের I/O আকার। Device আর Inode মিলে ফাইলের অনন্য পরিচয়; Links 1 মানে একটিই নাম। Access লাইনে অনুমতি দুই রূপে (অষ্টাধারে 0644 আর rwx), মালিকের uid/gid. তারপর চারটি সময় — Birth-ও আছে, অর্থাৎ এই file system তৈরির সময় রাখে। সময়ের পরীক্ষা: প্রথম cat-এ atime বদলেছে, কারণ atime (২০২০) mtime-এর চেয়ে নতুন ছিল না — দুটিই ২০২০-এ বসানো — আর ২৪ ঘণ্টার বেশি পুরোনোও ছিল; দ্বিতীয় cat-এ বদলায়নি, কারণ এখন atime mtime-এর চেয়ে নতুন — হুবহু relatime-এর নিয়ম, আর mount-এর বিকল্পেও লেখা relatime. chmod শুধু ctime বদলায় (inode বদল), ডেটা লেখা mtime আর ctime দুটিই, আর touch তিনটিই। আপনার মেশিন noatime দিয়ে mount করা থাকলে দুই cat-এই “same” দেখবেন। শেষ তিন লাইনের অর্থ পরের অংশে।

৩ · ফাইলের ধরন — OS কীভাবে চেনে

একটি ফাইলে কী আছে — ছবি, গান, প্রোগ্রাম, না লেখা — OS কীভাবে জানে? এখানে দুটি আলাদা প্রশ্ন মিশে থাকে। প্রথমটি kernel-এর চোখে ধরন: সাধারণ ফাইল, directory, symlink, device, pipe, নাকি socket. এটি inode-এর mode-এ লেখা থাকে, আর kernel তা মেনে চলে — directory-তে write() করা যায় না, device ফাইলে read() মানে driver-কে ডাকা। দ্বিতীয়টি বিষয়বস্তুর ধরন: JPEG না PDF. Unix kernel এটি জানে না এবং জানতে চায়ও না — এটি প্রোগ্রামের মাথাব্যথা।

ls -l-এর প্রথম অক্ষরধরনউদাহরণ
-সাধারণ ফাইল (regular file)notice.txt, /bin/ls
ddirectory/home
lsymbolic linkঅনেক distro-তে /bin → usr/bin
c / bcharacter / block device/dev/null, /dev/sda
pnamed pipe (FIFO, পর্ব ১২)mkfifo দিয়ে তৈরি
sUnix domain socket/run/...sock

বিষয়বস্তু চেনার দুই পথ — extension আর magic number

Windows — extension

নামের শেষের .docx, .exe, .jpg দেখে Explorer ঠিক করে কোন প্রোগ্রাম খুলবে (registry-তে file association). দ্রুত আর সরল, কিন্তু নাম বদলালেই “ধরন” বদলে যায় — বিষয়বস্তু একই থাকলেও।

নামের ওপর ভরসা

Unix — magic number

ফাইলের শুরুর কয়েকটি বাইট দেখে চেনা: ELF প্রোগ্রাম 7f 45 4c 46 (\177ELF), PNG 89 50 4e 47, PDF %PDF, ZIP (তাই docx/apk-ও) PK, script #!. file কমান্ড একটি বড় magic ডেটাবেস দিয়ে এটিই করে।

বিষয়বস্তুর ওপর ভরসা

উপরের স্ক্রিপ্টের শেষ অংশটি ঠিক এটিই দেখিয়েছে: photo.jpg নামের ফাইলের প্রথম বাইট j u s t — নাম যা-ই বলুক, ভেতরে লেখা। আর /bin/ls-এর শুরুতে 177 E L F (od অষ্টাধারে লেখে: 177 = 0x7f). Kernel-ও exec-এর সময় ঠিক এভাবে magic number দেখে: \177ELF হলে ELF loader, #! হলে পরের লেখা interpreter-কে ডাকে (পর্ব ১০)। এখানে extension-এর কোনো ভূমিকা নেই — Linux-এ hello.exe নামের ELF ফাইলও দিব্যি চলে।

নিরাপত্তার দিক। Windows-এ ডিফল্টভাবে পরিচিত extension লুকানো থাকে, তাই invoice.pdf.exe দেখায় invoice.pdf — ক্ষতিকর attachment-এর পুরোনো কৌশল। প্রতিরক্ষা: Explorer-এর View থেকে “File name extensions” চালু রাখুন, অচেনা attachment খোলার আগে আসল ধরন দেখুন।

৪ · ফাইলের কাজ — create, open, read, write, seek, truncate, delete

ফাইল একটি abstract data type-এর মতো: তার ভেতরে কী আছে তা নয়, তার ওপর কোন কাজ করা যায় সেটিই তার সংজ্ঞা। পাঠ্যবইয়ের ছয়-সাতটি মৌলিক কাজ, আর POSIX-এ তাদের system call (পর্ব ৫-এ দেখেছেন এগুলো কীভাবে kernel-এ ঢোকে আর ব্যর্থ হলে errno-তে কারণ আসে):

কাজPOSIXWin32OS ভেতরে কী করে
Createopen(O_CREAT), creatCreateFile(CREATE_NEW)নতুন inode নেয়, directory-তে নাম বসায়
OpenopenCreateFile(OPEN_EXISTING)path খোঁজে, অনুমতি দেখে, open-file entry বানায়, fd ফেরত দেয়
Readread, preadReadFileoffset থেকে পড়ে, offset এগোয়
Writewrite, pwriteWriteFileoffset-এ লেখে, দরকারে ফাইল বড় করে, mtime বদলায়
Reposition (seek)lseekSetFilePointerExশুধু offset বদলায় — কোনো ডিস্ক I/O নেই
Truncateftruncate, O_TRUNCSetEndOfFileআকার বদলায়; বাড়তি ব্লক মুক্ত করে, attribute থাকে
DeleteunlinkDeleteFileনাম সরায়; link count ০ আর কেউ খোলা না রাখলে ব্লক মুক্ত
ClosecloseCloseHandlefd মুক্ত, open-file entry-র refcount কমায়

open() কেন আলাদা একটি কাজ

প্রতিটি read-এ নাম দিলেই তো হতো — “/home/rafi/notes.txt থেকে বাইট ১০০ পড়ো”? সমস্যা হলো, নাম থেকে ফাইল খোঁজা খরুচে: প্রতিটি directory একে একে খুলে নাম মেলাতে হয়, প্রতিটি ধাপে অনুমতি যাচাই করতে হয়। open() এই কাজ একবার করে ফলাফল kernel-এর একটি টেবিলে রেখে দেয়, আর আপনাকে দেয় একটি ছোট সংখ্যা — file descriptor. এরপর প্রতিটি read(fd, …) সরাসরি সেই টেবিলের ঘরে যায়। একই কারণে অনুমতি যাচাই হয় open-এর সময়: ফাইল খোলার পর কেউ chmod করে অনুমতি কেড়ে নিলেও, আগে খোলা fd দিয়ে সাধারণত পড়া-লেখা চলতে থাকে।

ব্যাংকের টোকেন-কাউন্টারের কথা ভাবুন। প্রথমবার গিয়ে পরিচয়পত্র দেখান, ফর্ম ভরেন, কর্মকর্তা খাতায় আপনার অ্যাকাউন্ট খুঁজে বের করেন — এটি open. তারপর আপনাকে একটি টোকেন দেন: “৩ নম্বর”। এরপর যতবার টাকা তুলবেন বা জমা দেবেন, শুধু টোকেন দেখালেই চলে — এগুলো read আর write. কাজ শেষে টোকেন ফেরত — close.

Offset — ফাইলের ভেতরে “এখন কোথায় আছি”

প্রতিটি খোলা ফাইলের সাথে kernel একটি file offset (পাঠ্যবইয়ে “current-file-position pointer”) রাখে। read(fd, buf, 100) offset থেকে ১০০ বাইট পড়ে offset ১০০ এগিয়ে দেয়; তাই পরপর read ডাকলে ফাইল আপনিই ক্রমানুসারে পড়া হয়। lseek offset-কে যেকোনো জায়গায় নেয়: SEEK_SET (শুরু থেকে), SEEK_CUR (এখান থেকে), SEEK_END (শেষ থেকে)। আর pread/pwrite নির্দিষ্ট offset-এ পড়ে-লেখে কিন্তু offset বদলায় না — একাধিক থ্রেড একই fd ব্যবহার করলে এটিই নিরাপদ।

Offset ফাইলের শেষের পরেও নেওয়া যায়। সেখানে লিখলে মাঝের অংশটি হয়ে যায় একটি hole — যা পড়লে শূন্য আসে, কিন্তু ডিস্কে কোনো ব্লক নেয় না। এমন ফাইলকে বলে sparse file. নিচের প্রোগ্রামে offset-এর চলাফেরা আর একটি ১ MiB-এর sparse file দেখুন:

#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>

static long pos(int fd) { return (long)lseek(fd, 0, SEEK_CUR); }  /* current offset */

int main(void) {
    char buf[8] = {0};
    int fd = open("demo.dat", O_RDWR | O_CREAT | O_TRUNC, 0600);
    if (fd < 0) { perror("open"); return 1; }
    if (write(fd, "DHAKA-1205", 10) != 10) return 1;
    printf("after write(10)       : offset %ld\n", pos(fd));
    lseek(fd, 6, SEEK_SET);                              /* jump to byte 6 */
    if (read(fd, buf, 4) != 4) return 1;
    printf("lseek(6) + read(4)    : got \"%s\", offset %ld\n", buf, pos(fd));
    printf("read() at end of file : returns %zd\n", read(fd, buf, 4));
    printf("lseek(0, SEEK_END)    : %ld = file size\n", (long)lseek(fd, 0, SEEK_END));

    lseek(fd, 1024 * 1024, SEEK_SET);                    /* 1 MiB, far past the end */
    if (write(fd, "END", 3) != 3) return 1;              /* bytes 10..1048575: a hole */
    if (pread(fd, buf, 4, 5000) != 4) return 1;          /* read inside the hole */
    printf("4 bytes at 5000 (hole): %d %d %d %d\n", buf[0], buf[1], buf[2], buf[3]);

    struct stat st;
    fstat(fd, &st);
    printf("st_size       (ls -l) : %lld bytes\n", (long long)st.st_size);
    printf("st_blocks*512 (du)    : %lld bytes on disk\n", (long long)st.st_blocks * 512);
    close(fd);
    unlink("demo.dat");
    return 0;
}

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

after write(10)       : offset 10
lseek(6) + read(4)    : got "1205", offset 10
read() at end of file : returns 0
lseek(0, SEEK_END)    : 10 = file size
4 bytes at 5000 (hole): 0 0 0 0
st_size       (ls -l) : 1048579 bytes
st_blocks*512 (du)    : 8192 bytes on disk

১০ বাইট লেখার পর offset ১০ — লেখাও offset এগোয়। lseek(6) কোনো I/O না করে offset সরিয়েছে, তারপর ৪ বাইট পড়ায় এল "1205" আর offset আবার ১০। ফাইলের শেষে read ফেরত দেয় ০ — এটিই EOF-এর সংকেত, কোনো ত্রুটি নয়। SEEK_END দিয়ে শূন্য সরানো মানে আকার জানা। এরপর offset ১ MiB-এ নিয়ে ৩ বাইট লেখায় আকার হলো 1048576 + 3 = 1048579 বাইট, কিন্তু ডিস্কে মাত্র 8192 বাইট — দুটি 4 KiB ব্লক: শুরুর “DHAKA-1205”-এর ব্লক আর শেষের “END”-এর ব্লক। মাঝের প্রায় ১ MiB একটি hole: বাইট 5000 পড়লে শূন্য আসে, কিন্তু কোনো ব্লক নেই। (এই সংখ্যা file system-নির্ভর; hole না চেনে এমন file system-এ, যেমন FAT32-এ, পুরো ১ MiB-ই খরচ হতো।) ফাইলটি চলমান ফোল্ডারে, শুধু মালিকের অনুমতিতে (0600) তৈরি আর শেষে মুছে ফেলা হয়েছে।

ফাইলের আকার বনাম ডিস্ক খরচ — ls -l আর du

ls -l দেখায় st_size — logical আকার, শেষ বাইটের অবস্থান + ১। du দেখায় st_blocks × ৫১২ — আসলে কত জায়গা বরাদ্দ (Linux-এ st_blocks সবসময় ৫১২-বাইটের এককে, file system-এর ব্লক যত বড়ই হোক)। দুটি দুই দিকে আলাদা হতে পারে:

  • ছোট ফাইল, বড় খরচ: ১ বাইটের ফাইলও ৪ KiB ব্লকের file system-এ সাধারণত পুরো একটি ব্লক নেয় — ls -l 1, du 4K. এটি internal fragmentation; লাখো ছোট ফাইলে (যেমন node_modules) এর যোগফল বিশাল। (কিছু file system খুব ছোট ফাইল inode-এর ভেতরেই রাখে, তখন খরচ ০-ও দেখাতে পারে।)
  • বড় ফাইল, ছোট খরচ: sparse file — virtual machine-এর disk image, core dump, ডেটাবেস ফাইল। du --apparent-size আবার logical আকার দেখায়।

Sparse file-এর ফাঁদ। সাধারণ কপি টুল hole পড়ে শূন্য পায়, আর শূন্যগুলো সত্যিই লিখে ফেলতে পারে — ১০ GB-এর ফাইল যার আসল খরচ ১ GB, কপির পর ১০ GB নিয়ে ডিস্ক ভরিয়ে দেয়। GNU cp ডিফল্টে hole চেনার চেষ্টা করে (--sparse=auto), rsync-এ -S লাগে, আর অনেক backup বা নেটওয়ার্ক কপি টুল hole রাখে না।

মোছা মানে নাম সরানো — খোলা ফাইলের রহস্য

Linux-এ একটি প্রোগ্রাম ১০ GB-এর লগ ফাইল খুলে রেখেছে, আর আপনি rm দিয়ে ফাইলটি মুছলেন। ls-এ আর নেই, কিন্তু df বলছে জায়গা খালি হয়নি! কারণ unlink শুধু নাম সরিয়েছে; inode আর ব্লক থাকে যতক্ষণ কোনো প্রসেস ফাইলটি খোলা রাখে। প্রোগ্রাম বন্ধ হলে (বা fd বন্ধ করলে) তবেই মুক্ত। এমন ফাইল খুঁজতে lsof +L1 বা ls -l /proc/PID/fd-এ “(deleted)” লেখা দেখুন। Windows ভিন্ন: ডিফল্টে খোলা ফাইল মোছা যায় না (“The action can't be completed because the file is open in …”), যদি না খোলার সময় FILE_SHARE_DELETE দেওয়া হয় — অংশ ৯-এ দেখবেন।

৫ · তিন স্তরের open-file table — fd, open file description আর inode

পর্ব ১০-এ এক অনুচ্ছেদে দেখেছিলেন fork-এর পরে fd কপি হয় কিন্তু offset ভাগ হয়। এবার পুরো কাঠামোটি সঠিকভাবে আঁকি — পরীক্ষায় এর ছবি আঁকতে বলা হয়, আর বাস্তবে অনেক অদ্ভুত বাগের ব্যাখ্যা এখানেই। Unix-ধারার kernel খোলা ফাইলের তথ্য তিনটি স্তরে রাখে:

খোলা ফাইলের তিন স্তর (Unix / Linux)

Per-process fd table → system-wide open-file table → in-memory inode (vnode) table

  1. Per-process fd tablefd 3 → Xপ্রতিটি প্রসেসের নিজের; fd flag (FD_CLOEXEC)
  2. Open-file tableX: offset, flags, refসিস্টেম-জোড়া; প্রতিটি open()-এ নতুন entry
  3. Inode / vnode tableinode 5501প্রতি ফাইলে একটি; size, mode, ব্লকের ঠিকানা
ছবি ৪১.১ — fd হলো প্রসেসের টেবিলের একটি সূচক; offset থাকে মাঝের স্তরে; ফাইলের নিজের তথ্য শেষ স্তরে।
স্তরকী থাকেকে ভাগ করে
Per-process fd tablefd নম্বর → মাঝের entry-র pointer; close-on-exec flagকেউ না — প্রতিটি প্রসেসের নিজস্ব (একই প্রসেসের থ্রেডরা ভাগ করে)
System-wide open-file table (Linux: struct file; POSIX: open file description)offset; access mode (O_RDONLY…); status flag (O_APPEND, O_NONBLOCK); refcount; inode-এর pointerdup/dup2 আর fork-এ তৈরি সব fd
In-memory inode / vnode tableফাইলের attribute, ব্লকের ঠিকানা, lock-এর তালিকাফাইলটির সব open — যে প্রসেস যেভাবেই খুলুক
মূল ধারণা

নিয়মটি তিন লাইনে: প্রতিটি open() একটি নতুন মাঝের entry বানায় (তাই নিজস্ব offset). dup() আর fork() নতুন fd বানায়, কিন্তু একই মাঝের entry দেখায় (তাই offset আর O_APPEND ভাগ)। একই ফাইলের সব open শেষে একটিই inode দেখায় (তাই একজন লিখলে অন্য সবাই নতুন ডেটা দেখে)। পাঠ্যবইভেদে নাম আলাদা — কেউ বলে “file table”, কেউ “open file description” — কিন্তু Unix-এ offset মাঝের স্তরেই থাকে। কিছু পাঠ্যবই দুই স্তরের একটি সরল মডেল দেখায়, যেখানে file pointer প্রসেসের টেবিলে আর “open count” সিস্টেমের টেবিলে; সেই মডেলে fork-এর পর offset ভাগ হওয়া ব্যাখ্যা করা যায় না, তাই Unix-এর প্রশ্নে তিন স্তরের ছবিটিই আঁকুন।

নিচের ডায়াগ্রামে একটি প্রসেস notes.txt (inode 5501) খোলে, আবার খোলে, dup করে, তারপর fork. “X” আর “Y” হলো open-file table-এর দুটি entry; ref মানে কতগুলো fd এই entry দেখাচ্ছে:

1a = open("notes.txt", O_RDONLY)  → 32read(a, buf, 100)3b = open("notes.txt", O_RDONLY)  → 44c = dup(a)                        → 55pid = fork()6child: read(5, buf, 50)7child: exit(0)8parent: close(3)
তিন স্তরের অবস্থা
parent-এর fd table
child-এর fd table
entry X
entry Y
inode 5501-কে দেখা entry
  1. প্রথম open. Kernel path খুঁজে inode 5501 পেল, অনুমতি দেখল, open-file table-এ নতুন entry X বানাল (offset 0, O_RDONLY, ref ১), আর প্রসেসের fd table-এর সবচেয়ে ছোট খালি ঘর — 3 — X-কে দেখাল (0, 1, 2 আগে থেকেই stdin, stdout, stderr). open ফেরত দিল 3.
  2. ১০০ বাইট পড়া। read X-এর offset থেকে পড়ল আর X-এর offset ১০০ করল। Offset fd-তে নয়, X-এ — এটি পরের ধাপগুলোতে গুরুত্বপূর্ণ।
  3. একই ফাইল আবার open. নাম একই, inode একই, কিন্তু প্রতিটি open নতুন entry বানায়: Y, offset 0, ref ১। fd 4 → Y. এখন inode 5501-কে দুটি entry দেখাচ্ছে।
  4. dup(3). dup নতুন entry বানায় না; শুধু fd table-এর পরের খালি ঘর 5-কে X দেখায়। X-এর ref এখন ২। fd 3 আর fd 5 একই offset (১০০) ভাগ করছে — একটি দিয়ে পড়লে অন্যটির offset-ও এগোবে। Shell-এর 2>&1 ঠিক এটিই করে (dup2).
  5. fork. Child parent-এর fd table-এর একটি কপি পেল: 3→X, 4→Y, 5→X. কিন্তু X আর Y কপি হয়নি — তাদের ref বেড়েছে: X-কে এখন চারটি fd দেখাচ্ছে (দুই প্রসেসের 3 আর 5), Y-কে দুটি। Inode-এর দিক থেকে কিছুই বদলায়নি — এখনো দুটি entry, X আর Y.
  6. Child fd 5 দিয়ে ৫০ বাইট পড়ল। X-এর offset ১০০ থেকে ১৫০। Parent এখন fd 3 বা fd 5 দিয়ে পড়লে বাইট ১৫০ থেকে পাবে — child-এর পড়া parent-কেও সরিয়েছে। Y-এর offset এখনো ০।
  7. Child শেষ হলো। Exit-এর সময় kernel child-এর সব fd বন্ধ করল: X-এর ref ৪ থেকে ২, Y-এর ২ থেকে ১। ref শূন্য হয়নি, তাই কোনো entry মুছল না।
  8. Parent close(3). fd 3 খালি হলো, X-এর ref ১ — fd 5 এখনো X দেখাচ্ছে, offset ১৫০ অক্ষত। close মানে “এই fd ছেড়ে দিলাম”, ফাইল বন্ধ নয়; X মুছবে fd 5-ও বন্ধ হলে।

এবার আসল Linux-এ মিলিয়ে দেখুন। প্রোগ্রামটি একই ফাইল দুবার খোলে (a, b), তারপর c = dup(a). চারটি এক-বাইট পড়ার পর offset দেখে, আর দুটি ভিন্ন ধরনের flag বসায়: c-তে FD_CLOEXEC (fd flag, প্রথম স্তরে থাকে) আর a-তে O_APPEND (status flag, মাঝের স্তরে থাকে):

#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>

static char get(int fd) { char ch = '?'; if (read(fd, &ch, 1) != 1) ch = '!'; return ch; }

static void show(const char *name, int fd) {
    printf("%s: fd=%d offset=%ld FD_CLOEXEC=%d O_APPEND=%d\n", name, fd,
           (long)lseek(fd, 0, SEEK_CUR),
           (fcntl(fd, F_GETFD) & FD_CLOEXEC) != 0,     /* per-fd flag      */
           (fcntl(fd, F_GETFL) & O_APPEND) != 0);     /* per-description  */
}

int main(void) {
    int w = open("abc.txt", O_WRONLY | O_CREAT | O_TRUNC, 0600);
    if (w < 0 || write(w, "ABCDEFGHIJ", 10) != 10) { perror("setup"); return 1; }
    close(w);
    int a = open("abc.txt", O_RDONLY);    /* open #1: new description X */
    int b = open("abc.txt", O_RDONLY);    /* open #2: new description Y */
    int c = dup(a);                       /* no new description: c -> X */
    if (a < 0 || b < 0 || c < 0) { perror("open/dup"); return 1; }
    char r1 = get(a), r2 = get(b), r3 = get(c), r4 = get(a);   /* in this order */
    printf("1-byte reads: a=%c  b=%c  c=%c  a=%c\n", r1, r2, r3, r4);
    fcntl(c, F_SETFD, FD_CLOEXEC);        /* set on c only */
    fcntl(a, F_SETFL, O_APPEND);          /* set through a */
    show("a", a); show("b", b); show("c", c);
    struct stat sa, sb, sc;
    fstat(a, &sa); fstat(b, &sb); fstat(c, &sc);
    printf("same inode for a, b, c: %s\n",
           sa.st_ino == sb.st_ino && sb.st_ino == sc.st_ino ? "yes" : "no");
    unlink("abc.txt");
    return 0;
}

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

1-byte reads: a=A  b=A  c=B  a=C
a: fd=3 offset=3 FD_CLOEXEC=0 O_APPEND=1
b: fd=4 offset=1 FD_CLOEXEC=0 O_APPEND=0
c: fd=5 offset=3 FD_CLOEXEC=1 O_APPEND=1
same inode for a, b, c: yes

প্রতিটি লাইন তিন স্তরের একটি নিয়ম প্রমাণ করে। Offset: a প্রথমে A পড়ল; b আলাদা open, তাই b-ও A পড়ল (নিজস্ব offset); c dup, তাই a-র offset ১ থেকে পড়ল B; আবার a পড়ল C — c-এর পড়া a-কে সরিয়েছে। শেষে a আর c দুজনেরই offset ৩, b-এর ১। Status flag: O_APPEND বসানো হয়েছে শুধু a দিয়ে, অথচ c-তেও দেখা যাচ্ছে — কারণ এটি থাকে মাঝের স্তরে (open file description), যা a আর c ভাগ করে; b-তে নেই। fd flag: FD_CLOEXEC বসানো হয়েছে c-তে, আর a-তে নেই — কারণ এটি থাকে প্রসেসের fd table-এর ঘরে, প্রতিটি fd-এর আলাদা। এজন্য dup-এর নতুন fd-তে close-on-exec থাকে না; fork-এ অবশ্য পুরো fd table কপি হয়, তাই child-এর fd-তে flag-টিও থাকে, আর exec-এর সময় সেই fd বন্ধ হয় (পর্ব ১০)। Inode: তিনটিই একই inode — একই ফাইল। fd নম্বর 3, 4, 5 হলো সবচেয়ে ছোট খালি নম্বর, কারণ 0–2 আগে থেকে খোলা আর w আগেই বন্ধ হয়েছে।

আরও গভীরে: Linux-এ task_struct-এর files একটি files_struct-কে দেখায়, যার fdtable-এ fd নম্বর দিয়ে সূচিত struct file *-এর অ্যারে আর close-on-exec-এর একটি bitmap; struct file-এ থাকে f_pos (offset), f_flags, f_mode, refcount আর f_inode, আর inode-এ থাকে ফাইলের attribute আর page cache-এর address_space. শিক্ষণ-OS xv6-এ (পর্ব ৫৮) একই ছবি আরও সরল: প্রতিটি proc-এ ofile[NOFILE], সিস্টেম-জোড়া ftable-এ struct file (ref, off, readable, writable, ip), আর inode cache. Windows NT-তে একই তিন স্তর অন্য নামে — প্রসেসের handle table → FILE_OBJECT (যার CurrentByteOffset-ই offset) → file system-এর FCB/SCB.

৬ · Access method — sequential, direct, indexed আর ISAM

OS ফাইলকে বাইটের সারি হিসেবে দেয়; কিন্তু অনেক প্রোগ্রাম ফাইলকে দেখে logical record-এর সারি হিসেবে — যেমন একটি ব্যাংকের প্রতিটি অ্যাকাউন্টের ১২৮ বাইটের রেকর্ড, বা একটি পরীক্ষা বোর্ডের প্রতিটি ছাত্রের ফলাফল। Access method হলো সেই রেকর্ডগুলো কোন ক্রমে, কীভাবে খুঁজে পড়া হবে তার নিয়ম। কিছু OS (IBM mainframe, পুরোনো VMS) একাধিক access method নিজেই দিত; Unix আর Windows দেয় শুধু বাইট আর lseek — বাকি সব প্রোগ্রাম বা library-র ওপর।

Sequential access — একের পর এক

রেকর্ড পড়া হয় ক্রমানুসারে: read next offset এগিয়ে দেয়, write next শেষে যোগ করে, আর পেছনে যেতে হলে সাধারণত শুরুতে ফিরতে হয় (rewind). এটি টেপের মডেল — ক্যাসেট প্লেয়ারে সাত নম্বর গানে যেতে হলে ছয়টি পার হতে হয়। আজও সবচেয়ে বেশি ব্যবহৃত: লগ ফাইল, compiler-এর সোর্স পড়া, ভিডিও চালানো, cat, grep. OS-ও এই ধরন চেনে আর আগাম পড়ে রাখে (readahead), তাই ক্রমানুসারে পড়া HDD-তেও দ্রুত।

Direct (relative) access — সরাসরি রেকর্ড n

ফাইলকে দেখা হয় নম্বর দেওয়া রেকর্ড বা ব্লকের সারি হিসেবে, আর যেকোনো ক্রমে যেকোনোটি পড়া-লেখা যায়: read n, write n. এটি ডিস্কের মডেল — মোবাইলের গানের তালিকা থেকে সরাসরি ১৭ নম্বর গান। এখানে n হলো relative block number — ফাইলের শুরু থেকে গোনা নম্বর (০ থেকে), ডিস্কের আসল ব্লক নম্বর নয়। কোন relative ব্লক ডিস্কের কোন physical ব্লক, সেই অনুবাদ file system-এর (পর্ব ৪৩); প্রোগ্রাম শুধু relative নম্বর জানে, ফলে ফাইল ডিস্কের অন্য জায়গায় সরলেও প্রোগ্রাম বদলাতে হয় না।

নির্দিষ্ট দৈর্ঘ্যের রেকর্ডে (fixed-length record) সূত্রটি সরল:

রেকর্ড n-এর offset = n × R  (n শূন্য থেকে)  ·  ১ থেকে গুনলে (k − 1) × R

নিচের ডায়াগ্রামে R = 128 বাইট, file system-এর ব্লক 4096 বাইট, ফাইলে ১০০০টি রেকর্ড (128,000 বাইট)। রেকর্ড 37 পড়া, বদলানো আর শেষে নতুন রেকর্ড যোগের জায়গা খোঁজা:

1n = 37;  R = 128;  B = 40962off = n * R3lseek(fd, off, SEEK_SET)4read(fd, rec, R)5lseek(fd, -R, SEEK_CUR); write(fd, rec, R)6end = lseek(fd, 0, SEEK_END)
হিসাব আর offset
হিসাব
file offset
পরের পড়া কোন রেকর্ড
রেকর্ড 37-এর অবস্থা
  1. যা জানা। রেকর্ডের দৈর্ঘ্য R = 128, চাই রেকর্ড 37 (শূন্য থেকে গোনা — অর্থাৎ ৩৮তম রেকর্ড)। ফাইল খোলা, offset ০।
  2. Offset বের করা। আগে ৩৭টি রেকর্ড, প্রতিটি ১২৮ বাইট: 37 × 128 = 4736. এটি শুধু গুণ — এখনো কোনো system call হয়নি, offset ০-ই আছে। (১ থেকে গোনা প্রশ্নে “৩৭তম রেকর্ড” হলে উত্তর 36 × 128 = 4608 — পরীক্ষায় প্রশ্নটি ভালো করে পড়ুন।)
  3. lseek. Offset 4736. কোনো ডিস্ক I/O হয়নি, শুধু open-file entry-র একটি সংখ্যা বদলেছে। ডিস্কের দিক থেকে: 4736 ÷ 4096 = ভাগফল 1, ভাগশেষ 640 — অর্থাৎ ফাইলের relative block 1-এর বাইট 640 থেকে রেকর্ড শুরু, আর 640 + 128 = 768 ≤ 4096, তাই রেকর্ডটি একটি ব্লকের ভেতরেই। (4096 ÷ 128 = 32, ভাগ মিলে যায় বলে কোনো রেকর্ড দুই ব্লকে ভাগ হয় না।)
  4. read. ১২৮ বাইট মেমরিতে এল, offset 4864 — ঠিক রেকর্ড 38-এর শুরু। অর্থাৎ direct access-এর পর sequential পড়া আপনিই চলতে পারে; দুই পদ্ধতি একসাথে মেলে।
  5. Read-modify-write. রেকর্ড বদলে আবার একই জায়গায় লিখতে offset ফেরাতে হবে: lseek(fd, -128, SEEK_CUR) → 4736, তারপর write → আবার 4864. বিকল্প: pwrite(fd, rec, 128, 4736) — offset না ছুঁয়ে এক call-এ। দুটি প্রসেস একসাথে একই রেকর্ড বদলালে একজনের বদল হারাতে পারে (lost update, পর্ব ২২) — এজন্য অংশ ৭-এর record lock.
  6. কতটি রেকর্ড আছে? lseek(fd, 0, SEEK_END) ফেরত দেয় আকার: 128000. রেকর্ড সংখ্যা 128000 ÷ 128 = 1000 (০ থেকে 999). নতুন রেকর্ড যোগ হবে offset 128000-এ, অর্থাৎ রেকর্ড 1000 হিসেবে।

ব্লকের হিসাব — blocking factor আর unspanned রেকর্ড

রেকর্ডের দৈর্ঘ্য ব্লকের ভাজক না হলে দুটি পথ। Spanned (packed): রেকর্ডগুলো গায়ে গায়ে, কিছু রেকর্ড দুই ব্লকে ভাগ হয় — Unix-এ সাধারণ ফাইলে এটিই স্বাভাবিক, offset = n × R. Unspanned: একটি ব্লকে যত পুরো রেকর্ড ধরে, বাকি জায়গা ফাঁকা — পুরোনো রেকর্ড-ভিত্তিক সিস্টেম আর অনেক ডেটাবেসের page এভাবে চলে। প্রতি ব্লকে রেকর্ডের সংখ্যাকে বলে blocking factor: bfr = ⌊B ÷ R⌋.

উদাহরণ: B = 4096, R = 100, রেকর্ড 1000 (০ থেকে) কোথায়?

  • Unspanned: bfr = ⌊4096 ÷ 100⌋ = 40, প্রতি ব্লকে 4096 − 4000 = 96 বাইট নষ্ট। ব্লক = ⌊1000 ÷ 40⌋ = 25, ব্লকের ভেতরে স্থান = 1000 mod 40 = 0. File offset = 25 × 4096 + 0 × 100 = 102400.
  • Spanned: offset = 1000 × 100 = 100000; ব্লক = ⌊100000 ÷ 4096⌋ = 24, ব্লকের ভেতরে 100000 − 24 × 4096 = 100000 − 98304 = 1696.

একই রেকর্ড, দুই নিয়মে দুটি ভিন্ন offset আর ভিন্ন ব্লক — পরীক্ষায় প্রশ্নে কোন নিয়ম বলা আছে সেটিই আসল।

Indexed access — আগে সূচি, তারপর ব্লক

Direct access-এ রেকর্ড নম্বর জানা থাকতে হয়। কিন্তু প্রায়ই জানা থাকে একটি key — রোল নম্বর, অ্যাকাউন্ট নম্বর, মোবাইল নম্বর। তখন আলাদা একটি index রাখা হয়: key-এর ক্রমে সাজানো (key, ব্লক নম্বর) জোড়ার তালিকা। বইয়ের পেছনের সূচিপত্র বা অভিধানের পাশের অক্ষর-ট্যাবের মতো — আগে সূচিতে খুঁজে পাতা নম্বর, তারপর সরাসরি সেই পাতা।

  • Dense index: প্রতিটি রেকর্ডের জন্য একটি entry.
  • Sparse index: প্রতিটি ডেটা ব্লকের জন্য একটি entry (সেই ব্লকের প্রথম key) — ডেটা ফাইল key-এর ক্রমে সাজানো থাকতে হয়।

পূর্ণ হিসাব (পরীক্ষার পরিচিত ধরন): ১০,০০,০০০টি রেকর্ড, R = 128 বাইট, B = 4096 বাইট, ফাইল key-এর ক্রমে সাজানো। Index entry = 8 বাইট key + 8 বাইট ব্লক-pointer = 16 বাইট।

  1. bfr = 4096 ÷ 128 = 32, ডেটা ব্লক = 1,000,000 ÷ 32 = 31,250.
  2. প্রতি index ব্লকে entry = 4096 ÷ 16 = 256.
  3. Dense index: ⌈1,000,000 ÷ 256⌉ = ⌈3906.25⌉ = 3907 ব্লক। Sparse index: ⌈31,250 ÷ 256⌉ = ⌈122.07⌉ = 123 ব্লক।
  4. Index ছাড়া, সাজানো নয় ধরে linear খোঁজ: গড়ে 31,250 ÷ 2 = 15,625 ব্লক পড়া, সবচেয়ে খারাপে 31,250.
  5. সাজানো ডেটা ফাইলে সরাসরি binary search: ⌈log₂ 31,250⌉ = 15 ব্লক পড়া (215 = 32,768).
  6. Sparse index-এর ১২৩টি ব্লকে binary search: ⌈log₂ 123⌉ = 7, তারপর ১টি ডেটা ব্লক — মোট 8.
  7. Index-এর ওপরে আরেকটি index (multilevel): ১২৩টি entry একটি ব্লকেই ধরে (১২৩ ≤ ২৫৬)। তাই উপরের স্তর ১ ব্লক → নিচের স্তর ১ ব্লক → ডেটা ১ ব্লক = 3 পড়া। উপরের স্তরটি সাধারণত মেমরিতেই থাকে, তখন ২টি।

১৫,৬২৫ থেকে ৩ — এটিই index-এর শক্তি। এই “সূচির ওপর সূচি” ধারণাটিই আধুনিক রূপে B-tree আর B+ tree, যা প্রায় সব ডেটাবেস আর NTFS, XFS, Btrfs, APFS-এর মতো file system-এর directory বা metadata-য় ব্যবহৃত হয়।

ISAM — indexed sequential

১৯৬০-এর দশকে IBM-এর mainframe-এ জনপ্রিয় ISAM (Indexed Sequential Access Method) দুটি পদ্ধতি একসাথে দিত: key-এর ক্রমে সাজানো ডেটা, তাই পুরো ফাইল ক্রমানুসারে পড়া দ্রুত (মাসশেষে সব অ্যাকাউন্টের স্টেটমেন্ট); আবার একটি বহুস্তর index (master index → cylinder index → track index, ডিস্কের গঠন মেনে), তাই একটি key সরাসরি খোঁজাও দ্রুত। সমস্যা ছিল নতুন রেকর্ড ঢোকানো: সাজানো ক্রম রাখতে নতুন রেকর্ড যেত একটি overflow area-য়, শিকল দিয়ে জোড়া। ১৯৭০-এর দশকে IBM-এর VSAM আর তারপর B-tree এই সমস্যার সমাধান দেয় — B-tree ভরে গেলে নিজেই node ভাগ করে ভারসাম্য রাখে।

৭ · File locking — advisory, mandatory, shared আর exclusive

দুটি প্রসেস একই ফাইলের একই রেকর্ড একসাথে read-modify-write করলে একজনের বদল হারায় — পর্ব ২২-এর race condition, এবার ডিস্কে। সমাধান file lock. দুটি প্রশ্ন দিয়ে lock-কে শ্রেণিভাগ করা হয়:

Shared (read) lock

একসাথে অনেকে নিতে পারে — সবাই শুধু পড়বে। কেউ exclusive lock ধরে থাকলে পাওয়া যায় না। পর্ব ২৬-এর readers-writers-এর “reader”.

F_RDLCK · LOCK_SH

Exclusive (write) lock

একজনই নিতে পারে, আর তখন অন্য কেউ shared বা exclusive — কোনোটিই পায় না।

F_WRLCK · LOCK_EX

Advisory

Lock শুধু অন্য lock-কে আটকায়। যে প্রসেস lock না চেয়েই read/write করে, kernel তাকে থামায় না। সবাই নিয়ম মানলে তবেই কাজ করে।

Unix-এর ডিফল্ট

Mandatory

Kernel প্রতিটি read/write-এ lock দেখে; অন্যের lock করা অংশে I/O ব্যর্থ বা অপেক্ষমাণ হয়। কেউ এড়াতে পারে না।

Windows-এর byte-range lock

হলের কমন বাথরুমের দরজায় ছিটকিনি নেই, তাই ভেতরে গেলে বাইরে একটি কাগজ ঝোলানো হয় — “ভেতরে আছি”। যারা নিয়ম মানেন, কাগজ দেখে অপেক্ষা করেন। কিন্তু কেউ কাগজ উপেক্ষা করে দরজা ঠেললে কাগজ তাকে থামাতে পারে না — এটি advisory lock. দরজায় আসল ছিটকিনি থাকলে কেউই ঢুকতে পারত না — এটি mandatory lock.

Unix-এর দুই lock পরিবার — flock আর fcntl

বৈশিষ্ট্যflock() (BSD থেকে)fcntl() / POSIX record lock
পরিসরপুরো ফাইলযেকোনো byte range (l_start, l_len; len 0 মানে শেষ পর্যন্ত)
Lock-এর মালিকopen file description — তাই dup/fork-এর fd একই lock ভাগ করেপ্রসেস (PID) — একই প্রসেসের থ্রেডরা আলাদা নয়
কখন ছাড়েশেষ fd বন্ধ হলে বা LOCK_UNপ্রসেস ফাইলটির যেকোনো fd বন্ধ করলেই সব lock চলে যায়; প্রসেস শেষ হলে
অপেক্ষাLOCK_NB না দিলে অপেক্ষাF_SETLK সাথে সাথে ব্যর্থ (EAGAIN বা EACCES); F_SETLKW অপেক্ষা করে; deadlock ধরলে EDEADLK
মানদণ্ডPOSIX-এ নেই, কিন্তু বেশিরভাগ Unix-এ আছেPOSIX; NFS-এ সাধারণত এটিই কাজ করে

fcntl lock-এর “যেকোনো fd বন্ধ করলেই সব lock যায়” নিয়মটি কুখ্যাত: একটি library ফাইলটি নিজে খুলে পড়ে বন্ধ করলে আপনার প্রোগ্রামের lock নিঃশব্দে হারিয়ে যায়। এর সমাধানে Linux ৩.১৫ যোগ করে OFD lock (F_OFD_SETLK): fcntl-এর মতো byte range, কিন্তু মালিক open file description — flock-এর মতো। আর Linux-এ mandatory lock একসময় mount বিকল্প mand দিয়ে চালু করা যেত, কিন্তু অনির্ভরযোগ্য আর race-প্রবণ বলে ৫.১৫ থেকে সরিয়ে দেওয়া হয়েছে — অর্থাৎ Linux-এ বাস্তবে সব lock advisory.

নিচে একটি ব্যাংক লেজার ফাইলে চারটি প্রসেস A, B, C, D fcntl lock চায়। R = shared, W = exclusive:

1A: F_SETLK  F_RDLCK bytes 0-992B: F_SETLK  F_RDLCK bytes 0-993C: F_SETLK  F_WRLCK bytes 0-994D: F_SETLK  F_WRLCK bytes 200-2995C: F_SETLKW F_WRLCK bytes 0-996A: F_UNLCK  bytes 0-997B: F_UNLCK  bytes 0-998A: F_SETLK  F_RDLCK bytes 0-99
Inode-এর lock তালিকা
bytes 0–99
bytes 200–299
অপেক্ষমাণ
এই call-এর ফল
  1. A shared lock চাইল। bytes 0–99-তে কেউ নেই, তাই সাথে সাথে পেল।
  2. B-ও shared lock চাইল। Shared-এর সাথে shared মেলে — দুজনেই পড়বে, কোনো ঝুঁকি নেই। এখন 0–99-এ A(R) আর B(R).
  3. C exclusive চাইল, F_SETLK দিয়ে। Exclusive কোনো lock-এর সাথেই মেলে না, আর দুটি shared lock আছে। F_SETLK অপেক্ষা করে না: সাথে সাথে −1, errno = EAGAIN (POSIX EACCES-ও অনুমোদন করে; Linux EAGAIN দেয়)।
  4. D exclusive চাইল 200–299-এ। এই পরিসর 0–99-এর সাথে কোথাও মেলে না, তাই সংঘাত নেই — D পেল। এটিই byte-range lock-এর সুবিধা: পুরো ফাইল আটকে না রেখে শুধু নিজের রেকর্ড আটকানো।
  5. C এবার F_SETLKW দিয়ে। “W” মানে wait: lock না পাওয়া পর্যন্ত C ঘুমিয়ে থাকে (blocked state, পর্ব ৮)। Kernel C-কে inode-এর অপেক্ষার তালিকায় রাখল। আর অপেক্ষাটি deadlock তৈরি করলে (C যার জন্য অপেক্ষা করছে সে C-এর অন্য lock-এর জন্য অপেক্ষায়) kernel EDEADLK ফেরত দেয় — পর্ব ২৭-এর wait-for graph-এর একটি বাস্তব প্রয়োগ।
  6. A ছেড়ে দিল। কিন্তু B এখনো shared lock ধরে আছে, তাই C জাগে না। A-র ছাড়ার মুহূর্তে kernel অপেক্ষমাণদের পরীক্ষা করে — C-এর অনুরোধ এখনো B-এর সাথে সংঘাতে। খেয়াল করুন: এখন নতুন কেউ shared lock চাইলে Linux সাধারণত তাকে দিয়ে দেয়, যদিও C অপেক্ষায় — একের পর এক reader এলে writer অনির্দিষ্টকাল অপেক্ষা করতে পারে (starvation, পর্ব ২৬)।
  7. B ছাড়ল — C পেল। 0–99 এখন খালি, kernel C-কে জাগিয়ে exclusive lock দিল; C-এর F_SETLKW ০ ফেরত দিল।
  8. A আবার shared lock চাইল। C-এর exclusive lock আছে, তাই F_SETLK সাথে সাথে EAGAIN. কিন্তু মনে রাখুন — এটি advisory: A যদি lock না চেয়েই সরাসরি read() করত, kernel থামাত না, আর A হয়তো C-এর অর্ধেক-লেখা রেকর্ড পড়ত।

এবার আসল চালানো। Child একটি exclusive lock নিয়ে ধরে রাখে; parent একটি pipe দিয়ে নিশ্চিত হয় যে child lock পেয়েছে (sleep-এর আন্দাজ নয় — তাই ফল প্রতিবার একই), তারপর lock চেয়ে ব্যর্থ হয়, আর তবু write() করে দেখে:

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

static int lock(int fd, short type) {            /* whole file (len 0), do not wait */
    struct flock fl = { .l_type = type, .l_whence = SEEK_SET };
    return fcntl(fd, F_SETLK, &fl);
}
static const char *res(int r) { return r == 0 ? "granted" : errno == EAGAIN ?
    "denied (EAGAIN)" : errno == EACCES ? "denied (EACCES)" : strerror(errno); }
int main(void) {
    int fd = open("ledger.dat", O_RDWR | O_CREAT | O_TRUNC, 0600), ready[2], done[2];
    char c = 'x';
    if (fd < 0 || pipe(ready) || pipe(done)) { perror("setup"); return 1; }
    fflush(stdout);                               /* nothing left in the buffer at fork */
    pid_t pid = fork();
    if (pid == 0) {                  /* child: lock, tell the parent, wait for "done" */
        if (lock(fd, F_WRLCK) || write(ready[1], &c, 1) != 1 || read(done[0], &c, 1) != 1) _exit(1);
        _exit(0);                                 /* exit releases the lock */
    }
    close(ready[1]); close(done[0]);
    if (read(ready[0], &c, 1) != 1) return 1;     /* child now holds the lock */
    struct flock q = { .l_type = F_WRLCK, .l_whence = SEEK_SET };
    fcntl(fd, F_GETLK, &q);                       /* who blocks a write lock? */
    printf("F_GETLK : %s lock, owner is child: %s\n",
           q.l_type == F_WRLCK ? "write" : "other", q.l_pid == pid ? "yes" : "no");
    printf("parent read lock : %s\n", res(lock(fd, F_RDLCK)));
    printf("parent write lock: %s\n", res(lock(fd, F_WRLCK)));
    printf("parent write()   : %zd bytes written anyway\n", write(fd, "12345", 5));
    if (write(done[1], &c, 1) != 1) return 1;     /* tell the child to exit */
    waitpid(pid, NULL, 0);
    printf("after child exit : %s\n", res(lock(fd, F_WRLCK)));
    close(fd); unlink("ledger.dat");
    return 0;
}

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

F_GETLK : write lock, owner is child: yes
parent read lock : denied (EAGAIN)
parent write lock: denied (EAGAIN)
parent write()   : 5 bytes written anyway
after child exit : granted

F_GETLK lock নেয় না, শুধু জিজ্ঞেস করে “আমি write lock চাইলে কে আটকাবে?” — উত্তরে child-এর PID আর তার write lock. Parent আর child একই fd (একই open file description) ভাগ করে, তবু parent-এর অনুরোধ ব্যর্থ — কারণ fcntl lock-এর মালিক প্রসেস, description নয়। (flock হলে উল্টো হতো: দুজনে একই description-এর lock ভাগ করত।) Exclusive lock থাকায় shared আর exclusive দুটিই EAGAIN — Linux EAGAIN দেয়, অন্য কিছু সিস্টেম EACCES, তাই বহনযোগ্য কোডে দুটিই দেখুন। এরপর সবচেয়ে গুরুত্বপূর্ণ লাইন: lock ছাড়াই write() ৫ বাইট লিখে ফেলেছে — advisory lock নিয়ম-না-মানা প্রসেসকে থামায় না। শেষে child-এর exit-এ kernel তার সব lock ছেড়ে দিয়েছে, তাই parent এবার পেয়েছে। Pipe-এর handshake-এর কারণে ফল প্রতিবার একই — কোনো sleep-এর আন্দাজ নেই, আর fork-এর আগে fflush(stdout) রাখা হয়েছে যাতে buffer-এর লেখা দুবার না আসে।

Windows — lock সত্যিই বাধ্যতামূলক

Windows-এ দুই স্তরের সুরক্ষা। প্রথমত share mode: CreateFile-এর dwShareMode-এ ফাইল খোলার সময়ই বলে দেওয়া হয় অন্যরা একসাথে পড়তে, লিখতে বা মুছতে পারবে কি না। Excel একটি ফাইল খুলে রাখলে অন্য প্রোগ্রাম লিখতে চাইলে পায় ERROR_SHARING_VIOLATION — “The process cannot access the file because it is being used by another process”. দ্বিতীয়ত LockFileEx-এর byte-range lock: অন্য handle থেকে সেই অংশে ReadFile/WriteFile ব্যর্থ হয় ERROR_LOCK_VIOLATION দিয়ে।

৮ · Buffer, fsync আর mmap — লেখা কখন সত্যিই ডিস্কে

একটি C প্রোগ্রাম ফাইলে লেখার দুটি পথ পায়। Buffered stdio: fopen, fprintf, fwrite, fread — C library ব্যবহারকারীর মেমরিতে একটি buffer রাখে (glibc-এ সাধারণত ফাইলের st_blksize, প্রায়ই ৪ KiB), আর buffer ভরলে বা fflush/fclose-এ একটি write() ডাকে। Raw system call: open, read, write — প্রতিটি call সরাসরি kernel-এ। পর্ব ৫-এ দেখেছেন printf কেন system call নয় আর buffer কীভাবে system call-এর সংখ্যা কমায়; পর্ব ১০-এ দেখেছেন fork-এর আগে fflush না করলে buffer-এর লেখা দুবার আসে। এখানে প্রশ্নটি অন্য: লেখা কখন টেকসই?

একটি লেখার যাত্রাপথ — প্রতিটি ধাপে কে পার করায়

Application → stdio buffer → kernel page cache → device cache → স্থায়ী মাধ্যম

  1. stdio bufferfprintfপ্রসেসের মেমরি; ক্র্যাশে হারায়
  2. Page cachefflush → writekernel-এর RAM; প্রসেস ক্র্যাশে টেকে, বিদ্যুৎ গেলে হারায়
  3. ডিস্কের cachewriteback / fsyncড্রাইভের নিজের RAM
  4. স্থায়ী মাধ্যমfsync + flushবিদ্যুৎ গেলেও থাকে
ছবি ৪১.২ — fflush ডেটাকে kernel পর্যন্ত নেয়; fsync kernel-কে বলে ডিস্ক পর্যন্ত নিশ্চিত করতে।
  • fflush(fp): stdio buffer → kernel. এখন অন্য প্রসেস read করলে নতুন ডেটা দেখবে, আর আপনার প্রসেস ক্র্যাশ করলেও ডেটা kernel-এ টিকে থাকবে। কিন্তু বিদ্যুৎ চলে গেলে নয়।
  • write() ফিরে আসা মানে ডেটা page cache-এ dirty page হয়ে আছে; kernel-এর writeback সাধারণত কয়েক সেকেন্ডের মধ্যে (Linux-এ ডিফল্ট মোটামুটি ৩০ সেকেন্ডের পুরোনো dirty ডেটা) ডিস্কে লেখে।
  • fsync(fd): ফাইলের সব dirty ডেটা আর metadata (আকার, mtime, ব্লকের তালিকা) ডিস্কে লিখে, ডিস্ককে তার cache খালি করতে বলে, তারপর ফেরে। fdatasync(fd) একই, কিন্তু যে metadata ডেটা ফিরে পড়ার জন্য দরকার নয় (যেমন শুধু mtime) তা বাদ দেয় — তাই প্রায়ই দ্রুত। আকার বদলালে সেটি অবশ্য লিখতে হয়।
  • নতুন ফাইল তৈরি বা rename-এর পর directory-ও একটি ফাইল — তার নামের entry টেকসই করতে directory-র fd-তেও fsync লাগে। “নতুন ফাইলে লিখুন → fsync → rename দিয়ে পুরোনোটি প্রতিস্থাপন → directory fsync” — নিরাপদে কনফিগ ফাইল বদলানোর এই প্রচলিত কৌশল আর ক্র্যাশের পর file system কীভাবে নিজেকে ঠিক রাখে — journaling — পর্ব ৪৪-এর বিষয়।

fsync-এর ত্রুটিও পড়তে হয়। ডিস্কে লেখার ত্রুটি (যেমন নষ্ট sector বা খুলে ফেলা USB ড্রাইভ) প্রায়ই write()-এ নয়, আসে পরে — fsync() বা close()-এর ফেরত মানে। এই মান না দেখলে প্রোগ্রাম ভাবে সব ঠিক আছে। আর Linux-এ fsync একবার ব্যর্থ হলে আবার ডাকলেই একই ডেটা আবার লেখা হবে, এমন নিশ্চয়তা নেই — ভাবনার প্রশ্ন ৪ দেখুন।

কোনটি কখন — stdio, raw syscall না mmap

পথভালো যেখানেসাবধান
fopen/fgets/fprintfলাইন-ধরে লেখা, ছোট ছোট অনেক লেখা, বহনযোগ্য কোড (Windows-এও একই)দুই স্তরের buffer; fork, crash আর অন্য fd-এর সাথে মেশালে ক্রম গোলমাল
open/read/write/preadবড় ব্লক, নির্দিষ্ট offset, lock, fsync-এর নিয়ন্ত্রণ, pipe আর socketshort read/write — চাওয়ার চেয়ে কম বাইট ফিরতে পারে, loop দরকার; EINTR
mmapবড় ফাইলে random access, একাধিক প্রসেসের একই ডেটা দেখা, কপি ছাড়া পড়াI/O ত্রুটি আসে signal (SIGBUS) হয়ে; টেকসই করতে msync

Memory-mapped file-এ ফাইলটি প্রসেসের address space-এ বসে যায়; একটি pointer দিয়ে p[4736] পড়া মানেই ফাইলের বাইট 4736 পড়া — প্রথম ছোঁয়ায় page fault, kernel page cache থেকে page বসিয়ে দেয়। Offset বা lseek লাগে না; direct access এখানে অ্যারের সূচক। এর কৌশল, MAP_PRIVATE বনাম MAP_SHARED আর msync-এর একটি চালানো উদাহরণ পর্ব ৩৫-এ (Virtual memory) দেখেছেন — এখানে আর পুনরাবৃত্তি করছি না।

৯ · Windows-এর দিক — handle, CreateFile আর alternate data stream

Windows-এ fd-এর বদলে handle — একটি অস্বচ্ছ মান, যা প্রসেসের handle table-এর একটি ঘর দেখায়; সেই ঘর দেখায় kernel-এর একটি FILE_OBJECT-কে, যার ভেতরে offset (CurrentByteOffset). তিন স্তরের ছবিটি প্রায় একই। পার্থক্য হলো নকশার মেজাজে: Unix-এর open-এ কয়েকটি flag, Windows-এর CreateFile-এ সাতটি parameter — কী অধিকার চাই, অন্যদের কী করতে দেব, ফাইল না থাকলে কী হবে, আর নানা attribute.

HANDLE h = CreateFileW(L"ledger.dat",
        GENERIC_READ | GENERIC_WRITE,     /* what we want to do          */
        FILE_SHARE_READ,                  /* others may read, not write  */
        NULL, OPEN_ALWAYS,                /* open, or create if missing  */
        FILE_ATTRIBUTE_NORMAL, NULL);
if (h == INVALID_HANDLE_VALUE) { printf("error %lu\n", GetLastError()); return 1; }

LARGE_INTEGER pos = { .QuadPart = 37 * 128 };
SetFilePointerEx(h, pos, NULL, FILE_BEGIN);              /* = lseek(fd, 4736, SEEK_SET) */
DWORD got;
ReadFile(h, rec, 128, &got, NULL);                       /* = read(fd, rec, 128)        */

OVERLAPPED ov = { 0 };                                   /* lock bytes 0..99, exclusive */
LockFileEx(h, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY, 0, 100, 0, &ov);
FlushFileBuffers(h);                                     /* roughly = fsync(fd)         */
CloseHandle(h);                                          /* = close(fd)                 */
PS> Set-Content -Path notes.txt -Value "visible"
PS> Set-Content -Path notes.txt -Stream secret -Value "hidden text"
PS> (Get-Item notes.txt).Length                 # Explorer shows only the main stream
9
PS> Get-Item notes.txt -Stream * | Select-Object Stream, Length

Stream  Length
------  ------
:$DATA       9
secret      13

PS> Get-Content .\setup.exe -Stream Zone.Identifier   # a file downloaded by a browser
[ZoneTransfer]
ZoneId=3

NTFS-এর একটি বিশেষত্ব alternate data stream (ADS): একটি ফাইলের ভেতরে মূল ডেটা (:$DATA) ছাড়াও নাম দেওয়া বাড়তি stream থাকতে পারে, notes.txt:secret. Explorer-এর দেখানো আকারে এগুলো গোনা হয় না। সবচেয়ে পরিচিত ব্যবহার Zone.Identifier — ব্রাউজার ডাউনলোড করা ফাইলে এটি লিখে রাখে (“Mark of the Web”), আর Windows সেটি দেখে চালানোর আগে সতর্কবার্তা দেয়। প্রতিরক্ষার দিক থেকে মনে রাখুন: ADS-এ ডেটা লুকানো যায়, তাই তদন্তে dir /r বা Get-Item -Stream * দিয়ে stream দেখা হয়; আর FAT32/exFAT-এ বা অনেক zip-এ কপি করলে stream হারিয়ে যায়।

১০ · AI-এর যুগে ফাইল-প্রোগ্রামিং

“একটা C প্রোগ্রাম লিখে দাও যা একটি ফাইলের ১০০ নম্বর রেকর্ড বদলায়” — AI কয়েক সেকেন্ডে কোড দেবে, আর সেটি আপনার ল্যাপটপে প্রথমবার ঠিকঠাকই চলবে। ফাইল-সংক্রান্ত কোডে AI-এর সাধারণ ভুলগুলো:

AI-এর ভুলফলকীভাবে ধরবেন
write()-এর ফেরত মান না দেখা, বা ধরে নেওয়া যে সব বাইট একবারেই লেখা হয়েছেdisk full বা short write-এ নিঃশব্দে অর্ধেক ফাইলপ্রতিটি ফেরত মান দেখুন; loop করুন; fsync আর close-এর ত্রুটিও পড়ুন
“write ফিরেছে মানে ডেটা ডিস্কে”বিদ্যুৎ গেলে শেষ কয়েক সেকেন্ডের লেখা হারায়, শূন্য-আকারের কনফিগটেকসই দরকার হলে fsync; rename কৌশল (পর্ব ৪৪)
fcntl lock দিয়ে থ্রেড-সুরক্ষা, বা অন্য fd বন্ধ করায় lock হারানো না জানাrace condition যা কেবল লোডে দেখা দেয়থ্রেডের জন্য mutex; প্রসেসের জন্য OFD lock বা flock; দুই প্রসেস একসাথে হাজারবার চালিয়ে পরীক্ষা
“ctime = তৈরির সময়”; atime-এর ওপর ভরসা করে “অব্যবহৃত ফাইল” মোছার scriptভুল সিদ্ধান্ত, দরকারি ফাইল মুছে ফেলাstat চালিয়ে নিজে দেখুন; mount-এর relatime/noatime দেখুন; মোছার আগে শুধু তালিকা ছাপুন
“Permission denied” ঠিক করতে chmod 777 বা sudoসবার জন্য লেখার অনুমতি — নিরাপত্তা-ছিদ্রকে মালিক, কোন প্রসেস কোন user হিসেবে চলছে দেখুন; ন্যূনতম অনুমতি (পর্ব ৪৭)
Linux-এ Windows-এর path বা CreateFile, Windows-এ /proc; অথবা file কমান্ড ধরে নেওয়া যা আপনার মেশিনে নেইকোড compile হয় না, কমান্ড চলে নাপ্রম্পটে OS, distro, file system উল্লেখ করুন
রেকর্ডের offset হিসাবে ০ আর ১ থেকে গোনা গুলিয়ে ফেলা; unspanned ব্লকে n × R ব্যবহারপাশের রেকর্ড পড়া বা বদলানোএকটি ছোট ফাইলে ৩টি রেকর্ড লিখে নিজে মিলিয়ে দেখুন; হিসাব হাতে করুন

ফাইল-কোডের জন্য AI-কে প্রেক্ষাপট, শর্ত আর পরীক্ষার দাবি — তিনটিই দিন:

আমার পরিবেশ: Ubuntu 24.04, ext4, gcc 13. আমি C-তে একটি ছোট লেজার প্রোগ্রাম লিখছি: প্রতিটি রেকর্ড 128 বাইট, রেকর্ড নম্বর ০ থেকে। একসাথে দুটি প্রসেস চলতে পারে। ১. রেকর্ড n পড়া আর বদলানোর ফাংশন লেখো pread/pwrite দিয়ে; offset-এর হিসাব মন্তব্যে দেখাও। ২. প্রতিটি system call-এর ফেরত মান দেখো, short write হলে loop করো, errno ছাপো। ৩. একই রেকর্ডে দুই প্রসেসের সংঘাত ঠেকাতে কোন lock (fcntl, OFD lock না flock) কেন বেছে নিলে ব্যাখ্যা করো; fcntl lock-এর "যেকোনো fd বন্ধ হলে lock যায়" সমস্যাটি কীভাবে এড়ালে বলো। ৪. বদল টেকসই করতে কোথায় fsync লাগবে, আর তার খরচ কী — বলো। ৫. দুই প্রসেস একসাথে ১০,০০০ বার রেকর্ড বাড়িয়ে শেষে যোগফল মিলিয়ে দেখার একটি পরীক্ষা-script দাও। chmod 777, sudo বা "lock লাগবে না" ধরনের সহজ সমাধান দেবে না।

AI-এর লেখা ফাইল-কোড চালানোর আগে — চেকলিস্ট

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

  • Open-file table কেন লাগে, কোন স্তরে কী থাকে? — প্রতিবার নাম খোঁজা এড়াতে; per-process টেবিলে fd → entry, system-wide entry-তে offset, mode, refcount, আর inode টেবিলে ফাইলের attribute.
  • দুবার open করা আর dup করার পার্থক্য? — দুবার open মানে দুটি আলাদা entry, আলাদা offset; dup (আর fork) মানে একই entry, offset ভাগ।
  • R = 64 বাইটের রেকর্ডে রেকর্ড 50-এর (০ থেকে) offset? — 50 × 64 = 3200; lseek(fd, 3200, SEEK_SET).
  • atime, mtime, ctime-এর পার্থক্য? — শেষ পড়া, শেষ ডেটা বদল, শেষ inode বদল; ctime তৈরির সময় নয়, তৈরির সময় হলো birth time.
  • Advisory আর mandatory lock? — advisory শুধু lock চাওয়া অন্যদের আটকায় (Unix); mandatory প্রতিটি I/O-তে প্রয়োগ হয় (Windows-এর LockFileEx).

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

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

প্র ০১Mainframe-এর ফাইল রেকর্ড-বিন্যাস জানত, Unix-এর ফাইল জানে শুধু বাইট। Unix-এর এই সরলীকরণ কেন জিতল — আর এতে কী হারাল?

১৯৬০-এর দশকে IBM-এর OS/360-এ একটি ফাইল (dataset) তৈরির সময়ই job control language-এ বলে দিতে হতো রেকর্ডের ধরন (নির্দিষ্ট দৈর্ঘ্য না পরিবর্তনশীল), রেকর্ডের দৈর্ঘ্য, ব্লকের আকার, কতটা জায়গা লাগবে। এর সুবিধা ছিল বাস্তব: OS জানত ফাইলের গঠন, তাই নিজেই দক্ষভাবে ব্লক বানাত, ISAM-এর মতো access method দিত, আর ভুল বিন্যাসে পড়লে ত্রুটি দিত। DEC-এর VMS-এর RMS-ও একই পথে হেঁটেছিল।

Unix-এর স্রষ্টারা ছোট একটি মিনিকম্পিউটারে কাজ করছিলেন, আর তাঁরা উল্টো সিদ্ধান্ত নিলেন: kernel ফাইলের ভেতরের গঠন জানবে না। ফাইল মানে বাইটের সারি, শেষে কোনো বিশেষ চিহ্নও নেই — আকারই শেষ বলে দেয়। এর ফল ছিল অসাধারণ সংযোগযোগ্যতা। একটি প্রোগ্রামের আউটপুট আরেকটির ইনপুট হতে পারে, কারণ সবাই একই “ভাষা” বলে — বাইট, আর প্রথা অনুযায়ী newline-এ ভাগ করা লেখা। sort, grep, wc কোনো ফাইলের বিন্যাস না জেনেও যেকোনো ফাইলে কাজ করে; pipe সম্ভব হয় ঠিক এই কারণে। টার্মিনাল, টেপ, ডিস্ক — সব একই interface-এ।

কিন্তু কিছু হারিয়েছেও। রেকর্ডের গঠন, index, একাধিক প্রসেসের নিরাপদ হালনাগাদ — এগুলো এখন প্রতিটি অ্যাপ্লিকেশনকে নিজে বানাতে হয়। এজন্যই ডেটাবেস (SQLite, PostgreSQL) বাইট-ফাইলের ওপর নিজের page, B-tree, lock আর journal বানায় — mainframe OS যা দিত, তার অনেকটাই user space-এ ফিরে এসেছে। আবার metadata-র দিক থেকে ফাইলের “ধরন” হারিয়ে গিয়েছিল, যা পরে magic number, extension আর MIME type দিয়ে জোড়াতালি দিতে হয়েছে।

জয়ের কারণ শেষ পর্যন্ত নমনীয়তা: একটি সাধারণ interface-এর ওপর যেকোনো গঠন বানানো যায়, কিন্তু একটি নির্দিষ্ট গঠন থেকে সাধারণ interface-এ ফেরা কঠিন।

মূল উপলব্ধি: নিম্নস্তরে সরল ও সাধারণ abstraction দিলে ওপরের স্তরে উদ্ভাবনের স্বাধীনতা বাড়ে — দাম হলো, জটিলতা হারায় না, শুধু ওপরের স্তরে সরে যায়।

প্র ০২Offset কেন fd-তে না রেখে মাঝের open-file entry-তে রাখা হলো? প্রত্যেক fd-এর নিজস্ব offset থাকলে কী সমস্যা হতো?

ধরুন shell-এ লিখলেন (date; ls) > out.txt. Shell একবার out.txt খোলে, তারপর fork করে date চালায়, সেটি শেষ হলে আবার fork করে ls. দুটি প্রসেসই উত্তরাধিকারে পায় একই fd 1. যদি offset প্রতিটি fd-এর বা প্রতিটি প্রসেসের নিজস্ব হতো, ls লেখা শুরু করত offset ০ থেকে — date-এর আউটপুট মুছে দিয়ে। Offset ভাগ হয় বলেই ls-এর লেখা ঠিক date-এর পরে বসে। একই কারণে parent আর child একই লগ ফাইলে পরপর লিখতে পারে, আর 2>&1 দিয়ে stdout ও stderr একই ফাইলে মিশে যায় ঠিক ক্রমে।

আবার সব open-এর একটিই offset (inode-এ) হলেও চলত না: দুটি আলাদা প্রোগ্রাম একই ফাইল পড়লে একজনের পড়া অন্যজনকে সরিয়ে দিত — একজন বই পড়ছে আর অন্যজন তার বুকমার্ক সরিয়ে দিচ্ছে। তাই Unix-এর নকশা দুটি প্রয়োজনের মাঝে: “যারা একই উৎস থেকে এসেছে (dup, fork), তারা offset ভাগ করে; যারা আলাদাভাবে খুলেছে, তারা আলাদা।” মাঝের স্তরটির অস্তিত্বের কারণই এটি।

কিন্তু এই নকশার একটি দাম পরে প্রকাশ পায়। থ্রেডের যুগে একই প্রসেসের অনেক থ্রেড একই fd ব্যবহার করে; একজন lseek করে read করার মাঝখানে অন্যজন offset সরিয়ে দিতে পারে — দুটি call-এর মধ্যে race. সমাধান হিসেবে আসে pread/pwrite, যা offset নিজেই parameter হিসেবে নেয় আর ভাগ করা offset ছোঁয় না। আর একাধিক প্রসেস একই ফাইলের শেষে যোগ করলে “শেষে যাও, তারপর লেখো” দুই ধাপে race হয়; তাই O_APPEND: kernel প্রতিটি write-এর ঠিক আগে offset শেষে নিয়ে যায়, একটি অবিভাজ্য কাজ হিসেবে (local file system-এ; NFS-এ এই নিশ্চয়তা দুর্বল)।

মূল উপলব্ধি: কোন অবস্থা কে ভাগ করবে, সেটিই একটি interface-এর আসল নকশা। Unix-এর তিন স্তর ঠিক করে দেয় কে কী ভাগ করে — আর যখন সেই ভাগাভাগি নতুন যুগে সমস্যা করে, তখন নতুন call (pread, O_APPEND) দিয়ে ব্যতিক্রম যোগ হয়।

প্র ০৩Unix advisory lock বেছে নিল, Windows mandatory. কোনটি “সঠিক”? Linux কেন শেষ পর্যন্ত mandatory locking সরিয়েই দিল?

Mandatory lock শুনতে নিরাপদ: কেউ নিয়ম ভাঙতে পারবে না। কিন্তু Unix-এর জগতে এর কয়েকটি গুরুতর সমস্যা ছিল। প্রথমত, এটি একটি denial-of-service-এর রাস্তা খুলে দেয় — যে ফাইল পড়ার অনুমতি আছে, এমন যেকোনো ব্যবহারকারী তাতে lock নিয়ে বসে থাকলে অন্য সবার, এমনকি সিস্টেমের দরকারি প্রোগ্রামের কাজও আটকে যায়। দ্বিতীয়ত, mmap-এর সাথে এটি মেলানো কঠিন: map করা ফাইলে কোনো system call ছাড়াই পড়া-লেখা হয়, kernel প্রতিটি মেমরি-অ্যাক্সেসে lock দেখতে পারে না। তৃতীয়ত, Linux-এর বাস্তবায়নে race ছিল — lock পরীক্ষা আর আসল I/O-র মাঝে অবস্থা বদলে যেতে পারত, তাই নিশ্চয়তাটিই অসম্পূর্ণ। আর এটি চালু করতে বিশেষ mount বিকল্প আর ফাইলে অদ্ভুত একটি অনুমতি-সমন্বয় লাগত, তাই প্রায় কেউ ব্যবহারও করত না। এসব কারণে Linux ৫.১৫-এ সুবিধাটি সরিয়ে দেওয়া হয়।

Windows-এর প্রেক্ষাপট আলাদা। শুরু থেকেই ডেস্কটপ অ্যাপ্লিকেশনের জগৎ: Word বা Excel একটি নথি খুলে রাখলে অন্য প্রোগ্রাম সেটি বদলালে ব্যবহারকারীর কাজ নষ্ট হবে — তাই share mode আর byte-range lock বাধ্যতামূলক। এর সুবিধা সাধারণ ব্যবহারকারী টের পান না, কিন্তু অসুবিধা পান: “file is open in another program” বার্তা, আর চলমান DLL বা সিস্টেম ফাইল প্রতিস্থাপন করতে না পারায় আপডেটের পর restart. Unix-এ চলমান প্রোগ্রামের ফাইল unlink করে নতুনটি বসানো যায়; পুরোনো প্রসেস পুরোনো inode নিয়ে চলতে থাকে।

বাস্তবে দুই পক্ষই মাঝের দিকে এসেছে। Unix-এর ডেটাবেস ও অ্যাপ্লিকেশন নিজেদের মধ্যে advisory lock-এর প্রথা মেনে চলে (SQLite ঠিক এটিই করে), আর সমবায়ী প্রোগ্রামের জন্য সেটি যথেষ্ট। Windows-এ FILE_SHARE_DELETE দিয়ে খোলা ফাইল মোছার পথ রাখা যায়।

মূল উপলব্ধি: নিরাপত্তার কোনো ব্যবস্থাই বিনামূল্যে নয় — mandatory নিয়ম ভুলবশত ক্ষতি ঠেকায়, কিন্তু ইচ্ছাকৃত অবরোধ আর অনমনীয়তা ডেকে আনে; কোনটি ভালো তা নির্ভর করে কারা একসাথে কাজ করছে আর তারা কতটা পরস্পরকে বিশ্বাস করে তার ওপর।

প্র ০৪“fsync সফল হলো, মানে ডেটা নিরাপদ” — এই বিশ্বাস কতটা ঠিক? বাস্তবের কোন ঘটনা এর সীমা দেখিয়েছে?

২০১৮ সালে PostgreSQL-এর ডেভেলপাররা একটি অস্বস্তিকর আবিষ্কার করেন, যা পরে “fsyncgate” নামে পরিচিত হয়। PostgreSQL-এর ধারণা ছিল: fsync ব্যর্থ হলে পরে আবার fsync ডাকলে kernel একই dirty ডেটা আবার লেখার চেষ্টা করবে। কিন্তু Linux-এ লেখার ত্রুটির পর সেই page-গুলোকে অনেক ক্ষেত্রে “পরিষ্কার” চিহ্নিত করা হতো, আর ত্রুটির খবরটি একবারই জানানো হতো। ফলে দ্বিতীয় fsync সফল ফিরত, অথচ ডেটা কখনো ডিস্কে পৌঁছায়নি। আরও জটিলতা — অন্য একটি প্রসেস ফাইলটি খুলে fsync ডাকলে, ত্রুটির খবর সে পাবে কি না, তা kernel সংস্করণের ওপর নির্ভর করত। PostgreSQL এর উত্তরে fsync ব্যর্থ হলে চালিয়ে না গিয়ে ডেটাবেস বন্ধ করে (PANIC) journal থেকে পুনরুদ্ধারের পথ নেয়; Linux-ও ত্রুটি জানানোর পদ্ধতি উন্নত করে।

এর আগে, ২০০৯ সালের দিকে, ext4-এর delayed allocation নিয়ে আরেকটি বিতর্ক হয়েছিল: অনেক অ্যাপ্লিকেশন কনফিগ ফাইল বদলাত “নতুন ফাইলে লেখো, তারপর rename” দিয়ে, কিন্তু fsync ছাড়া। ext4 ডেটা লেখায় দেরি করত, rename আগে টেকসই হতো — ক্র্যাশের পর অনেক ব্যবহারকারী শূন্য-আকারের কনফিগ ফাইল পান। File system-এর ডেভেলপাররা বললেন, POSIX তো fsync ছাড়া কিছুর নিশ্চয়তা দেয় না; অ্যাপ্লিকেশনের ডেভেলপাররা বললেন, পুরোনো ext3-এ এটি কাজ করত। শেষে ext4 একটি heuristic যোগ করে — এমন rename-এর আগে ডেটা লিখে ফেলা — কিন্তু শিক্ষাটি থেকে যায়।

আরও স্তর আছে। ডিস্ক নিজে যদি cache flush-এর আদেশ ঠিকমতো না মানে (কিছু সস্তা ড্রাইভের এমন অভিযোগ আছে), OS-এর fsync কিছুই নিশ্চিত করতে পারে না। macOS-এ সাধারণ fsync ড্রাইভের cache পর্যন্ত যায় না, F_FULLFSYNC লাগে।

মূল উপলব্ধি: Durability একটি স্তরে স্তরে দেওয়া প্রতিশ্রুতির শিকল — অ্যাপ, library, kernel, file system, ড্রাইভ; শিকলের যেকোনো একটি দুর্বল আংটা পুরো নিশ্চয়তা ভেঙে দেয়, আর ত্রুটির ফেরত মান পড়াই প্রথম প্রতিরক্ষা।

প্র ০৫atime রাখা কি একটি ভুল নকশা ছিল? relatime-এর অদ্ভুত নিয়মটি (“atime ≤ mtime হলে তবেই আপডেট”) কেন এমন?

Unix-এর শুরুতে atime যোগ করা হয়েছিল যুক্তিসংগত কারণে: কোন ফাইল দীর্ঘদিন কেউ ছোঁয়নি, তা জেনে সেগুলো টেপে সরিয়ে ডিস্ক খালি করা যায়। কিন্তু এর একটি লুকানো খরচ আছে — প্রতিটি পড়া একটি metadata-লেখায় পরিণত হয়। একটি ওয়েব সার্ভার একই ছবি সেকেন্ডে হাজারবার পড়লে inode বারবার dirty হয়, journal-এ লেখা যায়, ল্যাপটপের ডিস্ক ঘুমাতে পারে না, আর SSD-তে অপ্রয়োজনীয় লেখা জমে। ২০০০-এর দশকের মাঝামাঝি Linux kernel-এর মেইলিং লিস্টে বেশ কয়েকজন অভিজ্ঞ ডেভেলপার প্রকাশ্যে atime-কে Unix-এর সবচেয়ে খরুচে নকশার একটি বলে সমালোচনা করেন, আর অনেক সার্ভারে প্রশাসকরা noatime দিয়ে mount করতেন।

তাহলে পুরোপুরি বাদ দেওয়া হলো না কেন? কারণ কিছু প্রোগ্রাম সত্যিই atime-এর ওপর নির্ভর করত। সবচেয়ে পরিচিত উদাহরণ — mutt-এর মতো কিছু ইমেইল প্রোগ্রাম mbox ফাইলে “নতুন মেইল আছে কি না” বুঝত atime আর mtime তুলনা করে: mtime নতুন হলে কেউ লিখেছে, atime তার পরে হলে কেউ পড়েছে।

relatime এই দুই চাহিদার আপস। এটি ঠিক সেই তথ্যটুকু রক্ষা করে যা এসব প্রোগ্রামের দরকার: “শেষ বদলের পরে কেউ কি পড়েছে?” তাই নিয়ম — atime যদি mtime বা ctime-এর চেয়ে পুরোনো হয় (অর্থাৎ বদলের পরে এখনো পড়া হয়নি), তবে পড়ায় আপডেট হয়; না হলে হয় না। ফলে পরপর হাজারটি পড়ায় একটিই লেখা। সঙ্গে ২৪ ঘণ্টার নিয়ম যোগ হয়, যাতে “শেষ কবে পড়া হয়েছে” মোটামুটি একদিনের নির্ভুলতায় থাকে। এটি ২০০৯ সালে kernel ২.৬.৩০-এ ডিফল্ট হয়। পরে Linux ৪.০-এ এসেছে lazytime: সময়ের আপডেট শুধু মেমরিতে রেখে পরে একসাথে ডিস্কে লেখা। উপরের bash স্ক্রিপ্টের দুই cat-এর ফলাফল ঠিক এই নিয়মের প্রতিফলন।

মূল উপলব্ধি: একবার প্রকাশিত interface-এর আচরণ সরানো প্রায় অসম্ভব, কারণ কেউ না কেউ তার ওপর নির্ভর করে; ভালো প্রকৌশল তখন খোঁজে ন্যূনতম আচরণটি, যা নির্ভরশীলদের রক্ষা করে আর বাকি খরচ বাদ দেয়।

অনুশীলন

খাতা-কলম নিন — একটি হিসাব, একটি হাতে-ট্রেস, একটি আউটপুট অনুমান আর একটি কমান্ড।

  1. রেকর্ডের হিসাব: রেকর্ড R = 200 বাইট, ব্লক B = 4096 বাইট। (ক) Unspanned বিন্যাসে blocking factor কত, প্রতি ব্লকে কত বাইট নষ্ট? (খ) রেকর্ড 457 (০ থেকে) কোন ব্লকে, ব্লকের কোন স্থানে, file offset কত? Spanned বিন্যাসে offset কত? (গ) ১০,০০০টি রেকর্ডে দুই বিন্যাসে কতটি ব্লক লাগবে?

    (ক) bfr = ⌊4096 ÷ 200⌋ = 20; ব্যবহৃত 20 × 200 = 4000, নষ্ট 96 বাইট প্রতি ব্লকে।

    (খ) ব্লক = ⌊457 ÷ 20⌋ = 22, স্থান = 457 mod 20 = 17. Offset = 22 × 4096 + 17 × 200 = 90112 + 3400 = 93512. Spanned: 457 × 200 = 91400.

    (গ) Unspanned: 10,000 ÷ 20 = 500 ব্লক (2,048,000 বাইট)। Spanned: 2,000,000 বাইট ÷ 4096 = 488.28, তাই 489 ব্লক। Unspanned-এ ১১টি ব্লক বেশি — নষ্ট জায়গার দাম, বিনিময়ে কোনো রেকর্ড দুই ব্লকে ভাগ হয় না, তাই একটি রেকর্ড পড়তে সবসময় একটি ব্লক পড়লেই চলে।

  2. হাতে ট্রেস — তিন স্তর: একটি প্রসেস চালায়: a = open("f", O_RDONLY); b = dup(a); fork(); তারপর child চালায় c = open("f", O_RDONLY); এরপর child read(b, buf, 10), parent read(a, buf, 5), child read(c, buf, 3). (ফাইলে ১০০ বাইট, সব read পুরো সফল।) শেষে (ক) system-wide open-file table-এ কতটি entry, প্রতিটির offset আর ref কত? (খ) parent এখন read(b, buf, 1) করলে কোন বাইট (০ থেকে) পাবে?

    প্রথম open → entry X (fd 3); dup → fd 4 → X (ref ২); fork → child-এর 3, 4 → X (ref ৪)। Child-এর নতুন open → entry Y, child-এর fd 5 (ref ১)।

    (ক) দুটি entry. X: child-এর ১০ বাইট আর parent-এর ৫ বাইট — দুটিই X দিয়ে, তাই offset 15, ref 4. Y: offset 3, ref 1. Inode একটিই, যাকে X আর Y দুজনেই দেখায়।

    (খ) parent-এর b (fd 4) → X, offset 15, তাই বাইট 15 আসবে, আর offset হবে 16. Child-এর Y-তে পড়ার কোনো প্রভাব নেই।

  3. আউটপুট অনুমান: Linux-এ এই অংশটি কী ছাপবে? (ধরুন সব call সফল।)
    int fd = open("t.txt", O_RDWR | O_CREAT | O_TRUNC | O_APPEND, 0600);
    write(fd, "ab", 2);
    lseek(fd, 0, SEEK_SET);
    write(fd, "cd", 2);
    char s[5] = {0};
    pread(fd, s, 4, 0);
    printf("%s %ld\n", s, (long)lseek(fd, 0, SEEK_CUR));
    
    int g = open("u.bin", O_RDWR | O_CREAT | O_TRUNC, 0600);
    write(g, "ABC", 3);
    lseek(g, 10, SEEK_SET);
    write(g, "Z", 1);
    printf("%ld\n", (long)lseek(g, 0, SEEK_END));
    abcd 4
    11

    প্রথম লাইন: O_APPEND-এ প্রতিটি write-এর ঠিক আগে kernel offset শেষে নিয়ে যায় — lseek(0) কোনো প্রভাব ফেলে না। তাই "cd" যোগ হয় "ab"-এর পরে, ফাইল "abcd". pread offset বদলায় না, আর শেষ write-এর পর offset ছিল 4. (পড়ার জন্য lseek ঠিকই কাজ করে; শুধু লেখা সবসময় শেষে।)

    দ্বিতীয় লাইন: "ABC" (বাইট 0–2), তারপর offset 10-এ "Z" (বাইট 10). আকার 11; বাইট 3 থেকে 9 একটি ছোট hole — পড়লে শূন্য। এত ছোট hole সাধারণত একই ব্লকে পড়ে, তাই ডিস্ক খরচ বাঁচে না; hole-এর লাভ হয় পুরো ব্লক জোড়া ফাঁকা থাকলে।

  4. কমান্ড লিখুন: Linux-এ (ক) একটি ১ GiB-এর sparse file তৈরি করে তার logical আকার আর আসল ডিস্ক খরচ দেখান; (খ) একটি ফাইলের শুধু তিনটি সময় আর birth time দেখার stat কমান্ড লিখুন; (গ) একটি চলমান প্রসেসের খোলা fd-গুলো আর কোনো fd-এর বর্তমান offset দেখার উপায় বলুন।
    $ truncate -s 1G big.img             # size set, no blocks allocated
    $ ls -lh big.img
    -rw-r--r-- 1 rafi rafi 1.0G Oct 26 10:12 big.img
    $ du -h big.img; du -h --apparent-size big.img
    0       big.img
    1.0G    big.img
    $ stat -c 'atime=%x%nmtime=%y%nctime=%z%nbirth=%w' big.img
    $ ls -l /proc/$$/fd                  # fds of the current shell
    $ cat /proc/$$/fdinfo/0              # pos: offset, flags: open flags (octal)

    truncate আকার বাড়ায় কিন্তু ব্লক দেয় না, তাই du 0. stat-এর %n নতুন লাইন; file system birth time না রাখলে %w দেখাবে -. /proc/PID/fdinfo/N-এর pos লাইনটিই ওই fd-এর open-file entry-র offset. অন্য ব্যবহারকারীর প্রসেসের জন্য এগুলো দেখতে অনুমতি লাগে; lsof -p PID একই তথ্য সুন্দর করে দেখায়, যদি ইনস্টল থাকে।

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

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

ক্রমUnix-ধারার OS-এ open("/home/rafi/notes.txt", O_RDONLY)-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Path-এর প্রতিটি directory ধরে ধরে নাম মিলিয়ে ফাইলের inode নম্বর খোঁজা
  2. Inode-এর অনুমতি দেখে যাচাই — এই প্রসেস কি পড়তে পারবে
  3. Inode-টি মেমরির inode টেবিলে আনা (আগে না থাকলে)
  4. System-wide open-file table-এ নতুন entry: offset 0, O_RDONLY, ref 1
  5. প্রসেসের fd টেবিলে সবচেয়ে ছোট খালি নম্বরে entry-টি বসানো
  6. সেই fd নম্বর প্রসেসকে ফেরত দেওয়া

Linux-এ একটি ফাইলে chmod 600 চালানো হলো, ডেটা বদলানো হয়নি। ফাইলের কোন সময়টি বদলাবে?

  • শুধু atime
  • শুধু mtime
  • শুধু ctime
  • Birth time

একটি প্রসেস একই ফাইল দুবার open() করে fd 3 আর fd 4 পেল, তারপর dup(3) করে পেল fd 5. fd 3 দিয়ে ২০ বাইট পড়ার পর fd 4 আর fd 5-এর offset কত?

  • fd 4 = 20, fd 5 = 20
  • fd 4 = 0, fd 5 = 20
  • fd 4 = 0, fd 5 = 0
  • fd 4 = 20, fd 5 = 0

নির্দিষ্ট দৈর্ঘ্যের রেকর্ডের একটি ফাইলে রেকর্ডের দৈর্ঘ্য 64 বাইট, রেকর্ড নম্বর ০ থেকে শুরু। রেকর্ড 50 পড়তে lseek-এ কোন offset দিতে হবে?

  • 3136
  • 114
  • 3264
  • 3200

Linux-এ প্রসেস P একটি ফাইলে fcntl দিয়ে exclusive (F_WRLCK) lock নিয়েছে। প্রসেস Q কোনো lock না চেয়ে সরাসরি ওই ফাইলে write() করলে কী হবে?

  • write সফল হবে
  • write −1 ফেরত দেবে, errno = EAGAIN
  • P lock ছাড়া পর্যন্ত write অপেক্ষা করবে
  • Q SIGBUS পাবে

একটি নতুন খালি ফাইলে lseek(fd, 1073741824, SEEK_SET) করে ১ বাইট লেখা হলো (4 KiB ব্লকের ext4). ls -l আর du মোটামুটি কী দেখাবে?

  • ls -l: 1 বাইট, du: 4 KiB
  • ls -l: 1073741825 বাইট, du: প্রায় 4 KiB
  • ls -l: 1073741825 বাইট, du: প্রায় 1 GiB
  • lseek ব্যর্থ হবে, কিছুই লেখা হবে না

একটি প্রোগ্রাম fprintf দিয়ে লেনদেনের তথ্য লিখছে। বিদ্যুৎ চলে গেলেও তথ্যটি যেন থাকে, তা নিশ্চিত করার সঠিক উপায় কোনটি?

  • শুধু fflush(fp)
  • শুধু fsync(fileno(fp))
  • fflush(fp), তারপর fsync(fileno(fp)), আর দুটির ফেরত মান যাচাই
  • লেখার পর কয়েক সেকেন্ড sleep

শব্দকোষ

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

মনে রাখুন

  1. ফাইল = সেকেন্ডারি স্টোরেজে নাম দেওয়া সম্পর্কিত তথ্য; OS ছড়ানো ব্লককে টানা বাইটের সারি হিসেবে দেখায়। নাম থাকে directory-তে, বাকি attribute inode-এ।
  2. atime = শেষ পড়া (relatime/noatime-এ সীমিত), mtime = শেষ ডেটা বদল, ctime = শেষ inode বদল (তৈরির সময় নয়), birth = তৈরি। ধরন চেনে Windows extension দিয়ে, Unix magic number দিয়ে।
  3. তিন স্তর: fd table (প্রসেসের) → open-file entry (offset, flags, ref) → inode. নতুন open = নতুন entry; dup/fork = একই entry, একই offset.
  4. Direct access: offset = n × R; unspanned-এ ব্লক = ⌊n ÷ bfr⌋. Index খোঁজাকে হাজার থেকে কয়েকটি ব্লক-পড়ায় নামায়; ISAM-এর উত্তরসূরি B+ tree.
  5. Lock: shared/exclusive; Unix-এ advisory (fcntl প্রসেসের, flock ও OFD description-এর), Windows-এ mandatory. Durability: fflush → kernel, fsync → ডিস্ক; ফেরত মান দেখুন। ls -l ≠ du.

পরের পর্বে: ফাইলের নাম আসলে কোথায় থাকে? Directory-র গঠন, path খোঁজা, hard link আর symbolic link, একটি ডিস্ক কীভাবে আরেকটির ফোল্ডারে “mount” হয়, আর VFS কীভাবে ext4, NTFS, FAT সবাইকে একই open/read/write-এর পেছনে রাখে।

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

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