从内核隔离到运行时实现
目录导读
- 容器技术本质 - 从虚拟化对比看容器核心价值
- 底层支撑 - Namespace与Cgroup源码级解析
- 运行时组件 - runC与containerd源码工作流
- 关键机制 - UnionFS镜像层叠与容器网络
- 常见问题FAQ - 面试级源码追问与解答
容器技术本质:不是虚拟机的轻量替代
容器常被误解为“轻量级虚拟机”,但从源码视角看,二者有本质区别,传统虚拟机(如KVM)通过Hypervisor模拟完整硬件,跑独立内核;而容器直接共享宿主机内核,仅通过内核特性实现资源隔离。
源码视角差异:
- 虚拟机涉及硬件虚拟化指令(如Intel VT-x),需修改Guest OS内核。
- 容器仅需内核支持Namespace与Cgroup,用户态进程无感知。
问答环节
Q:容器能跑Windows吗?
A:不能直接跑,Windows容器依赖Windows内核的Server Container,本质也是共享宿主内核,Linux容器同理。
底层支撑:Namespace与Cgroup源码级解析
容器隔离的核心是内核的Namespace(命名空间)与Cgroup(控制组)。
1 Namespace源码机制
Namespace将全局资源(如PID、网络、挂载点)虚拟化为独立视图,以 PID Namespace 为例,其内核代码位于/kernel/pid_namespace.c:
- 创建新命名空间时,调用
clone()带CLONE_NEWPID标志,内核为进程分配新PID视图。 fork()在新Namespace中,子进程PID从1开始(类似独立系统init)。- 关键函数
pid_nr_ns()根据Namespace结构体pid->numbers映射全局PID。
2 Cgroup v2代码结构
Cgroup用于限制CPU、内存、IO等资源,最新v2版代码在/kernel/cgroup/cgroup.c:
- 资源控制通过子系统实现(如
cpu、memory)。 - 当写入
/sys/fs/cgroup/xxx/memory.max,内核调用mem_cgroup_charge()在页分配时检查配额。 - 超出限制时触发OOM Killer或直接返回
-ENOMEM。
问答环节
Q:为什么宿主机ps看不到容器进程?
A:PID Namespace隔离后,容器进程在宿主机PID全局表仍存在,但ps默认只读当前Namespace,执行nsenter -t <容器PID> -m -p ps才能看到容器内视图。
运行时组件:runC与containerd源码工作流
现代容器运行时(如Docker)依赖OCI(Open Container Initiative)规范,典型执行链:Docker → containerd → runC。
1 runC:OCI运行时标准实现
runC是轻量级运行时,源码位置:github.com/opencontainers/runc。
- 核心结构:
libcontainer包封装Namespace/Cgroup创建。 - 启动流程:
- 解析
config.json(OCI标准规范)。 - 调用
linuxStandardInit(),创建Namespace(clone()带CLONE_NEWNS | CLONE_NEWPID)。 - 设置Cgroup(写入
/sys/fs/cgroup)。 - 最后
pivot_root()切换根文件系统,执行容器内CMD。
- 解析
2 containerd:高级运行时管理
containerd管理容器的生命周期(拉取镜像、运行、停止),源码:github.com/containerd/containerd。
- 关键协程:
TaskService处理gRPC请求。 - 创建容器流程:
Create()调用v2/runshim进程(如containerd-shim-runc-v2)。- shim fork出runC子进程,监控其退出状态。
- 容器内进程PID写入状态文件。
问答环节
Q:为什么用runC而不用Docker直接调内核?
A:解耦,Docker专注镜像管理,runC标准化容器执行,符合OCI规范,任何兼容runtime(如crun、youki)可替换。
关键机制:UnionFS镜像层叠与容器网络
1 UnionFS(OverlayFS)源码解读
容器镜像分层存储由联合文件系统实现,Linux内核OverlayFS代码在fs/overlayfs/:
- 元数据存储:
super_block结构保存lowerdir(基础层),upperdir(可写层)。 - 文件访问原理:读取时先查upper,找不到则fallback到lower。
- 写时复制:修改文件时执行
ovl_copy_up()将lower文件复制到upper后再修改。
2 容器网络(veth pair + bridge)
容器默认网络通过veth pair连接网桥(如docker0):
- 创建veth pair:
ip link add veth0 type veth peer name veth1。 - 一个端点放宿主机网桥,一个移入容器Namespace。
- 源码依赖内核
/net/core/dev.c的netdev_upper_dev_link()关联bridge。
问答环节
Q:如何查看容器网络命名空间?
A:ls -la /proc/<容器PID>/ns/net看到符号链接,若与其他容器同net:[402653...]则网络共享。
常见问题FAQ
Q1:容器启动时exec与run区别?
A:run创建新Namespace和Cgroup;exec复用已有命名空间(通过setns()加入),不额外隔离。
Q2:容器内修改/etc/hosts为何重启后消失?
A:容器文件系统基于UnionFS,修改在upper层(可写层),但重启创建新upper层,需挂载tmpfs或使用docker cp持久化。
Q3:源码学习建议?
A:先读runc的libcontainer包(约1万行C代码+Go),再学containerd的shim管理,重点关注clone()与/proc文件系统交互。
Q4:K8s如何调度容器?
A:K8s通过kubelet调用CRI(如containerd),再由runC创建,调度基于节点资源(Cgroup统计输出)。
从源码看容器的“伪隔离”
容器并非真正的沙箱,其共享内核的特性决定了攻击面,理解Namespace/Cgroup源码后,你能回答:
- 一个容器内的
kill -9 1为何可能导致整个节点崩溃? - 为何
--privileged模式能挂载宿主机设备?
建议深入阅读runc源码中的libcontainer/nsenter/nsexec.c,这是理解容器进程进入Namespace的“开关”。