Linux 式套娃,把“文件系統(tǒng)”安裝在一個(gè)“文件”上?
背景
“文件”在文件系統(tǒng)之中,這是人人理解的概念。但“文件”之上還有一個(gè)文件系統(tǒng)?那豈不是成套娃了。但這個(gè)其實(shí)是可以的。這個(gè)就涉及到今天我們要講的 loop 設(shè)備。
很多童鞋在學(xué)習(xí) Linux 的文件系統(tǒng)時(shí),涉及到對(duì)磁盤(pán)設(shè)備的格式化,掛載等操作,但苦于沒(méi)有一個(gè)真實(shí)的硬盤(pán),一時(shí)不知道如何實(shí)踐。這種時(shí)候就可以使用一個(gè)文件來(lái)模擬塊設(shè)備。這就是 loop 設(shè)備的作用。我們借助 loop 設(shè)備,可以讓一個(gè)文件被當(dāng)做一個(gè)塊設(shè)備來(lái)訪問(wèn)。
舉個(gè)例子,我們?cè)?ext4 的文件系統(tǒng)目錄下創(chuàng)建了一個(gè) minix_test.img 文件,把它當(dāng)作一個(gè)塊設(shè)備,在上面格式化 minix 的文件系統(tǒng),并掛載到 /mnt/minix 上。示意圖如下:
圖片
這種方式有兩個(gè)很明顯好處:
不需要真實(shí)的硬盤(pán),就可以格式化、掛載、測(cè)試文件系統(tǒng)。
可以近距離的觀察文件系統(tǒng)對(duì)塊設(shè)備的使用,比如如何劃分 inode 區(qū)域、數(shù)據(jù)區(qū)域、位圖區(qū)域等。這些都將反饋到文件上。
如何使用 loop 設(shè)備?
接下來(lái)看下如何使用 loop 設(shè)備。我們將會(huì)用一個(gè)普通文件上格式化成 minix 文件系統(tǒng),然后掛載到 Linux 目錄樹(shù)上。我們使用 loop 設(shè)備有兩種方式:
- 一種是直接 mount 帶上 -o loop 的參數(shù)。這種省去了顯式創(chuàng)建 loop 設(shè)備的過(guò)程,步驟簡(jiǎn)單。
- 另一種方式是先顯式的創(chuàng)建 loop 設(shè)備,該 loop 設(shè)備綁定一個(gè)文件,并提供了塊設(shè)備的對(duì)外接口。我們就可以把這個(gè) loop 設(shè)備當(dāng)作一個(gè)普通的塊設(shè)備文件,進(jìn)行格式化,然后掛載到目錄上。
方式一:mount -o loop
用 mount 掛載文件系統(tǒng)的時(shí)候,指定某個(gè)文件以 loop 設(shè)備的方式進(jìn)行掛載。具體操作如下:
首先,我們創(chuàng)建一個(gè) 1GiB 的文件:
dd if=/dev/zero of=./minix_test.img bs=1M count=1024
然后,我們?cè)谶@個(gè)文件上進(jìn)行 minix 文件系統(tǒng)的格式化:
mkfs.minix ./minix_test.img
最后,我們用 loop 設(shè)備的方式進(jìn)行掛載:
mount -o loop ./minix_test.img /mnt/minix/
這樣掛載成功之后,就可以在 /mnt/minix 下進(jìn)行操作了。該目錄掛載的是 minix 類(lèi)型的文件系統(tǒng)。minix 文件系統(tǒng)是一個(gè)磁盤(pán)類(lèi)型的文件系統(tǒng),它的數(shù)據(jù)是會(huì)寫(xiě)到磁盤(pán)進(jìn)行持久化。所以,我們?cè)谶@個(gè) /mnt/minix 目錄下做的任何操作,這些都會(huì)反映到 ./minix_test.img 這個(gè)文件上。這個(gè)文件就像磁盤(pán)一樣,在這個(gè)上面承載了一個(gè)文件系統(tǒng)的數(shù)據(jù)。
可以嘗試在 /mnt/minix 目錄下創(chuàng)建一個(gè)文件,然后用 hexdump 工具查看 minix_test.img 的內(nèi)容變化。如下:
# 在 minix 文件系統(tǒng)之上創(chuàng)建一個(gè)文件,并寫(xiě)入一個(gè)字符串
echo "hello world" >> /mnt/minix/hello.txt
# 查看 minix_test.img 的內(nèi)容
hexdump -C ./minix_test.img
如果你對(duì) minix 文件系統(tǒng)的分區(qū)熟悉的話,就可以明顯看到 minix 是如何在 minix_test.img 文件上劃分的 inode 區(qū)域、數(shù)據(jù)區(qū)域、位圖區(qū)域等。
方式二:先創(chuàng)建 loop 設(shè)備,再掛載使用
這種方式稍微步驟多一點(diǎn),但其實(shí)更容易讓人理解其中原理。實(shí)際的效果和方式一是等價(jià)的。我們可以使用 losetup 命令來(lái)管理 loop 設(shè)備。
首先,我們創(chuàng)建一個(gè) 1GiB 的文件:
dd if=/dev/zero of=./minix_test.img bs=1M count=1024
然后,我們?cè)谶@個(gè)文件上進(jìn)行 minix 文件系統(tǒng)的格式化:
mkfs.minix ./minix_test.img
再然后,創(chuàng)建和使用 loop 設(shè)備:
# 方式一:假定 /dev/loop5 是空閑可用的 loop 設(shè)備,下面把 /dev/loop5 和 minix_test.img 關(guān)聯(lián)起來(lái)
losetup /dev/loop5 ./minix_test.img
# 方式二:可以簡(jiǎn)單一點(diǎn),讓 losetup 命令自動(dòng)找到一個(gè)空閑的 loop 設(shè)備,然后進(jìn)行關(guān)聯(lián)
losetup --find --show ./minix_test.img
創(chuàng)建完 loop 設(shè)備之后,可以用 losetup 命令列舉當(dāng)前所有的 loop 設(shè)備,和它們關(guān)聯(lián)的文件。
最后,把 loop 設(shè)備掛載到目錄上:
# 假設(shè)上一個(gè)步驟創(chuàng)建的是 /dev/loop5
mount /dev/loop5 /mnt/minix
這種方式掛載的文件系統(tǒng)和方式一本質(zhì)上是一樣的。
mount 和 losetup 等命令的源代碼都在 util-linux 開(kāi)源庫(kù)中,感興趣的童鞋們可以自行查看。
什么是 loop 設(shè)備?
本質(zhì)上來(lái)講,loop 設(shè)備是塊設(shè)備的一種特殊的驅(qū)動(dòng)實(shí)現(xiàn)。接下來(lái)我們來(lái)簡(jiǎn)單看下 loop 設(shè)備的基本原理。
loop 設(shè)備的原理
loop 就是一種特殊的塊設(shè)備驅(qū)動(dòng)。loop 設(shè)備是一種 Linux 虛擬的偽設(shè)備,它和真實(shí)的塊設(shè)備不同,它并不代表一種特定的硬件設(shè)備,而僅僅是滿足 Linux 塊設(shè)備接口的一個(gè)虛擬設(shè)備。它的作用就是把一個(gè)文件模擬成一個(gè)塊設(shè)備。
loop 設(shè)備它是怎么模擬的塊設(shè)備?
loop 設(shè)備的代碼位于 Linux 的 drivers/block/loop.c 中。在這個(gè)文件中,它定義了塊設(shè)備驅(qū)動(dòng)的接口。
塊設(shè)備驅(qū)動(dòng)的編程范式:
- 首先,要分配并初始化一個(gè) gendisk 結(jié)構(gòu)體,這是內(nèi)核代表塊設(shè)備的核心結(jié)構(gòu)體。它包含了與磁盤(pán)相關(guān)的信息,loop 設(shè)備作為一種特殊的塊設(shè)備驅(qū)動(dòng),這個(gè)自然是不能少的。
- 然后,初始化一個(gè)請(qǐng)求隊(duì)列。塊設(shè)備使用請(qǐng)求隊(duì)列來(lái)管理對(duì)設(shè)備的 I/O 請(qǐng)求。文件系統(tǒng)調(diào)用 submit_bio 的調(diào)用時(shí),最終就是把請(qǐng)求投遞到驅(qū)動(dòng)的隊(duì)列中。
- 然后,請(qǐng)求處理函數(shù)。這個(gè)很容易理解,隊(duì)列里的請(qǐng)求總是要處理的,每個(gè)塊設(shè)備驅(qū)動(dòng)都可以自定義處理方式。
- 最后,塊設(shè)備操作表(block_device_operations),這個(gè)將包含對(duì)設(shè)備的操作方法,比如打開(kāi),讀寫(xiě)控制等。
loop 設(shè)備如何關(guān)聯(lián)到后端“文件” ?
用戶態(tài)的處理( losetup 或 mount )
- 打開(kāi)后端“文件”,拿到文件描述符。
- 打開(kāi) loop 設(shè)備文件,拿到 loop 設(shè)備的描述符。
- 調(diào)用 ioctl 把這兩個(gè)關(guān)聯(lián)起來(lái)ioctl(dev_fd, LOOP_CONFIGURE, &lc->config)
內(nèi)核的處理
- ioctl 的系統(tǒng)調(diào)用對(duì)應(yīng)調(diào)用 loop 中的 lo_ioctl 函數(shù)。
對(duì)應(yīng)了 block_device_operations 的 ioctl 方法。
- 當(dāng)設(shè)置參數(shù)為 LOOP_CONFIGURE 的時(shí)候,會(huì)調(diào)用 loop_configure 來(lái)分配和初始化 loop 設(shè)備。
- loop_configure
獲取到后端“文件”的句柄,也就是 struct file* 結(jié)構(gòu)。獲取到之后,會(huì)做一些校驗(yàn)工作。然后初始化 loop 設(shè)備相關(guān)的結(jié)構(gòu)體,隊(duì)列等。
最關(guān)鍵的當(dāng)然還是把 loop 設(shè)備和后端“文件”的句柄關(guān)聯(lián)起來(lái):lo->lo_backing_file = file; 這樣的話,等到讀寫(xiě) loop 設(shè)備的時(shí)候,就可以把請(qǐng)求轉(zhuǎn)發(fā)過(guò)去。
loop 設(shè)備的請(qǐng)求來(lái)自于哪里?
loop 設(shè)備它對(duì)外就是一個(gè)塊設(shè)備,如果在這個(gè)之上創(chuàng)建了文件系統(tǒng),并被當(dāng)作塊設(shè)備掛載到目錄上之后,那么它的請(qǐng)求來(lái)自于它之上的文件系統(tǒng)。
文件系統(tǒng)調(diào)用 submit_bio ,把請(qǐng)求投遞到塊層的隊(duì)列中。每一個(gè)塊層的設(shè)備它都需要實(shí)現(xiàn)一個(gè)入隊(duì)的處理,以供 submit_bio 的流程中調(diào)用。
static const struct blk_mq_ops loop_mq_ops = {
.queue_rq = loop_queue_rq,
.complete = lo_complete_rq,
};
loop 設(shè)備實(shí)現(xiàn)的入隊(duì)方法就是 loop_queue_rq 。文件系統(tǒng)調(diào)用 submit_bio 之后最終就會(huì)調(diào)用到 loop_queue_rq 這個(gè)函數(shù)。
> loop_queue_rq
> loop_queue_work
loop_queue_work 函數(shù)會(huì)把請(qǐng)求放到一個(gè) lo->workqueue 隊(duì)列中,每一個(gè) loop 設(shè)備都對(duì)應(yīng)有這么一個(gè)隊(duì)列。在創(chuàng)建設(shè)備文件的時(shí)候會(huì)同步生成。
loop 設(shè)備如何把請(qǐng)求傳遞給后端文件?
loop 設(shè)備還有一個(gè)名為 loop_workfn 的函數(shù),是專(zhuān)門(mén)用來(lái)處理投遞到該設(shè)備的請(qǐng)求。
> loop_workfn
> loop_handle_cmd
> do_req_filebacked
在 do_req_filebacked 函數(shù)中,會(huì)按照不同的命令類(lèi)型來(lái)處理請(qǐng)求,比如,寫(xiě)請(qǐng)求會(huì)調(diào)用 lo_write_simple ,讀請(qǐng)求會(huì)調(diào)用 lo_read_simple 等。
在這個(gè) lo_write_simple 函數(shù)中,就會(huì)調(diào)用 lo_write_bvec ,把寫(xiě)請(qǐng)求寫(xiě)入關(guān)聯(lián)的“文件”中。
static int lo_write_simple(struct loop_device *lo, struct request *rq,
loff_t pos)
{
// lo_backing_file 代表當(dāng)前 loop 設(shè)備關(guān)聯(lián)的文件
ret = lo_write_bvec(lo->lo_backing_file, &bvec, &pos);
}
在 lo_write_bvec 中,調(diào)用的是 vfs_iter_write 函數(shù)來(lái)進(jìn)行寫(xiě)入。這個(gè)函數(shù)其實(shí)是 VFS 層的一個(gè)封裝函數(shù),所以相當(dāng)于就是從頂層調(diào)用后端文件的寫(xiě)操作。
在 lo_write_bvec 中,最關(guān)鍵的是 lo->lo_backing_file 這個(gè)文件句柄的獲取。它的類(lèi)型是 struct file* ,代表了一個(gè)內(nèi)核打開(kāi)的文件。即該 loop 設(shè)備關(guān)聯(lián)的文件。它的賦值就是在 loop 設(shè)備創(chuàng)建的時(shí)候。
Linux 套娃之后,I/O 鏈路是什么樣的?
現(xiàn)在來(lái)看下,當(dāng)我們使用 loop 設(shè)備,來(lái)掛載一個(gè) minix 文件系統(tǒng),它的 I/O 路徑又會(huì)是怎么樣的呢?
假定創(chuàng)建的 minix_test.img 位于一個(gè) ext4 文件系統(tǒng)。
應(yīng)用程序 -> 系統(tǒng)調(diào)用 -> vfs -> minix 文件系統(tǒng) -> 塊層 -> loop 設(shè)備 -> 綁定的文件 -> vfs -> ext4 -> ...
示意圖如下:
圖片
loop 設(shè)備的典型應(yīng)用有哪些?
其實(shí),loop 設(shè)備在很多場(chǎng)景下我們都用過(guò)。以下是比較典型的例子:
- 系統(tǒng)模擬和測(cè)試:可以使用 loop 設(shè)備來(lái)模擬不同的存儲(chǔ)配置,無(wú)需使用物理硬件,就可以進(jìn)行軟件測(cè)試或系統(tǒng)配置實(shí)驗(yàn)。
- 文件系統(tǒng)開(kāi)發(fā):開(kāi)發(fā)者可以使用 loop 設(shè)備來(lái)掛載文件系統(tǒng),從而方便地測(cè)試和調(diào)試新的文件系統(tǒng)。
- ISO 映像掛載:Loop 設(shè)備還常用于掛載 ISO 文件,無(wú)需刻錄到物理介質(zhì)上,使其內(nèi)容可直接訪問(wèn)。
- 加密磁盤(pán):loop 設(shè)備還能和一些加密技術(shù)(如dm-crypt)結(jié)合,因?yàn)?loop 設(shè)備可以綁定幾乎任意類(lèi)型的文件,這就給了人們無(wú)限的想象空間。我們可以創(chuàng)建一個(gè)加密的磁盤(pán)鏡像,增強(qiáng)數(shù)據(jù)安全。
其實(shí),Linux 的套娃還遠(yuǎn)不止這些。舉個(gè)例子,loop 設(shè)備綁定的文件,它可能也是抽象出來(lái)的一個(gè)文件,比如是一個(gè)網(wǎng)絡(luò)文件系統(tǒng)抽象出來(lái)的文件。那這條 I/O 鏈路就更長(zhǎng)了。只要你敢想,在 Linux 中,存在無(wú)限套娃的可能。
總結(jié)
- 沒(méi)有磁盤(pán)也想玩磁盤(pán)類(lèi)文件系統(tǒng)?可以,用 loop 設(shè)備。只要你有文件,Linux 的 loop 設(shè)備都可以把文件變成一個(gè)“塊設(shè)備”。
- loop 設(shè)備就是一種特殊的塊設(shè)備驅(qū)動(dòng)。
- 文件之上的的文件系統(tǒng)的增、刪、改、查的 I/O 請(qǐng)求,它都將落到文件上。這個(gè)可以讓我們近距離的觀察到文件系統(tǒng)是如何管理磁盤(pán)的。
- hexdump 是個(gè)好工具,可以用來(lái)查看文件上的二進(jìn)制內(nèi)容。