在终端构建一个最小化容器

What I cannot create, I do not understand. —— Richard Feynman

操作步骤

0x00 准备环境

强烈建议使用虚拟机

0x01 构建容器文件系统

初始化文件系统目录

1
mkdir -p /tmp/container-demo/{lower,upper,work,merged}

此处的目录结构模拟 Docker 使用的 overlayfs 文件系统。 Overlayfs 允许我们将 2 个文件文件树(upperlower)组合成一个 merged 的组合视图。

下载一个 alpine 的 minirootfs 并解压到 lower 目录中

1
2
wget https://dl-cdn.alpinelinux.org/alpine/latest-stable/releases/x86_64/alpine-minirootfs-3.21.0-x86_64.tar.gz
tar -xvf alpine-minirootfs-3.21.0-x86_64.tar.gz -C lower

挂载 overlayFS 文件系统到 merged

1
mount -t overlay overlay -o lowerdir=lower,upperdir=upper,workdir=work merged

现在可以看到 merged 下存在一个 Alpine 文件系统:

1
2
[root@hyxcontainer-demo]# ls merged
bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var

现在测试可以直接通过 chroot 可以直接在新的文件系统下启动一个进程

1
2
3
4
5
6
7
8
[root@hyxcontainer-demo]# chroot merged /bin/sh
/ # /bin/cat /etc/os-release
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.21.0
PRETTY_NAME="Alpine Linux v3.21"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://gitlab.alpinelinux.org/alpine/aports/-/issues"

确认后退出 chroot

0x02 配置 cgroups

以下步骤基于 cgroup v1,cgroup v2 可以参考引用文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 创建 cgroup 目录
mkdir -p /sys/fs/cgroup/cpu/docker-demo
mkdir -p /sys/fs/cgroup/memory/docker-demo

# 配置 CPU 限制
# 设置最大 CPU 使用率为 10%(cfs_quota_us 和 cfs_period_us)
cd /sys/fs/cgroup/cpu/docker-demo
echo 10000 > cpu.cfs_quota_us
echo 100000 > cpu.cfs_period_us

# 配置内存限制
# 设置最大内存为 500 MiB
cd /sys/fs/cgroup/memory/docker-demo
echo 524288000 > memory.limit_in_bytes
# 禁止使用 swap
echo 0 > memory.swappiness

# 将当前进程加入 cgroup
echo $$ > /sys/fs/cgroup/cpu/docker-demo/tasks
echo $$ > /sys/fs/cgroup/memory/docker-demo/tasks

这里我们将 cpu.cfs_quota_us 设置为 10000 微秒,cpu.cfs_period_us 设置为 100000 微秒。这意味着在每 100000 微秒(0.1 秒)的时间内,docker-demo 这个 cgroup 内的所有进程总共最多可以使用 10000 微秒的 CPU 时间,相当于限制了 CPU 使用率为 10% (10000 / 100000 = 0.1)。

0x03 命名空间

cgroup 的目的在于限制资源,而命名空间的作用是隔离资源。

下面通过 unshare 新建一个命名空间

1
2
3
4
5
6
7
8
9
unshare \
--uts \
--pid \
--mount \
--mount-proc \
--net \
--ipc \
--fork \
/bin/bash

v1 版本的 cgroup 不支持携带 –cgroup 参数

测试 UTS 隔离

1
2
3
4
5
6
7
8
9
10
# 原终端
[root@hyxdocker-demo]# hostname
hyx
[root@hyxdocker-demo]# hostname mycontainer
[root@hyxdocker-demo]# hostname
mycontainer

# 宿主机
[root@hyxdocker-demo.slice]# hostname
hyx

测试 PID 隔离

1
2
3
4
5
6
7
8
9
# 原终端
[root@hyxdocker-demo]# ps
PID TTY TIME CMD
1 pts/6 00:00:00 bash
48 pts/6 00:00:00 ps

# 宿主机
root 26211 5755 0 17:01 pts/6 00:00:00 unshare --uts --pid --mount --mount-proc --net --ipc --fork /bin/bash
root 26213 26211 0 17:01 pts/6 00:00:00 /bin/bash

0x04 容器端设置

首先通过 pivot_root 重新挂载根目录,实现文件系统级别的隔离

1
2
3
4
5
6
7
8
9
10
11
12
# 切换到容器的根文件系统目录
cd /tmp/container-demo/merged/
# 将宿主机的根文件系统 / 设置为私有挂载
mount --make-rprivate /
# 创建一个名为 old_root 的目用于存放旧的根文件系统
mkdir old_root
# 根文件系统切换
pivot_root . old_root
# 卸载旧的根文件系统
/bin/umount -l /old_root
# 删除挂载旧根文件系统的目录(确保已取消挂载)
/bin/rm -rf /old_root

创建设备文件

1
2
3
4
5
6
7
8
# 创建 null 设备,用于丢弃不需要的输出
/bin/mknod -m 666 dev/null c 1 3
# 创建 zero 设备,用于提供无限的 0 数据流
/bin/mknod -m 666 dev/zero c 1 5
# 创建 tty 设备,关联到当前终端
/bin/mknod -m 666 dev/tty c 5 0
# 创建其他目录结构
/bin/mkdir -p dev/{pts,shm}

挂载虚拟文件系统

1
2
3
4
5
6
7
8
9
10
# 伪终端支持
/bin/mount -t devpts devpts dev/pts
# 用于进程间共享内存
/bin/mount -t tmpfs tmpfs dev/shm
# 用于访问内核信息
/bin/mount -t sysfs sysfs sys/
# 存储运行时的临时文件
/bin/mount -t tmpfs tmpfs run/
# 提供进程和系统信息的虚拟文件系统
/bin/mount -t proc proc proc/

启动 sh

1
exec /bin/busybox sh

0x05 使用容器

在新启动的容器中执行以下命令:

1
/ # while true; do true; done

在宿主机中确认 CPU 限制在 10%

1
2
3
4
5
6
7
8
9
[root@hyx~]# top
top - 09:17:01 up 2 days, 18:40, 4 users, load average: 0.06, 0.10, 0.10
Tasks: 334 total, 2 running, 332 sleeping, 0 stopped, 0 zombie
%Cpu(s): 2.9 us, 0.4 sy, 0.0 ni, 96.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 8009200 total, 2174408 free, 3032924 used, 2801868 buff/cache
KiB Swap: 15626236 total, 15626236 free, 0 used. 4062548 avail Mem

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
13079 root 20 0 1692 656 460 R 10.0 0.0 0:03.54 busybox

在容器中测试使用内存超过 500 MiB (容器内进程)即被终止

1
2
/ # tail /dev/zero
Killed

完成后取消挂载

1
umount /tmp/container-demo/merged

参考资料

https://michalpitr.substack.com/p/linux-container-from-scratch