一个用 C++20 从零实现的轻量级 Linux 容器运行时,不依赖 Docker 或任何容器框架。UContainer 直接使用 Linux 内核机制实现了现代容器技术的核心:命名空间隔离、cgroup 资源控制、OverlayFS 分层文件系统以及虚拟网络。
- 命名空间隔离 — 通过
clone()实现 UTS、PID、Mount、Network 命名空间隔离 - OverlayFS 文件系统 — 共享只读基础镜像 + 每个容器独立的可写层,与 Docker 镜像层机制完全一致
- cgroup v2 内存限制 — 通过 cgroup v2 层级结构由内核强制执行内存上限
- 双栈虚拟网络 — 每个容器独立的 veth pair,通过 iptables/ip6tables 实现 IPv4 和 IPv6 NAT
- 交互式 PTY Shell — 完整的伪终端支持,基于 epoll 的 I/O 多路复用
- Alpine 基础镜像自动管理 — 首次运行时自动下载并解压 Alpine Linux minirootfs,所有容器共享同一份基础镜像
- DNS 注入 — 读取宿主机
resolv.conf,过滤本地回环 DNS 存根,自动回退到公共解析器 - 容器重置 — 丢弃容器可写层,保留共享基础镜像不变
- Linux 内核 5.2+(cgroup v2)
- Root 权限(
clone()命名空间标志、mount、pivot_root均需要) - 宿主机已安装:
wget、tar、ip、iptables、ip6tables、nsenter - CMake 3.25+,支持 C++20 的编译器(GCC 12+ 或 Clang 14+)
# 1. 克隆代码
git clone https://github.com/Siaospeed/UContainer
cd ucontainer
# 2. 编译构建
mkdir build && cd build
cmake ..
make -j$(nproc)# 在容器中运行交互式 Shell
sudo ./ucontainer run <container_id>
# 运行指定命令
sudo ./ucontainer run <container_id> /bin/sh -c "echo hello"
# 设置内存限制(支持 K、M、G 后缀)
sudo ./ucontainer run --memory 128M <container_id>
# 重置容器可写层
sudo ./ucontainer reset <container_id> force容器 ID 由字母、数字、_ 和 - 组成,最长 64 个字符。每个容器在 /var/lib/ucontainer/ 下拥有独立的目录。
UContainer 使用 clone() 并传入 CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWNET 标志,在隔离的命名空间中创建子进程。调用时需要手动分配并传入栈内存,默认大小为 8 MiB。需要注意的是,CLONE_NEWNET 会在内核中初始化一套完整的网络协议栈、路由表和 loopback 接口,其栈消耗远高于其他命名空间类型,栈空间不足时会导致不可预期的崩溃。
UContainer 不为每个容器复制一份完整的基础镜像,而是使用 OverlayFS 联合挂载:
/var/lib/ucontainer/
├── images/
│ └── alpine-3.19/ ← 所有容器共享的只读下层(lower layer)
└── u_<container_id>/
├── upper/ ← 容器私有的可写层
├── work/ ← OverlayFS 内部使用
└── merged/ ← 联合挂载点,pivot_root 的目标
基础镜像只需解压一次,所有容器共享。容器内的所有写操作都落在自己的 upper/ 目录中。删除容器只会移除 upper/ 层,基础镜像完全不受影响。这与 Docker 镜像分层机制的本质相同。
OverlayFS 挂载完成后,通过 pivot_root 将容器根目录切换到 merged/,随后用 MNT_DETACH 卸载原宿主机根目录,容器因此获得完全隔离的文件系统视图。
每个容器在 /sys/fs/cgroup/ucontainer/<container_id>/ 下拥有独立的 cgroup 节点。根据 cgroup v2 的委托模型,必须先向父节点的 cgroup.subtree_control 写入 +memory +pids 开启控制器,再创建子目录——顺序颠倒会导致子 cgroup 中的 memory.max 写入失败。clone() 返回子进程 PID 后,在容器开始执行用户代码之前,将其加入对应的 cgroup。
UContainer 为每个容器创建一对 veth:
vethuc0_<id>留在宿主机,分配网关 IPvethuc1_<id>移入容器网络命名空间,重命名为eth0
同时配置 IPv4(10.85.86.0/24)和 IPv6(fc00:8586::/64)双栈地址。通过 iptables/ip6tables MASQUERADE 规则实现出站 NAT。UContainerNetwork 的析构函数负责在容器退出时清理 veth pair 和 NAT 规则。
在 clone() 之前调用 openpty() 创建主从伪终端对。slave fd 传入容器进程,通过 TIOCSCTTY 设为其控制终端。宿主侧使用 epoll 同时监听 stdin→master 和 master→stdout,在两个方向上透传原始字节流。TerminalStateGuard(RAII)在容器运行期间将宿主终端切换为 raw 模式,并在退出时(包括异常退出)可靠地恢复原始状态。
ucontainer
├── include/
│ ├── cgroup.h # cgroup v2 RAII 守卫 + ApplyLimits()
│ ├── terminal_state_guard.h # RAII 终端 raw 模式切换器
│ ├── ucontainer.h # 顶层协调器
│ ├── ucontainer_config.h # 配置结构体 + ParseArgs()
│ ├── ucontainer_network.h # veth + iptables 生命周期管理
│ ├── ucontainer_process.h # clone/exec/PTY/wait
│ ├── utils.h # 日志宏、校验工具函数
│ └── third_party/CLI11/ # 纯头文件命令行参数解析库
└── src/
├── main.cc
├── ucontainer_config.cc
├── core/
│ ├── ucontainer.cc
│ ├── ucontainer_process.cc
│ ├── ucontainer_network.cc
│ └── cgroup.cc
└── utils/
└── utils.cc
全面使用 RAII。 CgroupGuard、UContainerNetwork、TerminalStateGuard 均在析构函数中清理各自持有的资源,主流程中没有任何手动清理代码。
两阶段进程启动。 UContainerProcess::Spawn() 负责调用 clone() 并返回子进程 PID;UContainerProcess::Attach() 接管 PTY 并调用 waitpid()。这种拆分使 UContainer::Run() 可以在两个阶段之间插入 cgroup 和网络配置——此时子进程已存在但尚未开始交互执行。
Config 由 UContainer 统一持有。 UContainerProcess 和 UContainerNetwork 均持有 const UContainerConfig&。C++ 保证成员按声明顺序构造、逆序析构,config_ 声明在最前面,其生命周期覆盖所有其他成员,引用安全性有语言层面的保证。
- 同时只能运行一个容器(固定 IP/子网段,并发运行会冲突)
- 仅支持 x86-64 Alpine Linux
- 资源限制仅有内存,尚无 CPU 配额和 PID 数量限制
- 不支持容器暂停/恢复或检查点
- 需要 root 权限,不支持 rootless 模式
本项目采用 Apache License 2.0 协议开源。
你可以自由使用、修改、分发本项目,但必须:
- 保留原作者署名 (Siaospeed)
- 在引用时注明原项目来源
Copyright 2026 Siaospeed
欢迎提交 Issue 或 Pull Request。