পাঠ ৪১ · ৫৬-এর মধ্যে · মডিউল ৯
Home / Courses / Operating Systems (OS) / জার্নালিং

ফাইল সিস্টেম ইমপ্লিমেন্টেশন ও জার্নালিং

File system implementation & journaling
৮ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • একটি ফাইল সিস্টেম কীভাবে স্তরে স্তরে বাস্তবায়িত হয়, প্রতিটি স্তর কী দায়িত্ব পালন করে
  • ক্র্যাশ কনসিস্টেন্সি সমস্যা ঠিক কীভাবে ঘটে এবং কেন এটি একটি বাস্তব, গুরুতর সমস্যা
  • জার্নালিং (write-ahead logging) কীভাবে কাজ করে এবং পুরনো ধাঁচের fsck-স্টাইল ফুল-স্ক্যান রিকভারির চেয়ে কেন দ্রুত
  • Python দিয়ে একটি প্রকৃত commit/crash/recover সিমুলেশন — একটি হাতেকলমে যাচাইকৃত ইনকনসিস্টেন্ট অবস্থা তৈরি ও সংশোধন

১ · লেয়ার্ড ফাইল সিস্টেম ইমপ্লিমেন্টেশন

L04-এ আমরা দেখেছিলাম কীভাবে একটি OS স্তরে স্তরে সংগঠিত হতে পারে (লেয়ার্ড আর্কিটেকচার) — একটি ফাইল সিস্টেমও ঠিক একইভাবে স্তরে স্তরে বাস্তবায়িত হয়, প্রতিটি স্তর শুধু তার ঠিক নিচের/উপরের স্তরের সেবা জানলেই চলে।

অ্যাপ্লিকেশন লজিক্যাল ফাইল সিস্টেম (মেটাডেটা, L37-38) ফাইল-অর্গানাইজেশন মডিউল (লজিক্যাল→ফিজিক্যাল, L39) বেসিক ফাইল সিস্টেম (জেনেরিক ব্লক read/write) I/O কন্ট্রোল ও ডিভাইস ড্রাইভার (M10)
প্রতিটি স্তর শুধু তার ঠিক নিচের স্তরের সেবা ব্যবহার করে — L04-এর OS-লেয়ারিং নীতির সরাসরি প্রয়োগ।

২ · ক্র্যাশ কনসিস্টেন্সি সমস্যা

একটি ফাইল তৈরি বা আপডেট করতে প্রায়ই একাধিক পৃথক পদক্ষেপ লাগে — যেমন একটি নতুন ডেটা ব্লক বরাদ্দ করা (L40) এবং একটি ডিরেক্টরি এন্ট্রি (L38) লেখা। যদি সিস্টেম এই দুই ধাপের মাঝপথে ক্র্যাশ করে বা পাওয়ার হারায়, ফাইল সিস্টেম একটি ইনকনসিস্টেন্ট অবস্থায়Crash Consistency Problemএকটি মাল্টি-স্টেপ ফাইল-সিস্টেম আপডেটের মাঝপথে ক্র্যাশ হলে সিস্টেম একটি ইনকনসিস্টেন্ট, ক্ষতিগ্রস্ত অবস্থায় থেকে যেতে পারে। থেকে যেতে পারে — যেমন একটি ব্লক "বরাদ্দকৃত" চিহ্নিত, অথচ কোনো ফাইল সেটির মালিক নয় (একটি "অনাথ" বা orphan ব্লক)।

৩ · জার্নালিং — সমাধান

জার্নালিং (Journaling)Journaling / Write-Ahead Loggingপ্রকৃত পরিবর্তনের আগে সেই পরিবর্তনের বর্ণনা একটি আলাদা, সিকোয়েনশিয়াল লগে লিখে রাখার কৌশল, যাতে ক্র্যাশের পর দ্রুত সামঞ্জস্যপূর্ণ অবস্থা ফিরে পাওয়া যায়। (write-ahead logging) — আধুনিক সমাধান: প্রকৃত ফাইল-সিস্টেম ডেটা স্ট্রাকচারে পরিবর্তন করার আগে, প্রথমে উদ্দেশ্যকৃত পরিবর্তনের একটি বর্ণনা একটি আলাদা, সিকোয়েনশিয়াল "জার্নাল" (লগ)-এ লিখে রাখা হয়। জার্নাল এন্ট্রি নিরাপদে লেখা হয়ে গেলেই কেবল প্রকৃত পরিবর্তন প্রয়োগ করা হয়। ক্র্যাশ হলে, রিবুটের সময় ফাইল সিস্টেম জার্নাল রিপ্লে করে (বা অসম্পূর্ণ এন্ট্রি নিরাপদে বাতিল করে) সামঞ্জস্যপূর্ণ অবস্থা পুনরুদ্ধার করে — পুরনো ধাঁচের fsck-স্টাইল ফুল-ডিস্ক স্ক্যানের তুলনায় এটি অনেক দ্রুত, কারণ পুরো ডিস্ক পরীক্ষার বদলে শুধু জার্নালের সাম্প্রতিক অংশ পরীক্ষা করলেই চলে।

নিচের কোড সেলে আমরা এটি সত্যিকারভাবে সিমুলেট করব — প্রথমে একটি স্বাভাবিক, সম্পূর্ণ কমিট, তারপর একটি ফাইল তৈরির মাঝপথে একটি প্রকৃত ক্র্যাশ (ব্লক বরাদ্দ হলো কিন্তু ডিরেক্টরি এন্ট্রি লেখা হলো না), এবং সবশেষে recover() চালিয়ে দেখাব এই আসল ইনকনসিস্টেন্সি (একটি "অনাথ" ব্লক) সঠিকভাবে সনাক্ত ও সংশোধন হচ্ছে।

Python
# L41 -- জার্নালিং (write-ahead logging) সিমুলেশন, toy in-memory ফাইল সিস্টেম স্টেট

real_fs_state = {
    "directory": {},     # filename -> {"block": physical_block_num, "size": bytes}
    "data_blocks": {},    # block_num -> True (বরাদ্দকৃত) / অনুপস্থিত মানে ফ্রি
}
journal = []


def apply_op(op, state):
    category, key, value = op
    state[category][key] = value


def commit_transaction(operations, journal_log, state):
    """জার্নালে আগে লেখা (pending), তারপর real state-এ প্রয়োগ, শেষে committed চিহ্নিত।"""
    entry = {"id": len(journal_log), "operations": operations, "status": "pending"}
    journal_log.append(entry)
    for op in operations:
        apply_op(op, state)
    entry["status"] = "committed"
    return entry


def commit_transaction_but_crash_after(operations, journal_log, state, ops_before_crash):
    """একই প্রক্রিয়া, কিন্তু ops_before_crash সংখ্যক অপারেশনের পর 'ক্র্যাশ' হয়ে যায় --
    journal-এ এন্ট্রি থেকে যায় status='pending' অবস্থায়, real state আংশিক আপডেট হয়ে থমকে যায়।"""
    entry = {"id": len(journal_log), "operations": operations, "status": "pending"}
    journal_log.append(entry)
    for op in operations[:ops_before_crash]:
        apply_op(op, state)
    # <-- সিস্টেম ক্র্যাশ ঠিক এখানে -- বাকি অপারেশনগুলো প্রয়োগ হলো না, entry এখনও "pending"
    return entry


def find_orphan_blocks(state):
    """যেসব ডেটা ব্লক 'বরাদ্দকৃত' চিহ্নিত কিন্তু কোনো ডিরেক্টরি এন্ট্রি তাদের মালিক নয় --
    এটিই একটি বাস্তব ইনকনসিস্টেন্সির লক্ষণ (একটি পূর্ণাঙ্গ অপারেশনের বদলে আংশিক প্রয়োগের ফল)।"""
    referenced = {meta["block"] for meta in state["directory"].values()}
    allocated = {b for b, used in state["data_blocks"].items() if used}
    return allocated - referenced


def recover(journal_log, state):
    """journal স্ক্যান করে যেসব এন্ট্রি এখনও 'committed' নয় (ক্র্যাশের সময় অসম্পূর্ণ ছিল),
    তাদের সবগুলো অপারেশন পুনরায় (redo) প্রয়োগ করে সামঞ্জস্যপূর্ণ চূড়ান্ত অবস্থায় ফেরায়।"""
    recovered_ids = []
    for entry in journal_log:
        if entry["status"] != "committed":
            for op in entry["operations"]:
                apply_op(op, state)
            entry["status"] = "committed"
            recovered_ids.append(entry["id"])
    return recovered_ids


# --- ধাপ ১: একটি স্বাভাবিক, সম্পূর্ণ কমিট (ক্র্যাশ ছাড়াই) ---
commit_transaction(
    [("data_blocks", 3, True), ("directory", "readme.txt", {"block": 3, "size": 40})],
    journal, real_fs_state,
)
print("ধাপ ১ -- স্বাভাবিক কমিটের পর:")
print("  directory:", real_fs_state["directory"])
print("  data_blocks:", real_fs_state["data_blocks"])
print("  অনাথ (orphan) ব্লক:", find_orphan_blocks(real_fs_state))

# --- ধাপ ২: report.txt তৈরির মাঝপথে ক্র্যাশ -- ব্লক বরাদ্দ হলো, কিন্তু ডিরেক্টরি এন্ট্রি লেখা হলো না ---
report_ops = [("data_blocks", 7, True), ("directory", "report.txt", {"block": 7, "size": 120})]
commit_transaction_but_crash_after(report_ops, journal, real_fs_state, ops_before_crash=1)

print("\nধাপ ২ -- ক্র্যাশের পরের অবস্থা (রিবুটের ঠিক আগে):")
print("  directory:", real_fs_state["directory"])
print("  data_blocks:", real_fs_state["data_blocks"])
orphans_before = find_orphan_blocks(real_fs_state)
print("  অনাথ (orphan) ব্লক:", orphans_before, "<- ইনকনসিস্টেন্সি সনাক্ত: ব্লক 7 বরাদ্দ কিন্তু কোনো ফাইলের নয়")
assert orphans_before == {7}, "প্রত্যাশিত ইনকনসিস্টেন্সি তৈরি হয়নি!"

# --- ধাপ ৩: রিবুটের সময় recover() চলবে ---
recovered = recover(journal, real_fs_state)
print(f"\nধাপ ৩ -- recover() চলল -> পুনরুদ্ধার করা entry id: {recovered}")
print("  directory:", real_fs_state["directory"])
print("  data_blocks:", real_fs_state["data_blocks"])
orphans_after = find_orphan_blocks(real_fs_state)
print("  অনাথ (orphan) ব্লক:", orphans_after, "<- এখন সামঞ্জস্যপূর্ণ")
assert orphans_after == set(), "রিকভারির পরও ইনকনসিস্টেন্সি রয়ে গেছে!"

print("\n--- চূড়ান্ত journal অবস্থা ---")
for e in journal:
    print(f"  entry {e['id']}: status={e['status']}, ops={e['operations']}")

    
ধাপ ২-এর orphans_before সেট {7}-এর সমান হওয়াটাই আসল প্রমাণ যে আমরা একটি সত্যিকারের ইনকনসিস্টেন্ট অবস্থা তৈরি করেছি — ব্লক 7 "বরাদ্দকৃত" চিহ্নিত অথচ কোনো ডিরেক্টরি এন্ট্রি তার মালিক নয়। ধাপ ৩-এর পর এই সেট খালি হয়ে যাওয়া প্রমাণ করে recover() জার্নালের অসম্পূর্ণ এন্ট্রি রিপ্লে করে প্রকৃতপক্ষে সমস্যাটি সমাধান করেছে, শুধু দাবি করেনি।
মূল কথা · Key takeaway

জার্নালিং ফাইল সিস্টেমের "সব-অথবা-কিছুই না" (all-or-nothing) নিশ্চয়তা দেয় — একটি ট্রানজ্যাকশন হয় সম্পূর্ণভাবে প্রয়োগ হবে, নয়তো রিকভারির মাধ্যমে সম্পূর্ণভাবে প্রয়োগ করে দেওয়া হবে, কিন্তু কখনও আংশিক অবস্থায় আটকে থাকবে না। এই পাঠ দিয়েই M9 ফাইল সিস্টেম মডিউল সম্পূর্ণ হলো — L37-এর একক ফাইল থেকে শুরু করে L38-এর সংগঠন, L39-L40-এর সংরক্ষণ, এবং এই পাঠের নির্ভরযোগ্যতা পর্যন্ত। পরবর্তী মডিউল (M10) নিয়ে যাবে সেই ডিস্ক ব্লকগুলো হার্ডওয়্যার পর্যায়ে আসলে কীভাবে অ্যাক্সেস হয়।

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

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

প্র ০১ জার্নালে লেখাও তো নিজেই একটি ডিস্ক-রাইট অপারেশন — এটি কি নিজেই মাঝপথে ক্র্যাশ হয়ে সমস্যা তৈরি করতে পারে না?

তাত্ত্বিকভাবে যেকোনো রাইট অপারেশনই মাঝপথে ক্র্যাশ হতে পারে, কিন্তু জার্নাল এন্ট্রি ডিজাইন করা হয় ছোট, সিকোয়েনশিয়াল ও (বাস্তব সিস্টেমে) একটি একক অ্যাটমিক রাইট হিসেবে লেখার জন্য — যাতে হয় সম্পূর্ণ জার্নাল এন্ট্রি লেখা হয়, নয়তো একেবারেই না (আংশিক জার্নাল এন্ট্রি সহজে সনাক্তযোগ্য একটি চেকসাম/মার্কারের মাধ্যমে বাতিল করা হয়)। মূল কৌশলটি হলো — জটিল, বহু-ধাপের প্রকৃত ফাইল-সিস্টেম পরিবর্তনের ঝুঁকিকে একটি ছোট, সহজে-অ্যাটমিক-করা-যায় এমন জার্নাল-রাইটে রূপান্তর করা, সমস্যাটি সম্পূর্ণ দূর না করেই তার জটিলতা অনেক কমিয়ে ফেলা।

প্র ০২ fsck-স্টাইল ফুল-ডিস্ক স্ক্যান রিকভারি তো তত্ত্বগতভাবে যেকোনো ইনকনসিস্টেন্সি ধরতে পারে — জার্নালিং কী বাড়তি সুবিধা দেয় তাহলে?

গতি — এটিই মূল সুবিধা। fsck-কে পুরো ডিস্কের প্রতিটি মেটাডেটা স্ট্রাকচার পরীক্ষা করতে হয় (বিশাল ডিস্কে ঘণ্টার পর ঘণ্টা লাগতে পারে), কারণ এটি জানে না কোথায় সমস্যা হতে পারে — সবখানেই খুঁজতে হয়। জার্নালিং-এ ঠিক উল্টো — রিকভারি প্রক্রিয়া শুধু জার্নালের সাম্প্রতিক এন্ট্রিগুলো দেখে, কারণ এটি ঠিক জানে ক্র্যাশের সময় ঠিক কোন ট্রানজ্যাকশনগুলো "পেন্ডিং" ছিল — উপরের কোড সেলে recover() পুরো real_fs_state স্ক্যান করে না, শুধু journal-এর status != "committed" এন্ট্রিগুলো দেখে।

প্র ০৩ কোড সেলে recover() সব অপারেশন আবার (redo) প্রয়োগ করে দেয়, এমনকি যেগুলো ক্র্যাশের আগেই প্রয়োগ হয়ে গিয়েছিল (যেমন data_blocks[7]=True) — এটি কি সমস্যা তৈরি করে না?

না, কারণ প্রতিটি অপারেশন ইডেমপোটেন্ট (idempotent) — একবার প্রয়োগ করি বা দুইবার, ফলাফল একই। state["data_blocks"][7] = True যদি ইতিমধ্যে True থাকে, আবার True বসালে কিছুই বদলায় না। এই ইচ্ছাকৃত ডিজাইন সিদ্ধান্তই recover()-কে সহজ রাখে — এটিকে ঠিক মনে রাখতে হয় না ক্র্যাশের ঠিক আগে কোন কোন অপারেশন ইতিমধ্যে প্রয়োগ হয়েছিল, শুধু পুরো ট্রানজ্যাকশনটি নিরাপদে আবার প্রয়োগ করে দিলেই চূড়ান্ত ফলাফল সঠিক হয়।

অনুশীলন

  1. চিন্তা করুন: আপনার কম্পিউটার হঠাৎ পাওয়ার হারিয়ে বন্ধ হয়ে গেলে পরের বুটে প্রায়ই "চেকিং ফাইল সিস্টেম" জাতীয় একটি বার্তা দেখা যায় (বা আধুনিক সিস্টেমে খুব দ্রুত বুট হয়ে যায়) — কোনটি জার্নালিং ফাইল সিস্টেমের লক্ষণ?

    দ্রুত বুট হয়ে যাওয়াটাই জার্নালিং-এর লক্ষণ — এর মানে ফাইল সিস্টেম শুধু জার্নালের সাম্প্রতিক এন্ট্রি পরীক্ষা করেই সন্তুষ্ট হয়েছে (এই পাঠের recover()-এর মতো)। বিপরীতে, দীর্ঘ "চেকিং ফাইল সিস্টেম" বার্তা সাধারণত পুরনো, নন-জার্নালিং ফাইল সিস্টেম বা এমন পরিস্থিতির লক্ষণ যেখানে জার্নালও নিজে ক্ষতিগ্রস্ত হয়েছে, বাধ্য হয়ে সম্পূর্ণ fsck-স্টাইল স্ক্যান চালাতে হচ্ছে।

  2. পরীক্ষা করুন: কোড সেলে commit_transaction_but_crash_after(report_ops, journal, real_fs_state, ops_before_crash=0) চালিয়ে দেখুন (অর্থাৎ জার্নাল-রাইটের ঠিক পরেই, কোনো অপারেশন প্রয়োগের আগেই ক্র্যাশ) — recover() কি তখনও সঠিকভাবে কাজ করে?

    হ্যাঁ। ops_before_crash=0 মানে real_fs_state-এ কোনো পরিবর্তনই হয়নি, শুধু জার্নালে entry-টি "pending" অবস্থায় যোগ হয়েছে। recover() তখনও ঠিক একইভাবে কাজ করে — status != "committed" দেখে report_ops-এর দুটো অপারেশনই প্রথমবার প্রয়োগ করবে। এটি দেখায় জার্নালিং শুধু "আংশিক প্রয়োগ" নয়, বরং "সম্পূর্ণ প্রয়োগ হয়নি" (শূন্য অপারেশন প্রয়োগ সহ) সব পরিস্থিতিতেই একইভাবে কাজ করে।

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

আগের পাঠ
ফ্রি স্পেস ম্যানেজমেন্ট