首先给出结论:mount NS的确隔离mount信息,在不同的mount NS中,系统拥有自己独立的VFS目录树及挂载点信息,但是由于shared subtree机制,使用clone系统调用和CLONE_NEWNS参数,可能会得到和你预期不同的结果
在很多docker的入门书籍中,都会讲到namespace的内容,在介绍mount ns时,往往会通过在新创建的mount ns中挂载proc在显示效果:
一个简单的go语言示例如上图所示
主机NS(init 进程所在的NS)中/proc的内容如图所示:
可以看到有很多的进程,我们在子进程中执行mount操作:
mount -t proc proc /proc
ls /proc
会发现,只有两个进程了分别是1号进程,也就是我们fork出来的子进程,以及11号进程,这是我们进行ls操作产生的进程
这是很多资料就告诉我们,啊,你看mount ns隔离起作用了。
屁!
这时我们回到主机系统,会发现很多命令都无法使用了:
这其实就是因为主机的proc被修改了,需要在主机重新mount一次才能恢复正常,这也就说明了不同的mount NS之间挂载proc会彼此影响。
进一步验证,我们在子进程中执行--bind操作:
mount --bind . .
mount |grep ext4
会发现mount打印的信息中出现了第二条挂载点,我们在主机系统中也会看到相同的挂载点信息,查看进程所在mount ns(需要确保/proc被正确挂载):
ls -al /proc/self/ns/mnt
可以发现主机系统和子进程的确在不同的NS中,那是因为ns mount无法隔离吗?显然不是的,我们使用unshare 执行相同的操作:
unshare -m -p -f
mount proc proc /proc
mount --bind . .
查看子进程:
查看主机系统:
会发现,果真是隔离状态的,为什么clone系统调用不行呢?是因为shared subtree机制。
shared subtree
引入该机制是为了消除mount ns带来的不便,比如系统新增一块磁盘,我希望所有的NS都感知到新挂载的这块磁盘,那么如果NS 之间是完全隔离的,就需要每个都执行一次挂载操作,这是非常不变的,shared subtree保证了不同的NS 之间可以共享挂载信息
核心机制:
- peer group
表示了一个或多个挂载点的集合,下面两种情况属于统一group:
- 通过--bind操作挂载的源挂载点和目标挂载点(前提是源目录是个挂载点)
- 生成新mount ns时,复制过去的挂载点之间同在一个group
- propagate type(传播属性)
这是mount点的属性,其常见值有:
- MS_SHARED 该挂载点的删除操作、该挂载点下子挂载点的新增和删除操作都会同步到同一group中的其他mount点,且其他同group的mount点的操作也会同步到该mount点
- MS_PRIVATE 与1相反,不会将自己的信息共享出去,也不接受其他点的共享,从而实现真正的隔离
- MS_SLAVE 单向的共享,自己的更新不会影响到他人,但是他人的操作会同步到自己
我们通过命令:
cat /proc/self/mountinfo
可以看到,系统默认的挂载点都是shared的,clone系统调用会完全copy父进程的挂载点信息,因此子进程的挂载点也是shared,这就导致了我们前面提到的问题。
因此为了实现完全的隔离,我们可以在子进程中执行操作:
mount --make-rprivate /
--make-rprivate表示递归修改整个mount树的propagate type为private,这相当于给mount系统调用传递参数:MS_PRIVATE和MS_REC,如果你通过编程语言实现,那么这两个参数是必需的。
到这里我想说的已经说完了,留一个简单的思考题,以证明你看懂了,如果我不设置MS_REC参数,或者说,仅仅执行:
mount --make-private /
那么在子进程挂载proc会传播到到系统NS中吗?(会)
在子进程执行--bind会传播到到系统NS中吗?(不会)
如果你不知道为什么,那就去仔细的查一下propagate type参数的含义吧