深度剖析 Linux cp 的秘密

大綱

喜歡就關(guān)注我吧,奇伢云存儲卦洽。更多干貨第一時間送達。

[toc]

cp 引發(fā)的思考

cp 是啥 ? 是的斜棚,就是 Linux 是 Linux 下最常用的命令之一阀蒂,copy 的簡寫,小伙伴 100% 都用過弟蚀。

cp 命令處于 Coreutils 庫里蚤霞,是 GNU 項目維護的一個核心項目,提供 Linux 上核心的命令义钉。

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

背景是這樣的夜畴,奇伢今天用 cp 拷貝了一個 100 GiB 的文件,竟然一秒不到就拷貝完成了鉴嗤。一個 SATA 機械盤的寫能力能到 150 MiB/s (大部分的機械盤都是到不了這個值的)就算非常不錯了斩启,所以,正常情況下醉锅,copy 一個 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 分鐘垄琐,實際情況卻是我們 cp 一秒沒到就完成了工作,驚呆了经柴,為啥呢狸窘?并且還有一個更詭異的我文件系統(tǒng)大小才 40 GiB,為啥里面會有一個 100 G的文件呢坯认?

分析文件

我們先用 ls 看一把文件翻擒,顯示文件確實是 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 12:22:00.888871000 +0000
Modify: 2021-03-13 12:22:46.562243000 +0000
Change: 2021-03-13 12:22:46.562243000 +0000
 Birth: -

stat 命令輸出解釋:

  1. Size 為 107374182400(知識點:單位是字節(jié))陋气,也就是 100G 劳吠;
  2. Blocks 這個指標顯示為 4096(知識點:一個 Block 的單位固定是 512 字節(jié),也就是一個扇區(qū)的大泄谩)痒玩,這里表示為 2M;

劃重點

  • Size 表示的是文件大小议慰,這個也是大多數(shù)人看到的大写拦拧;
  • Blocks 表示的是物理實際占用空間别凹;

所以草讶,注意到一個新概念,文件大小實際物理占用番川,這兩個竟然不是相同的概念到涂。為什么會這樣?

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

文件系統(tǒng)

文件系統(tǒng)聽起來很高大上浇雹,通俗話就用來存數(shù)據(jù)的一個容器而已沉御,本質(zhì)和你的行李箱、倉庫沒有啥區(qū)別昭灵。只不過文件系統(tǒng)存儲的是數(shù)字產(chǎn)品而已吠裆。我有一個視頻文件,我把這個視頻放到這個文件系統(tǒng)里烂完,下次來拿试疙,要能拿到我完整的視頻文件數(shù)據(jù),這就是文件系統(tǒng)抠蚣,對外提供的就是存取服務祝旷。

現(xiàn)實的存取場景

就跟你在火車站使用的寄存服務一樣,包裹我能存進去嘶窄,稍后我能取出來怀跛,就可以了。問題來了柄冲,存進去吻谋?怎么取现横?仔細回憶下存儲行李的場景漓拾。

存行李的時候阁最,是不是要登記一些個人信息?對吧骇两,至少自己名字要寫上速种。可能還會給你一個牌子脯颜,讓你掛手上哟旗,這個東西就是為了標示每一個唯一的行李。

cp-存行李演示.gif

取行李的時候栋操,要報自己名字闸餐,有牌子的給他牌子,然后工作人員才能去特定的位置找到你的行李(不然機場那么多人矾芙,行李都長差不多舍沙,他肯定不知道你的行李是哪個)。

cp取行李演示.gif

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

文件系統(tǒng)

回到我們的文件系統(tǒng)葱绒,對比上面的行李存取行為感帅,可以做個簡單的類比;

  1. 登記名字就是在文件系統(tǒng)記錄文件名地淀;
  2. 生成的牌子就是元數(shù)據(jù)索引失球;
  3. 你的行李就是文件;
  4. 寄存室就是磁盤(容納東西的物理空間)帮毁;
  5. 管理員整套運行機制就是文件系統(tǒng)实苞;

上面的對應并不是非常嚴謹,僅僅是幫助大家理解文件系統(tǒng)而已烈疚,讓大家知道其實文件系統(tǒng)是非常樸實的一個東西黔牵,思想都來源于生活。

劃重點:文件系統(tǒng)的存儲介質(zhì)是磁盤爷肝,文件系統(tǒng)是軟件層面的猾浦,是管理員,管理怎么使用磁盤空間的軟件系統(tǒng)而已阶剑。

空間管理

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

如果,一個連續(xù)的大磁盤空間給你使用牧愁,你會怎么使用這段空間呢素邪?

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

cp的秘密-搓的空間管理方式.gif

這種方式非常容易實現(xiàn)兔朦,屬于眼前最簡單偷线,以后最麻煩的方式。因為會造成很多空洞沽甥,明明還有很多空間位置声邦,但是由于整個太大,形狀不合適(數(shù)據(jù)大邪谥邸)亥曹,哪里都放不下。因為你要放一個完整的空間恨诱。

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

怎么改進?有人會想厕鹃,既然整個放不進去兢仰,那就剁碎了唄。這里塞一點剂碴,那里塞一點把将,就塞進去了。

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

cp-切分后的管理方式.gif

圖示標號表示這個完整對象的 Block 的序號挑宠,用來復原對象用的。

隨之而來又有一個問題:你光會切成塊還不行颓影,取文件數(shù)據(jù)的時候各淀,要給完整的用戶數(shù)據(jù)出去,用戶不管你內(nèi)部怎么實現(xiàn)诡挂,他只想要的是最初的樣子碎浇。所以临谱,要有一個表記錄該文件對應所有 Block 的位置,要把每一個 Block 的位置記錄好奴璃,取文件的時候悉默,對照這表恢復出一個完整的塊給到用戶。

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

  1. 先寫數(shù)據(jù):數(shù)據(jù)先按照 Block 粒度存儲到磁盤的各個位置抄课;
  2. 再寫元數(shù)據(jù):然后把 Block 所在的各個位置保存起來,這也就是元數(shù)據(jù)雳旅,文件系統(tǒng)里叫做 inode(我用一本書來表示)跟磨;
cp - 文件系統(tǒng)write.gif

文件讀流程則是:

  1. 先讀元數(shù)據(jù),找到各個 Block 的位置岭辣;
  2. 然后讀數(shù)據(jù)吱晒,構(gòu)造一個完整的文件,給到用戶沦童;
cp- 文件系統(tǒng)read.gif

inode/block 概念

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

  1. 磁盤空間是按照 Block 粒度來劃分空間的,存儲數(shù)據(jù)的區(qū)域全都是 Block偷遗,我們叫做數(shù)據(jù)區(qū)域墩瞳;
  2. 文件存儲不再連續(xù)存儲在磁盤上,所以需要記錄元數(shù)據(jù)氏豌,這個我們叫做 inode喉酌;

文件系統(tǒng)中,一個 inode 唯一對應一個文件泵喘,inode 的個數(shù)則是在文件系統(tǒng)格式化的時候就確定好了的泪电,換言之,一個 local 文件系統(tǒng)支持的文件數(shù)是天然就有上限的纪铺。

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

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

1_lLJ2BlmnulX1WZGfUTt0vg.png

一個文件的對應一個 inode,這個文件需要按照 Block 切分存儲在磁盤上骏令,存儲的位置則由 inode 記錄起來蔬捷,通過 inode 則能找到 block,也就獲取到用戶數(shù)據(jù)伏社。

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

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

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

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

文件系統(tǒng)架構(gòu).png

bitmap 什么時候用呢长已?自然是寫的時候畜眨,也就是分配 inode 或者 block 的時候,因為只有分配的時候术瓮,你才需要找空閑的空間康聂。

上圖我為了突出本質(zhì)思想,類似于超級塊胞四,塊描述符都省略了早抠,這個感興趣可以自己擴展,這里只突出主干哈撬讽。

小結(jié)一下

  1. bitmap 本質(zhì)是個 bit 數(shù)組,占用空間極其少悬垃,用 0 來表示空閑游昼,1 表示在用。使用時機是在創(chuàng)建文件尝蠕,或者寫數(shù)據(jù)的時候烘豌;
  2. inode 則對應一個文件,里面存儲的是元數(shù)據(jù)看彼,主要是數(shù)據(jù) block 的位置信息廊佩;
  3. block 里面存儲的是用戶數(shù)據(jù)囚聚,用戶數(shù)據(jù)按照 block 大小(4k)切分标锄,離散的分布在磁盤上顽铸。讀的時候只有依賴于 inode 里面記錄的位置才能恢復出完整的文件;
  4. inode 和 block 的總個數(shù)在文件系統(tǒng)格式化的時候就確定了料皇,所以文件數(shù)和文件大小都是有上限的谓松;

一個文件真實的模樣

上面是抽象的樣子,現(xiàn)在我們看一個真實的 inode -> block 的樣子践剂。一個文件除了數(shù)據(jù)需要存儲之外鬼譬,一些元信心也需要存儲,例如文件類型逊脯,權(quán)限优质,文件大小,創(chuàng)建/修改/訪問時間等军洼,這些信息存在 inode 中巩螃,每個文件唯一對應一個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 */
};

重點

  • 上面的結(jié)構(gòu) mode牺六,uid,size汗捡,time 等信息就是我們常說的文件類型淑际,大小,創(chuàng)建修改等時間元數(shù)據(jù)扇住;
  • 注意到 i_block[EXT2_N_BLOCKS] 這個字段春缕,這個字段將會帶你找到數(shù)據(jù), 因為里面存儲的就是 block 所在的位置,也就是 block 的編號艘蹋;

再來锄贼,理解下什么叫做 block 的位置(編號)。

磁盤劃分 block.gif

位置就是編號女阀,記錄位置就是記錄編號宅荤,編號就是索引。

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

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

inode 文件索引.png

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

  1. 第一個 4k 數(shù)據(jù)在 [3*4K, 4*4K] 范圍稚配;
  2. 第二個 2k 數(shù)據(jù)在 [ 101*4K, 101*4K+2K] 范圍;

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

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

舉個栗子,一個文件 12K 的大小橙数,那么按照 4K 切分會存儲到 2 個 物理 block 上尊流。

文件第 0 個 4k 存儲到了 101 這個物理 block 上;
文件第 1 個 4k 存儲到了 30 這個物理 block 上灯帮;
文件第 2 個 4k 存儲到了 11 這個物理 block 上崖技;

文件邏輯空間上的編號是從 0 開始,到 2 結(jié)束钟哥,對應存儲的物理塊編號分別是 101迎献,30,11 腻贰。

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

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

那我們自然會思考选浑,怎么解決呢蓝厌?怎么才能支撐更大的文件?

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

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

每個編號占用 4 字節(jié)疹吃,這個數(shù)組就占用 100M 的空間(計算公式:(26214400\*4)/1024/1024)蹦疑。

100M !這里就有點夸張了萨驶,注意到 i_block 只是一個 inode 內(nèi)部的字段歉摧,是一個靜態(tài)分配的數(shù)組,也就是說腔呜,這個文件系統(tǒng)為了支持最大 100G 的文件存入叁温,每一個 inode 都要占用 100M 的內(nèi)存,就算你是一個 1K 的文件核畴,inode 也會占用這么大的內(nèi)存空間膝但。并且,這種方案擴展性差谤草,支持的文件 size 越大跟束,i_block[N] 消耗內(nèi)存情況越嚴重。

這是無法接受的咖刃。那么怎么才能解決這個問題呢泳炉?怎么才能讓你既能表示更大的文件,又能不浪費占用空間嚎杨?

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

  1. 第一點枫浙,核心在于浪費內(nèi)存空間(關(guān)鍵點是要保證 inode 內(nèi)存結(jié)構(gòu)的穩(wěn)定刨肃,無論文件怎么變,inode 結(jié)構(gòu)本身不能變)箩帚;
  2. 第二點真友,仔細思考你會發(fā)現(xiàn),無論是什么神仙方案紧帕,如果你要存儲一個按照 4k 切分的 100G 文件盔然,都是需要 100M 的空間來存儲索引( block 編號)桅打,但是 99.99% 的文件可能都沒有這么大;

我們前面用一個大數(shù)組來一把存儲 block 編號的方案固然簡單愈案,但是問題在于太過死板挺尾。核心問題在于存儲 block 編號的數(shù)組是預分配的,為了還沒有發(fā)生并且 99% 場景都不會發(fā)生的事情(文件大小達到 100G)站绪,卻不管三七二十一遭铺,提前準備好了完整的 block 索引數(shù)組,預分配就是浪費的根源恢准。

那么知道了這兩個問題魂挂,下一步分析下一個個解決:

索引存磁盤

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

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

為什么?

因為磁盤的空間比內(nèi)存大了不止一個量級眯漩。100M 對內(nèi)存來說很大芹扭,對磁盤來說很小。換句話說赦抖,用把用戶數(shù)據(jù)所在的 block 編號存到磁盤上去舱卡,這個也需要物理空間,使用的也是 block 來存儲队萤,只不過這種 block 存儲的是 block 編號信息轮锥,而不是用戶數(shù)據(jù)。

那么我們怎么通過 inode 找到用戶數(shù)據(jù)呢要尔?

因為這個 block 本身也有編號舍杜,我們則需要把這個存儲用戶 block 編號的 block 所在塊的編號存儲在 inode->i_block[15] 里,當讀數(shù)據(jù)的時候赵辕,我們需要先找到這個存儲編號的 block既绩,然后再通過里面存儲的用戶數(shù)據(jù)所在的 block 編號找到用戶所在的 block ,去讀數(shù)據(jù)还惠。

這個存儲用戶 block 編號的 block 所在塊的編號我們叫做間接索引饲握,然后我們根據(jù)跳轉(zhuǎn)的次數(shù)可以分類成一級索引,二級索引蚕键,三級索引救欧。顧名思義,一級索引就是跳轉(zhuǎn) 1 次就能定位到用戶數(shù)據(jù)锣光,二級索引就是跳轉(zhuǎn) 2 次笆怠,三級索引就是跳轉(zhuǎn) 3 次才能定位到用戶數(shù)據(jù)。那么 inode->i_block[15] 里面存儲的可以直接定位到用戶數(shù)據(jù)的 block 就是直接索引誊爹。

終于可以說回 ext2 的使用了蹬刷,ext2 的 inode->i_block[15] 數(shù)組瓢捉。知識點來了,按照約定办成,這 15 個槽位分作 4 個不同類別來用:

  1. 前 12 個槽位(也就是 0 - 11 )我們成為直接索引泊柬;
  2. 第 13 個位置,我們稱為 1 級索引诈火;
  3. 第 14 個位置,我們稱為 2 級索引状答;
  4. 第 15 個位置冷守,我們稱為 3 級索引
inode.png

好惊科,那我們在來看下直接索引拍摇,一級,二級馆截,三級索引的表現(xiàn)力充活。

直接索引:能存 12 個 block 編號,每個 block 4K蜡娶,就是 48K混卵,也就是說,48K 以內(nèi)的文件窖张,只需要用到 inode->i_block[15] 前 12 個槽位存儲編號就能完全 hold 住幕随。

一級索引

inode->i_block[12] 這個位置存儲的是一個一級索引,也就是說這里存儲的編號指向的 block 里面存儲的也是 block 編號宿接,里面的編號指向用戶數(shù)據(jù)赘淮。一個 block 4K,每個元素 4 字節(jié)睦霎,也就是有 1024 個編號位置可以存儲梢卸。

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

二級索引

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

三級索引

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

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

間接尋址.png

所以开皿,在我們 ext2 的文件系統(tǒng)上涧黄,通過這種間接塊索引的方式,最大能支撐的文件大小 = 48K + 4M + 4G + 4T 赋荆,約等于 4 T笋妥。文件系統(tǒng)最大支撐 16T 空間,因為 4 Byte 的整形最大數(shù)就是 2^32=4294967296 窄潭, 乘以 4K 就等于 16 T春宣。

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

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

在不超過 12 個數(shù)據(jù)塊的小文件的尋址是最快的嚷辅,訪問文件中的任意數(shù)據(jù)理論只需要兩次讀盤,一次讀 inode距误,一次讀數(shù)據(jù)塊簸搞。訪問大文件中的數(shù)據(jù)則需要最多五次讀盤操作:inode、一級間接尋址塊准潭、二級間接尋址塊趁俊、三級間接尋址塊、數(shù)據(jù)塊惋鹅。

多級索引和后分配

問題二解決:多級索引和后分配

一級索引不夠则酝,表現(xiàn)力太差,預留空間又太浪費闰集,不預留空間又無法擴展沽讹,怎么解決?

既然問題在于預分配武鲁,我們使用后分配(瘦分配爽雄,或精簡分配)解決。也就是說用戶文件數(shù)據(jù)有多大沐鼠,我才分配出多大的數(shù)組挚瘟。舉個例子,我們存儲 100 G 的文件饲梭,那么就要用到三級索引塊乘盖,最多分配 26214400 個槽位的數(shù)組(因為要 26214400 個 block)。如果是存儲 6K 的文件憔涉,那么只需要 2 個槽位的數(shù)組订框。

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

后分配這里說的是 block 索引編號數(shù)組的后分配,需要用到的時候才分配兜叨,而不是說穿扳,現(xiàn)在用戶存儲一個 1k 的文件衩侥,我上來就給他分配一個 100M 的索引數(shù)組,只是為了以后這個文件可能增長到 100 G矛物。

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

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

實際的栗子

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

  1. 創(chuàng)建一個文件骨杂,這個時候分配一個 inode;
  2. 在 [ 0雄卷,4K ] 的位置寫入 4K 數(shù)據(jù),這個時候只需要 一個 block 假設(shè)編號 102蛤售,把這個編號寫到 inode->i_block[0] 這個位置保存起來丁鹉;
  3. 在 [ 1T,1T+4K ] 的位置寫入 4K 數(shù)據(jù)悴能,這個時候需要分配一個 block 假設(shè)編號 7揣钦,因為這個位置已經(jīng)落到三級索引才能表現(xiàn)的空間了,所以需要還需要分配出 3 個索引塊漠酿;
  4. 寫入完成冯凹,close 文件;

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

  1. offset 為 1T宇姚,按照 4K 切分,也就是 block 268435456 塊(注意這個是虛擬文件塊夫凸,不是物理位置)浑劳;
  2. 先算出范圍:直接索引的范圍是 [0, 11] 個,一級索引 [12, 1035]夭拌,二級索引 [1036, 1049611], 三級索引 [1049612, 1074791435]魔熏,(有人如果不知道怎么來的話,可以往前看看 inode 的結(jié)構(gòu)鸽扁,直接索引 12個蒜绽,一級索引 1024 個,二級 1M 個桶现,三級 1G 個躲雅,然后算出來的);
  3. 268435456 落在三級索引 [1049612, 1074791435] 這個范圍巩那;

實際存儲如圖

計算索引:

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

實際的物理分配如圖:

1T文件的存儲塊.png

因為偏移已經(jīng)用到了 3 級索引吏夯,所以除了用戶數(shù)據(jù)的兩個 block 此蜈,中間還需要 3 個間接索引 block 分配出來。

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

流程如下

  1. 計算 offset 得出在第 268435456 的位置裆赵;
  2. 讀出三級索引 inode->i_block[14] 里存儲的 block 編號,找到對應的物理 block跺嗽,這個是第一級的 block战授;
  3. 然后讀該 block 的第 254+1 個槽位里的數(shù)據(jù),里面存儲的是第二級的 block 編號桨嫁,把這個編號讀出來植兰,通過這個編號找到對應的物理 block;
  4. 讀該 block 的第 1022 +1 個操作的數(shù)據(jù),里面存儲的是第三級的 block 編號,通過這個編號可以找到物理 block 的數(shù)據(jù)线脚,里面存儲的是用戶數(shù)據(jù)所在 block 的編號轰驳;
  5. 讀該 block 第 1012+1 個槽位里存儲的編號,找到物理 block,這個 block 里存的就是用戶數(shù)據(jù)了;

這個時候,我們的文件看起來是超大文件毡咏,size 等于 1T+4K ,但里面實際的數(shù)據(jù)只有 8 K逮刨,位置分別是 [ 0呕缭,4K ] ,[ 1T修己,1T+4K ]恢总。

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

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

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

文件的稀疏語義

什么是稀疏文件

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

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

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

為什么要支持稀疏語義张惹?

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

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

后分配還有一個優(yōu)點德挣,這也減少了首次寫入的時間恭垦,怎么理解?

因為格嗅,如果文件大小 1T番挺,就要分配 1T 的空間,那么初始分配需要寫入全零到空間屯掖,否則上面的數(shù)據(jù)可能是隨機數(shù)玄柏。

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

又一個知識點:有時候稀疏文件的空洞和用戶真正的全 0 數(shù)據(jù)是無法區(qū)分的绍坝,因為對外表現(xiàn)是一樣的徘意。

稀疏文件也要文件系統(tǒng)支持,并不是所有的文件系統(tǒng)都支持稀疏語義轩褐,比如 ext2 就沒有椎咧,ext4 才有稀疏語義,支持的標志是實現(xiàn)文件系統(tǒng)的 fallocate 接口把介。

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

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

truncate -s 100G  test.txt
  • ls -lh ./test.txt 命令看會發(fā)現(xiàn)是一個 100 G 的文件;
  • 但是 du -sh ./test.txt 會發(fā)現(xiàn)是一個 0 字節(jié)的文件脚牍;
  • stat ./test.txt 會發(fā)現(xiàn)是 Size: 107374182400 Blocks: 0 的文件向臀;

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

下面這種 1T 的文件,因為只寫了頭尾 8K 數(shù)據(jù)作谚,所以只需要分配 2 個 block 存儲用戶數(shù)據(jù)即可三娩。

1T文件的存儲塊.png

好,我們再深入思考下妹懒,文件系統(tǒng)為什么能做到這個雀监?

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

  1. 首先眨唬,最關(guān)鍵的是把磁盤空間切成離散的会前、定長的 block 來管理;
  2. 然后匾竿,通過 inode 能查找到所有離散的數(shù)據(jù)(保存了所有的索引)瓦宜;
  3. 最后,實現(xiàn)索引塊和數(shù)據(jù)塊空間的后分配岭妖;

這三點是層層遞進的临庇。

稀疏語義接口

為了知識的完整性,簡要介紹稀疏語義的幾個接口:

  1. preallocate(預分配):提供接口可以讓用戶預占用文件內(nèi)指定范圍的物理空間昵慌;
  2. punch hole(打洞):提供接口可以讓用戶釋放文件內(nèi)指定范圍的物理空間假夺;

這兩個操作剛好相反。

預分配的意思就是說斋攀,當你創(chuàng)建一個 1T的文件已卷,如果你沒寫數(shù)據(jù),這個時候其實沒有分配物理空間的淳蔼,支持稀疏語義的文件系統(tǒng)會提供一個 fallocate 接口給你侧蘸,讓你實現(xiàn)預分配,也就是說把這 1T 的物理空間現(xiàn)在就分配出來鹉梨。

思考:這個有什么好處呢闺魏?

  • 第一,如果你命中注定要 1T 的空間俯画,預分配是有好處的析桥,把空間分配的工作量集中在初始化的時候一把做了,避免了實時現(xiàn)場分配的開銷;
  • 第二泡仗,如果不提前占坑埋虹,很有可能等你想要的時候已經(jīng)沒有空間可占用了。所以你把物理空間先占好娩怎,就可以安心使用了搔课;

linux 提供了一個 fallocate 命令,可以用來預分配空間截亦。

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

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

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

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

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

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

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

稀疏文件的應用

Go 語言實現(xiàn)

稀疏文件本身和編程語言無具體關(guān)系嗅剖,我下面以 Go 為例,看下稀疏文件的預分配和打洞(punch hole)是怎么實現(xiàn)的嘁扼。

預分配實現(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 實現(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ù)而已趁啸。指定文件偏移和長度强缘,就能預分配物理空間或者釋放物理空間了。

這里有一個知識點:punch hole 的調(diào)用要保證 4k 對齊才能釋放空間莲绰。

舉個例子,比如:

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

這個很容易理解栅哀,因為磁盤的物理空間是劃分成 4k 的 block震肮,這個是最小單位了,不能再分了留拾,你無法切割一個最小的單位戳晌。

值得注意的是,就算你沒有 4k 對齊的發(fā)送調(diào)用痴柔,fallocate 也不會報錯沦偎,這個請注意了。

cp 的秘密

鋪墊了這么久的基礎(chǔ)知識,終于到我們的 cp 命令的解密了豪嚎∩ν眨回到最開始的問題,cp 一個 100G 的文件 1 秒都不到侈询,為什么這么快舌涨?

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

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

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

  1. --sparse=always :空間最省恳蹲;
  2. --sparse=auto :默認值虐块,速度最快;
  3. --sparse=never :吭呲吭呲 copy嘉蕾,最傻贺奠;

cp 默認的時候,sparseauto 策略错忱。auto儡率,always,never 分別是什么策略呢以清?

spare 三大策略

auto 策略

默認的情況下儿普,cp 會檢查源文件是否具有稀疏語義,對于不占物理空間的位置掷倔,目標文件不會寫入數(shù)據(jù)眉孩,從而形成空洞。

所以勒葱,對于我們的例子浪汪,真實的就只進行了 2M 的 IO ,預期的 100G 文件凛虽,只拷貝了 2M 的數(shù)據(jù)死遭,自然飛快了,自然驚艷所有人凯旋。

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

注意篷牌,不會對非空洞位置的文件內(nèi)容做判斷,如果用戶數(shù)據(jù)占用了物理塊踏幻,但是是全 0 數(shù)據(jù)枷颊,這種情況下,auto 模式不會識別该面,會以全零的數(shù)據(jù)寫入到目標文件夭苗。這個是跟 always 最大的區(qū)別。

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

always

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

在讀出源數(shù)據(jù)之后淮悼,就算這塊數(shù)據(jù)位置在源文件不是空洞,也會自己在程序里做一次判斷揽思,判斷是否是全 0 的數(shù)據(jù)袜腥,如果是,那么也會在目標文件里對應的位置創(chuàng)建空洞(不分配物理空間)钉汗。

這種方式則會導致源文件的 size 和目標文件一樣(三種策略下羹令,文件size 都是不變的),但是 物理 blocks 占用卻更小损痰。

never

這種方式最保守福侈,實現(xiàn)也最簡單。不管源文件是否是稀疏文件卢未,cp 完全不感知肪凛,讀出來的任何數(shù)據(jù)都直接寫入目標文件。也就是說尝丐,如果一個 100G 的文件显拜,就算只占用了 4K 的物理空間衡奥,也會創(chuàng)建出一個 100G 的目標文件爹袁,物理空間就占用 100G。

所以矮固,如果你 cp 的時候帶了這個參數(shù)失息,那么將會非常非常慢譬淳。

深入剖析 cp --sparse 源碼

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

cp 命令源碼在 GNU 項目的 coreutils 項目中,為 Linux 提供外圍的基礎(chǔ)命令工具绎秒∑滞看似極簡的 cp,其實代碼實現(xiàn)還挺有趣的见芹。

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

以一個 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;

這里會根據(jù)用戶傳入的參數(shù)玄呛,對應翻譯成一個枚舉值阅懦,該枚舉值就是 SPARSE_NEVERSPARSE_AUTO徘铝,SPARSE_ALWAYS 其中之一耳胎,默認用戶沒帶這個參數(shù)的話,就會是 SPARSE_AUTO

static enum Sparse_type const sparse_type[] =
{
  SPARSE_NEVER, SPARSE_AUTO, SPARSE_ALWAYS
};

所以惕它,main 函數(shù)里賦值了 x.sparse_mode 這個參數(shù)怕午,這個參數(shù)也是稀疏文件行為的指導參數(shù),后面怎么處理稀疏文件怠缸,就依賴于這個參數(shù)诗轻。

下面就是依次調(diào)用 do_copycopy揭北,copy_internal 函數(shù)扳炬,do_copycopy 這兩個函數(shù)就是處理一些封裝搔体,校驗恨樟,包括涉及目錄的一些邏輯,跟我們本次稀疏文件解密關(guān)系不大疚俱,直接略過劝术。

copy_internal 則是一個巨長的函數(shù),里面的邏輯多數(shù)是一些兼容性呆奕,適配場景的考慮养晋,也和本次關(guān)系不大。對于一個普通文件( regular 類型) 最終調(diào)用到 copy_reg 函數(shù)梁钾,才是普通文件 copy 的實現(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 才真正開始的。在這個函數(shù)里姆泻,首先 open 源文件和目標文件的句柄零酪,然后進行數(shù)據(jù)拷貝冒嫡。

static bool
copy_reg( ... ) 
{
  // 確認要拷貝數(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 模式,也是追求極致空間效率的策略月腋;
            // 所以這種方式不管源文件是否真的是稀疏文件蟀架,都會生成稀疏的目標文件;
            make_holes = true;
          // 如果是 sparse_auto 的策略榆骚,并且源文件是稀疏文件辜窑,那么目標文件也會是稀疏文件(也就是可以有洞洞的文件)
          if (x->sparse_mode == SPARSE_AUTO && sparse_src)
            make_holes = true;
        }

      // 如果到這里判斷不是目標不會是稀疏文件,那么就使用更有效率的方式來 copy寨躁,比如用更大的 buffer 來裝數(shù)據(jù)穆碎,一次 copy 更多;
      if (! make_holes)
        {
            // 略
        }

      // 源文件是稀疏文件的情況下职恳,可以使用 extent_copy 這種更有效率的方式進行拷貝所禀。
      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;
            
        }

      // 如果源文件判斷不是稀疏文件,那么就使用標準的 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;
        }
        // 略
    }
    
}

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

小結(jié)

  1. copy_reg 函數(shù)才是真正 cp 一個普通文件的邏輯所在操禀,源文件的打開褂策,目標文件的創(chuàng)建和數(shù)據(jù)的寫入都在這里;
  2. 拷貝之前颓屑,會先用 is_probably_sparse 函數(shù)來判斷源文件是否屬于稀疏文件斤寂;
  3. 如果是 sparse always 模式,那么無論源文件是否是稀疏文件揪惦,那么都會嘗試生成稀疏的目標文件(這種模式下遍搞,源文件如果是非稀疏文件,會判斷是否是全 0 數(shù)據(jù)器腋,如果是的話溪猿,還是會在目標文件中打洞);
  4. 如果是 sparse auto 模式纫塌,源文件是稀疏文件诊县,那么生成的目標文件也會是稀疏文件;
  5. 源文件為稀疏文件的時候措左,會嘗試使用效率更高的 extent_copy 函數(shù)來拷貝數(shù)據(jù)依痊;
  6. 如果是 never 模式,那么是調(diào)用 sparse_copy 函數(shù)來拷貝數(shù)據(jù)媳荒,并且里面不會嘗試 punch hole抗悍,拷貝過程會非常慢,會生成一個實打?qū)嵉哪繕宋募恚锢砜臻g占用完全和文件size一致缴渊;

上面的小結(jié),提到幾個有意思的點鱼炒,我們一起探秘下:

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

看了源碼你會發(fā)現(xiàn),非常簡單昔瞧,其實就是 stat 一下源文件指蚁,拿到文件大小 size,還有物理塊的占用個數(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);
}

舉個例子,文件大小 size 為 100G酬荞,物理占用塊 8 個搓劫,那么 100G/512字節(jié) > 8,所以就是稀疏文件混巧。

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

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

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

那么被辑,不禁又要問燎悍, extent_scan_read 又是怎么實現(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 這個是一個非常關(guān)鍵的特性宏怔,ioctl 搭配 FS_IOC_FIEMAP 這個函數(shù)能夠拿到文件的物理空間分配關(guān)系奏路,能夠讓用戶知道長達 100G 的文件中畴椰,哪些位置才是真正有物理塊存儲數(shù)據(jù)的,哪些位置是空洞鸽粉。

這個特性則由文件系統(tǒng)提供斜脂,也就是說,只有文件系統(tǒng)提供了這個對外接口触机,我們才能拿得到帚戳,比如 ext4,就支持這個接口儡首,ext2 就沒有片任。

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

這個函數(shù)是標準的 copy 函數(shù)对供,對比 extent_copy 來說,沒有 fiemap 的加持氛濒,那么這個函數(shù)就自己判斷是否是空洞犁钟,怎么判斷?

sparse_copy 認為泼橘,只要大塊連續(xù)的全 0 數(shù)據(jù)涝动,那么就認為是空洞,目標文件就不用寫入炬灭,直接打洞即可醋粟。

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

舉個例子鼻吮,現(xiàn)在 sparse_copy 從源文件里讀出 4k 的數(shù)據(jù)育苟,發(fā)現(xiàn)全都是 0,那么目標文件對應的位置就不會寫入椎木,而是直接 punch hole 打洞违柏,節(jié)省空間。

但是注意了香椎,這種行為只有在激進的 sparse always 策略才是這樣的漱竖。如果是其他策略,sparse_copy 不會做這樣做畜伐,而是老老實實的拷貝數(shù)據(jù)馍惹,哪怕是全 0 的數(shù)據(jù),也要如實的寫入到目標文件。

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

cp 快速的原因

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

因為源文件是稀疏文件啊们颜,文件看似 100G,實際只占用了 2M 的物理空間猎醇。文件系統(tǒng)將文件大小和物理空間占用這兩個概念解耦窥突,使得有更靈活的使用姿勢,更有效的使用物理空間硫嘶。

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

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

  1. auto 模式:默認模式囱井,最一致的模式(如果沒有用戶全0 塊數(shù)據(jù),那么可能也是速度最快的)衡未,會根據(jù)源文件的實際空間占用復制數(shù)據(jù),目標文件和源文件一致家凯。無論是文件 size 還是物理 blocks缓醋;
  2. always 模式:追求最小空間占用的模式,就算源文件不是稀疏文件绊诲,而僅僅是有些連續(xù)大塊的全 0 數(shù)據(jù)送粱,也會嘗試在目標文件上 punch hole,從而節(jié)省空間掂之,這種方式會導致目標文件的物理 blocks 可能比源文件要小抗俄;
  3. never 模式:最低效,速度最慢的方式世舰。這種方式無論源文件是啥橄镜,全都是實打?qū)嵉膹椭疲还苁强斩催€是全 0 數(shù)據(jù)冯乘,都會在目標文件寫入洽胶;

動畫演示(精髓)

cp src.txt dest.txt
cp-auto模式拷貝.gif
cp --sparse=always src.txt dest.txt
cp-always模式拷貝.gif
cp --sparse=never src.txt dest.txt
cp-never模式拷貝.gif

稀疏文件的應用

稀疏文件在哪些地方有應用呢?

  1. 數(shù)據(jù)庫快照:生成一個數(shù)據(jù)庫快照時會生成一個稀疏文件,稀疏文件一開始并不會占用磁盤空間姊氓。當源數(shù)據(jù)庫發(fā)生寫操作時丐怯,就把修改前的原數(shù)據(jù)塊復制且只復制一次到稀疏文件中;
  2. MySQL5.7 有一種數(shù)據(jù)壓縮方式翔横,其原理就是利用內(nèi)核Punch hole特性读跷,對于一個16kb的數(shù)據(jù)頁,在寫文件之前禾唁,除了 Page 頭之外效览,其他部分進行壓縮,壓縮后留白的地方使用 punch hole 進行 “打洞”荡短,在磁盤上表現(xiàn)為不占用空間丐枉,從而達到快速釋放物理空間的目的;
  3. qemu 磁盤鏡像文件的空間回收場景掘托;

一起做個實驗

最后我們演示下實驗瘦锹,檢驗看下你懂了嗎?找一臺 linux 機器闪盔,跟著運行下面的命令弯院。

初始條件準備

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

echo =========== test ======= > test.txt

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

truncate -s 1G ./test.txt

步驟三:把 1M 到 1M+4K 的位置預分配出來(并且是寫 0 分配听绳,預期到這里要占用 2 個 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 15:37:54.427903000 +0000
Modify: 2021-03-12 15:46:00.456246000 +0000
Change: 2021-03-12 15:46:00.456246000 +0000
 Birth: -

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

也就是說:

  1. 文件大小為 1G;
  2. 實際數(shù)據(jù)在 [0, 4K] 和 [1M, 1M+4K] 這兩個位置才有寫入孕荠;
  3. 其中 [0, 4K] 范圍為正常數(shù)據(jù)娩鹉, [1M, 1M+4K] 這段范圍的數(shù)據(jù)為全 0 數(shù)據(jù);

好稚伍,初始條件準備好了弯予,下面我們開始對 cp --sparse 的三個行為做實驗。

cp 的實驗驗證

默認策略:

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

always 策略:

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

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

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

以上三個命令敲完锈嫩,生成了三個文件,給大家 1 秒鐘的思考時間,思考下 test.txt.auto呼寸,test.txt.always艳汽,test.txt.never,這三個文件的屬性有何異同对雪。

.....
.....
.....

結(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 15:58:57.395725000 +0000
Modify: 2021-03-13 15:58:57.395725000 +0000
Change: 2021-03-13 15:58:57.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 15:59:01.064725000 +0000
Modify: 2021-03-13 15:59:01.064725000 +0000
Change: 2021-03-13 15:59:01.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 15:59:04.774725000 +0000
Modify: 2021-03-13 15:59:05.977725000 +0000
Change: 2021-03-13 15:59:05.977725000 +0000
 Birth: -
  • Size: 1073741824:文件大小 1G
  • Blocks: 2097160:物理空間占用 1G

所以河狐,你學會了嗎?

知識點總結(jié)

  1. 文件系統(tǒng)對外提供文件語義瑟捣,本質(zhì)只是管理磁盤空間的軟件而已馋艺;
  2. 經(jīng)典的文件系統(tǒng)主要劃分 3 大塊 superblock 區(qū),inode 區(qū)迈套,block 區(qū)(塊描述區(qū)捐祠,bitmap區(qū)這里暫不介紹)。一個文件在文件系統(tǒng)的內(nèi)部形態(tài)由一個 inode 記錄元數(shù)據(jù)加上 block 存儲用戶存儲用戶數(shù)據(jù)樣子交汤;
  3. 文件系統(tǒng)的 size 是文件大小雏赦,是邏輯空間大小劫笙,文件大小 size 和真實的物理空間并不是一個概念芙扎;
  4. 稀疏語義是文件系統(tǒng)提供的一種特性,根本用途是用來更有效的利用磁盤空間填大;
  5. 后分配空間是空間利用最有效的方式戒洼,公有云的云盤靠什么賺錢?就是后分配允华,你買了 2T 的云盤圈浇,在沒有寫入數(shù)據(jù)的時候,一個字節(jié)都沒給你分配靴寂,你卻是付出 2T 的價格磷蜀;
  6. stat 命令能夠查看物理空間占用,Blocks 表示的是扇區(qū)(512字節(jié))個數(shù)百炬;
  7. 稀疏文件的空洞和用戶真正的全 0 數(shù)據(jù)是無法區(qū)分的褐隆,因為對外表現(xiàn)是一樣的(這點非常重要);
  8. cp 命令通過調(diào)用 ioctl(fiemap)系統(tǒng)調(diào)用剖踊,可以獲取到文件空洞的分布情況庶弃,cp 過程中跳過這些空洞,極大的提高了效率(100G 的源文件德澈,cp 只做了十幾次 io 搞定了歇攻,所以 1 秒足以);
  9. cp 的 sparse 參數(shù)從速度最快梆造,空間最省缴守,數(shù)據(jù)最拷貝最多,各有特點,小小的 cp 命令出來的目標文件斧散,其實和源文件并不相同供常,只不過你沒注意到;
  10. 預分配和 punch hole 其實都是fallocate 調(diào)用鸡捐,只是參數(shù)不同而已栈暇,調(diào)用的時候,注意要 4k 對齊才能達到目的箍镜;
  11. 稀疏文件的 punch hole 應用有很多場景源祈,通常是用來快速釋放空間,比如鏡像文件色迂;

后記

本文通過一個日常隨處可見香缺、所有人都用過,但是都沒有細想過的 cp 命令切入歇僧,通過一個常常被我們忽略的現(xiàn)象來深入剖析其中的原理图张。這次通過分析 cp 的又獲得一點秘密的知識點呢。

我把這點小知識給小伙伴講了一小時诈悍,看到他感動欲哭的表情祸轮,我覺得他學fei了,非常滿意侥钳。是我想太多了嗎适袜?中午吃飯都沒叫我。

喜歡就關(guān)注我吧舷夺,奇伢云存儲苦酱。更多干貨第一時間送達。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末给猾,一起剝皮案震驚了整個濱河市疫萤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌敢伸,老刑警劉巖扯饶,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異详拙,居然都是意外死亡帝际,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進店門饶辙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蹲诀,“玉大人,你說我怎么就攤上這事弃揽「Γ” “怎么了则北?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長痕慢。 經(jīng)常有香客問我尚揣,道長,這世上最難降的妖魔是什么掖举? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任快骗,我火速辦了婚禮,結(jié)果婚禮上塔次,老公的妹妹穿的比我還像新娘方篮。我一直安慰自己,他們只是感情好励负,可當我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布藕溅。 她就那樣靜靜地躺著,像睡著了一般继榆。 火紅的嫁衣襯著肌膚如雪巾表。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天略吨,我揣著相機與錄音集币,去河邊找鬼。 笑死晋南,一個胖子當著我的面吹牛惠猿,可吹牛的內(nèi)容都是我干的羔砾。 我是一名探鬼主播负间,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼姜凄!你這毒婦竟也來了政溃?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤态秧,失蹤者是張志新(化名)和其女友劉穎董虱,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體申鱼,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡愤诱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了捐友。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片淫半。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖匣砖,靈堂內(nèi)的尸體忽然破棺而出科吭,到底是詐尸還是另有隱情昏滴,我是刑警寧澤,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布对人,位于F島的核電站谣殊,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏牺弄。R本人自食惡果不足惜姻几,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望势告。 院中可真熱鬧鲜棠,春花似錦、人聲如沸培慌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽吵护。三九已至盒音,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間馅而,已是汗流浹背祥诽。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留瓮恭,地道東北人雄坪。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像屯蹦,于是被迫代替她去往敵國和親维哈。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,601評論 2 353

推薦閱讀更多精彩內(nèi)容