笔者在《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= of=./disk1.img
$ dd if=/dev/zero bs=1M count= of=./disk2.img
$ dd if=/dev/zero bs=1M count= of=./disk3.img
$ dd if=/dev/zero bs=1M count= 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