在容器中如何挂载rootfs
本篇文章将介绍如何在容器中更改文件根目录。
本篇文章使用的环境为:
[root@ubuntu]:[~][tty:0]# hostnamectl
Static hostname: ubuntu
Icon name: computer-vm
Chassis: vm 🖴
Product UUID: d7a79327-c7b6-4c40-a8d2-abf3244c3abc
Virtualization: oracle
Operating System: Ubuntu 24.10
Kernel: Linux 6.11.0-9-generic
Architecture: x86-64
Hardware Vendor: innotek GmbH
Hardware Model: VirtualBox
Hardware Serial: 0
Firmware Version: VirtualBox
Firmware Date: Fri 2006-12-01
Firmware Age: 17y 11month 1w 6d
[root@ubuntu]:[~][tty:0]#
debian 12 默认内核不支持
pivot_root
命令,在重新编译内核的时候失败了,把机器系统搞坏了,所以用ubuntu
演示。
此前已经介绍了Linux
中的相关namespace
,这里简单回顾一下。
使用unshare
可以将命令挪到相应的namespace
中执行,案例如下:
[root@ubuntu]:[~][tty:0]# unshare -m -p -u -n -i -f /usr/bin/bash
(unshare) [root@ubuntu]:[~][tty:0]#
(unshare) [root@ubuntu]:[~][tty:0]#
(unshare) [root@ubuntu]:[~][tty:0]# mount -t proc proc /proc
(unshare) [root@ubuntu]:[~][tty:0]#
(unshare) [root@ubuntu]:[~][tty:0]# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 7696 4460 pts/0 S 08:45 0:00 /usr/bin/bash
root 9 0.0 0.1 8428 4012 pts/0 R+ 08:45 0:00 ps aux
(unshare) [root@ubuntu]:[~][tty:0]#
(unshare) [root@ubuntu]:[~][tty:0]# df -h
Filesystem Size Used Avail Use% Mounted on
/dev/sda4 92G 6.6G 81G 8% /
tmpfs 1.7G 0 1.7G 0% /dev/shm
...
tmpfs 1.7G 0 1.7G 0% /tmp
/dev/sda2 4.9G 172M 4.5G 4% /boot
(unshare) [root@ubuntu]:[~][tty:0]# exit
exit
[root@ubuntu]:[~][tty:0]#
上面例子,虽然成功在相应的namespace
的运行进程了,但是在该容器中,文件系统还是使用的宿主机的,本篇文章就简单来讲一下,如何在容器中挂载rootfs
。
获取rootfs
什么是rootfs
也称为根文件系统,它是系统启动的时候加载的第一个文件系统,包含了系统的基本目录结构和所必要的文件系统。
利用docker获取rootfs
这里使用docker pull
拉取一个镜像,解压开来便是rootfs
了。
[root@ubuntu]:[~][tty:0]# docker pull busybox
Using default tag: latest
latest: Pulling from library/busybox
a46fbb00284b: Pull complete
Digest: sha256:768e5c6f5cb6db0794eec98dc7a967f40631746c32232b78a3105fb946f3ab83
Status: Downloaded newer image for busybox:latest
docker.io/library/busybox:latest
[root@ubuntu]:[~][tty:0]#
使用docker pull
拉取一个最简单的busybox
镜像。
使用docker export
可以将其导出。
[root@ubuntu]:[~][tty:0]# docker run -d busybox
7512c7ab5a8760ccf1080612fbb48533a08b23e5cfe32b6276f828355500a982
[root@ubuntu]:[~][tty:0]# docker export -o rootfs.tar 7512c7ab5a8760ccf1080612fbb48533a08b23e5cfe32b6276f828355500a982
[root@ubuntu]:[~][tty:0]#
解压开来,便是完整的rootfs
了。
[root@ubuntu]:[~][tty:0]# mkdir rootfs
[root@ubuntu]:[~][tty:0]# tar xf rootfs.tar -C rootfs
[root@ubuntu]:[~][tty:0]#
查看busybox
完整的rootfs
。
[root@ubuntu]:[~][tty:0]# cd rootfs
[root@ubuntu]:[~/rootfs][tty:0]# ls
bin dev etc home lib lib64 proc root sys tmp usr var
[root@ubuntu]:[~/rootfs][tty:0]#
更改根文件系统
使用pivot_root更改根文件系统
如上,已经获取了rootfs
根文件系统,接下来将使用pivot_root
进行更改根文件系统。
pivot_root
是一个系统调用,用于更改文件系统的根目录,用法如下:
pivot_root new_root put_old
其中new_root
是指新的根文件系统,put_old
则是指老的根文件系统。
命令执行后,会将new_root
目录挂载为新的根文件系统,而老的根文件系统则会挂载到put_old
上。
new_root
和put_old
都要是独立的文件系统才可以,若不是的话,需要使用mount
进行声明式挂载。
注意,这里一定要在mount namespace
中执行pivot_root
操作。
[root@ubuntu]:[~][tty:0]# cd
[root@ubuntu]:[~][tty:0]# mkdir -p rootfs/put_old
[root@ubuntu]:[~][tty:0]#
[root@ubuntu]:[~][tty:0]# unshare -m -u -i -p -n -f /bin/bash
(unshare) [root@ubuntu]:[~][tty:0]#
(unshare) [root@ubuntu]:[~][tty:0]# mount --bind /root/rootfs /root/rootfs
(unshare) [root@ubuntu]:[~][tty:0]# mount --bind /root/rootfs/put_old /root/rootfs/put_old
(unshare) [root@ubuntu]:[~][tty:0]#
(unshare) [root@ubuntu]:[~][tty:0]# pivot_root /root/rootfs /root/rootfs/put_old
(unshare) [root@ubuntu]:[~][tty:tty]#
(unshare) [root@ubuntu]:[~][tty:tty]# cd
(unshare) [root@ubuntu]:[~][tty:tty]# ls -l
total 0
(unshare) [root@ubuntu]:[~][tty:tty]# ls -l /
total 56
drwxr-xr-x 2 root root 12288 Sep 26 21:31 bin
drwxr-xr-x 4 root root 4096 Nov 13 09:04 dev
drwxr-xr-x 3 root root 4096 Nov 13 09:04 etc
drwxr-xr-x 2 nobody nobody 4096 Sep 26 21:31 home
drwxr-xr-x 2 root root 4096 Sep 26 21:31 lib
lrwxrwxrwx 1 root root 3 Sep 26 21:31 lib64 -> lib
drwxr-xr-x 2 root root 4096 Nov 13 09:04 proc
drwxr-xr-x 20 root root 4096 Nov 13 08:34 put_old
drwx------ 2 root root 4096 Nov 13 09:16 root
drwxr-xr-x 2 root root 4096 Nov 13 09:04 sys
drwxrwxrwt 2 root root 4096 Sep 26 21:31 tmp
drwxr-xr-x 4 root root 4096 Sep 26 21:31 usr
drwxr-xr-x 4 root root 4096 Sep 26 21:31 var
(unshare) [root@ubuntu]:[~][tty:tty]#
(unshare) [root@ubuntu]:[~][tty:tty]# /bin/mount -t proc proc /proc
(unshare) [root@ubuntu]:[~][tty:tty]#
(unshare) [root@ubuntu]:[~][tty:tty]# /bin/df -h
Filesystem Size Used Available Use% Mounted on
/dev/sda4 92.0G 6.9G 80.3G 8% /put_old
udev 1.6G 0 1.6G 0% /put_old/dev
tmpfs 1.7G 0 1.7G 0% /put_old/dev/shm
tmpfs 340.2M 1.1M 339.1M 0% /put_old/run
tmpfs 5.0M 0 5.0M 0% /put_old/run/lock
...
tmpfs 1.7G 0 1.7G 0% /put_old/tmp
/dev/sda2 4.8G 171.7M 4.4G 4% /put_old/boot
overlay 92.0G 6.9G 80.3G 8% /put_old/var/lib/docker/overlay2/cc5c084fbc8408885238df38f319ed6b4c30633214a40cb826b509ebaf8a37ea/merged
overlay 92.0G 6.9G 80.3G 8% /put_old/var/lib/docker/overlay2/bafdfaa3dbd7563bd81d30ec86c03b7749a08b144304eae7c5de60922482dc5c/merged
/dev/sda4 92.0G 6.9G 80.3G 8% /
/dev/sda4 92.0G 6.9G 80.3G 8% /put_old
(unshare) [root@ubuntu]:[~][tty:tty]#
如上,在rootfs
目录下创建了put_old
目录,用以存放老的根文件系统。
使用unshare
进入namespace
后,对/root/rootfs
和/root/rootfs/put_old
进行声明式挂载。
而后使用pivot_root
进行切换目录,重新挂载proc
虚拟文件系统,使用df -h
查看文件挂载点,不难看出,老的文件系统已经被挂载到/put_old
目录中了。
卸载老的根文件系统
虽然切换为了新的根文件系统,但是挂载点都还在,也能操作/put_old
中的目录和文件,这里介绍将其卸载掉。
只需要使用umount -l
卸载掉即可,如下:
(unshare) [root@ubuntu]:[~][tty:tty]# umount -l /put_old
(unshare) [root@ubuntu]:[~][tty:tty]# /bin/df -h
Filesystem Size Used Available Use% Mounted on
/dev/sda4 92.0G 6.9G 80.3G 8% /
/dev/sda4 92.0G 6.9G 80.3G 8% /put_old
(unshare) [root@ubuntu]:[~][tty:tty]#
(unshare) [root@ubuntu]:[~][tty:tty]# /bin/mount
/dev/sda4 on / type ext4 (rw,relatime)
/dev/sda4 on /put_old type ext4 (rw,relatime)
proc on /proc type proc (rw,relatime)
(unshare) [root@ubuntu]:[~][tty:tty]#
umount -l
是惰性卸载,惰性卸载是指将挂载点标记为待卸载状态,但是不会立即进行卸载,惰性卸载后,从表面来看,都会卸载干净,但是它会等文件系统不再被使用的时候,才会真正卸载掉。
映射挂载点
若想卸载掉老文件系统,又想映射部分目录到该namespace
中,可以在惰性卸载前,进行声明式挂载即可,例如:
[root@ubuntu]:[~][tty:0]# unshare -m -u -i -p -n -f /bin/bash
(unshare) [root@ubuntu]:[~][tty:0]# mount --bind /root/rootfs /root/rootfs
(unshare) [root@ubuntu]:[~][tty:0]# mount --bind /root/rootfs/put_old /root/rootfs/put_old
(unshare) [root@ubuntu]:[~][tty:0]# pivot_root /root/rootfs /root/rootfs/put_old
(unshare) [root@ubuntu]:[~][tty:tty]# /bin/mount -t proc proc /proc
(unshare) [root@ubuntu]:[~][tty:tty]#
(unshare) [root@ubuntu]:[~][tty:tty]# df -h
Filesystem Size Used Available Use% Mounted on
/dev/sda4 92.0G 6.9G 80.3G 8% /put_old
udev 1.6G 0 1.6G 0% /put_old/dev
tmpfs 1.7G 0 1.7G 0% /put_old/dev/shm
tmpfs 340.2M 1.1M 339.1M 0% /put_old/run
tmpfs 5.0M 0 5.0M 0% /put_old/run/lock
...
tmpfs 340.2M 12.0K 340.2M 0% /put_old/run/user/1000
tmpfs 1.7G 0 1.7G 0% /put_old/tmp
/dev/sda2 4.8G 171.7M 4.4G 4% /put_old/boot
overlay 92.0G 6.9G 80.3G 8% /put_old/var/lib/docker/overlay2/cc5c084fbc8408885238df38f319ed6b4c30633214a40cb826b509ebaf8a37ea/merged
overlay 92.0G 6.9G 80.3G 8% /put_old/var/lib/docker/overlay2/bafdfaa3dbd7563bd81d30ec86c03b7749a08b144304eae7c5de60922482dc5c/merged
/dev/sda4 92.0G 6.9G 80.3G 8% /
/dev/sda4 92.0G 6.9G 80.3G 8% /put_old
(unshare) [root@ubuntu]:[~][tty:tty]#
(unshare) [root@ubuntu]:[~][tty:tty]# mkdir /put_old/root/
.bash_history .bashrc .dockerenv .lesshst .profile rootfs/ rootfs.tar .ssh/ .viminfo
(unshare) [root@ubuntu]:[~][tty:tty]# mkdir /put_old/root/dir1
(unshare) [root@ubuntu]:[~][tty:tty]# mkdir ~/dir2
(unshare) [root@ubuntu]:[~][tty:tty]#
(unshare) [root@ubuntu]:[~][tty:tty]# /bin/mount --bind /put_old/root/dir1 /root/dir2
(unshare) [root@ubuntu]:[~][tty:tty]#
(unshare) [root@ubuntu]:[~][tty:tty]# umount -l /put_old
(unshare) [root@ubuntu]:[~][tty:tty]#
(unshare) [root@ubuntu]:[~][tty:tty]# df -h
Filesystem Size Used Available Use% Mounted on
/dev/sda4 92.0G 6.9G 80.3G 8% /
/dev/sda4 92.0G 6.9G 80.3G 8% /put_old
/dev/sda4 92.0G 6.9G 80.3G 8% /root/dir2
(unshare) [root@ubuntu]:[~][tty:tty]#
(unshare) [root@ubuntu]:[~][tty:tty]#
如上操作和上述操作一致,只不过在惰性挂载之前,创建了2个目录,/put_old/root/dir1
和~/dir2
,而后进行声明式挂载,最后惰性卸载掉/put_old
,就可以看到宿主机的挂载点了。
实际写入也是可以的,如下:
(unshare) [root@ubuntu]:[~][tty:tty]# echo 'hello' > /root/dir2/123.ttx
(unshare) [root@ubuntu]:[~][tty:tty]#
在namespace
中,创建了/root/dir2/123.ttx
,并且写入值hello
,而在宿主机中同样查看该文件。
[root@ubuntu]:[~][tty:1]# cat dir1/123.ttx
hello
[root@ubuntu]:[~][tty:1]#
使用overlay来重复利用rootfs
上述有一个问题,若开启了多个容器,是需要为每一个容器都拷贝一份rootfs
,实际上rootfs
大多数情况下不会存在太多的修改,如果是这样的话,则非常浪费存储空间,好在overlay
可以解决该问题。
overlay
是一个特殊的文件系统,它允许将一个文件系统的视图叠加到另一个文件系统上,可以简单的分为基础层和可写层,当使用该文件系统的时候,需要指定这2个层次,当文件有修改的时候,会从基础层拷贝一份,到可写层,修改可写层的数据,不会影响最底层的原始数据。
这样的话,多个容器可以使用同一rootfs
,只需要为每个容器都指定不同的可行层即可。
使用overlay
进行简单的挂载
overlay
有以下层次:
lower directory
:基础只读文件系统层upper directory
:可写层work directory
:内部工作目录
下面将演示使用overlay
进行简单的挂载。
准备工作:
[root@ubuntu]:[~][tty:1]# mkdir overlay
[root@ubuntu]:[~][tty:1]# cd overlay/
[root@ubuntu]:[~/overlay][tty:1]#
[root@ubuntu]:[~/overlay][tty:1]# mkdir lower_directory
[root@ubuntu]:[~/overlay][tty:1]# mkdir upper_directory
[root@ubuntu]:[~/overlay][tty:1]# mkdir work_directory
[root@ubuntu]:[~/overlay][tty:1]# mkdir work
[root@ubuntu]:[~/overlay][tty:1]#
[root@ubuntu]:[~/overlay][tty:1]# echo "123456" > lower_directory/text.txt
[root@ubuntu]:[~/overlay][tty:1]# echo "hello" > lower_directory/test
[root@ubuntu]:[~/overlay][tty:1]#
如上创建了3个目录,分别为lower_directory
、upper_directory
、work_directory
以及work
目录。
并且在lower_directory
目录下创建了2个文件:text.txt
和test
分别写入了内容。
使用overlay
进行挂载:
[root@ubuntu]:[~/overlay][tty:1]# mount -t overlay overlay -o lowerdir=lower_directory,upperdir=upper_directory,workdir=work_directory ./work
[root@ubuntu]:[~/overlay][tty:1]#
挂载overlay
指定了基础只读文件系统层目录、可写层目录以及内部工作目录,最后是将overlay
挂载到了./work
中。
查看挂载信息:
[root@ubuntu]:[~/overlay][tty:1]# df -h
Filesystem Size Used Avail Use% Mounted on
tmpfs 341M 1.2M 340M 1% /run
/dev/sda4 92G 7.0G 81G 8% /
tmpfs 1.7G 0 1.7G 0% /dev/shm
...
overlay 92G 7.0G 81G 8% /var/lib/docker/overlay2/cc5c084fbc8408885238df38f319ed6b4c30633214a40cb826b509ebaf8a37ea/merged
overlay 92G 7.0G 81G 8% /var/lib/docker/overlay2/bafdfaa3dbd7563bd81d30ec86c03b7749a08b144304eae7c5de60922482dc5c/merged
overlay 92G 7.0G 81G 8% /root/overlay/work
[root@ubuntu]:[~/overlay][tty:1]#
可以看到,文件类型是overlay
,已经挂载到/root/overlay/work
目录中了。
查看各个目录的文件:
[root@ubuntu]:[~/overlay][tty:1]# tree
.
├── lower_directory
│ ├── test
│ └── text.txt
├── upper_directory
├── work
│ ├── test
│ └── text.txt
└── work_directory
└── work
6 directories, 4 files
[root@ubuntu]:[~/overlay][tty:1]# md5sum lower_directory/* work/*
b1946ac92492d2347c6235b4d2611184 lower_directory/test
f447b20a7fcbf53a5d5be013ea0b15af lower_directory/text.txt
b1946ac92492d2347c6235b4d2611184 work/test
f447b20a7fcbf53a5d5be013ea0b15af work/text.txt
[root@ubuntu]:[~/overlay][tty:1]#
可以看到,其work
目录和lower_directory
目录中的文件都是一样的,其md5
也是一样的,说明overlay
已经将基础系统层的文件映射进来了。
此时若修改work
目录下的某个文件,再查看文件信息:
[root@ubuntu]:[~/overlay][tty:1]# echo "123" >> work/text.txt
[root@ubuntu]:[~/overlay][tty:1]#
[root@ubuntu]:[~/overlay][tty:1]# tree
.
├── lower_directory
│ ├── test
│ └── text.txt
├── upper_directory
│ └── text.txt
├── work
│ ├── test
│ └── text.txt
└── work_directory
└── work
6 directories, 5 files
[root@ubuntu]:[~/overlay][tty:1]#
可以看到,可写层也增加了一个text.txt
,具体的逻辑是当我们视图修改work/text.txt
内容是,overlay
会先将只读文件系统层的text.txt
拷贝到upper_directory
中,然后再修改其内容,当我们读取work
目录下内容的时候,当upper_directory
目录中有文件的时候,会读取该目录下内容,没有才会在lower_directory
进行尝试读取。
校验一下各个文件的md5
:
[root@ubuntu]:[~/overlay][tty:1]# md5sum lower_directory/text.txt work/text.txt upper_directory/text.txt
f447b20a7fcbf53a5d5be013ea0b15af lower_directory/text.txt
6104fbccc189d29b24969b514bb7de5a work/text.txt
6104fbccc189d29b24969b514bb7de5a upper_directory/text.txt
[root@ubuntu]:[~/overlay][tty:1]#
通过上述三个目录中的`text.txt
的md5
值可以判断,其修改的值被存入到了upper_directory
中。
使用overlay
挂载rootfs
想要每个容器都获取“独立”的rootfs
,那在创建namespace
之前,就需要先进行挂载,需要指定不同的lowerdir
、upperdir
以及workdir
。
每个容器启动之前,都需要进行overlay
的挂载。
在此之前,需要为容器创建相应的overlay
目录。
[root@ubuntu]:[~][tty:0]# mkdir {upper,work,busybox_container_01}
[root@ubuntu]:[~][tty:0]#
busybox_container_01
就是为即将启动的容器,创建的根目录,其他的,则和上节是一样的。
将rootfs
挂载到busybox_container_01
中。
[root@ubuntu]:[~][tty:0]# mount -t overlay overlay -o lowerdir=rootfs,upperdir=upper,workdir=work busybox_container_01
[root@ubuntu]:[~][tty:0]#
这里的lowerdir
需要修改为rootfs
。
使用unshare
创建namespace
。
[root@ubuntu]:[~][tty:0]# unshare -m -p -u -n -i -f /bin/bash
(unshare) [root@ubuntu]:[~][tty:0]# mkdir -p busybox_container_01/put_old
(unshare) [root@ubuntu]:[~][tty:0]#
(unshare) [root@ubuntu]:[~][tty:0]# mount --bind /root/busybox_container_01 /root/busybox_container_01
(unshare) [root@ubuntu]:[~][tty:0]# mount --bind /root/busybox_container_01/put_old /root/busybox_container_01/put_old
(unshare) [root@ubuntu]:[~][tty:0]#
(unshare) [root@ubuntu]:[~][tty:0]# pivot_root /root/busybox_container_01 /root/busybox_container_01/put_old
(unshare) [root@ubuntu]:[~][tty:tty]#
(unshare) [root@ubuntu]:[~][tty:tty]# cd
(unshare) [root@ubuntu]:[~][tty:tty]#
(unshare) [root@ubuntu]:[~][tty:tty]# /bin/mount -t proc proc /proc
(unshare) [root@ubuntu]:[~][tty:tty]#
(unshare) [root@ubuntu]:[~][tty:tty]# umount -l /put_old
(unshare) [root@ubuntu]:[~][tty:tty]#
(unshare) [root@ubuntu]:[~][tty:tty]# /bin/df -h
Filesystem Size Used Available Use% Mounted on
overlay 92.0G 6.9G 80.3G 8% /
overlay 92.0G 6.9G 80.3G 8% /put_old
(unshare) [root@ubuntu]:[~][tty:tty]#
如上便成功了。
如果容器中有修改,会相应的修改到/root/upper
内容,例如:
(unshare) [root@ubuntu]:[~][tty:tty]# echo 'busybox' > 123.txt
(unshare) [root@ubuntu]:[~][tty:tty]#
会同步修改到upper
目录中,而不会影响到原始的rootfs
。
[root@ubuntu]:[~][tty:1]# cat upper/root/123.txt
busybox
[root@ubuntu]:[~][tty:1]#
[root@ubuntu]:[~][tty:1]# ls rootfs/root/123.txt
ls: cannot access 'rootfs/root/123.txt': No such file or directory
[root@ubuntu]:[~][tty:1]#
可以看到,在宿主机中,rootfs/root/123.txt
并没有存在该文件,而upper/root/123.txt
则存在,所以使用overlay
可以重复利用rootfs
,节省磁盘存储空间。
总结
若一个进程使用mount namespace
后,会使用和宿主机相同的rootfs
,为了将容器和宿主机rootfs
分离开来,可以使用pivot_root
进行更改容器中的根文件系统。
在此基础上上,若要运行多个容器的话,需要拷贝多个rootfs
才行,好在可以使用overlay
文件系统,可以有效的利用rootfs
,又达到了文件系统的隔离性。
在容器中如何挂载rootfs
https://wangli2025.github.io/2024/11/13/How_to_mount_rootfs_in_a_container.html
本站均为原创文章,采用 CC BY-NC-ND 4.0 协议。转载请注明出处,不得用于商业用途。