1. 程式人生 > 其它 >Linux之28——mount命令進階

Linux之28——mount命令進階

筆者在《Linux mount 命令》一文中介紹了 mount 命令的基本用法,本文我們接著介紹 mount 命令的一些高階用法,比如 bind mounts(繫結掛載)和 shared subtree。

bind mounts

一個繫結掛載就是相關目錄樹的另外一個檢視。典型情況下,掛載會為儲存裝置建立樹狀的檢視。而繫結掛載則是把一個現有的目錄樹複製到另外一個掛載點下。通過繫結掛載得到的目錄和檔案與原始的目錄和檔案是一樣的,無論從掛載目錄還是原始目錄執行的變更操作都會立即反映在另外一端。
簡單的說就是可以將任何一個掛載點、普通目錄或者檔案掛載到其它的地方
繫結掛載是一項非常有用的技術,它可以實現跨檔案系統的資料共享,這是容器技術實現自身檔案系統的基礎。

基本功能
先來演示一個繫結掛載的基本功能,即將源目錄繫結到目標目錄,然後在目標目錄下就可以看到源目錄裡的檔案樹:

$ mkdir -p bind/bind1/sub1
$ mkdir -p bind/bind2/sub2
$ sudo mount --bind ./bind/bind1 ./bind/bind2

繫結掛載後 bind/bind2 目錄下的內容和 bind/bind1 目錄下是一樣的。再看看詳細的掛載資訊:

./bind/bind2 目錄掛載的是整個 /dev/mapper/ubuntu--vg-root 檔案系統!

繫結掛載的一個非常有用的 case 是解決當前磁碟空間不足的問題。比如由於日誌檔案的不斷增長,當前磁碟容量不夠了,那就新新增一塊磁碟,然後通過繫結掛載把日誌目錄移到新的磁碟上:

$ sudo mv /var/log /opt/var_log
$ sudo mkdir /var/log
$ sudo mount --bind /opt/var_log /var/log

這裡 /opt 目錄掛載了一個非常大的檔案系統(新新增的磁碟)用來儲存日誌檔案,執行上面的命令後,日誌檔案就儲存在新的磁碟上了,簡單吧。

只讀的繫結掛載
我們可以在繫結掛載的時候指定 readonly 屬性,這樣原來的目錄還是能讀寫,但目標目錄為只讀:

$ sudo mount -o bind,ro ./bind/bind1 ./bind/bind2
$ touch ./bind/bind2/sub1/abc

以繫結掛載自己的方式把目錄變為只讀


還可以把目錄以只讀方式繫結掛載到自己,這樣目錄就變成只讀的了。umount 後,目錄回到可讀寫的狀態。

繫結掛載單個檔案
我們也可以繫結掛載單個檔案,這個功能尤其適合需要在不同版本的配置檔案之間切換的時候。
先建立兩個用於測試的檔案:

$ echo aaa > bind/aa
$ echo bbb > bind/bb

繫結掛載後,檔案 bb 裡面看到的是 aa 的內容:

$ sudo mount --bind bind/aa bind/bb
$ cat bind/bb

即使我們刪除 aa 檔案,我們還是能夠通過 bb 看到 aa 裡面的內容:

$ rm bind/aa
$ cat bind/bb

在 umount 檔案 bb 後,bb 檔案的內容出現了,不過 aa 檔案的內容再也找不到了:

$ sudo umount bind/bb
$ cat bind/bb

submounts
需要注意的是,繫結掛載並不會處理子掛載點。比如我們在 /mydisk 目錄下掛載了一個單獨的檔案系統,當我們繫結掛載整個根目錄時,新的目錄下的 mydisk 目錄並沒有掛載原來的檔案系統:

$ sudo mount --bind / /home/nick/test

此時檢視 /home/nick/test/mydisk 目錄,下面沒有原來檔案系統中的內容。
解決 submounts 問題的辦法是使用 --rbind 選項代替 --bind 選項。--rbind 選項會告訴系統核心:找到所有的子掛載點並把它們掛載到新的目錄下。

移動掛載點

顧名思義,就是把舊的掛載點移動到新的目錄去。

$ sudo mount --move olddir newdir

如果掛載點的父掛載點為 shared 型別,就不能移動該掛載點。我們可以通過 findmnt 命令檢視掛載點的型別:

預設情況下的掛載點都是 shared 型別,我們需要把父掛載點改為 private 型別才能支援移動操作:

$ sudo mount --make-private /
$ findmnt -o TARGET,PROPAGATION /

這樣掛載點就從 mydisk 移動到了 mydisk2。移動掛載點的操作並不區分繫結掛載點和非繫結掛載點。

Shared subtree

在某些情況下,比如系統添加了一個新的硬碟,這個時候如果 mount namespace 是完全隔離的,想要在各個 namespace 裡面用這個硬碟,就需要在每個 namespace 裡面手動 mount 這個硬碟,這個是很麻煩的,這時 Shared subtree 就可以幫助我們解決這個問題。
Shared subtree 就是一種控制子掛載點能否在其他地方被看到的技術,它只會在 bind mount 和 mount namespace 中用到。
我們在介紹移動掛載點時提到了掛載點的型別,其實叫 propagation type(傳播型別)。Propagation type 和 peer group 都是隨著 shared subtree 引入的概念。

peer group
peer group 就是一個或多個掛載點的集合,他們之間可以共享掛載資訊。在下面兩種情況下會使兩個掛載點屬於同一個 peer group (前提條件是掛載點的 propagation type 是 shared):

  • 利用 mount --bind 命令,將會使源和目標掛載點屬於同一個 peer group,當然前提條件是源必須要是一個掛載點。
  • 當建立新的 mount namespace 時,新 namespace 會拷貝一份老 namespace 的掛載點資訊,於是新的和老的namespace 裡面的相同掛載點就會屬於同一個 peer group。

propagation type
每個掛載點都有一個 propagation type 標誌,由它來決定當在一個掛載點的下面建立和移除掛載點的時候,是否會傳播到屬於相同 peer group 的其他掛載點下面,也即同一個 peer group 裡的其他的掛載點下面是不是也會建立和移除相應的掛載點。當前一共有 4 種不同型別的 propagation type:

  • shared:從名字就可以看出,掛載資訊會在同一個 peer group 的不同掛載點之間共享傳播。當一個掛載點下面新增或者刪除掛載點的時候,同一個 peer group 裡的其他掛載點下面也會掛載和解除安裝同樣的掛載點。
  • private:跟上面的剛好相反,掛載資訊根本就不共享,也即 private 的掛載點不會屬於任何 peer group。
  • slave:資訊的傳播是單向的,在同一個 peer group 裡面,master 的掛載點下面發生變化的時候,slave 的掛載點下面也跟著變化。但反之則不然,slave 下發生變化的時候不會通知 master,master 不會發生變化。
  • unbindable:這個和 private 相同,只是這種型別的掛載點不能作為 bind mount 的源,主要用來防止遞迴巢狀情況的出現。

注意:

  • propagation type 是掛載點的屬性,對每個掛載點來說都是獨立的。
  • 掛載點是有父子關係的,比如掛載點 / 和 /mnt,/mnt 是 / 的子掛載點,/ 是 /mnt 的父掛載點。
  • 預設情況下,如果父掛載點是 shared,那麼子掛載點也是 shared 的,否則子掛載點將會是 shared,跟爺爺掛載點沒有關係。

下面我們通過繫結掛載來演示 shared subtree。

第一步,準備 demo 資料
先準備備 4 個虛擬磁碟檔案,並在上面建立 ext2 檔案系統,用於後續的掛載測試:

$ mkdir disks && cd disks
$ dd if=/dev/zero bs=1M count=32 of=./disk1.img
$ dd if=/dev/zero bs=1M count=32 of=./disk2.img
$ dd if=/dev/zero bs=1M count=32 of=./disk3.img
$ dd if=/dev/zero bs=1M count=32 of=./disk4.img
$ mkfs.ext2 ./disk1.img
$ mkfs.ext2 ./disk2.img
$ mkfs.ext2 ./disk3.img
$ mkfs.ext2 ./disk4.img

然後準備兩個目錄用於掛載上面建立的磁碟:

$ mkdir disk1 disk2

最後確保根目錄的 propagation type 是 shared:

$ sudo mount --make-shared /

第二步,檢視 propagation type 和 peer group
預設情況下,子掛載點會繼承父掛載點的 propagation type。我們也可以顯式的指定掛載點的 propagation type:

# 以 shared 方式掛載 disk1
$ sudo mount --make-shared ./disk1.img ./disk1
# 以 private 方式掛載 disk2
$ sudo mount --make-private ./disk2.img ./disk2

/proc/[pid]/mountinfo 檔案比 mount 命令的輸出包含更多關於掛載點的資訊,接下來我們配合 cat、grep 和 sed 命令來檢視掛載點資訊。先來看看掛載點 disk1 和 disk2 的資訊:

$ cat /proc/self/mountinfo |grep disk | sed 's/ - .*//'

上圖中 shared:1 表示掛載點 /tmp/disks/disk1 是以 shared 方式掛載,且 peer group id 為 1。而掛載點 /tmp/disks/disk2 沒有相關資訊,表示是以 private 方式掛載。

下面分別在 disk1 和 disk2 目錄下建立目錄 disk3 和 disk4,然後掛載 disk3.img,disk4.img 到這兩個目錄:

$ sudo mkdir ./disk1/disk3 ./disk2/disk4
$ sudo mount ./disk3.img ./disk1/disk3
$ sudo mount ./disk4.img ./disk2/disk4

再次檢視掛載點資訊:

$ cat /proc/self/mountinfo |grep disk| sed 's/ - .*//'

上圖中第一列的數字表示掛載點 ID,第二列的數字表示父掛載點 ID。從輸出的結果來看,384 和 386 的掛載型別都是 shared,而 385 和 387 的掛載型別都是 private。這就說明在預設情況下,子掛載點會繼承父掛載點的 propagation type。

第三步,觀察 shared mount 和 private mount
先 umount 掉 disk3 和 disk4,並建立兩個新的目錄 bind1 和 bind2 用於繫結掛載測試:

$ sudo umount ./disk1/disk3
$ sudo umount ./disk2/disk4
$ mkdir bind1 bind2

現在以繫結掛載的方式掛載 disk1 到 bind1,disk2 到 bind2:

$ sudo mount --bind ./disk1 ./bind1
$ sudo mount --bind ./disk2 ./bind2

檢視此時的掛載點資訊:

$ cat /proc/self/mountinfo |grep disk| sed 's/ - .*//'

很顯然,預設情況下 bind1 和 bind2 的 propagation type 均繼承自父掛載點 28 (即根掛載點 /),且都是 shared。
由於 bind2 的源掛載點 disk2 是 private 的,所以 bind2 和 disk2 沒有在同一個 peer group 裡面,而是重新建立了一個新的 peer group 255,且這個 peer group 裡面只有 bind2 一個掛載點。因為掛載點 384(/tmp/disks/disk1) 和 386(/tmp/disks/bind1) 都是 shared 型別且是通過繫結掛載的方式關聯在一起的,所以他們屬於同一個 peer group 1

這時 disk3 和 disk4 目錄都是空的:

$ ls bind1/disk3/
$ ls bind2/disk4/
$ ls disk1/disk3/
$ ls disk2/disk4/

下面讓我們再次掛載 disk3.img 和 disk4.img:

$ sudo mount ./disk3.img ./disk1/disk3
$ sudo mount ./disk4.img ./disk2/disk4

由於掛載點 /tmp/disks/disk1 和掛載點 /tmp/disks/bind1 屬於同一個 peer group,所以在 disk3 目錄掛載了 disk3.img 後,在兩個目錄下都能看到 disk3 目錄下的內容:

而掛載點 /tmp/disks/disk2 是 private 型別的,所以在他下面掛載 disk4 不會通知 bind2,於是 bind2 下的 disk4 目錄是空的:

讓我們再來看看掛載點 /tmp/disks/disk1/disk3 和 /tmp/disks/bind1/disk3:

$ cat /proc/self/mountinfo |egrep "disk3"| sed 's/ - .*//'

雖然 388(/tmp/disks/disk1/disk3) 和 389(/tmp/disks/bind1/disk3) 的父掛載點不一樣,但由於他們父掛載點屬於同一個 peer group,且 /tmp/disks/disk1/disk3 是以預設方式掛載的,所以 388 和 389 也屬於同一個 peer group
注意:/tmp/disks/disk1/disk3 和 /tmp/disks/bind1/disk3 並不是同一個掛載點,而是通過傳播建立的具有相同屬性的不同的掛載點
如果 umount bind1/disk3,disk1/disk3 也相應的自動 umount 掉了:

$ sudo umount bind1/disk3
$ cat /proc/self/mountinfo |grep disk3

此時已經查不到 disk3 相關的 mount 記錄了。

第四步,觀察 slave mount
先 umount 掉除 disk1 外的所有其他掛載點:

$ sudo umount ./disk2/disk4
$ sudo umount ./bind1
$ sudo umount ./bind2
$ sudo umount ./disk2

通過下面的命令確認只剩下掛載點 /tmp/disks/disk1:

$ cat /proc/self/mountinfo |grep disk| sed 's/ - .*//'

然後分別顯式的用 shared 和 slave 的方式進行下面的繫結掛載:

$ sudo mount --bind --make-shared ./disk1 ./bind1
$ sudo mount --bind --make-slave ./bind1 ./bind2

觀察此時的掛載點資訊:

$ cat /proc/self/mountinfo |grep disk| sed 's/ - .*//'

掛載點 384、385 和 386 都屬於同一個 peer group,最後一行中的 master:1 表示掛載點 /tmp/disks/bind2 是 peer group 1 的 slave。

接下來把 disk3.img 掛載到 disk1 的子目錄 disk3 下:

$ sudo mount ./disk3.img ./disk1/disk3/

看看現在的掛載點資訊:

$ cat /proc/self/mountinfo |grep disk| sed 's/ - .*//'

除了 /tmp/disks/disk1/disk3 掛載點,/tmp/disks/bind1 和 /tmp/disks/bind2 下面也都新增了掛載點 disk3。這說明 master 的掛載事件會被傳播給 slave,所以 slave 也跟著變了。

最後讓我們 umount ./disk1/disk3/,再把 disk3.img 掛載到 bind2 的子目錄 disk3 下:

$ sudo umount ./disk1/disk3/
$ sudo mount ./disk3.img ./bind2/disk3/

再次觀察掛載點資訊:

$ cat /proc/self/mountinfo |grep disk| sed 's/ - .*//'

由於掛載點 /tmp/disks/bind2 的 propagation type 是 slave,所以 /tmp/disks/disk1 和 /tmp/disks/bind1 兩個掛載點下面不會掛載 disk3。從掛載點 387 的型別可以看出,當父掛載點 386 是 slave 型別時,預設情況下其子掛載點 387 是 private 型別。

總結

本文主要介紹了繫結掛載(bind mount) 和 shared subtree。而 shared subtree 在 mount namespace 中的表現和繫結掛載中的表現類似,筆者在《Linux Namespace : Mount》一文中有詳細的介紹。
實際使用中,如果遇到複雜的掛載操作,就需要考慮父掛載點是否和其他掛載點有 peer group 關係。如果有且父掛載點的型別是 shared,那麼你掛載的裝置除了在當前掛載點可見,在與父掛載點具有相同 peer group 的掛載點下面也是可見的。

參考:
Man page
Linux mount (第一部分)
Linux mount (第二部分 - Shared subtrees)
Shared subtrees