概述
Linux 下有 3 種“拷貝”,分別是 ln,cp,mv,這 3 個命令貌似都能 copy 出一個新的文件出來。
細心的小伙伴看到我給 “拷貝” 打上了雙引號?因為 Linux 的這 3 個命令有極大的區(qū)別,雖然用戶看起來是拷貝出了新文件。
你是否曾經(jīng)遇到過以下問題,想通原因了嗎?:
ln 創(chuàng)建鏈接文件,軟鏈接可以跨文件系統(tǒng),硬鏈接跨文件系統(tǒng)會報錯,為什么?;
mv 好像有時候快,有時候非常慢,有些時候還會殘留垃圾,為什么?;
cp 拷貝數(shù)據(jù)有時快,有時候非常慢,源文件和目標文件所占物理空間竟然不一致?
本篇文章看完,希望你以上問題不再有疑問,從容使用 ln,mv,cp 命令。
溫馨提示:
以下我們只討論文件的簡單操作,關于目錄操作或者復雜參數(shù)的操作不在我們本次主題以內(nèi),我們忽略;
coreutils 庫的代碼版本用的是 8.3;
我們來看下簡單的 3 個命令操作。首先在執(zhí)行以下命令之前,準備一個不小的 test 的普通文件(比如 1G )。
“拷貝”命令一:ln
# 創(chuàng)建一個軟鏈接文件
ln -s 。/test 。/test_soft_link
# 創(chuàng)建一個硬連接文件
ln 。/test 。/test_hard_link
你會發(fā)現(xiàn)當前目錄出現(xiàn)了兩個新文件 test_soft_link ,test_hard_link 。并且你會發(fā)現(xiàn)拷貝速度好快?為什么呢?
“拷貝”命令二:mv
把 test 文件“拷貝”到 。/backup/ 目錄
mv 。/test 。/backup/
更神奇的是,好像 copy 一個 1 G 的文件,速度也賊快?
“拷貝”命令三:cp
把 test 文件“拷貝”到 。/backup/ 目錄
cp 。/test 。/backup/
上面我們看到,好像 ln,mv,cp 這 3 個命令都是“拷貝”?好像都進行了數(shù)據(jù)復制出了新的文件?
答案:當然不是。這 3 個看起來都是復制出了新文件,但其實天壤之別。我們一個個來揭秘。
在揭秘這 3 個命令之前,我們必須先復習文件的基礎知識點,Linux 的文件和目錄的關系。
Linux 的文件和目錄
在 深度剖析 Linux cp 的秘密 一文中,我們詳細剖析了文件系統(tǒng)的形態(tài)。有幾個關鍵知識點:
文件系統(tǒng)內(nèi)有 3 個關鍵區(qū)域:超級塊區(qū)域,inode 區(qū)域,數(shù)據(jù) block 區(qū)域;
其中一個 inode 和一個文件對應,包含了文件的元數(shù)據(jù)信息;
一個 inode 有唯一的編號,可以理解成就是單調(diào)遞增的整數(shù)。比如 1,2,3,4,5,6,,,,;
關于上面,我們注意到 inode 其實標識的是一個平坦的結(jié)構(gòu),inode 索引到數(shù)據(jù) data 區(qū)域,每個 inode 都有唯一編號。
問題來了:Linux 的目錄是一個倒掛的樹形結(jié)構(gòu)呀,為什么上面說 inode 是平坦的結(jié)構(gòu)?如下:
Linux 的文件確實是樹形結(jié)構(gòu),inode 也確實是平坦的結(jié)構(gòu)。你會感覺到因為是因為之前故意忽略了一個幾個東西:目錄文件和 dentry 項。這是兩個非常重要的概念,我們逐個解釋下。
文件系統(tǒng)中其實有兩種文件類型,分為:
普通文件(這里把鏈接文件包含在普通文件以內(nèi))
目錄文件
可以通過 inode-》i_mode 字段,使用 S_ISREG,S_ISDIR 這兩個宏來判斷是哪個類型。普通文件很容易理解,就是普通的數(shù)據(jù)文件,inode 里面存儲元數(shù)據(jù),inode 可以索引到 block,block 里面存儲用戶的數(shù)據(jù)。目錄文件 inode 存儲元數(shù)據(jù),block 里面存儲的是目錄條目。目錄條目是什么樣子的東西?
舉個形象的例子:在當前 testdir 目錄下,有 dir1,dir2,dir3 這三個文件。假設 dir1 的 inode 編號是 1024,dir2 是 1025,dir3 是 1026。
那么現(xiàn)實是這樣的:
testdir 這個目錄首先會對應有一個 inode,inode-》i_mode 的類型是目錄,并且還會有 block 塊,通過 inode-》i_blocks 能索引到這些 block;
block 里面存儲的內(nèi)容很簡單,是一個個目錄條目,內(nèi)核的名字縮寫為 dirent,每一個 dirent 本質(zhì)就是一個 文件名字 到 inode 編號的映射,所以,testdir 這個目錄文件的 block 里存了 3 條記錄 [dir1, 1024],[dir2, 1025],[dir3, 1026];
所以,目錄到底是什么呢?就存儲形態(tài)而已,目錄也是文件,存儲的是 名字 到 inode number 的映射表。dirent 其實就是 directory entry 的縮寫。
好像還沒講到樹形結(jié)構(gòu)?
其實已經(jīng)講了一半了,樹形結(jié)構(gòu)的數(shù)據(jù)結(jié)構(gòu)基礎已經(jīng)有了,就是目錄文件和 dirent 的實現(xiàn)。
假設葉子結(jié)點的為普通文件
針對開篇的圖,其實磁盤上存儲了 3 個目錄文件
這個時候,讀者朋友你是不是都可以用筆畫出一個樹形結(jié)構(gòu)了,內(nèi)存的樹形結(jié)構(gòu)也是這么來的。通過磁盤的映射數(shù)據(jù)構(gòu)造出來。在內(nèi)存中,這個樹形結(jié)構(gòu)的節(jié)點用 dentry 來表示(通常翻譯成目錄項,但是筆者認為這個翻譯很容易讓人誤解)。
以下是筆者從內(nèi)核精簡出來的 dentry 結(jié)構(gòu)體,通過這個總結(jié)到幾個信息:
dentry 綁定到唯一一個 inode 結(jié)構(gòu)體;
dentry 有父,子,兄弟的索引路徑,有這個就足夠在內(nèi)存中構(gòu)建一個樹了,并且事實也確實如此;
struct dentry {
// 。。。
struct dentry *d_parent; /* 父節(jié)點 */
struct qstr d_name; // 名字
struct inode *d_inode; // inode 結(jié)構(gòu)體
struct list_head d_child; /* 兄弟節(jié)點 */
struct list_head d_subdirs; /* 子節(jié)點 */
};
所以,看到現(xiàn)在理解了嗎?父、子 指針,這就是經(jīng)典的樹形結(jié)構(gòu)需要的字段呀。目錄文件類型為樹形結(jié)構(gòu)提供了存儲到磁盤持久化的一種形態(tài),是一種 map 表項的形態(tài),每一個表項我們叫做 dirent 。文件樹的結(jié)構(gòu)在內(nèi)存中以 dentry 結(jié)構(gòu)體體現(xiàn)。
劃重點:仔細理解下 dirent 和 dentry 的概念和形態(tài),仔細理解磁盤的數(shù)據(jù)形態(tài)和內(nèi)存的數(shù)據(jù)結(jié)構(gòu)形態(tài),后面要考的。
ln 命令
ln 是 Linux 的基礎命令之一,是 link 的縮寫,顧名思義就是跟鏈接文件相關的一個命令。一般語法如下:
ln [OPTION]。。。 TARGET LINK_NAME
ln 可以用來創(chuàng)建一個鏈接文件,有趣的是,鏈接文件有兩個不同的類別:
軟鏈接文件
硬鏈接文件
1 什么是軟鏈接文件?
無論是軟鏈接還是硬鏈接都是“鏈接”文件,也就是說,通過這個鏈接文件都能找到背后的那個“源文件”。首先說結(jié)論:
軟鏈接文件是一個全新的文件,有獨立的 inode,有自己的 block ,而這個文件類型是“鏈接文件”的類型而已;
這個軟鏈接文件的內(nèi)容是一段 path 路徑,這個路徑直接指向源文件;
所以,你明白了嗎?軟鏈接文件就是一個文件而已,文件里面存儲的是一個路徑字符串。所以軟鏈接文件可以非常靈活,鏈接文件本身和源解耦,只通過一段路徑字符串尋路。
所以,軟鏈接文件是可以跨文件系統(tǒng)創(chuàng)建的。
有興趣的小伙伴可以去看源碼實現(xiàn),在 coreutils 庫里,調(diào)用棧如下:
main -》 do_link -》 force_symlinkat -》 symlinkat
也就是說最終調(diào)用的是系統(tǒng)調(diào)用 symlinkat 來完成創(chuàng)建,而這個 symlinkat 系統(tǒng)調(diào)用在內(nèi)核由不同的文件系統(tǒng)實現(xiàn)。舉個例子,如果是 minix 文件系統(tǒng),那么對應的函數(shù)就是 minix_symlink。minix_symlink 這個函數(shù)上來就是新建一個 inode ,然后在對應的目錄文件中添加一個 dirent 。來來來,我們看一眼 minix_symlink 的主干代碼:
static int minix_symlink(struct inode * dir, struct dentry *dentry,
const char * symname)
{
// 。。。
// 新建一個 inode,inode 類型為 S_IFLNK 鏈接類型
inode = minix_new_inode(dir, S_IFLNK | 0777, &err);
if (!inode)
goto out;
// 填充鏈接文件內(nèi)容
minix_set_inode(inode, 0);
err = page_symlink(inode, symname, i);
if (err)
goto out_fail;
// 綁定 dentry 和 inode
err = add_nondir(dentry, inode);
//。。。
}
劃重點:軟鏈接文件是新建了一個文件,文件類型是鏈接文件,文件內(nèi)容就是一段字符串路徑。分配新的 inode,內(nèi)存對應新的 dentry ,當然了,也新增了一個 dirent 。軟鏈文件可以跨越不同的文件系統(tǒng)。
2 什么是硬鏈接文件?
現(xiàn)在我們知道了,軟鏈接文件怎么找到源文件的?通過路徑找到的,路徑就存儲在軟鏈接文件中。硬鏈接文件又怎么辦到的呢?
硬鏈接很神奇,硬鏈接其實是新建了一個 dirent 而已。下面是重點:
硬鏈接文件其實并沒有新建文件(也就是說,沒有消耗 inode 和 文件所需的 block 塊);
硬鏈接其實是修改了當前目錄所在的目錄文件,加了一個 dirent 而已,這個 dirent 用一個新的 name 名字指向原來的 inode number;
重點來了,由于新舊兩個 dirent 都是指向同一個 inode,那么就導致了一個限制:不能跨文件系統(tǒng)。因為,不同文件系統(tǒng)的 inode 管理都是獨立的。
感興趣的同學可以試下,跨文件系統(tǒng)創(chuàng)建硬鏈接就會報告如下錯誤:Invalid cross-device link
sh-4.4# ln /dev/shm/source.txt 。/dest.txt
ln: failed to create hard link ‘。/dest.txt’ =》 ‘/dev/shm/source.txt’: Invalid cross-device link
有興趣的小伙伴可以去看源碼實現(xiàn),在 coreutils 庫里,調(diào)用棧如下:
main -》 do_link -》 force_linkat -》 linkat
也就是說最終調(diào)用的是系統(tǒng)調(diào)用 linkat 來完成創(chuàng)建,而這個 linkat 系統(tǒng)調(diào)用在內(nèi)核由不同的文件系統(tǒng)實現(xiàn)。舉個例子,如果是 minix 文件系統(tǒng),那么對應的函數(shù)就是 minix_link。這個函數(shù)從內(nèi)存上來講是把一個 dentry 和 inode 關聯(lián)起來。從磁盤數(shù)據(jù)結(jié)構(gòu)上來講,會在對應目錄文件中增加一個 dirent 項。
劃重點:硬鏈接只增加了一個 dirent 項,只修改了目錄文件而已。不涉及到 inode 數(shù)量的變化。新的 name 指向原來的 inode。
mv 命令
mv 是 move 的縮寫,從效果上來看,是把源文件搬移到另一個位置。
你是否思考過 mv 命令內(nèi)部是怎么實現(xiàn)的呢?
是把源文件拷貝到目標位置,然后刪除源文件嗎?所以,說 mv 貌似也是“拷貝”?
其實,并不是,準確的說不完全是。
對于 mv 的討論,要拆分成源和目的文件是否在同一個文件系統(tǒng)。
1 源 和 目的 在同一個文件系統(tǒng)
mv 命令的核心操作是系統(tǒng)調(diào)用 rename ,rename 從內(nèi)核實現(xiàn)來說只涉及到元數(shù)據(jù)的操作,只涉及到 dirent 的增刪(當然不同的文件系統(tǒng)可能略有不同,但是大致如是)。通常操作是刪除源文件所在目錄文件中的 dirent,在目標目錄文件中添加一個新的 dirent 項。
劃重點:inode number 不變,inode 不變,不增不減,還是原來的 inode 結(jié)構(gòu)體,所以數(shù)據(jù)完全沒有拷貝。
mv 的調(diào)用棧如下,感興趣的可以自己調(diào)試。
main -》 renameat2
main -》 movefile -》 do_move -》 copy -》 copy_internal -》 renameat2
我們用例子來直觀看下,首先準備好一個 source.txt 文件,用 stat 命令看下元數(shù)據(jù)信息:
sh-4.4# stat source.txt
File: source.txt
Size: 0 Blocks: 0 IO Block: 4096 regular empty file
Device: 78h/120d Inode: 3156362 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
我們看到 inode 編號是:3156362 。然后執(zhí)行 mv 命令:
sh-4.4# mv source.txt dest.txt
然后 stat 看下 dest.txt 文件的信息:
sh-4.4# stat dest.txt
File: dest.txt
Size: 0 Blocks: 0 IO Block: 4096 regular empty file
Device: 78h/120d Inode: 3156362 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
發(fā)現(xiàn)沒?inode 編號還是 3156362 。
2 源 和 目的 在不同的文件系統(tǒng)
還記得之前我們提過,由于硬鏈接是直接在目錄文件中添加一個 dirent,名字直接指向源文件的 inode ,不同文件系統(tǒng)都是獨立的一套 inode 管理系統(tǒng),所以硬鏈接不能跨文件系統(tǒng)。
那么問題來了,mv 遇到跨文件系統(tǒng)的場景呢,怎么處理?是否還是 rename ?
舉個例子,如下命令,源和目的是不同的文件系統(tǒng)。我虛擬機的掛載點如下:
sh-4.4# df -h
Filesystem Size Used Avail Use% Mounted on
overlay 59G 3.5G 52G 7% /
tmpfs 64M 0 64M 0% /dev
shm 64M 0 64M 0% /dev/shm
我故意挑選 /home/qiya/testdir 和 /dev/shm/ ,這兩個目錄分別對應了 “/” 和 “/dev/shm/” 的掛載點的文件系統(tǒng),分屬兩個不同的文件系統(tǒng)。我們先提前看下源文件的信息(主要是 inode 信息):
sh-4.4# stat /dev/shm/source.txt
File: /dev/shm/source.txt
Size: 0 Blocks: 0 IO Block: 4096 regular empty file
Device: 7fh/127d Inode: 163990 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
我們執(zhí)行以下 mv 命令:
sh-4.4# mv /dev/shm/source.txt /home/qiya/testdir/dest.txt
然后看下目的文件信息:
sh-4.4# stat dest.txt
File: dest.txt
Size: 0 Blocks: 0 IO Block: 4096 regular empty file
Device: 78h/120d Inode: 3155414 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
對比有沒有發(fā)現(xiàn),inode 的信息是不一樣的,inode number 是不一樣的(是不是跟上面同一文件系統(tǒng)下的 mv 現(xiàn)象不一致)什么原因呢?我下面一一道來,從原理出剖析。
當系統(tǒng)調(diào)用 rename 的時候,如果源和目的不在同一文件系統(tǒng)時,會報告 EXDEV 的錯誤碼,提示該調(diào)用不能跨文件系統(tǒng)。
#define EXDEV 18 /* Cross-device link */
所以,rename 是不能用于跨文件系統(tǒng)的,這個時候怎么辦?
劃重點:這個時候操作分成兩步走,先 copy ,后 remove 。
第一步:走不了 rename ,那么就退化成 copy ,也就是真正的拷貝。讀取源文件,寫入目標位置,生成一個全新的目標文件副本;
這里調(diào)用的 copy_reg 的函數(shù)封裝(要知道這個函數(shù)是 cp 命令的核心函數(shù),在 深度剖析 Linux cp 的秘密 有深入剖析過 );
ln,mv,cp 是在 coreutils 庫里的命令,公用函數(shù)本身就是可以復用的;
第二步:刪除源文件,使用 rm 函數(shù)刪除;
思考問題:mv 跨文件系統(tǒng)的時候,如果第一步成功了,第二步失敗了(比如沒有刪除權(quán)限)會怎么樣?
會導致垃圾。也就是說,目標處創(chuàng)建了一個新文件,源文件并沒有刪除。這個小實驗有興趣的可以試下。
cp 命令
cp 命令才是真正的數(shù)據(jù)拷貝命令,即拷貝元數(shù)據(jù),也會拷貝數(shù)據(jù)。cp 命令也是我之前花了萬字篇幅分析的命令,詳細可見:深度剖析 Linux cp 的秘密。這里就不再贅述,下面提煉出關于拷貝的 3 種模式。
涉及到數(shù)據(jù)拷貝的,關鍵有個 --sparse 參數(shù),可以控制拷貝數(shù)據(jù)的 IO 次數(shù)。
1 auto 模式
重點:跳過文件空洞。是 cp 默認的模式
cp src.txt dest.txt
2 always 模式
重點:跳過文件空洞,還會跳過全 0 數(shù)據(jù),是空間最省的模式。
cp --sparse=always src.txt dest.txt
3 never 模式
重點:無腦拷貝,從頭拷貝到尾,不識別物理空洞和全 0 數(shù)據(jù),是速度最慢的一種模式。
cp --sparse=never src.txt dest.txt
復用之前畫的這 3 張圖,很形象的體現(xiàn)了 cp 的行為。
總結(jié)
目錄文件是一種特殊的文件,可以理解成存儲的是 dirent 列表。dirent 只是名字到 inode 的映射,這個是樹形結(jié)構(gòu)的基礎;
常說目錄樹在內(nèi)存中確實是一個樹的結(jié)構(gòu),每個節(jié)點由 dentry 結(jié)構(gòu)體表示;
ln -s 創(chuàng)建軟鏈接文件,軟鏈接文件是一個獨立的新文件,有一個新的 inode ,有新的 dentry,文件類型為 link,文件內(nèi)容就是一條指向源的路徑,所以軟鏈的創(chuàng)建可以無視文件系統(tǒng),跨越山河;
ln 默認創(chuàng)建硬連接,硬鏈接文件只在目錄文件里添加了一個新 dirent 項 《新name:原inode》,文件 inode 還是和原文件同一個,所以硬鏈接不能跨文件系統(tǒng)(因為不同的文件系統(tǒng)是獨立的一套 inode 管理方式,不同的文件系統(tǒng)實例對 inode number 的解釋各有不同);
ln 命令貌似創(chuàng)建出了新文件,但其實不然,ln 只跟元數(shù)據(jù)相關,涉及到 dirent 的變動,不涉及到數(shù)據(jù)的拷貝,起不到數(shù)據(jù)備份的目的;
mv 其實是調(diào)用 rename 調(diào)用,在同一個文件系統(tǒng)中不涉及到數(shù)據(jù)拷貝,只涉及到元數(shù)據(jù)變更( dirent 的增刪 ),所以速度也很快。但如果 mv 的源和目的在不同的文件系統(tǒng),那么就會退化成真正的 copy ,會涉及到數(shù)據(jù)拷貝,這個時候速度相對慢一些,慢成什么樣子?就跟 cp 命令一樣;
cp 命令才是真正的數(shù)據(jù)拷貝命令,速度可能相對慢一些,但是 cp 命令有 --spare 可以優(yōu)化拷貝速度,針對空洞和全 0 數(shù)據(jù),可以跳過,從而針對稀疏文件可以節(jié)省大量磁盤 IO;
編輯:jq
-
數(shù)據(jù)
+關注
關注
8文章
7026瀏覽量
89026 -
Linux
+關注
關注
87文章
11304瀏覽量
209483 -
代碼
+關注
關注
30文章
4788瀏覽量
68603
原文標題:深度剖析 Linux 的 3 種“拷貝”命令
文章出處:【微信號:LinuxHub,微信公眾號:Linux愛好者】歡迎添加關注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關推薦
評論