題幹與適用情境
某服務發布了 /srv/releases/v1/app.conf,把 /srv/live/pinned.conf 建成硬連結,把 /srv/live/current.conf 建成相對符號連結。一個 worker 先開啟發布檔案,維運人員接著重新命名 並刪除若干目錄項目。
請解釋檔名、硬連結、符號連結、inode 和已開啟檔案描述符分別參照什麼,並預測每一步結果。接著為 不可變快照、可移動發布指標和跨檔案系統參照選擇合適機制。本文限定 Linux 一般檔案;檔案系統特有 快照和 Windows 捷徑不在範圍內。
面試官考察什麼
第一項訊號是物件模型。目錄項目把一個名稱綁定到 inode。硬連結是指向同一 inode 的另一個目錄 項目,不存在特殊的「原始檔名」。符號連結是另一個檔案系統物件,其內容是一段在使用時解析的路徑。
第二項訊號是生命週期推演。unlink 刪除一個目錄項目,不一定立即銷毀檔案物件。只要還有其他硬 連結或已開啟檔案描述,inode 和資料就會繼續存在。符號連結本身可以存在,但它保存的路徑可能已無法 解析。
第三項訊號是能否預測操作,而非背誦比較表。重新命名一個硬連結不影響其他同伴;重新命名符號連結 操作的是連結自身。移動相對符號連結或目標可能改變路徑含義。硬連結不能跨檔案系統,因為 inode 身分只在單一檔案系統內有效。
最後看工程判斷:檢查物件身分與連結計數,需要時進行同檔案系統原子替換,並避免在不受信任路徑上 使用「先檢查、後開啟」的符號連結競態。
回答前需要釐清的問題
- 來源檔案與新硬連結是否位於同一個掛載檔案系統? 若不是,
link會回傳EXDEV;複製或
符號連結解決的是另一種需求。
- 呼叫端需要固定物件或可移動名稱? 硬連結固定一個 inode;發布別名通常應使用可替換目錄項目的
符號連結。
- 目標是一般檔案或目錄? Linux 通常禁止建立目錄硬連結,符號連結可以指向檔案或目錄。
- 符號連結保存絕對路徑或相對路徑? 相對目標從符號連結所在目錄開始解析,不是從行程目前工作
目錄開始。
- 是否有行程已經開啟檔案? 重新命名或刪除路徑不會把既有描述符改指其他物件。
- 切換對讀取端是否必須原子可見?
rename的原子替換限於同一掛載檔案系統;跨檔案系統需要
不同發布協定。
- 路徑元件是否由不受信任使用者控制? 若是,先
lstat再open存在競態,必須在開啟
操作本身強制執行遍歷策略。
30 秒回答框架
「硬連結是指向同一 inode 的另一個目錄項目,因此多個名稱共享資料和中繼資料。符號連結擁有自己的 inode,保存一段存取時才解析的路徑。刪除一個硬連結名稱只會減少連結計數;其他硬連結或已開啟描述符 仍可讓物件存活。刪除或重新命名目標則可能讓符號連結懸空。
我會用 ls -li、stat、lstat 或 readlink 以及預先開啟的描述符驗證。需要在同一檔案系統 固定精確物件時用硬連結;需要可替換發布別名或跨檔案系統參照時用符號連結;原子切換別名時先建立暫時 符號連結,再在同一檔案系統內 rename。不受信任路徑不能分兩步檢查,開啟時就要限制符號連結和 目錄越界。」
分步驟深入解答
第一步:建立名稱到物件的模型
對一般檔案而言,目錄保存名稱與 inode 參照。inode 保存檔案類型、擁有者、權限、時間戳記、大小、 資料區塊對應和硬連結計數。檔案內容不屬於某個特殊的「原始檔名」。
硬連結為同一 inode 增加另一個目錄項目。透過任意名稱修改位元組或權限,其他名稱都會看到變化。 符號連結則有自己的 inode,內容是一段如 ../releases/v1/app.conf 的字串;一般路徑查找會繼續 解析這段字串指向的名稱。
第二步:執行具體實驗
以下命令建立題設,Shell 檔案描述符會故意保持開啟:
mkdir -p /srv/releases/v1 /srv/live
printf 'version=1\n' > /srv/releases/v1/app.conf
ln /srv/releases/v1/app.conf /srv/live/pinned.conf
ln -s ../releases/v1/app.conf /srv/live/current.conf
exec 3< /srv/releases/v1/app.conf
ls -li /srv/releases/v1/app.conf /srv/live/pinned.conf
readlink /srv/live/current.conf
stat -L -c '%F %i %h' /srv/live/current.conf
stat -c '%F %i %h' /srv/live/current.conf
mv /srv/releases/v1/app.conf /srv/releases/v1/app.conf.moved
cat /srv/live/pinned.conf
cat /srv/live/current.conf
cat <&3
rm /srv/releases/v1/app.conf.moved /srv/live/pinned.conf
cat <&3
exec 3<&-重新命名前,發布路徑和 pinned.conf 顯示相同 inode,硬連結計數為二。readlink 輸出符號連結 保存的字串。一般 stat 跟隨符號連結查看目標,範例中的不跟隨檢查則查看符號連結物件自身。
第三步:預測重新命名行為
同檔案系統內的 mv 使用重新命名操作處理來源目錄項目。inode 沒有移動,所以 pinned.conf 和 描述符 3 繼續可用。current.conf 仍保存 ../releases/v1/app.conf;舊名稱消失後,解除參照會 失敗。若重新命名符號連結本身,移動的是連結物件,不是目標;由於相對路徑從連結新的所在目錄解析, 含義還可能改變。
若發布需要切換 current 且不能出現名稱缺失空窗,應先在同一目錄建立完整的暫時符號連結,再用 rename 替換 current。替換後才開啟的讀取端會解析舊或新目錄項目;已經開啟舊目標的行程繼續 使用舊描述符。
第四步:預測刪除與空間回收
刪除 app.conf.moved 只減少一個硬連結,pinned.conf 仍指向 inode,資料繼續存在。再刪除 pinned.conf 後,最後一個目錄項目消失,但描述符 3 仍持有開啟參照,cat <&3 仍能讀取。只有 最後一個硬連結和最後一個開啟參照都消失,儲存空間才可回收。
這也解釋了為什麼刪除正在寫入的大型日誌不一定釋放空間,但不能因此任意截斷未知描述符。應先確認所屬 行程及其日誌輪替契約,必要時依安全流程傳送訊號或重新啟動。
第五步:套用檔案系統與目錄邊界
inode 編號只在所屬檔案系統內唯一。硬連結無法從另一個檔案系統命名該 inode,link 會回傳 EXDEV。符號連結保存路徑,存取時可以跨掛載點解析,也可以指向目錄或尚不存在的目標。
Linux 阻止一般使用者建立目錄硬連結,以避免檔案樹出現環和遍歷歧義。目錄符號連結允許存在,但移動 相對符號連結可能使其失效,不同遞迴工具也可能選擇不同的跟隨策略。備份、刪除和發布工具需要明確這些 策略。
第六步:依不變條件選擇
若不變條件是「新增名稱必須持續指向同一個 inode」,物件位於同一檔案系統且確實需要共享中繼資料, 選擇硬連結。若不變條件是「這個別名解析到目前保存的路徑」,尤其目標是目錄、發布指標或跨檔案系統 名稱,選擇符號連結。若需要獨立位元組、中繼資料、保留週期或檔案系統位置,選擇複製。
兩種連結都不是備份。硬連結名稱共享同一物件的修改與損壞;符號連結不保存目標資料。備份需要獨立故障 邊界、獨立保留邊界和還原測試。
第七步:驗證身分與安全路徑解析
比較裝置號碼與 inode,不要只比較 inode,因為不同檔案系統可能重複使用相同編號。用 stat 檢查 硬連結計數,用不跟隨連結的檢查查看符號連結自身,用 readlink 查看其內容,並在描述符保持開啟時 實際執行重新命名與刪除序列。
對不受信任路徑,lstat(path) 後再 open(path) 會讓攻擊者有機會替換中間元件。Linux 上應以 可信目錄描述符為起點開啟,並透過 openat2 的解析限制禁止符號連結或離開目錄。安全規則必須由最終 回傳描述符的那次路徑查找執行。
高品質示範回答
「我先從目錄項目解釋。app.conf 與 pinned.conf 是同一 inode 的兩個平等名稱,所以共享內容、 權限、擁有者和時間戳記。current.conf 是獨立的符號連結 inode,保存 ../releases/v1/app.conf,相對路徑從 /srv/live 開始解析。
發布路徑重新命名後,硬連結與已經開啟的描述符仍指向原檔案物件。符號連結保存的仍是舊路徑,所以變成 懸空連結。刪除移動後的名稱時,硬連結繼續讓物件存活;再刪除硬連結後,開啟描述符依然可用,核心要等 它關閉後才能回收物件。
我會用硬連結固定同檔案系統內的精確成品,用符號連結作為可移動的 current 別名,需要獨立保留時 建立真正副本。更新 current 時,先建立暫時連結,再在同一目錄原子重新命名。我會用裝置號碼與 inode、連結計數、readlink、懸空存取和開啟描述符測試證明結果。不受信任路徑則在開啟操作內強制 禁止符號連結與目錄越界。」
常見錯誤
- 把硬連結稱為指向原始檔名的指標 → 所有硬連結都是同一 inode 的平等目錄項目 → **說明名稱、
inode 身分與連結計數。**
- 聲稱刪除一定立即銷毀檔案 → 其他名稱或開啟參照可能仍然存在 → **同時追蹤連結計數與檔案
描述符。**
- 把符號連結當成保存 inode 參照 → 它保存的路徑日後可能解析到別處或失敗 → **檢查連結內容與
解析起點。**
- 只比較 inode 編號 → inode 編號僅在檔案系統內唯一 → 比較裝置號碼加 inode。
- 用硬連結跨掛載點或指向目錄 → Linux 會拒絕這些情境 → 依獨立性需求選擇符號連結或複製。
- 移動相對符號連結卻不重新核對 → 連結所在目錄決定解析起點 → 移動後重新計算或產生目標。
- 把任一種連結當作備份 → 一個共享物件,另一個只保存路徑 → 建立可獨立還原的副本。
- 先檢查路徑、稍後再開啟 → 攻擊者可以在兩步之間替換符號連結 → 開啟時原子執行遍歷限制。
追問及應對
追問一:不同硬連結有各自的權限和擁有者嗎?
沒有。權限、擁有者、大小和時間戳記屬於共享 inode,透過一個名稱修改後,所有同伴都會看到。目錄項目 名稱和父目錄權限是另一層;重新命名或刪除名稱由所在目錄的權限決定。
追問二:相對符號連結移動後為什麼可能失效?
其內容從符號連結所在目錄開始解析。移動連結會改變這個起點,卻不改變保存的字串。絕對符號連結雖不受 該移動影響,但在容器、chroot、不同掛載配置或其他主機中可能指錯。應依搬移需求選擇。
追問三:rename 能否跨檔案系統原子替換 current?
不能。Linux rename 跨掛載檔案系統會回傳 EXDEV。應先在目標檔案系統內發布完整物件,再在目標 目錄建立暫時別名,並於同一目錄重新命名覆蓋 current;先前的複製階段需要另外定義清理與復原。
追問四:最後一個路徑已刪除,磁碟空間為什麼仍被占用?
某個開啟檔案描述可能仍參照 inode。先定位描述符及所屬行程,再使用應用程式安全的輪替或重新啟動 流程。只有最後一個名稱和最後一個開啟參照都消失,空間才會回收;刪除路徑只證明該名稱不存在。
追問五:stat、lstat 與 readlink 有何差異?
stat 通常跟隨最終符號連結並報告目標;lstat 報告連結物件;readlink 回傳連結保存的路徑, 但不解析它。驗證時要同時證明別名自身和它目前指向的物件。
追問六:上傳目錄如何防止符號連結競態?
以可信目錄描述符為起點開啟,讓路徑查找本身強制停留在該目錄下並禁止不允許的符號連結,再用基於描述 符的操作檢查結果。先做路徑檢查、再另外開啟會留下被替換空窗。