Skip to content

在容器中如何挂载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_rootput_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_directoryupper_directorywork_directory以及work目录。

并且在lower_directory目录下创建了2个文件:text.txttest分别写入了内容。

使用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.txtmd5值可以判断,其修改的值被存入到了upper_directory中。

使用overlay挂载rootfs

想要每个容器都获取“独立”的rootfs,那在创建namespace之前,就需要先进行挂载,需要指定不同的lowerdirupperdir以及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 协议。转载请注明出处,不得用于商业用途。

Comments