National Central University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try an AI Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

3

Local AI

--

Uedu Code

--

中央大學 AQI 80 24°C PM2.5 21
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 17

檔案系統與當機復原

inode、目錄、連結,寫到一半斷電怎麼辦
Interaction實作 40 minute(s)

Learning Objectives

  • 說明 inode、資料區塊、目錄各存什麼,以及檔名為什麼不在 inode 裡
  • 算出直接與間接指標能表示的最大檔案,判斷讀某個位置要讀幾個區塊
  • 預測硬連結、符號連結、刪除開著的檔案會發生什麼
  • 說明寫入為什麼會造成不一致,比較 fsck 與日誌的做法,以及 ext4 三種模式的差別
  • 分辨 write 與 fsync 給的保證

刪了檔案,空間卻沒有回來

伺服器磁碟滿了,你刪掉一個幾 GB 的紀錄檔,df 顯示的可用空間卻完全沒變。重開服務之後空間才回來。

這不是 bug。下面這支程式打開檔案、刪掉它、再用原本的 fd 讀,在真正的 Linux 上跑的結果:

before unlink: links=1 size=11
open by name: not found
after unlink: links=0 size=11
read through fd: still here

在真正的 Linux 上實際執行的結果(Compiler Explorer 的 Linux x86-64 環境,gcc 13.3,2026-11-04)。

用名稱已經找不到這個檔案,連結數是 0,內容卻還讀得到。要理解這個結果,得先知道「名稱」和「檔案」在檔案系統裡是兩件事。這一課從這裡開始,接著處理另一個問題:寫到一半斷電,磁碟上會留下什麼。

inode、資料區塊、目錄

磁碟被切成固定大小的區塊(block,常見 4 KB)。檔案系統主要有四種東西:

結構 存什麼
資料區塊 檔案的內容
inode 一個檔案的 metadata:類型、權限、擁有者、大小、時間、連結數、內容在哪些區塊。每個 inode 有一個編號
目錄 也是一個檔案,內容是一張「名稱 → inode 編號」的對照表
點陣圖(bitmap) 哪些 inode、哪些資料區塊已經被使用

inode 裡沒有檔名。 檔名只存在目錄裡,所以一個檔案可以有很多名稱,也可以沒有名稱。

開啟 /home/a/b.txt 時,核心這樣走:讀根目錄的 inode → 讀根目錄內容,找到 home 的 inode 編號 → 讀 home 的 inode → 讀 home 的內容,找到 a……一路走到 b.txt。路徑越深,要讀的區塊越多,所以核心會把這些查找結果留在記憶體裡(dentry cache)。

open 回傳的 fd 直接指向 inode(第 4 課的開啟檔案表),之後的讀寫不再經過名稱。這就是剛才那個結果的原因:名稱刪了,inode 還被 fd 指著。

互動練習:inode 怎麼找到內容

inode 的大小固定,放不下大檔案的所有區塊編號。經典的做法:前 12 個指標直接指到資料區塊,第 13 個指到一個裝滿指標的區塊(一層間接),第 14 個是兩層,第 15 個是三層。換區塊大小看看最大檔案怎麼變,再輸入一個位置,看要走哪條路。

用 Python 算位置走哪一層

區塊 4 KB、指標 4 位元組,一個間接區塊放得下 1024 個指標。程式先算出這個位置在第幾塊,再一層一層扣掉前面能表示的範圍。

實作:算出要讀幾個區塊

補完 reads:給檔案裡的位置,回傳讀取這個位置要讀幾個區塊(間接區塊加上資料區塊,inode 已經在記憶體裡)。超過最大檔案時回傳 None。區塊 4 KB、指標 4 位元組。

你的輸出
預期輸出

ext4 的 extent

一個 1 GB 的檔案用區塊對應(區塊 4 KB),要記 26 萬多個區塊編號。但如果這些區塊本來就是連續的,只要記「從第幾塊開始,連續幾塊」就夠了。這就是 extent。

依 kernel.org 的 ext4 文件,inode 內的 60 位元組放得下 4 個 extent,一個 extent 最多涵蓋 32768 個區塊(4 KB 區塊時是 128 MiB);放不下時長成一棵樹。代價是檔案越破碎,extent 越多,所以檔案系統配置時會盡量找連續的空間。

這和第 12 課的外部碎片、第 16 課硬碟怕移動是同一件事的不同面向。

互動練習:名稱與 inode

這是在瀏覽器裡跑的小檔案系統。每個動作都會改變左邊的目錄和右邊的 inode 表,留意連結數和區塊什麼時候被釋放。

互動練習:用系統呼叫做一次

同樣的事用 C 寫(open 回傳 fd,失敗回傳 -1;link、symlink、unlink 就是剛才三個動作的系統呼叫)。右邊的「檔案與 fd」面板會顯示 inode 編號與連結數,編號是教學核心的示意值。

寫一個區塊要改三個地方

在檔案後面加一個區塊,磁碟上至少要改三個地方:資料區塊本身、inode(大小、多一個指標)、點陣圖(這個區塊被使用了)。這三個位置不相鄰,不可能一次寫完。電源隨時可能斷,而且磁碟可能不照程式下的順序寫。

只寫完一部分會怎樣?

寫了什麼 後果
只有資料 沒有人指到它,等於沒寫。不一致沒有發生,只是新資料不見了
只有點陣圖 區塊被標成使用中,卻沒有人指到它:空間遺失
只有 inode inode 指到一個標成空閒的區塊,之後可能分配給別的檔案,兩個檔案共用同一塊:嚴重不一致
inode 與點陣圖 metadata 全部一致,但區塊裡是之前別人留下的舊資料:檔案出現別人的內容

最後一種最麻煩,因為看起來完全正常。

fsck(file system check)是傳統的解法:開機時掃描所有 inode 和點陣圖,比對有沒有矛盾,再修正。它有兩個問題:磁碟越大掃得越久,而且它只看得出 metadata 之間的矛盾,看不出區塊裡的內容是不是這個檔案的。

日誌(journal)借用資料庫的想法:要改之前,先把「要改什麼」寫到磁碟上一塊專門的區域,寫完加一個 commit 記號,才開始改真正的位置。斷電後只要看日誌:

  • 有 commit:照日誌重做一次(重播)。就算已經做過一部分,重做結果也一樣。
  • 沒有 commit:這筆交易不算數,直接丟棄。真正的位置還沒被碰過。

不用掃描整個磁碟,也不會停在一半。

互動練習:在任何一步斷電

一步一步寫,隨時按「現在斷電」,再按「開機恢復」。磁碟上的狀態、不一致與恢復結果都是模擬器檢查磁碟算出來的。「列出所有斷電點」會把這個模式每一步斷電的結果全部算給你看,寫入順序 D、I、B 分別代表資料區塊、inode、點陣圖。

ext4 的三種模式

ext4 的掛載選項 data= 決定資料要受多少保護(kernel.org ext4 文件):

模式 什麼進日誌 資料的順序 斷電後可能出現
data=journal 資料與 metadata 先進日誌,再到最終位置 metadata 與資料都一致
data=ordered(預設) 只有 metadata 相關的資料先寫完,metadata 才 commit metadata 一致,不會指到還沒寫的區塊
data=writeback 只有 metadata 不保證 metadata 一致,但最近寫入的檔案可能出現錯誤的內容

kernel.org 文件也寫明:data=journal 已被標示為棄用,預計 2028 年 1 月移除;它同時會關閉 delayed allocation 與 O_DIRECT。

日誌不是每次寫入都馬上 commit,預設每 5 秒一次(掛載選項 commit=)。所以沒有呼叫 fsync 的程式,斷電時最多可能損失好幾秒的資料,檔案系統仍然是一致的。日誌保護的是檔案系統自己的結構,不是你的資料有沒有寫完。

模擬器簡化了:一個交易只放一個動作,也沒有模擬 delayed allocation。實際上 ext4 會延後配置區塊,把很多動作併成一個交易。

write 與 fsync 的差別

呼叫 回傳時保證什麼
write 資料進了頁快取,其他行程讀得到。不保證已經在磁碟上
fsync(fd) 這個檔案的資料與 metadata 已送到裝置,裝置回報完成才回傳
fdatasync(fd) 同上,但不是讀取資料必需的 metadata(例如修改時間)可以不寫
close 不保證寫到磁碟

fsync 很慢,因為真的要等裝置。需要資料不遺失的程式(資料庫、郵件伺服器)才在關鍵時刻呼叫,不是每次寫入都呼叫。

還有一個常被忽略的細節:man 2 fsync 寫明,對檔案 fsync 不一定讓目錄裡的那筆名稱也寫到磁碟,要對目錄也 fsync。

所以要安全地更新一個設定檔,標準的作法是:

fd = open("config.tmp", O_WRONLY | O_CREAT | O_TRUNC, 0644);
write(fd, data, len);
fsync(fd);                   // 新內容確實寫到磁碟
close(fd);
rename("config.tmp", "config");  // rename 不會讓別人看到寫一半的內容
// 再對所在的目錄 fsync,確保名稱的改動也寫到磁碟

斷電時你看到的要嘛是完整的舊檔,要嘛是完整的新檔。這個技巧把「改很多地方」變成「改一個名稱」,和日誌的 commit 是同一種思路:讓最後一步成為唯一的決定點。

確認一下

1.

ln a.txt b.txt 之後,執行 rm a.txt,再執行 cat b.txt。會怎樣?

2.

區塊 1 KB、指標 4 位元組、12 個直接指標。只用直接與一層間接,最大檔案是多少?

3.

在沒有日誌的檔案系統上,先寫 inode、再寫點陣圖、最後寫資料。在寫完點陣圖之後斷電,fsck 會怎麼回報?

4.

一支程式呼叫 write 成功之後沒有呼叫 fsync,10 毫秒後斷電。檔案系統是 ext4 預設模式。最可能的結果是?

5.

為什麼日誌的 commit 區塊要在其他日誌區塊都寫完之後才寫?

6.

程式要更新設定檔,避免斷電時留下寫一半的內容。下面哪個作法最安全?

第 1 次作答

重點整理

  • inode 存 metadata 與內容的位置,沒有檔名;目錄是名稱到 inode 的對照表。檔案要連結數歸零而且沒有 fd 開著才會消失。
  • 直接與間接指標讓小檔案快、大檔案也放得下;ext4 用 extent 記連續的範圍。
  • 硬連結指向 inode,符號連結指向名稱。
  • 一次寫入要改好幾個地方,斷電會留下不一致。fsck 掃描全部而且看不出內容對錯;日誌先寫下意圖,靠 commit 決定重播或丟棄。
  • ext4 預設 ordered:只有 metadata 進日誌,但資料先寫完。日誌保護結構,要保護你的資料得自己呼叫 fsync。

下一課是最後一課:一台機器怎麼假裝成很多台。

這一課完成了!
Next lesson:虛擬化與容器
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。