一区二区三区三上|欧美在线视频五区|国产午夜无码在线观看视频|亚洲国产裸体网站|无码成年人影视|亚洲AV亚洲AV|成人开心激情五月|欧美性爱内射视频|超碰人人干人人上|一区二区无码三区亚洲人区久久精品

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

Linux下最常用命令之一copy引發(fā)的思考

Linux愛好者 ? 來源:奇伢云存儲(chǔ) ? 作者:奇伢云存儲(chǔ) ? 2021-04-06 16:36 ? 次閱讀
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

cp 引發(fā)的思考

cp 是啥 ? 是的,就是 Linux 是 Linux 下最常用的命令之一,copy 的簡(jiǎn)寫,小伙伴 100% 都用過。

cp 命令處于 Coreutils 庫(kù)里,是 GNU 項(xiàng)目維護(hù)的一個(gè)核心項(xiàng)目,提供 Linux 上核心的命令。

今天用 cp 命令,把小伙伴驚到了,引發(fā)了我對(duì)其中細(xì)節(jié)的思考。

背景是這樣的,奇伢今天用 cp 拷貝了一個(gè) 100 GiB 的文件,竟然一秒不到就拷貝完成了。一個(gè) SATA 機(jī)械盤的寫能力能到 150 MiB/s (大部分的機(jī)械盤都是到不了這個(gè)值的)就算非常不錯(cuò)了,所以,正常情況下,copy 一個(gè) 100G 的文件至少要 682 秒 ( 100 GiB/ 150 MiB/s ),也就是 11 分鐘。

sh-4.4# time cp 。/test.txt 。/test.txt.cp

real 0m0.107s

user 0m0.008s

sys 0m0.085s

上面是我們理論分析,最少要 11 分鐘,實(shí)際情況卻是我們 cp 一秒沒到就完成了工作,驚呆了,為啥呢?并且還有一個(gè)更詭異的我文件系統(tǒng)大小才 40 GiB,為啥里面會(huì)有一個(gè) 100 G的文件呢?

分析文件

我們先用 ls 看一把文件,顯示文件確實(shí)是 100 GiB.

sh-4.4# ls -lh

-rw-r--r-- 1 root root 100G Mar 6 12:22 test.txt

但是再用 du 命令看卻只有 2M ,這是怎么回事?(且所在的文件系統(tǒng)總空間都沒 100G 這么大)

sh-4.4# du -sh 。/test.txt

2.0M 。/test.txt

再看 stat 命令顯示的信息:

sh-4.4# stat 。/test.txt

File: 。/test.txt

Size: 107374182400 Blocks: 4096 IO Block: 4096 regular file

Device: 78h/120d Inode: 3148347 Links: 1

Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)

Access: 2021-03-13 1200.888871000 +0000

Modify: 2021-03-13 1246.562243000 +0000

Change: 2021-03-13 1246.562243000 +0000

Birth: -

stat 命令輸出解釋:

Size 為 107374182400(知識(shí)點(diǎn):?jiǎn)挝皇亲止?jié)),也就是 100G ;

Blocks 這個(gè)指標(biāo)顯示為 4096(知識(shí)點(diǎn):一個(gè) Block 的單位固定是 512 字節(jié),也就是一個(gè)扇區(qū)的大?。?,這里表示為 2M;

劃重點(diǎn):

Size 表示的是文件大小,這個(gè)也是大多數(shù)人看到的大??;

Blocks 表示的是物理實(shí)際占用空間;

所以,注意到一個(gè)新概念,文件大小和實(shí)際物理占用,這兩個(gè)竟然不是相同的概念。為什么會(huì)這樣?

這里先梳理下文件系統(tǒng)的基礎(chǔ)知識(shí),文件系統(tǒng)究竟是怎么存儲(chǔ)文件的?(以 Linux 上 ext系列的文件系統(tǒng)舉例)

文件系統(tǒng)

文件系統(tǒng)聽起來很高大上,通俗話就用來存數(shù)據(jù)的一個(gè)容器而已,本質(zhì)和你的行李箱、倉(cāng)庫(kù)沒有啥區(qū)別。只不過文件系統(tǒng)存儲(chǔ)的是數(shù)字產(chǎn)品而已。我有一個(gè)視頻文件,我把這個(gè)視頻放到這個(gè)文件系統(tǒng)里,下次來拿,要能拿到我完整的視頻文件數(shù)據(jù),這就是文件系統(tǒng),對(duì)外提供的就是存取服務(wù)。

現(xiàn)實(shí)的存取場(chǎng)景

就跟你在火車站使用的寄存服務(wù)一樣,包裹我能存進(jìn)去,稍后我能取出來,就可以了。問題來了,存進(jìn)去?怎么?。孔屑?xì)回憶下存儲(chǔ)行李的場(chǎng)景。

存行李的時(shí)候,是不是要登記一些個(gè)人信息?對(duì)吧,至少自己名字要寫上??赡苓€會(huì)給你一個(gè)牌子,讓你掛手上,這個(gè)東西就是為了標(biāo)示每一個(gè)唯一的行李。

82bb19b2-947e-11eb-8b86-12bb97331649.gif

取行李的時(shí)候,要報(bào)自己名字,有牌子的給他牌子,然后工作人員才能去特定的位置找到你的行李(不然機(jī)場(chǎng)那么多人,行李都長(zhǎng)差不多,他肯定不知道你的行李是哪個(gè))。

8313a514-947e-11eb-8b86-12bb97331649.gif

劃重點(diǎn):存的時(shí)候必須記錄一些關(guān)鍵信息(記錄ID、給身份牌),取的時(shí)候才能正確定位到。

文件系統(tǒng)

回到我們的文件系統(tǒng),對(duì)比上面的行李存取行為,可以做個(gè)簡(jiǎn)單的類比;

登記名字就是在文件系統(tǒng)記錄文件名;

生成的牌子就是元數(shù)據(jù)索引;

你的行李就是文件;

寄存室就是磁盤(容納東西的物理空間);

管理員整套運(yùn)行機(jī)制就是文件系統(tǒng);

上面的對(duì)應(yīng)并不是非常嚴(yán)謹(jǐn),僅僅是幫助大家理解文件系統(tǒng)而已,讓大家知道其實(shí)文件系統(tǒng)是非常樸實(shí)的一個(gè)東西,思想都來源于生活。

劃重點(diǎn):文件系統(tǒng)的存儲(chǔ)介質(zhì)是磁盤,文件系統(tǒng)是軟件層面的,是管理員,管理怎么使用磁盤空間的軟件系統(tǒng)而已。

空間管理

現(xiàn)在思考文件系統(tǒng)是怎么管理空間的?

如果,一個(gè)連續(xù)的大磁盤空間給你使用,你會(huì)怎么使用這段空間呢?

直觀的一個(gè)想法,我把進(jìn)來的數(shù)據(jù)就完整的放進(jìn)去。

845dad84-947e-11eb-8b86-12bb97331649.gif

這種方式非常容易實(shí)現(xiàn),屬于眼前最簡(jiǎn)單,以后最麻煩的方式。因?yàn)闀?huì)造成很多空洞,明明還有很多空間位置,但是由于整個(gè)太大,形狀不合適(數(shù)據(jù)大?。?,哪里都放不下。因?yàn)槟阋乓粋€(gè)完整的空間。

這種不能利用的空間我們稱之為碎片,準(zhǔn)確的說是外部碎片,這種碎片在內(nèi)存池分配內(nèi)存的時(shí)候最常見,產(chǎn)生的原理是一樣的。

怎么改進(jìn)?有人會(huì)想,既然整個(gè)放不進(jìn)去,那就剁碎了唄。這里塞一點(diǎn),那里塞一點(diǎn),就塞進(jìn)去了。

對(duì),思路完全正確。改進(jìn)的方式就是切分,把空間按照一定粒度切分。每個(gè)小粒度的物理塊命名為 Block,每個(gè) Block 一般是 4K 大小,用戶數(shù)據(jù)存到文件系統(tǒng)里來自然也是要切分,存儲(chǔ)到每一個(gè) Block 。Block 粒度越小則外部碎片則會(huì)越少(注意:元數(shù)據(jù)量會(huì)越大),可以盡可能的利用到空間,并且完整的用戶數(shù)據(jù)文件存儲(chǔ)到磁盤上則不再連續(xù),而是切成一個(gè)個(gè) Block 大小的數(shù)據(jù)塊存到磁盤的各個(gè)角落上。

87a7e734-947e-11eb-8b86-12bb97331649.gif

圖示標(biāo)號(hào)表示這個(gè)完整對(duì)象的 Block 的序號(hào),用來復(fù)原對(duì)象用的。

隨之而來又有一個(gè)問題:你光會(huì)切成塊還不行,取文件數(shù)據(jù)的時(shí)候,要給完整的用戶數(shù)據(jù)出去,用戶不管你內(nèi)部怎么實(shí)現(xiàn),他只想要的是最初的樣子。所以,要有一個(gè)表記錄該文件對(duì)應(yīng)所有 Block 的位置,要把每一個(gè) Block 的位置記錄好,取文件的時(shí)候,對(duì)照這表恢復(fù)出一個(gè)完整的塊給到用戶。

所以,寫流程再完善一下就是這樣子:

先寫數(shù)據(jù):數(shù)據(jù)先按照 Block 粒度存儲(chǔ)到磁盤的各個(gè)位置;

再寫元數(shù)據(jù):然后把 Block 所在的各個(gè)位置保存起來,這也就是元數(shù)據(jù),文件系統(tǒng)里叫做 inode(我用一本書來表示);

a3b10e38-947e-11eb-8b86-12bb97331649.gif

文件讀流程則是:

先讀元數(shù)據(jù),找到各個(gè) Block 的位置;

然后讀數(shù)據(jù),構(gòu)造一個(gè)完整的文件,給到用戶;

a45798ca-947e-11eb-8b86-12bb97331649.gif

inode/block 概念

好,現(xiàn)在我們引出了兩個(gè)概念:

磁盤空間是按照 Block 粒度來劃分空間的,存儲(chǔ)數(shù)據(jù)的區(qū)域全都是 Block,我們叫做數(shù)據(jù)區(qū)域;

文件存儲(chǔ)不再連續(xù)存儲(chǔ)在磁盤上,所以需要記錄元數(shù)據(jù),這個(gè)我們叫做 inode;

文件系統(tǒng)中,一個(gè) inode 唯一對(duì)應(yīng)一個(gè)文件,inode 的個(gè)數(shù)則是在文件系統(tǒng)格式化的時(shí)候就確定好了的,換言之,一個(gè) local 文件系統(tǒng)支持的文件數(shù)是天然就有上限的。

block 固定大小,每個(gè) 4k(大部分文件系統(tǒng)都是,這里不做糾結(jié)),block 意圖存儲(chǔ)打散的用戶數(shù)據(jù)。

無論是 inode 區(qū),還是 block 區(qū),本質(zhì)上都是在線性的磁盤空間上。文件系統(tǒng)的空間層次如下:

a79b7830-947e-11eb-8b86-12bb97331649.png

一個(gè)文件的對(duì)應(yīng)一個(gè) inode,這個(gè)文件需要按照 Block 切分存儲(chǔ)在磁盤上,存儲(chǔ)的位置則由 inode 記錄起來,通過 inode 則能找到 block,也就獲取到用戶數(shù)據(jù)。

現(xiàn)在有一個(gè)新的小問題,inode 區(qū)和 block 區(qū)都是在初始化就構(gòu)造好的。存儲(chǔ)一個(gè)文件的時(shí)候,需要取一個(gè)空閑的 inode,然后把數(shù)據(jù)切分成 4k 大小存儲(chǔ)到空閑的 block 上,對(duì)吧?

劃重點(diǎn):空閑的inode,空閑的 block。 這個(gè)很關(guān)鍵,已經(jīng)存儲(chǔ)了數(shù)據(jù)的地方不能再讓寫,不然會(huì)把別人的數(shù)據(jù)覆蓋掉。

那么,怎么區(qū)分空閑和已經(jīng)在用的 inode ,block 呢?

答案是 :inode 區(qū)和 block 區(qū)分別需要另一張表,用來表示 inode 是否在用,block 是否在用,這個(gè)表的名字我們叫做 bitmap 表。bitmap 是一個(gè) bit 數(shù)組,用 0 表示空閑,1 表示在用,如下:

a7a9e384-947e-11eb-8b86-12bb97331649.png

bitmap 什么時(shí)候用呢?自然是寫的時(shí)候,也就是分配 inode 或者 block 的時(shí)候,因?yàn)橹挥蟹峙涞臅r(shí)候,你才需要找空閑的空間。

上圖我為了突出本質(zhì)思想,類似于超級(jí)塊,塊描述符都省略了,這個(gè)感興趣可以自己擴(kuò)展,這里只突出主干哈。

小結(jié)一下:

bitmap 本質(zhì)是個(gè) bit 數(shù)組,占用空間極其少,用 0 來表示空閑,1 表示在用。使用時(shí)機(jī)是在創(chuàng)建文件,或者寫數(shù)據(jù)的時(shí)候;

inode 則對(duì)應(yīng)一個(gè)文件,里面存儲(chǔ)的是元數(shù)據(jù),主要是數(shù)據(jù) block 的位置信息;

block 里面存儲(chǔ)的是用戶數(shù)據(jù),用戶數(shù)據(jù)按照 block 大小(4k)切分,離散的分布在磁盤上。讀的時(shí)候只有依賴于 inode 里面記錄的位置才能恢復(fù)出完整的文件;

inode 和 block 的總個(gè)數(shù)在文件系統(tǒng)格式化的時(shí)候就確定了,所以文件數(shù)和文件大小都是有上限的;

一個(gè)文件真實(shí)的模樣

上面是抽象的樣子,現(xiàn)在我們看一個(gè)真實(shí)的 inode -》 block 的樣子。一個(gè)文件除了數(shù)據(jù)需要存儲(chǔ)之外,一些元信心也需要存儲(chǔ),例如文件類型,權(quán)限,文件大小,創(chuàng)建/修改/訪問時(shí)間等,這些信息存在 inode 中,每個(gè)文件唯一對(duì)應(yīng)一個(gè)inode 。

看一下 inode 的數(shù)據(jù)結(jié)構(gòu)(就以 linxu ext2 為例,該結(jié)構(gòu)定義在 linux/fs/ext2/ext2.h 頭文件中 ):

struct ext2_inode {

__le16 i_mode; /* File mode */

__le16 i_uid; /* Low 16 bits of Owner Uid */

__le32 i_size; /* Size in bytes */

__le32 i_atime; /* Access time */

__le32 i_ctime; /* Creation time */

__le32 i_mtime; /* Modification time */

__le32 i_dtime; /* Deletion Time */

__le16 i_gid; /* Low 16 bits of Group Id */

__le16 i_links_count; /* Links count */

__le32 i_blocks; /* Blocks count */

__le32 i_flags; /* File flags */

__le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */

__le32 i_file_acl; /* File ACL */

__le32 i_dir_acl; /* Directory ACL */

__le32 i_faddr; /* Fragment address */

};

重點(diǎn):

上面的結(jié)構(gòu) mode,uid,size,time 等信息就是我們常說的文件類型,大小,創(chuàng)建修改等時(shí)間元數(shù)據(jù);

注意到 i_block[EXT2_N_BLOCKS] 這個(gè)字段,這個(gè)字段將會(huì)帶你找到數(shù)據(jù), 因?yàn)槔锩娲鎯?chǔ)的就是 block 所在的位置,也就是 block 的編號(hào);

再來,理解下什么叫做 block 的位置(編號(hào))。

a7b59a62-947e-11eb-8b86-12bb97331649.gif

位置就是編號(hào),記錄位置就是記錄編號(hào),編號(hào)就是索引。

我們看到有一個(gè)數(shù)組:i_block[EXT2_N_BLOCKS],這個(gè)數(shù)組是存儲(chǔ) block 位置的數(shù)組。其中 EXT2_N_BLOCKS 是一個(gè)宏定義,值為 15 。也就是說,i_block 是一個(gè) 15 個(gè)元素的數(shù)組,每個(gè)元素是 4 字節(jié)(32 bit)大小。

舉個(gè)例子,假設(shè)我們現(xiàn)在有一個(gè) 6k 的文件,那么只需要 2 個(gè) block 就可以存下了,假設(shè)現(xiàn)在數(shù)據(jù)就存儲(chǔ)在編號(hào)為 3 和 101 這兩個(gè) block 上,那么如下圖:

a96f6db0-947e-11eb-8b86-12bb97331649.png

i_block[15] 第一個(gè)元素存的是 3,第二個(gè)存儲(chǔ)的是 101,其他槽位沒用用到,由于 inode 的內(nèi)存是置零分配的,所以里面的值為 0,表示沒有在使用 。 我們通過 [3, 101] 這兩個(gè) block 就能拼裝出完整的用戶數(shù)據(jù)了。用戶的 6k 文件組成如下:

第一個(gè) 4k 數(shù)據(jù)在 [3*4K, 4*4K] 范圍;

第二個(gè) 2k 數(shù)據(jù)在 [ 101*4K, 101*4K+2K] 范圍;

好,現(xiàn)在我們知道了每個(gè)定長(zhǎng) block 都有唯一編號(hào),我們的 i_block[15] 數(shù)組 通過有序存儲(chǔ)這個(gè)編號(hào)找到文件數(shù)據(jù)所在的位置,并且拼裝出完整文件。

思考問題:區(qū)分文件的切分成 4k 塊的編號(hào)和 磁盤上物理 4k 塊的編號(hào)的區(qū)別。

舉個(gè)栗子,一個(gè)文件 12K 的大小,那么按照 4K 切分會(huì)存儲(chǔ)到 3 個(gè) 物理 block 上。

文件第 0 個(gè) 4k 存儲(chǔ)到了 101 這個(gè)物理 block 上;文件第 1 個(gè) 4k 存儲(chǔ)到了 30 這個(gè)物理 block 上;文件第 2 個(gè) 4k 存儲(chǔ)到了 11 這個(gè)物理 block 上;

文件邏輯空間上的編號(hào)是從 0 開始,到 2 結(jié)束,對(duì)應(yīng)存儲(chǔ)的物理塊編號(hào)分別是 101,30,11 。

思考問題:這么一個(gè) inode 結(jié)構(gòu)能夠表示多大的文件?

我們看到 inode-》i_block[15] 是一個(gè)一維數(shù)組,里面能存 15 個(gè)元素。也就是能存 15 個(gè) block 的編號(hào),那么如果直接存儲(chǔ)文件的 block 編號(hào)最大能表示 60K (15*4K) 的文件。換句話說,如果我拿著 15 個(gè)槽位全部用來存儲(chǔ)文件的編號(hào),這個(gè)文件系統(tǒng)支撐的最大文件卻就是 60K。驚呆了?(注意:ext2 文件系統(tǒng)是可以創(chuàng)建 4T 以內(nèi)的文件的?。。?/p>

那我們自然會(huì)思考,怎么解決呢?怎么才能支撐更大的文件?

最直接思考就是用更大的數(shù)組,把 inode-》i_block 數(shù)組變得更大。比如,如果你想要支持 100G 的文件:

那么,需要 i_block 數(shù)組大小為 26214400 (計(jì)算公式:100*1024*1024/4),也就是要分配一個(gè) i_block[26214400] 的數(shù)組。

每個(gè)編號(hào)占用 4 字節(jié),這個(gè)數(shù)組就占用 100M 的空間(計(jì)算公式:(26214400*4)/1024/1024)。100M !這里就有點(diǎn)夸張了,注意到 i_block 只是一個(gè) inode 內(nèi)部的字段,是一個(gè)靜態(tài)分配的數(shù)組,也就是說,這個(gè)文件系統(tǒng)為了支持最大 100G 的文件存入,每一個(gè) inode 都要占用 100M 的內(nèi)存,就算你是一個(gè) 1K 的文件,inode 也會(huì)占用這么大的內(nèi)存空間。并且,這種方案擴(kuò)展性差,支持的文件 size 越大,i_block[N] 消耗內(nèi)存情況越嚴(yán)重。這是無法接受的。

思考問題:怎么才能讓你既能表示更大的文件,又能不浪費(fèi)占用空間?

我們仔細(xì)分析這個(gè)問題,你會(huì)發(fā)現(xiàn),這里有 2 個(gè)核心問題:

第一點(diǎn),核心在于浪費(fèi)內(nèi)存空間(關(guān)鍵點(diǎn)是要保證 inode 內(nèi)存結(jié)構(gòu)的穩(wěn)定,無論文件怎么變,inode 結(jié)構(gòu)本身不能變);

第二點(diǎn),仔細(xì)思考你會(huì)發(fā)現(xiàn),無論是什么神仙方案,如果你要存儲(chǔ)一個(gè)按照 4k 切分的 100G 文件,都是需要 100M 的空間來存儲(chǔ)索引( block 編號(hào)),但是 99.99% 的文件可能都沒有這么大;

我們前面用一個(gè)大數(shù)組來一把存儲(chǔ) block 編號(hào)的方案固然簡(jiǎn)單,但是問題在于太過死板。核心問題在于存儲(chǔ) block 編號(hào)的數(shù)組是預(yù)分配的,為了還沒有發(fā)生并且 99% 場(chǎng)景都不會(huì)發(fā)生的事情(文件大小達(dá)到 100G),卻不管三七二十一,提前準(zhǔn)備好了完整的 block 索引數(shù)組,預(yù)分配就是浪費(fèi)的根源。

那么知道了這兩個(gè)問題,下一步分析下一個(gè)個(gè)解決:

索引存磁盤

問題一的解決:索引存磁盤:

既然問題在于浪費(fèi)內(nèi)存,inode 內(nèi)存分配不靈活,那就可以看把 inode-》i_block 下放到磁盤。

為什么?

因?yàn)榇疟P的空間比內(nèi)存大了不止一個(gè)量級(jí)。100M 對(duì)內(nèi)存來說很大,對(duì)磁盤來說很小。換句話說,用把用戶數(shù)據(jù)所在的 block 編號(hào)存到磁盤上去,這個(gè)也需要物理空間,使用的也是 block 來存儲(chǔ),只不過這種 block 存儲(chǔ)的是 block 編號(hào)信息,而不是用戶數(shù)據(jù)。

那么我們?cè)趺赐ㄟ^ inode 找到用戶數(shù)據(jù)呢?

因?yàn)檫@個(gè) block 本身也有編號(hào),我們則需要把這個(gè)存儲(chǔ)用戶 block 編號(hào)的 block 所在塊的編號(hào)存儲(chǔ)在 inode-》i_block[15] 里,當(dāng)讀數(shù)據(jù)的時(shí)候,我們需要先找到這個(gè)存儲(chǔ)編號(hào)的 block,然后再通過里面存儲(chǔ)的用戶數(shù)據(jù)所在的 block 編號(hào)找到用戶所在的 block ,去讀數(shù)據(jù)。

這個(gè)存儲(chǔ)用戶 block 編號(hào)的 block 所在塊的編號(hào)我們叫做間接索引,然后我們根據(jù)跳轉(zhuǎn)的次數(shù)可以分類成一級(jí)索引,二級(jí)索引,三級(jí)索引。顧名思義,一級(jí)索引就是跳轉(zhuǎn) 1 次就能定位到用戶數(shù)據(jù),二級(jí)索引就是跳轉(zhuǎn) 2 次,三級(jí)索引就是跳轉(zhuǎn) 3 次才能定位到用戶數(shù)據(jù)。那么 inode-》i_block[15] 里面存儲(chǔ)的可以直接定位到用戶數(shù)據(jù)的 block 就是直接索引。

終于可以說回 ext2 的使用了,ext2 的 inode-》i_block[15] 數(shù)組。知識(shí)點(diǎn)來了,按照約定,這 15 個(gè)槽位分作 4 個(gè)不同類別來用:

前 12 個(gè)槽位(也就是 0 - 11 )我們成為直接索引;

第 13 個(gè)位置,我們稱為 1 級(jí)索引;

第 14 個(gè)位置,我們稱為 2 級(jí)索引;

第 15 個(gè)位置,我們稱為 3 級(jí)索引;

a9ba7210-947e-11eb-8b86-12bb97331649.png

好,那我們?cè)趤砜聪轮苯铀饕?,一?jí),二級(jí),三級(jí)索引的表現(xiàn)力。

直接索引:能存 12 個(gè) block 編號(hào),每個(gè) block 4K,就是 48K,也就是說,48K 以內(nèi)的文件,只需要用到 inode-》i_block[15] 前 12 個(gè)槽位存儲(chǔ)編號(hào)就能完全 hold 住。

一級(jí)索引:

inode-》i_block[12] 這個(gè)位置存儲(chǔ)的是一個(gè)一級(jí)索引,也就是說這里存儲(chǔ)的編號(hào)指向的 block 里面存儲(chǔ)的也是 block 編號(hào),里面的編號(hào)指向用戶數(shù)據(jù)。一個(gè) block 4K,每個(gè)元素 4 字節(jié),也就是有 1024 個(gè)編號(hào)位置可以存儲(chǔ)。

所以,一級(jí)索引能尋址 4M(1024 * 4K)空間 。

二級(jí)索引:

二級(jí)索引是在一級(jí)索引的基礎(chǔ)上多了一級(jí)而已,換算下來,有了 4M 的空間用來存儲(chǔ)用戶數(shù)據(jù)的編號(hào)。所以二級(jí)索引能尋址 4G (4M/4 * 4K) 的空間。

三級(jí)索引:

三級(jí)索引是在二級(jí)索引的基礎(chǔ)上又多了一級(jí),也就是說,有了 4G 的空間來存儲(chǔ)用戶數(shù)據(jù)的 block 編號(hào)。所以二級(jí)索引能尋址 4T (4G/4 * 4K) 的空間。

最后,看一眼完整的表示圖:

a9c24616-947e-11eb-8b86-12bb97331649.png

所以,在我們 ext2 的文件系統(tǒng)上,通過這種間接塊索引的方式,最大能支撐的文件大小 = 48K + 4M + 4G + 4T ,約等于 4 T。文件系統(tǒng)最大支撐 16T 空間,因?yàn)?4 Byte 的整形最大數(shù)就是 2^32=4294967296 , 乘以 4K 就等于 16 T。

ext2 文件系統(tǒng)支持的最大單文件大小和文件系統(tǒng)最大容量就是這么算出來的(溫馨提示:ext4 文件系統(tǒng)不僅兼容間接塊的實(shí)現(xiàn),還使用的是 extent 模式來管理的空間,最大支持單文件 16 TB ,文件系統(tǒng)最大 1 EB)。

思考:這種多級(jí)索引尋址性能表現(xiàn)怎么樣?

在不超過 12 個(gè)數(shù)據(jù)塊的小文件的尋址是最快的,訪問文件中的任意數(shù)據(jù)理論只需要兩次讀盤,一次讀 inode,一次讀數(shù)據(jù)塊。訪問大文件中的數(shù)據(jù)則需要最多五次讀盤操作:inode、一級(jí)間接尋址塊、二級(jí)間接尋址塊、三級(jí)間接尋址塊、數(shù)據(jù)塊。

多級(jí)索引和后分配

問題二解決:多級(jí)索引和后分配

一級(jí)索引不夠,表現(xiàn)力太差,預(yù)留空間又太浪費(fèi),不預(yù)留空間又無法擴(kuò)展,怎么解決?

既然問題在于預(yù)分配,我們使用后分配(瘦分配,或精簡(jiǎn)分配)解決。也就是說用戶文件數(shù)據(jù)有多大,我才分配出多大的數(shù)組。舉個(gè)例子,我們存儲(chǔ) 100 G 的文件,那么就要用到三級(jí)索引塊,最多分配 26214400 個(gè)槽位的數(shù)組(因?yàn)橐?26214400 個(gè) block)。如果是存儲(chǔ) 6K 的文件,那么只需要 2 個(gè)槽位的數(shù)組。

索引數(shù)組的后分配

后分配這里說的是 block 索引編號(hào)數(shù)組的后分配,需要用到的時(shí)候才分配,而不是說,現(xiàn)在用戶存儲(chǔ)一個(gè) 1k 的文件,我上來就給他分配一個(gè) 100M 的索引數(shù)組,只是為了以后這個(gè)文件可能增長(zhǎng)到 100 G。

數(shù)據(jù)的后分配

既然這里說到,關(guān)于后分配還有一個(gè)層面,就是數(shù)據(jù)所占的空間也是用到了才分配,這個(gè)也就是涉及到今天 cp的秘密的核心問題。

實(shí)際的栗子

先看下下正常的文件寫入要做的事情(注意這里只描述主干,實(shí)際流程可能,有優(yōu)化):

創(chuàng)建一個(gè)文件,這個(gè)時(shí)候分配一個(gè) inode;

在 [ 0,4K ] 的位置寫入 4K 數(shù)據(jù),這個(gè)時(shí)候只需要 一個(gè) block 假設(shè)編號(hào) 102,把這個(gè)編號(hào)寫到 inode-》i_block[0] 這個(gè)位置保存起來;

在 [ 1T,1T+4K ] 的位置寫入 4K 數(shù)據(jù),這個(gè)時(shí)候需要分配一個(gè) block 假設(shè)編號(hào) 7,因?yàn)檫@個(gè)位置已經(jīng)落到三級(jí)索引才能表現(xiàn)的空間了,所以需要還需要分配出 3 個(gè)索引塊;

寫入完成,close 文件;

這里解釋下文件偏移位置 [1T, 1T+4K] 為什么落到三級(jí)索引。

offset 為 1T,按照 4K 切分,也就是 block 268435456 塊(注意這個(gè)是虛擬文件塊,不是物理位置);

先算出范圍:直接索引的范圍是 [0, 11] 個(gè),一級(jí)索引 [12, 1035],二級(jí)索引 [1036, 1049611], 三級(jí)索引 [1049612, 1074791435],(有人如果不知道怎么來的話,可以往前看看 inode 的結(jié)構(gòu),直接索引 12個(gè),一級(jí)索引 1024 個(gè),二級(jí) 1M 個(gè),三級(jí) 1G 個(gè),然后算出來的);

268435456 落在三級(jí)索引 [1049612, 1074791435] 這個(gè)范圍;

實(shí)際存儲(chǔ)如圖:

計(jì)算索引:

12 + 1024 + 1024 * 1024 + 1024 * 1024 * 254 + 1024 * 1022 + 1012 = 268435456

實(shí)際的物理分配如圖:

aa0f6bc6-947e-11eb-8b86-12bb97331649.png

因?yàn)槠埔呀?jīng)用到了 3 級(jí)索引,所以除了用戶數(shù)據(jù)的兩個(gè) block ,中間還需要 3 個(gè)間接索引 block 分配出來。

如果要讀 [1T, 1T+4K] 這個(gè)位置的數(shù)據(jù)怎么辦?

流程如下:

計(jì)算 offset 得出在第 268435456 的位置;

讀出三級(jí)索引 inode-》i_block[14] 里存儲(chǔ)的 block 編號(hào),找到對(duì)應(yīng)的物理 block,這個(gè)是第一級(jí)的 block;

然后讀該 block 的第 254+1 個(gè)槽位里的數(shù)據(jù),里面存儲(chǔ)的是第二級(jí)的 block 編號(hào),把這個(gè)編號(hào)讀出來,通過這個(gè)編號(hào)找到對(duì)應(yīng)的物理 block;

讀該 block 的第 1022 +1 個(gè)操作的數(shù)據(jù),里面存儲(chǔ)的是第三級(jí)的 block 編號(hào),通過這個(gè)編號(hào)可以找到物理 block 的數(shù)據(jù),里面存儲(chǔ)的是用戶數(shù)據(jù)所在 block 的編號(hào);

讀該 block 第 1012+1 個(gè)槽位里存儲(chǔ)的編號(hào),找到物理 block,這個(gè) block 里存的就是用戶數(shù)據(jù)了;

這個(gè)時(shí)候,我們的文件看起來是超大文件,size 等于 1T+4K ,但里面實(shí)際的數(shù)據(jù)只有 8 K,位置分別是 [ 0,4K ] ,[ 1T,1T+4K ]。

重點(diǎn):文件 size 只是 inode 里面的一個(gè)屬性,實(shí)際物理空間占用則是要看用戶數(shù)據(jù)放了多少個(gè) block 。

劃重點(diǎn):沒寫數(shù)據(jù)的地方不用分配物理 block 塊。

沒寫數(shù)據(jù)不分配物理塊?那是什么?那就是我們下面要說的稀疏文件。

文件的稀疏語義

什么是稀疏文件

終于到我們文件的稀疏語義了,稀疏語義什么意思?

稀疏文件英文名 sparse file 。稀疏文件本質(zhì)上就是計(jì)算機(jī)文件,用戶不感知,文件系統(tǒng)支持稀疏文件只是為了更有效率的使用磁盤空間而已。稀疏文件就是后分配空間的一種實(shí)現(xiàn)形式,做到真正用時(shí)才分配,最大效率的利用磁盤空間。

就以上面舉的栗子,文件大小 1T,但是實(shí)際數(shù)據(jù)只有 8K,這種就是稀疏文件,邏輯大小和實(shí)際物理空間是可以不等的。文件大小只是一個(gè)屬性,文件只是數(shù)據(jù)的容器,沒有用戶數(shù)據(jù)的位置可以不分配空間。

為什么要支持稀疏語義?

還是以上面 1T 的文件舉例,如果這 1T 的文件只有首尾分別寫了 4K 的數(shù)據(jù),而文件系統(tǒng)卻要分配 1T 的物理空間,這里將帶來巨大的浪費(fèi)。何不等存了用戶數(shù)據(jù)的時(shí)候再分配了,實(shí)際數(shù)據(jù)有多少,才去分配多大的 block ,何必著急的預(yù)分配呢?

后分配本著用多少給多少的原則,盡量有效的利用空間。

后分配還有一個(gè)優(yōu)點(diǎn),這也減少了首次寫入的時(shí)間,怎么理解?

因?yàn)?,如果文件大?1T,就要分配 1T 的空間,那么初始分配需要寫入全零到空間,否則上面的數(shù)據(jù)可能是隨機(jī)數(shù)。

對(duì)于稀疏文件空洞的地方,不占用物理空間,但要保證讀的時(shí)候返回全 0 數(shù)據(jù)的語義,即可。

又一個(gè)知識(shí)點(diǎn):有時(shí)候稀疏文件的空洞和用戶真正的全 0 數(shù)據(jù)是無法區(qū)分的,因?yàn)閷?duì)外表現(xiàn)是一樣的。

稀疏文件也要文件系統(tǒng)支持,并不是所有的文件系統(tǒng)都支持稀疏語義,比如 ext2 就沒有,ext4 才有稀疏語義,支持的標(biāo)志是實(shí)現(xiàn)文件系統(tǒng)的 fallocate 接口。

怎么創(chuàng)建一個(gè)稀疏文件?

可以使用 truncate 命令在一個(gè) ext4 的文件系統(tǒng)創(chuàng)建一個(gè)文件。

truncate -s 100G test.txt

你 ls -lh 。/test.txt 命令看會(huì)發(fā)現(xiàn)是一個(gè) 100 G 的文件;

但是 du -sh 。/test.txt 會(huì)發(fā)現(xiàn)是一個(gè) 0 字節(jié)的文件;

stat 。/test.txt 會(huì)發(fā)現(xiàn)是 Size: 107374182400 Blocks: 0 的文件;

這就是一個(gè)典型的稀疏文件。size 只是文件的邏輯大小,實(shí)際的物理空間占用還是得看 Blocks 這個(gè)數(shù)值。

下面這種 1T 的文件,因?yàn)橹粚懥祟^尾 8K 數(shù)據(jù),所以只需要分配 2 個(gè) block 存儲(chǔ)用戶數(shù)據(jù)即可。

aa0f6bc6-947e-11eb-8b86-12bb97331649.png

好,我們?cè)偕钊胨伎枷?,文件系統(tǒng)為什么能做到這個(gè)?

這也是為什么理解稀疏語義要先了解文件系統(tǒng)的實(shí)現(xiàn)的原因。

首先,最關(guān)鍵的是把磁盤空間切成離散的、定長(zhǎng)的 block 來管理;

然后,通過 inode 能查找到所有離散的數(shù)據(jù)(保存了所有的索引);

最后,實(shí)現(xiàn)索引塊和數(shù)據(jù)塊空間的后分配;

這三點(diǎn)是層層遞進(jìn)的。

稀疏語義接口

為了知識(shí)的完整性,簡(jiǎn)要介紹稀疏語義的幾個(gè)接口:

preallocate(預(yù)分配):提供接口可以讓用戶預(yù)占用文件內(nèi)指定范圍的物理空間;

punch hole(打洞):提供接口可以讓用戶釋放文件內(nèi)指定范圍的物理空間;

這兩個(gè)操作剛好相反。

預(yù)分配的意思是?

就是說,當(dāng)你創(chuàng)建一個(gè) 1T的文件,如果你沒寫數(shù)據(jù),這個(gè)時(shí)候其實(shí)沒有分配物理空間的,支持稀疏語義的文件系統(tǒng)會(huì)提供一個(gè) fallocate 接口給你,讓你實(shí)現(xiàn)預(yù)分配,也就是說把這 1T 的物理空間現(xiàn)在就分配出來。

思考:這個(gè)有什么好處呢?

第一,如果你命中注定要 1T 的空間,預(yù)分配是有好處的,把空間分配的工作量集中在初始化的時(shí)候一把做了,避免了實(shí)時(shí)現(xiàn)場(chǎng)分配的開銷;

第二,如果不提前占坑,很有可能等你想要的時(shí)候已經(jīng)沒有空間可占用了。所以你把物理空間先占好,就可以安心使用了;

linux 提供了一個(gè) fallocate 命令,可以用來預(yù)分配空間。

fallocate -o 0 -l 4096 。/test.txt

這個(gè)命令的意思就是給 text.txt 這個(gè)文件 [0, 4K] 的位置分配好物理空間。

打洞(punch hole) 是干啥的呢?

這個(gè)調(diào)用允許你把已經(jīng)占用的物理空間釋放掉,從而達(dá)到快速釋放的目的。這種操作在虛擬機(jī)鏡像的場(chǎng)景用得多,通常用于快速釋放空間,punch hole 能夠讓業(yè)務(wù)更有效的利用空間。

linux 提供了一個(gè) fallocate 命令也可以用來 punch hole 空間。

fallocate -p -o 0 -l 4096 。/test.txt

這個(gè)命令的意思是把 test.txt [ 0, 4K ] 的物理空間釋放掉。

Go 語言實(shí)現(xiàn)

稀疏文件本身和編程語言無具體關(guān)系,可以用任何語言實(shí)現(xiàn),我下面以 Go 為例,看下稀疏文件的預(yù)分配和打洞(punch hole)是怎么實(shí)現(xiàn)的。

預(yù)分配實(shí)現(xiàn):

func PreAllocate(f *os.File, sizeInBytes int) error {

// use mode = 1 to keep size

// see FALLOC_FL_KEEP_SIZE

return syscall.Fallocate(int(f.Fd()), 0x0, 0, int64(sizeInBytes))

}

punch hole 實(shí)現(xiàn):

// mode 0 change to size 0x0

// FALLOC_FL_KEEP_SIZE = 0x1

// FALLOC_FL_PUNCH_HOLE = 0x2

func PunchHole(file *os.File, offset int64, size int64) error {

err := syscall.Fallocate(int(file.Fd()), 0x1|0x2, offset, size)

if err == syscall.ENOSYS || err == syscall.EOPNOTSUPP {

return syscall.EPERM

}

return err

}

可以看到,本質(zhì)上都是系統(tǒng)調(diào)用 fallocate ,然后帶不同的參數(shù)而已。指定文件偏移和長(zhǎng)度,就能預(yù)分配物理空間或者釋放物理空間了。

這里有一個(gè)知識(shí)點(diǎn):punch hole 的調(diào)用要保證 4k 對(duì)齊才能釋放空間。

舉個(gè)例子,比如:

punch hole [0, 6k] 的數(shù)據(jù),你會(huì)發(fā)現(xiàn)只有 [0, 4k] 的數(shù)據(jù)物理塊被釋放了,[4k, 6k] 所占的 4k 物理塊還占著空間呢。

這個(gè)很容易理解,因?yàn)榇疟P的物理空間是劃分成 4k 的 block,這個(gè)是最小單位了,不能再分了,你無法切割一個(gè)最小的單位。

值得注意的是,就算你沒有 4k 對(duì)齊的發(fā)送調(diào)用,fallocate 也不會(huì)報(bào)錯(cuò),這個(gè)請(qǐng)注意了。

cp 的秘密

鋪墊了這么久的基礎(chǔ)知識(shí),終于到我們的 cp 命令的解密了?;氐阶铋_始的問題,cp 一個(gè) 100G 的文件 1 秒都不到,為什么這么快?

說到現(xiàn)在,這個(gè)問題就很清晰了,這個(gè) 100G 的文件是個(gè)稀疏文件,盲猜一手:cp 的時(shí)候只拷貝了有效數(shù)據(jù),空洞是直接跳過的。 往前看 stat 命令和 ls 命令顯示的差距就知道了。

接下來我們具體看一下 cp 的實(shí)現(xiàn)。

cp 有一個(gè)參數(shù) --sparse 很有意思,sparse 這個(gè)參數(shù)控制這 cp 命令對(duì)稀疏文件的行為,這個(gè)參數(shù)有三個(gè)值可選:

--sparse=always :空間最??;

--sparse=auto :默認(rèn)值,速度最快;

--sparse=never :吭呲吭呲 copy,最傻;

cp 默認(rèn)的時(shí)候,sparse 是 auto 策略。auto,always,never 分別是什么策略呢?

spare 三大策略

auto 策略

默認(rèn)的情況下,cp 會(huì)檢查源文件是否具有稀疏語義,對(duì)于不占物理空間的位置,目標(biāo)文件不會(huì)寫入數(shù)據(jù),從而形成空洞。

所以,對(duì)于我們的例子,真實(shí)的就只進(jìn)行了 2M 的 IO ,預(yù)期的 100G 文件,只拷貝了 2M 的數(shù)據(jù),自然飛快了,自然驚艷所有人。

auto 是默認(rèn)策略,使用該模式的時(shí)候,cp 內(nèi)部實(shí)現(xiàn)是通過系統(tǒng)調(diào)用拿到文件的空洞位置情況,然后對(duì)這些位置目標(biāo)文件會(huì)保持空洞。

注意,不會(huì)對(duì)非空洞位置的文件內(nèi)容做判斷,如果用戶數(shù)據(jù)占用了物理塊,但是是全 0 數(shù)據(jù),這種情況下,auto 模式不會(huì)識(shí)別,會(huì)以全零的數(shù)據(jù)寫入到目標(biāo)文件。這個(gè)是跟 always 最大的區(qū)別。

auto 策略下 cp 的文件的文件,size,物理 block 數(shù)量都和源文件一致。

always

這種方式是最激進(jìn)的,追求空間的最小化。在 auto 的基礎(chǔ)之上,還多做了一步:對(duì)源文件內(nèi)容做了判斷。

在讀出源數(shù)據(jù)之后,就算這塊數(shù)據(jù)位置在源文件不是空洞,也會(huì)自己在程序里做一次判斷,判斷是否是全 0 的數(shù)據(jù),如果是,那么也會(huì)在目標(biāo)文件里對(duì)應(yīng)的位置創(chuàng)建空洞(不分配物理空間)。

這種方式則會(huì)導(dǎo)致源文件的 size 和目標(biāo)文件一樣(三種策略下,文件size 都是不變的),但是 物理 blocks 占用卻更小。

never

這種方式最保守,實(shí)現(xiàn)也最簡(jiǎn)單。不管源文件是否是稀疏文件,cp 完全不感知,讀出來的任何數(shù)據(jù)都直接寫入目標(biāo)文件。也就是說,如果一個(gè) 100G 的文件,就算只占用了 4K 的物理空間,也會(huì)創(chuàng)建出一個(gè) 100G 的目標(biāo)文件,物理空間就占用 100G。

所以,如果你 cp 的時(shí)候帶了這個(gè)參數(shù),那么將會(huì)非常非常慢。

深入剖析 cp --sparse 源碼

上面的都是結(jié)論,現(xiàn)在我們通過源碼再深入理解下 cp 的原理,一起圍觀下 cp 的代碼實(shí)現(xiàn)。

cp 命令源碼在 GNU 項(xiàng)目的 coreutils 項(xiàng)目中,為 Linux 提供外圍的基礎(chǔ)命令工具??此茦O簡(jiǎn)的 cp,其實(shí)代碼實(shí)現(xiàn)還挺有趣的。

cp 的入口代碼在 cp.c 文件中(以下基于 coreutils 8.30 版本):

以一個(gè) cp 文件的命令舉例,我們一起走一下源碼視角的旅途:

cp 。/src.txt dest.txt

首先,在 main 函數(shù)里初始化參數(shù):

switch (c)

{

case SPARSE_OPTION:

x.sparse_mode = XARGMATCH (“--sparse”, optarg,

sparse_type_string, sparse_type);

break;

這里會(huì)根據(jù)用戶傳入的參數(shù),對(duì)應(yīng)翻譯成一個(gè)枚舉值,該枚舉值就是 SPARSE_NEVER,SPARSE_AUTO,SPARSE_ALWAYS 其中之一,默認(rèn)用戶沒帶這個(gè)參數(shù)的話,就會(huì)是 SPARSE_AUTO:

static enum Sparse_type const sparse_type[] =

{

SPARSE_NEVER, SPARSE_AUTO, SPARSE_ALWAYS

};

所以,main 函數(shù)里賦值了 x.sparse_mode 這個(gè)參數(shù),這個(gè)參數(shù)也是稀疏文件行為的指導(dǎo)參數(shù),后面怎么處理稀疏文件,就依賴于這個(gè)參數(shù)。

下面就是依次調(diào)用 do_copy ,copy,copy_internal 函數(shù),do_copy,copy 這兩個(gè)函數(shù)就是處理一些封裝,校驗(yàn),包括涉及目錄的一些邏輯,跟我們本次稀疏文件解密關(guān)系不大,直接略過。

copy_internal 則是一個(gè)巨長(zhǎng)的函數(shù),里面的邏輯多數(shù)是一些兼容性,適配場(chǎng)景的考慮,也和本次關(guān)系不大。對(duì)于一個(gè)普通文件( regular 類型) 最終調(diào)用到 copy_reg 函數(shù),才是普通文件 copy 的實(shí)現(xiàn)所在。

else if (S_ISREG (src_mode)

|| (x-》copy_as_regular && !S_ISLNK (src_mode)))

{

copied_as_regular = true;

// 普通文件的拷貝

if (! copy_reg (src_name, dst_name, x, dst_mode_bits & S_IRWXUGO,

omitted_permissions, &new_dst, &src_sb))

goto un_backup;

普通文件的 copy 就是從函數(shù) copy_reg 才真正開始的。在這個(gè)函數(shù)里,首先 open 源文件和目標(biāo)文件的句柄,然后進(jìn)行數(shù)據(jù)拷貝。

static bool

copy_reg( 。.. )

{

// 確認(rèn)要拷貝數(shù)據(jù)

if (data_copy_required)

{

// 獲取到塊大小,buffer 大小等參數(shù)

size_t buf_alignment = getpagesize ();

size_t buf_size = io_blksize (sb);

size_t hole_size = ST_BLKSIZE (sb);

bool make_holes = false;

// 關(guān)鍵函數(shù)來啦,is_probably_sparse 函數(shù)就是用來判斷源文件是否是稀疏文件的;

bool sparse_src = is_probably_sparse (&src_open_sb);

if (S_ISREG (sb.st_mode))

{

if (x-》sparse_mode == SPARSE_ALWAYS)

// sparse_always 模式,也是追求極致空間效率的策略;

// 所以這種方式不管源文件是否真的是稀疏文件,都會(huì)生成稀疏的目標(biāo)文件;

make_holes = true;

// 如果是 sparse_auto 的策略,并且源文件是稀疏文件,那么目標(biāo)文件也會(huì)是稀疏文件(也就是可以有洞洞的文件)

if (x-》sparse_mode == SPARSE_AUTO && sparse_src)

make_holes = true;

}

// 如果到這里判斷不是目標(biāo)不會(huì)是稀疏文件,那么就使用更有效率的方式來 copy,比如用更大的 buffer 來裝數(shù)據(jù),一次 copy 更多;

if (! make_holes)

{

// 略

}

// 源文件是稀疏文件的情況下,可以使用 extent_copy 這種更有效率的方式進(jìn)行拷貝。

if (sparse_src)

{

if (extent_copy (source_desc, dest_desc, buf, buf_size, hole_size,

src_open_sb.st_size,

make_holes ? x-》sparse_mode : SPARSE_NEVER,

src_name, dst_name, &normal_copy_required))

goto preserve_metadata;

}

// 如果源文件判斷不是稀疏文件,那么就使用標(biāo)準(zhǔn)的 sparse_copy 函數(shù)來拷貝。

if (! sparse_copy (source_desc, dest_desc, buf, buf_size,

make_holes ? hole_size : 0,

x-》sparse_mode == SPARSE_ALWAYS, src_name, dst_name,

UINTMAX_MAX, &n_read,

&wrote_hole_at_eof))

{

return_val = false;

goto close_src_and_dst_desc;

}

// 略

}

}

以上對(duì)于 copy_reg 的代碼我做了極大的簡(jiǎn)化,把關(guān)鍵流程梳理了出來。

小結(jié):

copy_reg 函數(shù)才是真正 cp 一個(gè)普通文件的邏輯所在,源文件的打開,目標(biāo)文件的創(chuàng)建和數(shù)據(jù)的寫入都在這里;

拷貝之前,會(huì)先用 is_probably_sparse 函數(shù)來判斷源文件是否屬于稀疏文件;

如果是 sparse always 模式,那么無論源文件是否是稀疏文件,那么都會(huì)嘗試生成稀疏的目標(biāo)文件(這種模式下,源文件如果是非稀疏文件,會(huì)判斷是否是全 0 數(shù)據(jù),如果是的話,還是會(huì)在目標(biāo)文件中打洞);

如果是 sparse auto 模式,源文件是稀疏文件,那么生成的目標(biāo)文件也會(huì)是稀疏文件;

源文件為稀疏文件的時(shí)候,會(huì)嘗試使用效率更高的 extent_copy 函數(shù)來拷貝數(shù)據(jù);

如果是 never 模式,那么是調(diào)用 sparse_copy 函數(shù)來拷貝數(shù)據(jù),并且里面不會(huì)嘗試 punch hole,拷貝過程會(huì)非常慢,會(huì)生成一個(gè)實(shí)打?qū)嵉哪繕?biāo)文件,物理空間占用完全和文件size一致;

上面的小結(jié),提到幾個(gè)有意思的點(diǎn),我們一起探秘下幾個(gè)問題。

問題一:is_probably_sparse 函數(shù)是怎么來判斷源文件的?

看了源碼你會(huì)發(fā)現(xiàn),非常簡(jiǎn)單,其實(shí)就是 stat 一下源文件,拿到文件大小 size,還有物理塊的占用個(gè)數(shù)(假設(shè)物理塊 512 字節(jié)),比一下就知道了。

static bool

is_probably_sparse (struct stat const *sb)

{

return (HAVE_STRUCT_STAT_ST_BLOCKS

&& S_ISREG (sb-》st_mode)

&& ST_NBLOCKS (*sb) 《 sb-》st_size / ST_NBLOCKSIZE);

}

舉個(gè)例子,文件大小 size 為 100G,物理占用塊 8 個(gè),那么 100G/512字節(jié) 》 8,所以就是稀疏文件。

文件大小 size 為 4K,物理占用塊 8 個(gè),那么 4K/512字節(jié) == 8,所以就不是稀疏文件。

問題二:extent_copy 為什么更有效率?

關(guān)鍵在于里面的一個(gè)子函數(shù) extent_scan_read 的實(shí)現(xiàn),extent_scan_read 位于 extent-scan.c 文件中。extent_scan_read 位于 extent_copy 開頭,用來獲取到源文件的空洞位置信息。這個(gè)就是 extent_copy 高效率的根本原因。extent_scan_read 通過這個(gè)函數(shù)能夠拿到文件的空洞的詳細(xì)位置,那么拷貝數(shù)據(jù)的時(shí)候,就能針對(duì)性的跳過這些空洞,只拷貝有效的位置即可。

那么,不禁又要問, extent_scan_read 又是怎么實(shí)現(xiàn)的呢?

答案是:ioctl 系統(tǒng)調(diào)用,搭配 FS_IOC_FIEMAP 參數(shù),也就是 fiemap 的調(diào)用。

/* Call ioctl(2) with FS_IOC_FIEMAP (available in linux 2.6.27) to obtain a map of file extents excluding holes. */

fiemap 這個(gè)是一個(gè)非常關(guān)鍵的特性,ioctl 搭配 FS_IOC_FIEMAP 這個(gè)函數(shù)能夠拿到文件的物理空間分配關(guān)系,能夠讓用戶知道長(zhǎng)達(dá) 100G 的文件中,哪些位置才是真正有物理塊存儲(chǔ)數(shù)據(jù)的,哪些位置是空洞。

這個(gè)特性則由文件系統(tǒng)提供,也就是說,只有文件系統(tǒng)提供了這個(gè)對(duì)外接口,我們才能拿得到,比如 ext4,就支持這個(gè)接口,ext2 就沒有。

問題二:sparse_copy 為什么慢,里面喲是做了啥?

這個(gè)函數(shù)是標(biāo)準(zhǔn)的 copy 函數(shù),對(duì)比 extent_copy 來說,沒有 fiemap 的加持,那么這個(gè)函數(shù)就自己判斷是否是空洞,怎么判斷?

sparse_copy 認(rèn)為,只要大塊連續(xù)的全 0 數(shù)據(jù),那么就認(rèn)為是空洞,目標(biāo)文件就不用寫入,直接打洞即可。

判斷是否全 0 的函數(shù)是is_nul,位于 system.h 頭文件中,實(shí)現(xiàn)非常簡(jiǎn)單,就是看整個(gè)內(nèi)存塊是否全部為 0 。

舉個(gè)例子,現(xiàn)在 sparse_copy 從源文件里讀出 4k 的數(shù)據(jù),發(fā)現(xiàn)全都是 0,那么目標(biāo)文件對(duì)應(yīng)的位置就不會(huì)寫入,而是直接 punch hole 打洞,節(jié)省空間。

但是注意了,這種行為只有在激進(jìn)的 sparse always 策略才是這樣的。如果是其他策略,sparse_copy 不會(huì)做這樣做,而是老老實(shí)實(shí)的拷貝數(shù)據(jù),哪怕是全 0 的數(shù)據(jù),也要如實(shí)的寫入到目標(biāo)文件。

所以,always 模式下,目標(biāo)文件所占物理空間比源文件小的根本原因就在于 sparse_copy 這個(gè)函數(shù)的實(shí)現(xiàn)。

cp 快速的原因

梳理到這里,cp 的秘密已經(jīng)徹底揭開了,cp 一個(gè) 100G 的文件為什么那么快?

因?yàn)樵次募窍∈栉募。募此?100G,實(shí)際只占用了 2M 的物理空間。文件系統(tǒng)將文件大小和物理空間占用這兩個(gè)概念解耦,使得有更靈活的使用姿勢(shì),更有效的使用物理空間。

cp 默認(rèn)的情況下,通過文件系統(tǒng)提供的 fiemap 接口,獲取到文件所有的空洞信息,然后跳過這些空洞,只 copy 有效的數(shù)據(jù),極大的減少了磁盤 io 的數(shù)據(jù)量,所以才那么快。

總結(jié)下 cp --sparse 三個(gè)參數(shù)的特點(diǎn):

auto 模式:默認(rèn)模式,最一致的模式(如果沒有用戶全0 塊數(shù)據(jù),那么可能也是速度最快的),會(huì)根據(jù)源文件的實(shí)際空間占用復(fù)制數(shù)據(jù),目標(biāo)文件和源文件一致。無論是文件 size 還是物理 blocks;

always 模式:追求最小空間占用的模式,就算源文件不是稀疏文件,而僅僅是有些連續(xù)大塊的全 0 數(shù)據(jù),也會(huì)嘗試在目標(biāo)文件上 punch hole,從而節(jié)省空間,這種方式會(huì)導(dǎo)致目標(biāo)文件的物理 blocks 可能比源文件要??;

never 模式:最低效,速度最慢的方式。這種方式無論源文件是啥,全都是實(shí)打?qū)嵉膹?fù)制,不管是空洞還是全 0 數(shù)據(jù),都會(huì)在目標(biāo)文件寫入;

動(dòng)畫演示(精髓):

精髓所在,前面知識(shí)點(diǎn)就算全都忘了,只記得這三張圖,你也賺了。

cp src.txt dest.txt

aa8d2f7a-947e-11eb-8b86-12bb97331649.gif

cp --sparse=always src.txt dest.txt

cb55e3dc-947e-11eb-8b86-12bb97331649.gif

cp --sparse=never src.txt dest.txt

cc09585e-947e-11eb-8b86-12bb97331649.gif

稀疏文件的應(yīng)用

稀疏文件在哪些地方有應(yīng)用呢?

數(shù)據(jù)庫(kù)快照:生成一個(gè)數(shù)據(jù)庫(kù)快照時(shí)會(huì)生成一個(gè)稀疏文件,稀疏文件一開始并不會(huì)占用磁盤空間。當(dāng)源數(shù)據(jù)庫(kù)發(fā)生寫操作時(shí),就把修改前的原數(shù)據(jù)塊復(fù)制且只復(fù)制一次到稀疏文件中;

MySQL5.7 有一種數(shù)據(jù)壓縮方式,其原理就是利用內(nèi)核Punch hole特性,對(duì)于一個(gè)16kb的數(shù)據(jù)頁,在寫文件之前,除了 Page 頭之外,其他部分進(jìn)行壓縮,壓縮后留白的地方使用 punch hole 進(jìn)行 “打洞”,在磁盤上表現(xiàn)為不占用空間,從而達(dá)到快速釋放物理空間的目的;

qemu 磁盤鏡像文件的空間回收?qǐng)鼍埃?/p>

一起做個(gè)實(shí)驗(yàn)

最后我們演示下實(shí)驗(yàn),檢驗(yàn)看下你懂了嗎?找一臺(tái) linux 機(jī)器,跟著運(yùn)行下面的命令。

初始條件準(zhǔn)備

步驟一:創(chuàng)建一個(gè)文件(預(yù)期占用 1 個(gè) block)。

echo =========== test ======= 》 test.txt

步驟二:truncate 成 1G 的稀疏文件。

truncate -s 1G 。/test.txt

步驟三:把 1M 到 1M+4K 的位置預(yù)分配出來(并且是寫 0 分配,預(yù)期到這里要占用 2 個(gè) block,也就是 8K 數(shù)據(jù))。

fallocate -o 1048576 -l 4096 -z 。/test.txt

步驟四:stat 命令檢查下情況。

sh-4.4# stat test.txt

File: test.txt

Size: 1073741824 Blocks: 16 IO Block: 4096 regular file

Device: 6ah/106d Inode: 3148347 Links: 1

Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)

Access: 2021-03-12 1554.427903000 +0000

Modify: 2021-03-12 1500.456246000 +0000

Change: 2021-03-12 1500.456246000 +0000

Birth: -

我們看到 Size: 1073741824 Blocks: 16 ,Size 大小等于 1G,stat 顯示的 Blocks 是扇區(qū)(512字節(jié))的個(gè)數(shù),也就是說,物理空間占用 8K,符合預(yù)期。

也就是說:

文件大小為 1G;

實(shí)際數(shù)據(jù)在 [0, 4K] 和 [1M, 1M+4K] 這兩個(gè)位置才有寫入;

其中 [0, 4K] 范圍為正常數(shù)據(jù), [1M, 1M+4K] 這段范圍的數(shù)據(jù)為全 0 數(shù)據(jù);

好,初始條件準(zhǔn)備好了,下面我們開始對(duì) cp --sparse 的三個(gè)行為做實(shí)驗(yàn)。

cp 的實(shí)驗(yàn)驗(yàn)證

默認(rèn)策略:

cp 。/test.txt 。/test.txt.auto

always 策略:

cp --sparse=always 。/test.txt 。/test.txt.always

never 策略(這條命令敲下去可能有點(diǎn)慢哦,并且要預(yù)留好足夠空間):

cp --sparse=never 。/test.txt 。/test.txt.never

以上三個(gè)命令敲完,生成了三個(gè)文件,給大家 1 秒鐘的思考時(shí)間,思考下 test.txt.auto,test.txt.always,test.txt.never,這三個(gè)文件的屬性有何異同。

。..。. 。..。. 。..。.

結(jié)果揭秘:

test.txt.auto

sh-4.4# stat 。/test.txt.auto

File: 。/test.txt.auto

Size: 1073741824 Blocks: 16 IO Block: 4096 regular file

Device: 6ah/106d Inode: 3148348 Links: 1

Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)

Access: 2021-03-13 1557.395725000 +0000

Modify: 2021-03-13 1557.395725000 +0000

Change: 2021-03-13 1557.395725000 +0000

Birth: -

Size: 1073741824:文件大小 1G

Blocks: 8:物理空間占用 8K

test.txt.always

sh-4.4# stat 。/test.txt.always

File: 。/test.txt.always

Size: 1073741824 Blocks: 8 IO Block: 4096 regular file

Device: 6ah/106d Inode: 3148349 Links: 1

Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)

Access: 2021-03-13 1501.064725000 +0000

Modify: 2021-03-13 1501.064725000 +0000

Change: 2021-03-13 1501.064725000 +0000

Birth: -

Size: 1073741824:文件大小 1G

Blocks: 8:物理空間占用 4K

test.txt.never

sh-4.4# stat 。/test.txt.never

File: 。/test.txt.never

Size: 1073741824 Blocks: 2097160 IO Block: 4096 regular file

Device: 6ah/106d Inode: 3148350 Links: 1

Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)

Access: 2021-03-13 1504.774725000 +0000

Modify: 2021-03-13 1505.977725000 +0000

Change: 2021-03-13 1505.977725000 +0000

Birth: -

Size: 1073741824:文件大小 1G

Blocks: 2097160:物理空間占用 1G

所以,你學(xué)會(huì)了嗎?

知識(shí)點(diǎn)總結(jié)

文件系統(tǒng)對(duì)外提供文件語義,本質(zhì)只是管理磁盤空間的軟件而已;

經(jīng)典的文件系統(tǒng)主要?jiǎng)澐?3 大塊 superblock 區(qū),inode 區(qū),block 區(qū)(塊描述區(qū),bitmap區(qū)這里暫不介紹)。一個(gè)文件在文件系統(tǒng)的內(nèi)部形態(tài)由一個(gè) inode 記錄元數(shù)據(jù)加上 block 存儲(chǔ)用戶存儲(chǔ)用戶數(shù)據(jù)樣子;

文件系統(tǒng)的 size 是文件大小,是邏輯空間大小,文件大小 size 和真實(shí)的物理空間并不是一個(gè)概念;

稀疏語義是文件系統(tǒng)提供的一種特性,根本用途是用來更有效的利用磁盤空間;

后分配空間是空間利用最有效的方式,公有云的云盤靠什么賺錢?就是后分配,你買了 2T 的云盤,在沒有寫入數(shù)據(jù)的時(shí)候,一個(gè)字節(jié)都沒給你分配,你卻是付出 2T 的價(jià)格;

stat 命令能夠查看物理空間占用,Blocks 表示的是扇區(qū)(512字節(jié))個(gè)數(shù);

稀疏文件的空洞和用戶真正的全 0 數(shù)據(jù)是無法區(qū)分的,因?yàn)閷?duì)外表現(xiàn)是一樣的(這點(diǎn)非常重要);

cp 命令通過調(diào)用 ioctl(fiemap)系統(tǒng)調(diào)用,可以獲取到文件空洞的分布情況,cp 過程中跳過這些空洞,極大的提高了效率(100G 的源文件,cp 只做了十幾次 io 搞定了,所以 1 秒足以);

cp 的 sparse 參數(shù)從速度最快,空間最省,數(shù)據(jù)最拷貝最多,各有特點(diǎn),小小的 cp 命令出來的目標(biāo)文件,其實(shí)和源文件并不相同,只不過你沒注意到;

預(yù)分配和 punch hole 其實(shí)都是fallocate 調(diào)用,只是參數(shù)不同而已,調(diào)用的時(shí)候,注意要 4k 對(duì)齊才能達(dá)到目的;

稀疏文件的 punch hole 應(yīng)用有很多場(chǎng)景,通常是用來快速釋放空間,比如鏡像文件。

原文標(biāo)題:深度剖析 Linux cp 命令的秘密

文章出處:【微信公眾號(hào):Linux愛好者】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

責(zé)任編輯:haq

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • Linux
    +關(guān)注

    關(guān)注

    87

    文章

    11511

    瀏覽量

    213832
  • 命令
    +關(guān)注

    關(guān)注

    5

    文章

    737

    瀏覽量

    22882

原文標(biāo)題:深度剖析 Linux cp 命令的秘密

文章出處:【微信號(hào):LinuxHub,微信公眾號(hào):Linux愛好者】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

    評(píng)論

    相關(guān)推薦
    熱點(diǎn)推薦

    SSH常用命令詳解

    SSH常用命令詳解
    的頭像 發(fā)表于 06-04 11:30 ?651次閱讀

    飛凌嵌入式ElfBoard ELF 1板卡-uboot常用命令之文件系統(tǒng)命令

    uboot支持fat格式、ext格式的文件系統(tǒng)。我們主要講解fat格式文件系統(tǒng)常用命令使用。 、fatinfo fatinfo顯示選中的mmc設(shè)備指定分區(qū)的文件系統(tǒng)信息, fatinfo
    發(fā)表于 05-26 17:32

    Linux常用命令大全

    Linux常用命令是指在Linux操作系統(tǒng)中廣泛使用的命令工具,這些命令工具可以完成各種不同的任務(wù),如管理文件和目錄、操作進(jìn)程、網(wǎng)絡(luò)通信、軟
    的頭像 發(fā)表于 05-03 18:08 ?1128次閱讀

    Docker Compose的常用命令

    大家好,今天給大家分享Docker Compose的常用命令,以及docker-compose文件的屬性。Docker Compose 是個(gè)用于定義和運(yùn)行多容器 Docker 應(yīng)用應(yīng)用的重要工具
    的頭像 發(fā)表于 04-30 13:40 ?454次閱讀

    Docker常用命令大全

    Docker 是種開源的應(yīng)用容器引擎,廣泛應(yīng)用于開發(fā)、部署和運(yùn)行分布式應(yīng)用。掌握 Docker 常用命令對(duì)于開發(fā)人員和運(yùn)維人員來說非常重要。本文將為大家整理常用的Docker 命令,
    的頭像 發(fā)表于 04-22 12:47 ?403次閱讀

    Linux常用命令行總結(jié)

    學(xué)習(xí)了段時(shí)間的linux之后,開始著手基本命令的學(xué)習(xí),這里主要記錄些學(xué)習(xí)過程中重要的知識(shí)點(diǎn)供以后查閱。
    的頭像 發(fā)表于 03-03 10:40 ?497次閱讀
    <b class='flag-5'>Linux</b><b class='flag-5'>常用命令</b>行總結(jié)

    Linux ip命令常用操作

    Linux?ip命令常用操作 在Linux系統(tǒng)中,ip命令是用于管理網(wǎng)絡(luò)接口和路由的強(qiáng)大工具。相比于舊的?ifconfig
    的頭像 發(fā)表于 02-19 10:23 ?678次閱讀

    Linux實(shí)時(shí)查看日志的四種命令詳解

    如何在Linux中實(shí)時(shí)查看日志文件的內(nèi)容?那么有很多實(shí)用程序可以幫助用戶在文件更改或不斷更新時(shí)輸出文件的內(nèi)容。在Linux中實(shí)時(shí)顯示文件內(nèi)容的常用命令是tail命令(有效地管理文件)。
    的頭像 發(fā)表于 01-13 10:45 ?3074次閱讀
    <b class='flag-5'>Linux</b>實(shí)時(shí)查看日志的四種<b class='flag-5'>命令</b>詳解

    Linux常用命令

    文件快捷鍵 ln -s /data /home/nvidia/ vi 查看文件命令,例如: vi xxx.log 退出方式::+q+回車 tail 顯示文件后幾行,例如: tail xxx.log 也可以
    的頭像 發(fā)表于 11-06 16:04 ?386次閱讀
    <b class='flag-5'>Linux</b><b class='flag-5'>常用命令</b>

    詳解kubectl常用命令

    詳解kubectl常用命令
    的頭像 發(fā)表于 11-05 15:39 ?1366次閱讀
    詳解kubectl<b class='flag-5'>常用命令</b>

    Linux實(shí)用命令大全

    Linux實(shí)用命令大全
    的頭像 發(fā)表于 10-23 13:50 ?656次閱讀
    <b class='flag-5'>Linux</b>實(shí)<b class='flag-5'>用命令</b>大全

    Linux磁盤分區(qū)擴(kuò)容方法

    linux分區(qū)常用命令:fdisk,修改MBR分區(qū)表,MBR格式,被修改的分區(qū)大小最大為2T。
    的頭像 發(fā)表于 10-23 11:46 ?1094次閱讀
    <b class='flag-5'>Linux</b>磁盤分區(qū)擴(kuò)容方法

    Vim編輯器之Vim常用操作命令

    文件從“般模式”進(jìn)入“編輯模式”,可以使用以下指令:進(jìn)入“編輯模式”后,如下圖:從“編輯模式”按ESC會(huì)再次進(jìn)入“般模式”。6)撤銷操作在“般模式”可以使用以下指令:3、Vim
    發(fā)表于 08-23 09:21

    shell基本介紹及常用命令之shell介紹

    提示符是“$”,在命令提示符后邊輸入命令即可和系統(tǒng)進(jìn)行交互操作。Ubuntu默認(rèn)的Shell是Bash(Bourne Again Shell)。Linux命令有很多,功能比較強(qiáng)大,
    發(fā)表于 08-15 09:28

    Windows操作系統(tǒng)中的常用命令

    這些命令不僅能提高工作效率,還能幫助用戶解決許多復(fù)雜的問題。本系列文章將詳細(xì)介紹Windows操作系統(tǒng)中的常用命令,幫助你成為Windows極客!
    的頭像 發(fā)表于 08-07 15:40 ?1174次閱讀
    Windows操作系統(tǒng)中的<b class='flag-5'>常用命令</b>