本文首發於我的部落格,轉載請註明出處:http://kohpoll.github.io/blog/2016/05/30/hardlink-symlink-require-in-nodejs/
最近在使用 fs.symlink
實現軟鏈時,發現文件裡面寫的是:fs.symlink(target, path)
;然而 man ln
的時候顯示的是:ln source_file target_file
;而且,require
模組的時候其實還會處理軟鏈但是處理的又不是想象中那樣。於是,我徹底被相關東西繞暈。這篇文章算是我的學習筆記,希望對你有幫助。
inode
我們首先來看看 linux 系統裡面的一個重要概念:inode。
我們知道,檔案儲存在硬碟上,硬碟儲存的最小單位是扇區(sector,每個扇區 512 B)。而作業系統讀取檔案時,按塊讀取(連續的多個扇區),也就是說檔案存取的最小單位是塊(block,塊通常是 4 KB)。
除了檔案資料,我們還必須儲存檔案的元資訊(如:檔案大小、檔案建立者、檔案資料的塊位置、檔案讀/寫/執行許可權、檔案時間戳等等),這種儲存檔案元資訊的結構就稱為 inode。我們可以使用 stat
命令檢視檔案的 inode 資訊。在 Node 中,呼叫 fs.stat
後返回的結果中也有相關資訊
每個 inode 都有一個唯一的號碼標誌,linux 系統內部使用 inode 的號碼來識別檔案,並不使用檔名。我們開啟一個檔案時,系統首先找到檔名對應的 inode 號碼,然後透過 inode 號碼獲取 inode 資訊,最後根據 inode 資訊中的檔案資料所在的 block 讀出資料。
實際上,在 linux 系統中,目錄也是一種檔案。目錄檔案包含一系列目錄項,每個目錄項由兩部分組成:所包含檔案的檔名,以及該檔名對應的 inode 號碼。我們可以使用 ls -i
來列出目錄中的檔案以及它們的 inode 號碼。這其實也解釋了僅更改目錄的讀許可權,並不能實現讀取目錄下所有檔案內容的原因,通常需要 chmod -R
來進行遞迴更改。
總結下:
硬碟存取的最小單位是扇區,檔案存取的最小單位是塊(連續的扇區)
儲存檔案元資訊(檔案大小、建立者、塊位置、時間戳、許可權等非資料資訊)的結構稱為 inode
每個 inode 擁有一個唯一號碼,系統內部透過它來識別檔案
目錄也是一種檔案,其內容包含一系列目錄項(每個目錄項由檔案的檔名和檔案對應的 inode 號碼組成)
硬連結和軟連結
硬連結
一般情況,一個檔名“唯一”對應一個 inode。但是,linux 允許多個檔名都指向同一個 inode。這表示我們可以使用不同的檔名訪問同樣的內容;對檔案內容進行修改將“反映”到所有檔案;刪除一個檔案不影響另一個檔案的訪問 。這種機制就被稱為“硬連結”。
我們可以使用 ln source target
來建立硬連結(注意:source
是本身已存在的檔案,target
是將要建立的連結)。
形象化的表示為下圖:
需要注意的是,只能給檔案建立硬連結,而不能給目錄建立硬連結。另外,source
檔案必須存在,否則將會報錯。
刪除一個檔案為什麼不影響另一個檔案的訪問呢?實際上,檔案 inode 中還有一個連結數的資訊,每多一個檔案指向這個 inode,該數字就會加 1,每少一個檔案指向這個 inode,該數字就會減 1,當值減到 0,系統就自動回收 inode 及其對應的 block 區域。很像是一種引用計數的垃圾回收機制。
當我們對某個檔案建立了硬連結後,對應的 inode 的連結數會是 2(原檔案本身已經有一個指向),當刪除一個檔案時,連結數變成 1,並沒達到回收的條件,所以我們還是可以訪問檔案。
軟連結
軟連結類似於 windows 中的”快捷方式“。兩個檔案雖然 inode 號碼不一樣,但是檔案 A 內部會指向檔案 B 的 inode。當我們讀取檔案 A 時,系統就自動導向檔案 B,檔案 A 就是檔案 B 的軟連結(或者叫符號連結)。這表示我們同樣可以使用不同的檔名訪問同樣的內容;對檔案內容修改將”反映“到所有檔案。但是當我們刪除掉原始檔 B 時,再訪問檔案 A 時會報錯 “No such file or directory”。
我們可以使用 ln -s source target
來建立軟連結(注意:表示讓 target
“指向”source
)。
形象化的表示為下圖:
和硬連結不同,我們可以給目錄建立軟連結,這帶來許多便利。比如我們有一個模組有很多個版本,分別存放在 1.0.0、2.0.0 這樣的目錄下面,當更新模組時,只需要建立一個軟連結指向最新版本號的目錄就能很方便的切換版本。
另外,建立軟連結時,source
是可以不存在的。這很像一種”執行時“機制,而不是“編譯時”機制,建立的時候不報錯,等執行的時候發現找不到就報錯了。
總結
使用
ln source target
建立硬連結;使用ln -s source target
建立軟連結硬連結不會建立額外 inode,和原始檔共用同一個 inode;軟連結會建立額外一個檔案(額外 inode),指向原始檔的 inode
建立硬連結時,
source
必須存在且只能是檔案;建立軟連結時,source
可以不存在而且可以是目錄刪除原始檔不會影響硬連結檔案的訪問(因為 inode 還在);刪除原始檔會影響軟連結檔案的訪問(因為指向的 inode 已經不存在了)
對於已經建立的同名連結,不能再次建立,除非刪掉或者使用
-f
引數
關於軟連結的補充
上面的例子 ln -s file file-soft
給我們的感覺像是 file-soft
是“憑空”出現的。當我們跨目錄來建立軟連結時,可能會“幻想”這樣的命令也是可以生效的:ln -s ~/development/mod ~/production/dir-not-exits/mod
。
這裡並沒有 ~/production/dir-not-exits/
這個目錄,而軟連結本質上是一個新的“檔案”,所以,我們不可能正確建立軟連結(會報錯說 “no such file or directory”)。
如果我們先透過 mkdir
建立好目錄 ~/production/dir-not-exits/
,再進行軟連結,即可達到預期效果。
fs.symlink
在 node 中,我們可以使用方法 fs.symink(target, path)
建立軟連結(符號連結),沒有直接的方法建立硬連結(就算透過子程式的方式直接指向 shell 命令也不能跨平臺)。
如果是對目錄建立連結,請總是傳遞第三個引數 dir
(雖然第三個引數只在 windows 下生效,這可以保證程式碼跨平臺):fs.symlink(target, path, 'dir')
。
為啥這個介面的引數會是 target
和 path
。實際上這是一個 linux 的 API,symlink(target, linkpath)。它是這樣描述的:建立一個名為 linkpath
的符號連結並且含有內容 target
。其實就是讓 linkpath
指向 target
,和 ln -s source target
功能一樣,讓 target
指向 source
。
是不是有點暈?其實我們只需要明白 ln -s
和 fs.symlink
後面傳遞的兩個引數順序是一致的,只是叫法不一樣,使用起來也就沒那麼糾結了:
ln -s file file-soft # file-soft -> file
ln -s dir dir-soft # dir-soft -> dir
fs.symlinkSync('file', 'file-soft'); // file-soft -> file
fs.symlinkSync('dir', 'dir-soft', 'dir'); // dir-soft -> dir
require
在 Node 中,我們經常透過 require
來引用模組。非常有趣的是,require
引用模組時,會“考慮”符號連結,但是卻使用模組的真實路徑作為 __filename
、__dirname
,而不是符號連結的路徑。
考慮下面的目錄結構:
- app
- index.js // require('dep1')
- node_modules
- dep1 -> ../../mods/dep1 //符號連結
- mods
- dep1
- index.js
以及下面的檔案內容:
// index.js
console.log('index.js', __dirname, __filename);
require('dep1');
// dep1/index.js
console.log('dep1', __dirname, __filename);
console.log(module.paths);
執行 node index.js
後輸出是下面這樣:
index.js /Users/kohpoll/Workspace/test/app /Users/kohpoll/Workspace/test/app/index.js
dep1 /Users/kohpoll/Workspace/test/mods/dep1 /Users/kohpoll/Workspace/test/mods/dep1/index.js
[ '/Users/kohpoll/Workspace/test/mods/dep1/node_modules',
'/Users/kohpoll/Workspace/test/mods/node_modules',
'/Users/kohpoll/Workspace/test/node_modules',
'/Users/kohpoll/Workspace/node_modules',
'/Users/kohpoll/node_modules',
'/Users/node_modules',
'/node_modules' ]
我們發現,index.js
可以成功的 require('dep1')
。這很好啊,這讓我們除錯本地開發中的 npm 模組很方便。我們只需要去 require
模組的檔案所在的 node_modules
下面建立一個符號連結就行了。
但是在模組 dep1
中,__dirname
、__filename
都變成了模組實際的路徑,更要命的是模組查詢路徑 module.paths
也變成了從實際路徑開始查詢。
這會帶來什麼問題?
再考慮下面的目錄結構:
- app
- index.js // require('dep1')
- node_modules
- dep1 -> ../../mods/dep1 // require('dep2')
- dep2 -> ../../mods/dep2 // 符號連線
- mods
- dep1
- index.js
- dep2
- index.js
以及下面的檔案內容:
// index.js
console.log('index.js', __dirname, __filename);
require('dep1');
// dep1/index.js
console.log('dep1', __dirname, __filename);
console.log(module.paths);
require('dep2');
// dep2/index.js
console.log('dep2', __dirname, __filename);
console.log(module.paths);
當我們再執行 node index.js
時,輸出是下面這樣:
index.js /Users/kohpoll/Workspace/test/app /Users/kohpoll/Workspace/test/app/index.js
dep1 /Users/kohpoll/Workspace/test/mods/dep1 /Users/kohpoll/Workspace/test/mods/dep1/index.js
[ '/Users/kohpoll/Workspace/test/mods/dep1/node_modules',
'/Users/kohpoll/Workspace/test/mods/node_modules',
'/Users/kohpoll/Workspace/test/node_modules',
'/Users/kohpoll/Workspace/node_modules',
'/Users/kohpoll/node_modules',
'/Users/node_modules',
'/node_modules' ]
module.js:339
throw err;
^
Error: Cannot find module 'dep2'
at Function.Module._resolveFilename (module.js:337:15)
at Function.Module._load (module.js:287:25)
at Module.require (module.js:366:17)
at require (module.js:385:17)
at Object.<anonymous> (/Users/kohpoll/Workspace/test/mods/dep1/index.js:6:1)
at Module._compile (module.js:435:26)
at Object.Module._extensions..js (module.js:442:10)
at Module.load (module.js:356:32)
at Function.Module._load (module.js:311:12)
at Module.require (module.js:366:17)
發現了嗎?dep1
根本就 require
不到 dep2
,因為 dep2
不在它的查詢路徑裡面!
關於這個問題,github 上有一個冗長的 issue 在討論。問題解決起來確實很麻煩,而且會 break 掉一大堆已有功能,所以,最終的結論是在找到更好的方法前給 node v6 增加了一個 --preserve-symlinks
選項來禁止這種 require
的行為,而是使用全新的 require
邏輯。有興趣和閒情的可以去圍觀:https://github.com/nodejs/node/issues/3402(真的好長......)。
至於全新的 require
邏輯會不會有新的坑,在沒有具體實踐前,我也不知道。
那我們上面的情況有辦法解決嗎?其實也有,那就是將目錄結構調整成下面這樣,從而讓 dep2
能在 dep1
的查詢路徑裡面:
- app
- index.js // require('dep1')
- node_modules
- dep1 -> ../../mods/node_modules/dep1 // 符號連結
- dep2 -> ../../mods/node_modules/dep2 // 符號連結
- mods
- node_modules
- dep1
- index.js
- dep2
- index.js