從根上理解 Node.js 的 Fs 模塊:一起設計一個文件系統(tǒng)
Node.js 提供了 File System 的 api,可以讀寫文件、目錄、修改權限、創(chuàng)建軟鏈等。
可能大家 api 用的比較熟練,但對于這些 api 的原理不一定理解。要想真正理解 File System,還得從根上來看。
下面我們從 0 到 1 設計一個文件系統(tǒng)試試。
從 0 到 1 設計一個文件系統(tǒng)
什么是文件呢?
這樣一份比較完整的資料就是文件。
但是計算機的持久化存儲是在硬盤,主要是磁盤和 SSD 固定硬盤:
計算機里的文件是一個邏輯概念,并不是物理上存在的一個個實體。
那么如果給了這樣一個硬盤,讓我們自己造一個文件系統(tǒng)出來,實現(xiàn)文件的功能。怎么做呢?
簡單想了下:挨著放。
然后還要記錄下索引,啥文件在啥位置:
這樣是可以,但是有個問題,萬一文件 B 被刪除了,那對應的空間就要釋放:
然后又來了一個文件 D,發(fā)現(xiàn)放不進去啊,這地方太小了。
A 和 B 中間這塊空間就是碎片,碎片會把磁盤可用空間割裂成不連續(xù)的很多小塊。
怎么辦呢?如何更好的利用磁盤空間?
分塊!把文件分成一小塊一小塊的,比如 1k 為一個塊,可以不用連續(xù)存,把所有塊記錄在索引中就行了。
索引咋記錄的看不清,放大點來看:
每個文件的索引中都記錄了把數(shù)據(jù)存在了哪幾個塊,這樣一塊一塊的讀出來就行了。
而且文件刪除了,那些塊還可以繼續(xù)用,不會有很大的碎片。
妙啊!
難怪文件系統(tǒng)第一步就是分塊,內存管理第一步是分頁,這樣才能高效利用空間啊。
這種索引節(jié)點,可以叫做 index node,簡稱 inode 就好了。
而且,除了文件名和存放的塊以外,還可以記錄其他信息,比如創(chuàng)建時間、修改時間、文件權限、所屬用戶等等。
到這里就可以對文件下個物理層面上的定義了: 文件就是 inode 記錄的信息和它所索引的一系列數(shù)據(jù)塊。
那我寫文件利用了一個塊,刪除文件釋放了一個塊,怎么知道呢?得單獨記錄下來塊的狀態(tài)。
數(shù)據(jù)塊只有空閑和被占用兩個狀態(tài),一個二進制位就行了。通過一段二進制數(shù)把所有塊的空閑狀態(tài)記錄下來。一個位代表一個值,這叫做位圖。在這里就是塊位圖。
硬盤中大部分是數(shù)據(jù)塊,開頭的一段來放 inode 所在的塊和數(shù)據(jù)塊的塊位圖。
inode 也是存在塊里的,比如我們規(guī)定只能用 5 個塊放 inode,那 inode 總量是有限的,也就是是說文件系統(tǒng)可以創(chuàng)建的文件是有上限的。
我們也要記錄下所有 inode 的空閑狀態(tài),也就是 inode 位圖。
簡單理一下我們設計的文件系統(tǒng):
為了更好的利用硬盤空間,我們對數(shù)據(jù)進行了分塊,每個文件用到了哪些塊記錄在 inode 里。inode 還記錄了文件的創(chuàng)建時間、修改時間、權限等信息。
通過塊位圖來記錄數(shù)據(jù)塊空閑狀態(tài),通過 inode 位圖來記錄 inode 的空閑狀態(tài)。
但我如果想知道硬盤中有幾個塊、用了幾個,有幾個 inode、用了幾個,怎么辦呢?
簡單,遍歷一遍塊位圖和 inode 位圖,就知道個數(shù)了。
但每次這么算太慢了,這就像我們設計數(shù)據(jù)庫的時候,一個論壇下面有多少個帖子,這個數(shù)據(jù)不會是每次用 sql 查詢的,而是在帖子增刪的時候動態(tài)維護一個字段在數(shù)據(jù)庫表中,直接查詢即可。
那我們也設計一個塊用來存這種統(tǒng)計信息,也就是:
這個塊是較高層的統(tǒng)計信息,我們可以叫它超級塊。
現(xiàn)在把我們設計好的文件系統(tǒng)交給用戶,就可以通過它來高效利用硬盤了。
發(fā)布版本:神光文件系統(tǒng) V1.0。
但現(xiàn)在我們的文件系統(tǒng)好像還不是很好用,只能創(chuàng)建文件,那如果我創(chuàng)建了 1000 個文件呢?
查詢起來慢,也容易命名沖突。
怎么辦呢?
命名空間!目錄!就像文件夾的思想一樣。
那怎么實現(xiàn)呢?
每個 inode 是一個文件,那把 inode 組織成樹不就行了。
比如我們有兩個文件 B 和 C:
我們創(chuàng)建一個目錄 A:
在 inode 里面添加一個 isDirectory 的屬性,如果是目錄,那么就讀取數(shù)據(jù)塊的內容,找到其中的 inode 節(jié)點編號,就知道該目錄下的文件了。
這就是目錄的實現(xiàn)原理:通過 inode 的 isDirectory 屬性區(qū)分是文件還是目錄,如果是目錄就讀取數(shù)據(jù)塊中的 inode 信息來查找子文件,如果是文件,則直接讀取數(shù)據(jù)塊作為文件內容。
從一個 inode 到另一個 inode 的查找順序叫做路徑(path)。
比如這樣一個文件的 inode 查找順序:
那文件路徑就是 /A/D/dongdong.jpg
這就是文件路徑的本質:文件路徑就是 inode 查找順序
現(xiàn)在我們支持目錄的嵌套了,可以把文件、目錄組織成樹形結構方便管理。
發(fā)布版本:神光文件系統(tǒng) v2.0。
現(xiàn)在一個 inode 只有一條路徑過來,因為是樹嘛,那如果我想兩條路徑都可以找到同一個 inode 呢?
比如 /A/D/dongdong.jpg 可以訪問到該文件:
我想通過 /A/B/dongdong.jpg 也能訪問,怎么辦呢?
直接指過去不就行了。
這樣確實可以有兩條路徑找到同一個文件,這個額外的鏈接我們起名叫做硬鏈接。
但是因為一個節(jié)點有兩個父節(jié)點,就不再是樹了,變成了圖。所以,文件樹這個概念嚴格意義上來說還是存在問題的,可能是個文件圖。
但是我如果想給 dongdong.jpg 換個名字,叫 dongdong2.jpg 呢?
現(xiàn)在都是同一個 inode 節(jié)點,改了就都改了。但我只想改 /A/B 的 path 的文件名,別的不改。
那就再創(chuàng)建個 inode 節(jié)點,用來改名,然后這個 inode 節(jié)點指向 dongdong.jpg。
這種不直接指過去,多了一個 inode 來改名,之后再指過去,這種我們叫做軟鏈接。
為什么叫硬呢?因為改不了,直接指向同一個 inode。
為什么叫軟呢?因為可以改,多了一層 inode 用來改名。
所以我們分別起名硬軟鏈接。
硬鏈接和軟鏈接都是用于多條路徑可以查找到同一個文件的,但是硬鏈接不能單獨改名,軟連接可以。
monorepo 的實現(xiàn)就是基于軟連接的,可以指向同一個目錄 inode,而且還可以起個別名。
實現(xiàn)了軟硬鏈接,可以發(fā)新版本了。
發(fā)布版本:神光文件系統(tǒng) v3.0。
復盤一下我們設計的文件系統(tǒng):
v1.0:
通過數(shù)據(jù)塊來存儲文件內容,提高硬盤利用效率
通過 inode 記錄所用的數(shù)據(jù)塊和文件的創(chuàng)建時間、權限等信息
通過塊位圖記錄數(shù)據(jù)塊的空閑狀態(tài)
通過 inode 位圖記錄 inode 空閑狀態(tài)。
通過超級塊記錄 inode 和數(shù)據(jù)塊的統(tǒng)計信息。
這個版本實現(xiàn)了文件的存取,但是不支持目錄。
v2.0:
通過 inode 中添加一個屬性來記錄是文件還是目錄
目錄的數(shù)據(jù)塊中存放具體文件列表的 inode 信息,讀取目錄的時候可以讀取出文件列表。
按照目錄 inode、文件 inode 的一層層的查找順序叫做文件路徑。
這個版本實現(xiàn)了目錄和路徑的功能。
v3.0:
通過多個目錄 inode 包含同一個 inode 的方式,來實現(xiàn)多條路徑查找同一文件的功能,叫做硬鏈接。
目錄先創(chuàng)建一個 inode 節(jié)點用于改名,然后該 inode 節(jié)點指向目標 inode 節(jié)點,這叫做軟連接。
這個版本實現(xiàn)了多條路徑查找統(tǒng)一文件的軟硬鏈接功能。
真實的文件系統(tǒng)也是類似的實現(xiàn),目前有很多文件系統(tǒng),比如 ext2、FAT 等,原理和我們設計的文件系統(tǒng)差不多。
文件系統(tǒng)設計完了,回到最開始的目標,我們是想深入理解 Node.js 的 File System 的 api。下面就來看一下。
Node.js 的文件系統(tǒng) api
Node.js 通過 V8 注入了 fs 的 api 給 js 用,底層是通過 c++ 調用操作系統(tǒng)的文件系統(tǒng)功能,也就是我們上面設計的那種文件系統(tǒng)。
我們調用的 fs 的 api 最終就是調用了操作系統(tǒng)的文件系統(tǒng)功能。
自己設計了一個文件系統(tǒng)之后,我們再來看下 fs 的 api,是不是理解更深了:
- fs.stat 獲取 inode 中的信息的
- fs.chmod 修改文件權限,也是修改 inode 信息
- fs.chown 修改所屬用戶,也是修改 inode 信息
- fs.copyFile 復制 inode 和數(shù)據(jù)塊,把新的 inode 添加到對應目錄的 inode 內容中
- fs.link 創(chuàng)建軟硬鏈接的,也就是多條路徑查找同一個文件的 inode,軟連接還可以改名
- fs.mkdir 創(chuàng)建目錄 inode 的
- fs.rmdir 讀取目錄 inode 包含的所有文件 inode 的
具體 api 還有很多,但都是用來操作我們上面設計的那個文件系統(tǒng)的。
從根上理解了文件系統(tǒng),用這些 api 也會得心應手。
總結
為了真正理解 Node.js 的 fs 模塊,我們一起設計了一個文件系統(tǒng):
- 把文件分成不同數(shù)據(jù)塊,這樣可以高效利用磁盤空間。
- 文件的索引節(jié)點 index node 中記錄了所包含的數(shù)據(jù)塊和創(chuàng)建時間、權限、是否是目錄等信息。
- 通過塊位圖記錄數(shù)據(jù)塊空閑狀態(tài)。
- 通過 inode 位圖記錄 inode 空閑狀態(tài)。
- 通過超級塊記錄硬盤的 inode、數(shù)據(jù)塊的使用信息。
- 通過 inode 對應的數(shù)據(jù)塊內容包含文件 inode 信息列表的方式實現(xiàn)了目錄節(jié)點。
我們得出一些重要結論:
文件本質上就是 inode + 數(shù)據(jù)塊。
路徑本質上就是查找目標 inode 的路徑。
硬鏈接本質上就是多個目錄 inode 包含同一個 inode。
軟連接本質上就是多創(chuàng)建了一個 inode 用于改名,對應數(shù)據(jù)塊中指向目標 inode。
Node.js 的 fs api 是通過 c++ 注入 v8 的對操作系統(tǒng)能力的調用,理解了文件系統(tǒng),再學那些 api 就很輕松了。