問題與使用情境
題目包含三個不同保證。原子可見性要求重新開啟目標路徑的讀者只能看到一個完整版本。當機持久性要求已確認版本重新開機後仍可存取。寫入者隔離決定兩個更新競爭時誰獲勝。單獨一次 rename() 在檔案系統契約內只解決第一項。
假設使用正確實作 fsync() 和同檔案系統原子替換的本機 Linux 檔案系統,寫入者已序列化,以前確認的版本也由同一協定寫入。64 KiB 是面試假設。網路檔案系統、謊報快取刷新的硬體、多檔案交易和惡意目錄修改需要另外定義契約。
適用對象包括設定快照、本機代理程式狀態、檢查點清單和編輯器儲存。狀態跨多個檔案或需要並行交易時,嵌入式資料庫或日誌通常比繼續手寫協定可靠。
面試官考察什麼
第一個訊號是能否區分 write() 成功、rename() 原子性和持久提交。Linux 文件說明 write() 可能短寫,成功回傳也不證明資料已落盤。fsync(file) 會刷新檔案資料及相關中繼資料,卻不一定持久化目錄項目,最後這條邊需要 fsync(parent_directory)。
第二個訊號是順序:新位元組必須先持久化,命名空間才能把目標名稱指向它;目錄項目隨後必須持久化,呼叫端才能收到成功。漏掉或顛倒屏障都會留下當機窗口。
第三個訊號是誠實的失敗語意。fsync() 可能延遲回報 EIO、ENOSPC 或配額錯誤。最終目錄同步失敗時,rename 可能已可見,但持久性未知,介面應回傳「不確定」,不能聲稱成功或保證回復舊版。
回答前的釐清問題
- 原子是否涵蓋程序結束和主機斷電? 程序結束不會清空核心頁面快取;題目要求主機當機持久性,因此必須同步。
- 部署檔案系統的契約是什麼? NFS、FUSE、overlay 和特殊儲存堆疊可能不同,必須驗證實際環境。
- 是否有多個寫入者? 基礎方案序列化寫入者;唯一暫存名稱只防檔名衝突,不防後寫覆蓋。
- 是否同時更新多個檔案? 一次 rename 只能提交一個路徑替換,多檔案不變量需要世代目錄、日誌或資料庫。
- 舊檔案描述符能否繼續讀舊版? 可以;rename 改變目錄映射,已開啟的描述符仍指向舊 inode。
- 什麼內容才有效? 發布前驗證序列化、schema、總和檢查碼與版本;檔案系統原子性不能修復完整但錯誤的負載。
30 秒回答框架
「我先區分原子可見性和持久性。開啟父目錄,在同一目錄用排他建立產生唯一暫存檔,循環寫完全部位元組,設定必要中繼資料,再 fsync 暫存檔。關閉後用 rename 原子替換目標,再 fsync 父目錄,最後才回傳成功。檔案同步保證新 inode 內容持久,rename 發布一個完整版本,目錄同步保證名稱到 inode 的映射持久。我會序列化寫入者,把包括最終目錄同步失敗在內的系統呼叫錯誤回傳為非成功或不確定狀態,啟動時清理孤兒暫存檔,並在每個邊界做斷電測試,不會把終止程序當成持久性證明。」
分步深入分析
呼叫端視角的提交點是父目錄 fsync 成功。協定如下:
durableReplace(parentDir, targetName, bytes):
dirfd = open(parentDir, read-only | directory | close-on-exec)
tmpName = uniqueSiblingName(targetName)
tmpfd = openat(dirfd, tmpName, create | exclusive | write-only | close-on-exec, mode)
writeAll(tmpfd, bytes) // 重試 EINTR,短寫後推進偏移
setRequiredMetadata(tmpfd) // 權限和擁有者屬於契約時先設定
fsync(tmpfd) // 檢查延遲 I/O 和配置錯誤
close(tmpfd) // 檢查回傳值
renameat(dirfd, tmpName, dirfd, targetName)
fsync(dirfd) // 持久化命名空間變更
close(dirfd)
return committed暫存檔必須與目標同目錄,使 rename 留在同一檔案系統,避免 EXDEV 後退化成非原子複製。使用排他建立和不可預測的程序內後綴,不跟隨攻擊者預先建立的符號連結。權限等必要中繼資料應在檔案同步前設定。
即使是一般檔案也要實作 writeAll。正回傳值小於要求長度就是短寫,應推進緩衝區繼續寫;只在合適條件下重試中斷。成功 write() 只表示核心接受資料,close() 也不是提交屏障,延遲錯誤可能到 fsync() 才出現。
接著同步暫存檔。fsync 刷新修改的資料和 inode 中繼資料,並等待裝置回報完成。fdatasync 可省略與後續讀取無關的中繼資料,但仍要持久化檔案大小等必要資訊。通用面試回答優先用較容易稽核的 fsync,除非實測和檔案系統契約支援最佳化。
檔案同步成功後才執行 rename。目標已存在時,Linux 保證其他程序開啟目標不會觀察到路徑缺失。已開啟舊檔案的讀者可讀完舊 inode,之後開啟的讀者解析到新 inode;兩者都是完整版本。
rename 修改的是目錄中繼資料。檔案同步不一定持久化目錄項目,因此 rename 後必須同步父目錄。正確順序構成證明:
新內容持久
-> 目標名稱原子指向新 inode
-> 目標名稱映射持久
-> 才能向呼叫端確認成功rename 前當機可能留下舊目標和孤兒暫存檔。rename 後、目錄同步前,新目標可能目前可見,但重新開機後是否可達尚無承諾。目錄同步成功後,已同步內容和目錄映射才組成提交版本。復原程序只清理能確認歸屬且符合命名規則的過期暫存檔,不能因上次回傳錯誤就刪除目標。
錯誤狀態不能只有布林值。rename 前失敗是 not_committed,舊目標仍是權威版本。rename 後目錄同步失敗是 indeterminate:新檔案可能可見,也可能無法承受當機。回傳錯誤並保留診斷,復原時檢查世代編號或總和檢查碼。盲目寫回舊值會再製造一次未提交變更,還可能覆蓋更晚的寫入者。
多個寫入者可用所有寫入者都遵守的鎖包住完整讀改寫流程,或攜帶預期世代並拒絕舊更新。唯一暫存名稱只隔離暫存。未序列化時,兩次 rename 各自原子,後者仍會靜默覆蓋前者。多個關聯檔案應發布一個不可變世代目錄,再持久切換單一清單指標,或改用日誌、SQLite。
嚴格模式每次提交至少支付檔案與目錄兩個落盤屏障。若只需原子可見性,可提供名稱明確的寬鬆模式:暫存檔加 rename,但允許斷電遺失最新版。高頻更新可把多項邏輯修改合併進一次日誌提交,或使用支援 group commit 的資料庫;不能為了效能測試暗中削弱契約。
測試要涵蓋短寫、EINTR、ENOSPC、檔案同步 EIO、檔案同步前後當機、rename 前中後當機、目錄同步失敗。重新開機後斷言目標可解析,只是最後確認版本或允許出現的未確認新版本,絕不能是位元組混合;每個已確認世代都必須存活。SIGKILL 只能測試清理和描述符行為,受控 VM 或儲存當機才能涵蓋揮發性快取遺失。
高品質範例回答
「我先定義三個契約:讀者只看到完整版本、已確認更新承受主機當機、寫入者已序列化;目前目標也假定由同一協定提交。
我開啟父目錄,在目標旁排他建立唯一暫存檔。循環處理短寫,設定必要權限,對暫存檔呼叫並檢查 fsync,再關閉。然後用 rename 替換目標。這是原子可見邊界:現有讀者可繼續使用舊 inode,新開啟取得完整新 inode。
rename 還不是持久提交。它修改了目錄項目,而檔案同步不一定持久化目錄項目。因此我同步父目錄,只有成功後才回傳成功。rename 前當機保留舊目標和可能的暫存檔;rename 後、目錄同步前失敗屬於不確定狀態,應回傳錯誤並在復原時檢查世代,不能承諾回復舊版。
我會注入短寫和每個系統呼叫錯誤,並在每個邊界讓 VM 當機。判定條件是目標始終為有效舊世代或新世代,絕無撕裂混合,而且每個已確認世代重新開機後仍存在。需要並行寫入者或多檔案交易時,我增加世代檢查與鎖,或改用日誌/資料庫,不會聲稱 rename 能解決這些問題。」
常見錯誤
- 原地覆寫目標 → 當機會暴露截斷或混合內容 → 完整暫存同目錄檔案後再 rename。
- 假設一次
write()寫完 → 短寫會截斷暫存檔 → 循環到全部完成或出錯。 - 暫存檔未同步就 rename → 持久名稱可能指向未完整落盤的資料 → 先同步新 inode。
- 把 rename 當持久提交 → 原子可見不代表目錄項目落盤 → rename 後同步父目錄。
- 暫存目錄位於另一掛載點 → rename 回傳
EXDEV或退化成複製 → 在目標目錄建立暫存檔。 - 忽略
fsync錯誤 → 延遲儲存失敗變成假成功 → 傳播非成功或不確定結果。 - 只用
SIGKILL測試 → 核心快取仍存活 → 增加受控主機或儲存當機。 - 讓多個寫入者競爭 → 各自原子仍會遺失一個版本 → 序列化或比較預期世代。
追問與回答
追問 1:為什麼 fsync(temp) 還不夠?
它持久化暫存 inode 的內容和相關中繼資料。目標路徑屬於目錄項目;rename 修改映射後,Linux 明確說明檔案同步不一定持久化包含它的目錄,因此確認前還要同步父目錄。
追問 2:其他程序已開啟目標時,rename 仍然原子嗎?
路徑查找的替換是原子的。舊描述符繼續指向舊 inode,新開啟解析到新 inode。讀者若要在多次讀取中維持同一世代,應保持一個描述符,不要中途重新開啟。
追問 3:rename 後目錄 fsync 失敗,介面回傳什麼?
回傳帶「不確定提交狀態」的錯誤。rename 可能已可見,應用程式無法證明它會否承受重新開機。記錄預期世代和錯誤,停止確認成功,由復原程序驗證目標;盲目回復舊版可能破壞更晚更新。
追問 4:fdatasync 能取代 fsync 嗎?
平台契約和實測支援時可以。它省略與資料讀取無關的中繼資料,但仍須持久化檔案大小等必要資訊,也不能省略 rename 後的父目錄同步。通用回答使用 fsync 較容易稽核。
追問 5:如何原子更新三個檔案?
三次獨立 rename 會暴露混合世代。把所有檔案寫進不可變世代目錄並持久化,再原子切換並同步一個清單或指標;也可使用日誌或嵌入式資料庫。讀者解析一次世代並在整個操作中保持它。
追問 6:兩個寫入者如何避免遺失更新?
使用所有寫入者都遵守的鎖涵蓋完整讀改提交流程,或攜帶預期世代並拒絕過期提交。唯一暫存名稱只能防暫存衝突;原子 rename 不比較業務版本,天然允許後寫覆蓋。