为容器定制独立的 rootfs
一、问题引出:仅隔离 /proc 远远不够
上一节通过 CLONE_NEWNS 实现了 /proc 挂载点的隔离。但仔细想一想,容器里面不仅能看到 /proc,还能看到 /etc、/dev、/bin、/mnt 等目录。如果仅隔离了 /proc 一个目录,所有其他目录都是共享宿主机的。
这意味着在容器里面对这些共享目录进行改动,改的直接就是宿主机的。验证一下:
容器内:
[root@container container_test]# echo 111 > /mnt/1.txt
宿主机上:
[root@web01 ~]# cat /mnt/1.txt
111
直接改的就是宿主机!如果宿主机上还有其他容器,其他容器也没有隔离这些目录,那大家互相都能看到、互相影响 —— 这就不叫隔离了。
结论:必须为容器定制一整套它自己专门的目录结构,即一整套独立的文件系统。
二、容器只需要 rootfs,不需要 bootfs
回顾一下:一个完整的操作系统分为 bootfs + rootfs 两部分。
- bootfs:内核及内核相关的启动文件
- rootfs:根目录下的那一整套目录结构(
/bin、/home、/dev、/etc等)
对于容器来说,只需要一套自己的 rootfs 即可,不需要 bootfs。原因是:容器本质就是名称空间里跑了很多进程,这些进程打交道的对象是 VFS(虚拟文件系统),即根目录下某个目录下的某个文件,根本不需要直接跟内核打交道。只要为容器定制好一个专属的 rootfs,容器里所有进程都以为自己有一个独立的操作系统 —— 其实只是被"骗"了,容器里的操作系统顶多算半个(只有 rootfs,没有 bootfs)。
重要结论:同一台宿主机上运行的所有容器,共享宿主机操作系统的内核。一旦宿主机内核有安全漏洞,所有容器都会受影响。 这跟虚拟机不同 —— 虚拟机有完整的 bootfs + rootfs。
三、实现 chroot —— 切换容器的根目录
3.1 思路
在宿主机上创建一个 rootfs 目录,往里面放一整套 Linux 目录结构。容器启动后,切换到这个目录并以它作为根路径。容器看到的 / 其实是宿主机上的 rootfs 目录,所有改动都发生在 rootfs 下面,跟宿主机的真正根目录隔离开了。
如果有多个容器,就用不同的目录(如 rootfs_111、rootfs_222),各自的改动互不影响。
3.2 代码修改:添加 chdir + chroot
在 container_main 中 execv 之前加入:
/* chroot 隔离目录 */
if ( chdir("./rootfs") != 0 || chroot("./") != 0 ){
perror("chdir/chroot");
}
chdir("./rootfs"):切换到 rootfs 目录(使用相对路径,相对于容器引擎所在目录)chroot("./"):以当前目录作为容器文件系统的根
3.3 第一次尝试 —— rootfs 为空导致报错
创建空的 rootfs 目录后直接运行:
[root@test04 container_test]# mkdir rootfs
[root@test04 container_test]# gcc -o test6 test.c
[root@test04 container_test]# ./test6
Parent - start a container! [ 2124]
Container - inside the container! [ 1]
Something's wrong!
Parent - container stopped!
报错了!因为 chroot 后,容器的根变成了 rootfs,执行 /bin/bash 时去 rootfs/bin/ 下找 bash —— 根本没有。添加 perror("exec1111111111") 后可以看到明确错误:
[root@test04 container_test]# ./test6
Parent - start a container! [ 2149]
Container - inside the container! [ 1]
exec1111111111: No such file or directory
Something's wrong!
Parent - container stopped!
四、手动填充 rootfs
正常情况下 rootfs 的内容来源于容器镜像(镜像里打包好了完整的 rootfs)。但现在没有镜像,所以手动从宿主机拷贝。
4.1 创建目录结构
[root@test04 container_test]# for i in `ls /`;do mkdir rootfs/$i; done
[root@test04 container_test]# ls rootfs/
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
4.2 拷贝关键文件
[root@test04 container_test]# cp -r /bin/* rootfs/bin/
[root@test04 container_test]# mkdir rootfs/usr/bin
[root@test04 container_test]# mkdir rootfs/usr/libexec
[root@test04 container_test]# cp -r /usr/bin/* rootfs/usr/bin/
[root@test04 container_test]# cp -r /lib/* rootfs/lib/
[root@test04 container_test]# cp -r /lib64/* rootfs/lib64/
[root@test04 container_test]# cp -r /usr/libexec/* rootfs/usr/libexec/
[root@test04 container_test]# cp -r /etc/* rootfs/etc/
4.3 验证 chroot 效果
[root@test04 container_test]# gcc -o test6 test.c
[root@test04 container_test]# ./test6
Parent - start a container! [ 2312]
Container - inside the container! [ 1]
bash-4.2# ls
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
bash-4.2# pwd
/
容器内看到的 / 其实就是宿主机上的 rootfs 目录。验证写入隔离:
容器内:
bash-4.2# echo 6666 > /opt/111.txt
bash-4.2# exit
宿主机上:
[root@test04 ~]# cat container_test/rootfs/opt/111.txt
6666
容器内写入 /opt/111.txt,实际写到了 container_test/rootfs/opt/111.txt,而非宿主机的 /opt/111.txt —— 目录隔离成功。
五、完善容器:挂载虚拟文件系统与配置文件
光有 rootfs 的实际文件还不够,一个可用的操作系统还需要虚拟文件系统(proc、sysfs、devtmpfs 等)。并且一些经常变动的配置文件(hosts、hostname、resolv.conf)应该单独挂载,方便管理。
5.1 准备 conf 目录(配置文件源)
这个 conf 目录独立于 rootfs,在外面作为源头,挂载到容器内:
[root@test04 container_test]# mkdir conf
[root@test04 container_test]# echo '1.1.1.1 www.xxx.com' >> conf/hosts
[root@test04 container_test]# echo 'egonxxx' > conf/hostname
[root@test04 container_test]# echo 'nameserver 8.8.8.8' > conf/resolv.conf
5.2 准备存储卷目录(模拟 docker run -v)
[root@test04 container_test]# mkdir /tmp/t1
[root@test04 container_test]# echo 999999999 > /tmp/t1/1.txt
5.3 最终完整代码
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>
#include <sys/mount.h>
/* 定义一个给 clone 用的栈,栈大小1M */
#define STACK_SIZE (1024 * 1024)
static char container_stack[STACK_SIZE];
char* const container_args[] = {
"/bin/bash",
NULL
};
int container_main(void* arg)
{
printf("Container - inside the container! [%5d]\n", getpid());
sethostname("container",10);
/* 挂载虚拟文件系统 */
if (mount("proc", "rootfs/proc", "proc", 0, NULL) !=0 ) { perror("proc"); }
if (mount("sysfs", "rootfs/sys", "sysfs", 0, NULL)!=0) { perror("sys"); }
if (mount("none", "rootfs/tmp", "tmpfs", 0, NULL)!=0) { perror("tmp"); }
if (mount("udev", "rootfs/dev", "devtmpfs", 0, NULL)!=0) { perror("dev"); }
if (mount("devpts", "rootfs/dev/pts", "devpts", 0, NULL)!=0) { perror("dev/pts"); }
if (mount("shm", "rootfs/dev/shm", "tmpfs", 0, NULL)!=0) { perror("dev/shm"); }
if (mount("tmpfs", "rootfs/run", "tmpfs", 0, NULL)!=0) { perror("run"); }
/* 挂载关键配置文件 */
if (mount("conf/hosts", "rootfs/etc/hosts", "none", MS_BIND, NULL)!=0 ||
mount("conf/hostname", "rootfs/etc/hostname", "none", MS_BIND, NULL)!=0 ||
mount("conf/resolv.conf", "rootfs/etc/resolv.conf", "none", MS_BIND, NULL)!=0 ) {
perror("conf");
}
/* 模仿docker run命令中的 -v, --volume=[] 参数干的事 */
if (mount("/tmp/t1", "rootfs/mnt", "none", MS_BIND, NULL)!=0) { perror("mnt"); }
/* chroot 隔离目录 */
if ( chdir("./rootfs") != 0 || chroot("./") != 0 ){ perror("chdir/chroot"); }
execv(container_args[0], container_args);
perror("exec1111111111");
printf("Something's wrong!\n");
return 1;
}
int main()
{
printf("Parent - start a container! [%5d]\n", getpid());
int container_pid = clone(container_main, container_stack+STACK_SIZE,
CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWIPC | CLONE_NEWUTS | SIGCHLD, NULL);
waitpid(container_pid, NULL, 0);
printf("Parent - container stopped!\n");
return 0;
}
5.4 编译运行
[root@test04 container_test]# gcc -o test6 test.c
[root@test04 container_test]# ./test6
Parent - start a container! [ 2408]
Container - inside the container! [ 1]
注意:如果报错,可能是之前实验残留了挂载信息,需要先清理:
mount | grep container_test for i in `mount | grep container_test | awk '{print $3}'`;do umount -l $i;done mount | grep container_test # 确认清理干净后再执行 ./test6
5.5 验证所有隔离效果
虚拟文件系统与挂载点:
bash-4.2# df
Filesystem 1K-blocks Used Available Use% Mounted on
none 1013948 0 1013948 0% /tmp
udev 1003228 0 1003228 0% /dev
shm 1013948 0 1013948 0% /dev/shm
tmpfs 1013948 0 1013948 0% /run
/dev/sda3 19936256 3938812 15997444 20% /mnt
bash-4.2# mount
proc on /proc type proc (rw,relatime)
sysfs on /sys type sysfs (rw,relatime)
none on /tmp type tmpfs (rw,relatime)
udev on /dev type devtmpfs (rw,relatime,size=1003228k,nr_inodes=250807,mode=755)
devpts on /dev/pts type devpts (rw,relatime,mode=600,ptmxmode=000)
shm on /dev/shm type tmpfs (rw,relatime)
tmpfs on /run type tmpfs (rw,relatime)
/dev/sda3 on /etc/hosts type xfs (rw,relatime,attr2,inode64,noquota)
/dev/sda3 on /etc/hostname type xfs (rw,relatime,attr2,inode64,noquota)
/dev/sda3 on /etc/resolv.conf type xfs (rw,relatime,attr2,inode64,noquota)
/dev/sda3 on /mnt type xfs (rw,relatime,attr2,inode64,noquota)
容器内看到的挂载信息完全是自己的,跟宿主机隔离。
其他隔离验证:
bash-4.2# hostname
container
bash-4.2# ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
bash-4.2# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 11824 1756 pts/1 S 09:23 0:00 /bin/bash
root 6 0.0 0.0 51732 1704 pts/1 R+ 09:27 0:00 ps aux
配置文件验证(conf 目录挂载):
bash-4.2# cat /etc/hosts
1.1.1.1 www.xxx.com
bash-4.2# cat /etc/hostname
egonxxx
bash-4.2# cat /etc/resolv.conf
nameserver 8.8.8.8
都是从外面 conf/ 目录挂载进来的内容。
六、整体总结
至此,我们自己实现了一个相对完整的容器引擎 Demo。总结容器启动时做的几大块事情:
6.1 创建容器(clone 的两件事)
| 事情 | 说明 |
|---|---|
| 创建子进程 | 容器里的 1 号进程(第一个进程) |
| 初始化名称空间 | 通过系统调用参数,在子进程外围建一个"篱笆",规定哪些资源隔离 |
6.2 容器内的四大挂载块
| 挂载类别 | 说明 | 对应 Docker |
|---|---|---|
| 虚拟文件系统 | proc、sysfs、tmpfs、devtmpfs、devpts 等,容器自己独立一套 | Docker 自动处理 |
| 关键配置文件 | hosts、hostname、resolv.conf 单独从外部挂载,方便变动 | Docker 也是单独挂载这三个文件 |
| rootfs(独立目录结构) | 容器自己的一整套文件系统,通过 chdir + chroot 切换 | 来源于容器镜像 |
| 存储卷 | 宿主机目录关联到容器内,容器读写直接写到宿主机 | docker run -v 参数 |
6.3 关于容器镜像的预告
后面真正使用时,rootfs 不会手动创建和填充。 现成的做好的镜像,本质就是一个打包好的 rootfs(各种目录及文件)。通过镜像启动容器,就是把镜像作为源,将里面的目录和文件挂载到容器中 —— 跟我们手动拷贝宿主机文件到 rootfs 是一个道理,只不过来源换成了镜像。
关于 Namespace 的内容到此全部介绍完毕。

浙公网安备 33010602011771号