上一篇文章介绍了UTS Namespace,容器里面每个容器都可以单独挂载存储,这就需要挂载隔离。Mount Namespace是Linux最早支持的命名空间,支持在不同的Namespace中可以看到不同的挂载视图。
我们可以通过unshare分离出一个新的挂载空间,然后在这个新的mount空间内将/usr/bin目录挂载到/mnt目录下面,当exit退回后,mount失效了。如下所示。
- # unshare --mount
- # mount --bind /usr/bin/ /mnt/
- # ls /mnt/cp
- /mnt/cp
- # exit
- # ls /mnt/cp
- ls: 无法访问'/mnt/cp': 没有那个文件或目录
其实,unshare --mount的本质就是clone的时候指定flag为CLONE_NEWNS,后续Demo原理类似。
内核原理
要理解Mount Namespace必须先了解mount原理,mount可以将一个文件系统挂载到另外一个文件系统下面。下图是将一个文件系统挂载到根文件系统的”/home“路径下面,mount的关系通过一个内核结构体 mount标识,mount结构体中核心的就是mnt_mountpoint指向挂载的目标路径,在图中就是根文件系统的/home路径。
上图中home有两个dentry,一个是根文件系统下的dentry,它是目标挂载点,另外一个dentry是被挂载文件系统的dentry。譬如将一个挂载盘格式EXT4文件系统,那么这个dentry就挂载盘的根文件系统dentry。通过mount struct就可以将外部文件系统挂载到根文件系统上面。
每个mount struct里面都有一个mnt_namespace,它是一个指向挂载命令空间的指针,之前命名空间概述文章中介绍的ns_proxy也是指向这个mnt_namespace。这样进程和它的挂载空间就可以关联起来了。
Mount Namespace还有一个特殊的地方就是挂载传播(mount propagation),挂载传播是指由一个挂载对象的状态变化导致的其它挂载对象的挂载与解除挂载动作的事件。针对容器这个场景就是,如果Docker内执行了一次挂载后,宿主机上是否能看到这个挂载,反过来,如果宿主机执行了某个挂载,容器内是否能看见。常见的有三种挂载方式:
共享关系(share relationship),如果两个挂载对象具有共享关系,那么一个挂载对象中的挂载事件会传播到另一个挂载对象,反之亦然。也就是说容器和宿主机的挂载会互相可见。
从属关系(slave relationship),如果两个挂载对象形成从属关系,那么一个挂载对象中的挂载事件会传播到另一个挂载对象,但是反过来不行;在这种关系中,从属对象是事件的接收者。也就是说主机挂载容器可见,反过来不行。
私有关系(private relationship),之间互相不传播,相互独立。也就是说相互不可见。
实战DEMO
在上面的程序中添加CLONE_NEWNS便可以创建一个新的Mount Namespace。代码还是基于之前PID的代码,加上CLONE_NEWNS。
- package main
- import (
- "fmt"
- "os"
- "os/exec"
- "syscall"
- )
- func main() {
- cmd := exec.Command("/bin/sh")
-
- cmd.Stdin = os.Stdin
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
-
- cmd.Env = []string{"PS1=-[ns-process]- # "}
-
- cmd.SysProcAttr = &syscall.SysProcAttr{
- Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,
- }
-
- if err := cmd.Run(); err != nil {
- fmt.Printf("Error running the /bin/sh command - %s\n", err)
- os.Exit(1)
- }
- }
通过go run 直接运行后,执行文章最开始unshare的Demo。你会发现
因为默认情况下挂载传播是share共享,所以当我们在新的Mount Namespace执行挂载后,也会传播到主机上面。我们可以在主机上面先将根目录调整到私有挂载,这样容器内的挂载就不会传播到主机上面了。
- # mount --make-rprivate /
上面的Demo也可以正常运行了。