JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

目錄、路徑與掛載

第 2 篇導覽讓你握著一個檔案描述符——但 open() 一開始究竟是怎麼找到那個檔案的?這篇導覽會跟著一個路徑名,一段一段地走過目錄樹,說明共用如何重塑了那棵樹,並揭開那個把每一顆磁碟和 USB 隨身碟悄悄接成同一棵無縫大樹的把戲:掛載。

目錄不過是一種特別的檔案

讀完第 2 篇導覽,你已經能打開一個檔案、讀它,並握著一個索引進開檔表的檔案描述符。但我們略過了一個小小的奇蹟:「open(\"/home/jo/notes.txt\")」這個呼叫,是怎麼把那串字元,變成一顆裝著數百萬個檔案的磁碟上正確的那一個的?答案就是目錄——而你首先要拋掉的觀念是:目錄並不是某種把檔案裝在裡頭的神奇容器。它不是。目錄本身也只是一個檔案;它的特別之處只在於它裝的內容。一般檔案裝的是你的文字或像素,目錄裝的則是一張小小的對照表,把人能讀的名字,對應到每個項目在磁碟上的身分。

那個「在磁碟上的身分」是什麼?在大多數類 Unix 系統上,它是一個叫做 inode 號(索引節點號)的數字——把它想成檔案真正的序號,是那個實際擁有檔案位元組與其屬性(大小、擁有者、時間戳記、權限位元)的東西。因此,一條目錄項目薄得驚人:它幾乎只是一對(名字、inode 號)。「notes.txt」這個名字住在目錄裡;關於這個檔案的其他一切,都住在目錄所指向的那個 inode 裡。把這個拆分記在心裡,因為正是這個觀念,會讓第 4 篇導覽的硬連結與符號連結,從神秘突然變得理所當然。

從一張扁平清單,到一棵樹——再到一張圖

最早的系統整台機器只有一個目錄——所有檔案的一張扁平清單。這在兩個使用者都想要一個叫「report」的檔案之前都還行得通;或者,在你有一萬個檔案、要把它們全都取成唯一名字變成一場惡夢之前,也還行。解法是讓一條目錄項目指向另一個目錄,這樣目錄就能層層相套。這就給了你早已熟到骨子裡的樹狀目錄:最上面有一個根(在 Unix 上寫成「/」,在 Windows 的某個磁碟區上則是「C:\\」),分岔成子目錄、再分岔,而檔案就是葉子。樹狀結構是幾乎每個系統都採用的目錄結構,它最大的優點是:每個檔案都恰好有一個位置、一個完整的名字。

但純粹的樹有個極限。假設兩個程式設計師想要真正共用一個原始碼檔案——不是兩份會逐漸分歧的副本,而是那唯一一份真正的檔案,這樣不論誰改動,兩人都看得到。樹禁止這件事,因為在樹裡每個節點恰好只有一個父親、一個名字。演進的下一步,是把這條規則放寬成無環圖目錄:允許一個檔案(甚至一整個目錄)能被一條以上的路徑抵達。現在「那個」檔案可以同時掛在 /home/jo/project/main.c 和 /home/sam/shared/main.c 底下,而且字面上就是同一份位元組。這正是檔案共用之所以可能的結構性原因,也是通往第 4 篇導覽的大門:一個硬連結,正好就是圖裡指向同一個 inode 的第二個名字。

「無環」這個詞做著實在的工作,值得停一拍。一張允許多條路徑的圖,如果你不小心,就可能含有一個環——目錄 A 連到 B,B 又連回 A。環是毒藥:一個為了算出某資料夾大小、或為了遞迴刪除它而走訪整棵樹的程式,會永遠迴圈下去;而對 inode 做參考計數(有幾個名字指向我?)也會在一個結構能指向自己時崩潰。所以系統會費盡力氣讓目錄圖保持無環,這正是為什麼指向目錄的硬連結通常被禁止、而指向一般檔案的硬連結卻沒問題的一大原因。我們在這裡講得誠實,因為教科書的圖示很少告訴你這些規則為何存在。

一段一段地走一條路徑

現在我們可以回答先前迴避的問題了:open() 究竟是怎麼從一個字串找到檔案的?一個路徑名就是穿過目錄圖的一條路線,寫成一串以斜線分隔的名字。它有兩種口味。絕對路徑從根開始,像 /home/jo/notes.txt——它從樹頂給出完整的指路,所以不論你站在哪裡,它都指同一個東西。相對路徑,像 project/main.c,則是從你的目前工作目錄開始讀——也就是你的行程此刻「身處」的那個資料夾,核心會替每個行程各自記住它。相對路徑是一種簡寫,就像「往前數兩扇門」只有在別人知道你現在站在哪扇門前時,才講得通。

核心解析一條路徑,靠的是一個有耐心的迴圈,叫做路徑名解析,或者說「走」路徑。它不是一次查找;而是每一段各查一次。要解析 /home/jo/notes.txt,核心從根 inode 開始,讀根目錄的表,找到名叫「home」的項目和它的 inode,讀那個 inode(它最好是個目錄),讀它的表去找「jo」,就這樣一路往下。每一個「/」都是更深一步,每一步都是一次目錄讀取加一次名字查找,而只要某一段不存在、或你沒有權限進入沿途的某個目錄,這趟走訪就會立刻失敗。這就是幾項誠實成本的來源,也是為什麼又深又長的路徑開起來會稍微慢一點的原因。

resolve  /home/jo/notes.txt   (an absolute path)

  start: root inode "/"
   |  read root's table -> "home" => inode 2
   v
  inode 2  (dir)
   |  read its table -> "jo"   => inode 9
   v
  inode 9  (dir)
   |  read its table -> "notes.txt" => inode 47
   v
  inode 47 (regular file)  ==>  open it, hand back an fd

  one component  =  one directory read + one name lookup
  a missing name OR no 'x' permission on a dir  =>  walk fails
解析一條路徑是一個迴圈,不是單一步驟:每一段(以斜線分隔)都做一次目錄讀取加一次名字查找,最後停在檔案的 inode 上。

掛載:把多顆磁碟縫成一棵樹

這裡有件你不斷在用、卻渾然不覺的事。你的機器有不只一個儲存裝置——一顆主硬碟、也許還有第二顆 SSD、一根 USB 隨身碟、一個網路共享——然而在 Unix 上,你看不到「磁碟 1」和「磁碟 2」。你看到的是一棵從「/」開始的單一大樹,而那根 USB 隨身碟,就只是以一個資料夾的樣子出現,比方說 /media/usb。兩顆實體上分開、各自有自己根目錄的磁碟,怎麼可能住在同一棵樹裡?這個把戲叫做掛載,它是整個檔案系統介面裡最優雅的點子之一。

掛載的意思是:拿第二個檔案系統的根,把它嫁接到你已經有的那棵樹裡某個空目錄上。被選中的那個目錄就是掛載點。在你把 USB 隨身碟掛在 /media/usb 之後,路徑解析的規則就變了:每當走訪抵達 /media/usb 時,核心會悄悄地從原本那個(空的)目錄,轉向 USB 隨身碟檔案系統的根,並從那裡繼續走。把它想成把一棵樹的枝條嫁接到另一棵樹上——一旦接好,你從外面分辨不出一棵樹在哪裡結束、下一棵又從哪裡開始;它全都只是一條連續的枝幹。這道接縫,是刻意設計成看不見的。

  1. 挑一個掛載點:現有大樹裡的某個目錄,例如 /media/usb。按慣例它是空的,因為掛載會把原本在那裡的東西藏起來,直到你卸載為止。
  2. 讀取新檔案系統的超級區塊——裝置上那個小小的標頭,它說「我是一個 ext4 檔案系統、我的根 inode 在這裡、我的區塊大小是這個」。核心靠它來理解這顆磁碟的版面配置。
  3. 把這次嫁接記進一張記憶體中的掛載表:「inode X 那個目錄現在被蓋住了;任何抵達它的解析,都應跳到裝置 Z 上的根 inode Y。」
  4. 從此以後,每一條穿越掛載點的路徑都被悄悄轉向。使用者空間看到的是一棵樹;核心卻知道它橫跨了好幾個裝置。

為什麼這種設計是一場勝利(以及它在哪裡漏水)

退一步,欣賞一下目錄加掛載這套設計替你買到了什麼。一個程式可以打開「/etc/config」,而不必知道、也不必在乎它究竟住在開機用的 SSD、一個網路共享、還是一顆 RAM 磁碟上——路徑是它唯一需要的位址,其餘的事核心會在掛載表背後處理掉。這正是你在第 1 篇導覽中,於檔案共用與檔案抽象本身上看到的那份慷慨:上頭是一個乾淨的名字,底下是雜亂的硬體。這也是為什麼同一條路徑在不同機器上可以代表不同的實體東西——對可攜性而言這是優點,但若你假設路徑是放諸四海皆準的,它就是個陷阱。

現在來看誠實的角落,因為那棵無縫的大樹藏著真實的接縫。第一,你真的沒辦法跨掛載點做硬連結:硬連結不過是指向某個 inode 號的第二個名字,但 inode 號只在單一檔案系統內才唯一,所以磁碟 A 上的一個名字,無法指向磁碟 B 上的一個 inode。(這正好是符號連結要填補的缺口,因為符號連結存的是一個路徑字串、而不是一個 inode——第 4 篇導覽會大量倚重這個區別。)第二,這種轉向是有代價的:走訪時穿越一個掛載點是一點點額外的工,而走一條長路徑是許多次目錄讀取,這就是為什麼系統會積極地把解析過的路徑片段快取起來。

再一個誠實的點,它連向第 5 篇導覽。走一條路徑不只是一次資料查找;它是一連串的權限檢查。要在前往某個檔案的途中穿過一個目錄,你的行程需要那個目錄的執行(「x」)位元——光是為了通過它就需要,而不是為了列出它的內容。所以一個檔案可能完全可讀,卻徹底搆不著,因為某個祖先目錄拒絕你進入,就像一棟大樓,你的鑰匙開得了辦公室的門、卻開不了大廳的門。我們會在第 5 篇導覽裡,把權限位元與存取控制清單完整拆開來談;現在,你只要先記住這個出人意料的事實:搆到一個檔案、和讀取一個檔案,是兩種分開的權限,在走訪的兩個不同時點上各自被決定。