容器技术源码剖析?

访客 源码剖析 1

从内核隔离到运行时实现

目录导读

  1. 容器技术本质 - 从虚拟化对比看容器核心价值
  2. 底层支撑 - Namespace与Cgroup源码级解析
  3. 运行时组件 - runC与containerd源码工作流
  4. 关键机制 - UnionFS镜像层叠与容器网络
  5. 常见问题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

  • 资源控制通过子系统实现(如cpumemory)。
  • 当写入/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创建。
  • 启动流程
    1. 解析config.json(OCI标准规范)。
    2. 调用linuxStandardInit(),创建Namespace(clone()CLONE_NEWNS | CLONE_NEWPID)。
    3. 设置Cgroup(写入/sys/fs/cgroup)。
    4. 最后pivot_root()切换根文件系统,执行容器内CMD。

2 containerd:高级运行时管理

containerd管理容器的生命周期(拉取镜像、运行、停止),源码:github.com/containerd/containerd

  • 关键协程TaskService处理gRPC请求。
  • 创建容器流程
    1. Create()调用v2/run shim进程(如containerd-shim-runc-v2)。
    2. shim fork出runC子进程,监控其退出状态。
    3. 容器内进程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.cnetdev_upper_dev_link()关联bridge。

问答环节
Q:如何查看容器网络命名空间?
A:ls -la /proc/<容器PID>/ns/net看到符号链接,若与其他容器同net:[402653...]则网络共享。


常见问题FAQ

Q1:容器启动时execrun区别?
A:run创建新Namespace和Cgroup;exec复用已有命名空间(通过setns()加入),不额外隔离。

Q2:容器内修改/etc/hosts为何重启后消失?
A:容器文件系统基于UnionFS,修改在upper层(可写层),但重启创建新upper层,需挂载tmpfs或使用docker cp持久化。

Q3:源码学习建议?
A:先读runclibcontainer包(约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的“开关”。

标签: 容器技术 源码剖析

抱歉,评论功能暂时关闭!