【Docker 入门系列】:从 Namespace、CGroup 到 LXC:理解容器运行时、rootfs 与 LXC 实战

🔥 本文专栏:Docker

🌸作者主页:努力努力再努力wz

💪 今日博客励志语录:真正决定你能走多远的,不是你有没有走过弯路,而是你是否愿意在看清弯路之后,继续往前走。


思维导图

text 复制代码
为什么需要 Docker
        ↓
微服务带来大量独立服务实例
        ↓
程序不仅依赖可执行文件,还依赖运行环境
        ↓
需要"程序 + 用户态运行环境"的标准化交付
        ↓
容器化
        ↓
Docker 不重新实现操作系统隔离
        ↓
Linux Kernel 提供底层能力
        │
        ├── Namespace:隔离"能看到什么"
        ├── CGroup:限制"能使用多少"
        ├── Mount:组织文件系统视图
        ├── Network:组织网络环境
        └── Capability / LSM:限制权限
        ↓
问题:这些能力都很底层,难道上层程序每次都自己组织?
        ↓
LXC / liblxc
        ↓
把零散 Linux 内核能力组织成"容器"这一高层语义
        ↓
LXC CLI
lxc-create / lxc-start / lxc-attach / lxc-stop ...
        ↓
容器静态定义
config + rootfs
        ↓
lxc-start
        ↓
运行时实例
进程 + Namespace + CGroup + Mount + Network
        ↓
lxc-attach
        ↓
在已有容器运行环境中启动新的进程

一、先回顾:Docker 到底在做什么

在继续学习 LXC 之前,先把前面已经建立的 Docker 心智模型重新串起来。

最开始的单体应用中,所有模块都部署在同一个节点上:

text 复制代码
一个节点
   ↓
一个单体应用
   ↓
登录模块 / 消息模块 / 后台模块 / ...

不同模块的负载通常并不相同,但单体应用只能以整个程序为单位进行部署和扩容。

当其中某个模块成为性能瓶颈以后,最直接的方式就是纵向提升整台机器的硬件配置,但是这种方式存在两个问题:

text 复制代码
硬件存在上限
+
扩容粒度太粗

于是进一步演化到水平扩容:

text 复制代码
Node-1  → 完整应用
Node-2  → 完整应用
Node-3  → 完整应用

虽然总体吞吐量提升了,但是复制的仍然是完整应用,依旧无法只针对高负载模块进行扩容。

因此继续拆分到微服务:

text 复制代码
LoginService
MessageService
FriendService
AdminService

各个服务成为独立进程,可以独立部署、独立升级、独立扩容。

但是新的问题也随之出现:

服务实例越来越多,而一个程序真正运行起来,并不只有一个可执行文件,还依赖动态库、配置文件以及各种第三方依赖。

如果每一台节点都需要人工重新配置这些运行环境,部署成本会迅速上升。

因此 Docker 所解决的一个核心问题就是:

将程序以及其用户态运行环境一起组织和交付,而不是把"环境是否正确"留给部署节点临时解决。


二、Docker 并没有重新实现一套隔离机制

刚开始接触 Docker 时,很容易产生一个直觉:

text 复制代码
Docker
↓
自己创造一个独立运行环境
↓
容器

但继续向下拆以后会发现,真正提供隔离能力的其实还是 Linux 内核。

Docker 需要的是:

text 复制代码
进程隔离
网络隔离
文件系统挂载视图隔离
主机名隔离
用户隔离
...

这些能力 Linux 本身已经通过 Namespace 提供。

例如:

text 复制代码
PID Namespace      → 进程视图
Network Namespace  → 网络视图
Mount Namespace    → 挂载视图
UTS Namespace      → hostname 等视图
User Namespace     → UID/GID 映射

所以 Namespace 更适合记成一句话:

Namespace 决定一个进程"能够看到什么"。

但是只有隔离视图还不够。

假设同一个节点运行多个服务容器:

text 复制代码
容器 A → 服务 A
容器 B → 服务 B
容器 C → 服务 C

它们虽然相互看不到彼此的很多资源,但是最终仍然是宿主 Linux 上的进程,CPU 调度、内存分配、I/O 等仍然由同一个宿主内核管理。

如果某一个进程疯狂消耗资源,就可能影响同节点上的其他服务。

因此还需要:

text 复制代码
CGroup
↓
限制 / 统计进程组对资源的使用

所以可以先固定一个最重要的模型:

text 复制代码
Namespace → 我能看到什么
CGroup    → 我能使用多少

真正执行这些隔离与资源限制的主体始终是:

text 复制代码
Linux Kernel

Docker、LXC 等用户态工具更多是在负责:

text 复制代码
描述
组织
配置
管理

而不是自己在用户态不断循环判断:

text 复制代码
这个进程还能不能申请内存?
这个进程能不能看到另一个 PID?
这个网络包应该走哪张网卡?

这些真正发生在进程运行过程中的判断,最终仍然由内核执行。


三、重新认识 clone、unshare 与 setns

在理解容器时,会经常看到几个 Linux 接口:

text 复制代码
clone()
unshare()
setns()

这里不需要把每一个接口的实现细节钻得很深,只需要建立它们的大致职责。

1. clone:创建进程时直接创建新的 Namespace

clone() 本身可以创建新的 task,根据不同参数控制新任务与父任务共享哪些资源。

当传入类似:

text 复制代码
CLONE_NEWPID
CLONE_NEWNET
CLONE_NEWNS
...

这样的标志时,就可以在创建新进程的同时,为其建立新的 Namespace 环境。

可以粗略理解为:

text 复制代码
clone(CLONE_NEW*)
        ↓
创建新进程
+
创建新的 Namespace
+
让新进程位于其中

2. unshare:让当前执行上下文不再共享某些 Namespace

unshare() 的核心是:

将当前进程原本与其他进程共享的某些执行上下文拆分出来。

因此我们此前用 unshare 做实验,本质上就是直接体验 Linux 内核原生提供的 Namespace 能力。

3. setns:加入已经存在的 Namespace

如果某个 Namespace 已经存在,希望新的进程进入其中,则会涉及:

text 复制代码
setns()

这个思想后面和 lxc-attach 会直接联系起来。


四、为什么还需要 LXC

既然 Linux 已经提供:

text 复制代码
Namespace
CGroup
Mount
Network
Capability
...

那么是不是直接调用这些接口就够了?

理论上当然可以。

但是如果真的自己实现一个完整容器,需要组织的流程可能是:

text 复制代码
解析配置
    ↓
创建 Namespace
    ↓
准备 CGroup
    ↓
准备 rootfs
    ↓
组织 Mount
    ↓
配置网络
    ↓
设置权限
    ↓
创建容器第一个进程
    ↓
执行 init
    ↓
管理退出和清理

这就像我们写网络服务器时,虽然底层有:

cpp 复制代码
socket()
bind()
listen()
epoll_create()
epoll_ctl()
...

但真正的服务器通常不会要求上层业务每次重新手工组织所有这些细节,而是会进一步封装成:

cpp 复制代码
Server server;
server.start();

这里的 start() 并不是简单一对一包装某一个系统调用,而是代表:

一个完整的、更高层次的行为语义。

LXC 的价值也是类似的。


五、LXC 与 liblxc:不是单纯给系统调用套一个壳

这里首先需要区分两个概念:

text 复制代码
LXC
liblxc

如果单独说"像一个库",更准确对应的是:

text 复制代码
liblxc

它会把 Linux 底层零散的容器机制进一步封装成:

text 复制代码
创建容器
启动容器
停止容器
查询状态
进入容器
...

这样的高层容器语义。

关系可以理解为:

text 复制代码
Linux Kernel
提供 Namespace / CGroup / Mount / Network ...
        ↓
      liblxc
把底层机制按照"容器"这一对象组织起来
        ↓
LXC CLI / 上层程序

这里最重要的一点是:

liblxc 并不是简单做 clone() → lxc_clone() 这种一对一包装,而是把一系列底层操作组合成完整的容器生命周期操作。

例如一个"启动容器"的动作,背后可能包含:

text 复制代码
读取配置
↓
准备 rootfs
↓
准备 Namespace
↓
配置 CGroup
↓
配置网络
↓
组织 Mount
↓
设置权限
↓
创建进程
↓
exec init

这更像我们自己服务器中的 Server::start(),而不是 Socket::bind() 这种非常薄的 API 包装。


六、LXC CLI:每一个命令本身就是独立程序

这里还有一个容易混淆的地方。

LXC 并不是像 redis-cli 一样:

text 复制代码
先启动一个客户端
↓
客户端一直等待输入
↓
再输入 create/start/stop

而是提供了一组独立的命令行工具:

text 复制代码
lxc-create
lxc-start
lxc-stop
lxc-attach
lxc-info
lxc-destroy
...

当我们输入:

bash 复制代码
lxc-start -n test

大致发生:

text 复制代码
Bash 读取用户输入
        ↓
解析命令与参数
        ↓
判断 lxc-start 是外部命令
        ↓
fork 创建子进程
        ↓
exec 对应的 /usr/bin/lxc-start
        ↓
进入 lxc-start 的 main(argc, argv)
        ↓
解析参数
        ↓
调用 liblxc
        ↓
liblxc 再组织底层 Linux 能力

因此这里有一个清晰的层次:

text 复制代码
Bash
  ↓
LXC CLI
  ↓
liblxc
  ↓
Linux 系统调用 / 内核接口
  ↓
Linux Kernel

实战:确认 LXC CLI 确实是一组独立程序

安装完成以后:

bash 复制代码
lxc-start --version
lxc-checkconfig

可以看到当前实验环境使用:

text 复制代码
LXC 5.0.3
Linux Kernel 6.8.0-101-generic
CGroup v2 挂载于 /sys/fs/cgroup

同时 Namespace、CGroup、veth、bridge 等能力均已启用。

继续执行:

bash 复制代码
which lxc-start
ls /usr/bin/lxc-*

可以直接看到:

text 复制代码
/usr/bin/lxc-start
/usr/bin/lxc-create
/usr/bin/lxc-stop
/usr/bin/lxc-attach
/usr/bin/lxc-info
...

这就直接验证了:

LXC 并不是一个始终等待用户输入的统一交互进程,而是一组面向不同容器操作的独立 CLI 程序。


七、LXC 容器的生命周期:先建立整体认知

最核心的一组命令可以按照生命周期来记:

text 复制代码
lxc-create
    ↓
创建静态容器对象

lxc-ls / lxc-info
    ↓
查看容器

lxc-start
    ↓
把静态定义实例化成真正运行中的容器

lxc-attach
    ↓
在已经运行的容器环境中启动新的进程

lxc-stop
    ↓
停止运行实例

lxc-destroy
    ↓
删除持久化容器对象

这里最关键的一组区别是:

text 复制代码
容器"存在"
≠
容器"正在运行"

八、lxc-create:创建的到底是什么

这次实验执行:

bash 复制代码
sudo lxc-create -n test -t download -- -d ubuntu -r noble -a amd64

参数可以先这样理解:

text 复制代码
-n test
→ 容器名称 test

-t download
→ 使用 download 模板

-d ubuntu
→ Ubuntu 发行版

-r noble
→ Ubuntu Noble

-a amd64
→ amd64 架构

执行过程中可以看到:

text 复制代码
Downloading the image index
Downloading the rootfs
Downloading the metadata
Unpacking the rootfs

这里尤其要注意:

lxc-create 并没有立即启动一个容器进程。

它首先创建的是一个持久化的容器定义

默认情况下可以在:

text 复制代码
/var/lib/lxc/test/

下面看到:

text 复制代码
config
rootfs/

也就是说:

text 复制代码
lxc-create
    ↓
准备静态容器对象
    ↓
config + rootfs

九、template 模板究竟是干什么的

lxc-create 中还有一个非常重要的概念:

text 复制代码
template

这里我们使用:

bash 复制代码
-t download

模板最核心的作用可以理解成:

告诉 LXC:这个容器的基础用户态文件系统,也就是 rootfs,应该怎么准备。

为什么不同模板会和 Ubuntu、Debian、BusyBox 等环境相关?

因为虽然这些容器最终共享的是宿主机 Linux Kernel,但是不同发行版的用户态文件系统并不完全相同。

例如它们都可能存在:

text 复制代码
/bin
/etc
/usr
/lib

但是目录中的:

text 复制代码
程序
动态库
配置文件
包管理工具
用户态组件

可能完全不同。

所以:

text 复制代码
Ubuntu 容器
↓
Ubuntu 风格的 rootfs

Debian 容器
↓
Debian 风格的 rootfs

这里需要注意:

模板主要用于创建阶段准备 rootfs,并不是每次 lxc-start 都重新执行模板。

可以把配置文件和模板分开理解:

text 复制代码
template
↓
解决"rootfs 怎么准备出来"

config
↓
解决"这个容器以后应该怎么运行"

十、config:容器的静态运行描述

创建完成以后执行:

bash 复制代码
sudo cat /var/lib/lxc/test/config

这次实验中可以看到一些非常直观的配置:

text 复制代码
lxc.rootfs.path = dir:/var/lib/lxc/test/rootfs
lxc.uts.name = test

lxc.net.0.type = veth
lxc.net.0.link = lxcbr0
lxc.net.0.flags = up

同时还有:

text 复制代码
lxc.include = /usr/share/lxc/config/common.conf

说明当前容器自己的 config 不一定保存所有配置,它还可以 include 公共配置。

所以一个容器配置大致可能描述:

text 复制代码
rootfs 在哪里
hostname 是什么
网络如何建立
挂载关系怎么组织
CGroup 资源约束
Namespace 相关设置
权限与安全配置
启动相关配置
...

这里可以先记:

config 描述"将来启动时这个容器应该是什么样"。


十一、rootfs:容器看到的根文件系统从哪里来

创建完成以后:

bash 复制代码
sudo ls /var/lib/lxc/test/rootfs/

可以看到:

text 复制代码
bin
boot
dev
etc
home
lib
proc
root
run
sbin
sys
tmp
usr
var
...

这就是模板为容器准备出来的用户态根文件系统内容。

这里可以先建立一个非常简单的认知:

text 复制代码
宿主机看到:
/var/lib/lxc/test/rootfs

容器进程看到:
/

当然,中间真正实现这个效果,还会涉及:

text 复制代码
Mount Namespace
挂载
根切换

但当前不需要继续深入 pivot_rootdentry 等实现细节。

只需要知道:

LXC 会在容器自己的挂载视图中组织这棵 rootfs,并最终让它成为容器进程眼中的根目录 /


十二、再联系 Mount Namespace:为什么同一个路径世界会变得不同

此前学习 Mount Namespace 时已经知道:

Mount Namespace 隔离的是一套挂载关系视图。

这里需要修正一个容易出现的表述:

并不是"每个进程一定都有一套完全独立的挂载视图",而是:

text 复制代码
每个进程属于一个 Mount Namespace
↓
多个进程可以共享同一个 Mount Namespace
↓
共享同一套挂载视图

当进程访问路径时,仍然通过 VFS 做路径解析。

例如:

text 复制代码
/a/b/c

解析到 /a/b 时,如果在当前 Mount Namespace 中 /a/b 是一个挂载点:

text 复制代码
VFS 解析到 /a/b
        ↓
发现当前挂载视图中这里是 mount point
        ↓
切换到被挂载文件系统
        ↓
继续查找 c

这里不是"不经过 VFS",而是 VFS 根据当前进程的 Mount Namespace 所维护的挂载关系进行切换。

所以容器文件系统视图可以先粗略理解为:

text 复制代码
rootfs 提供"文件内容"
+
Mount Namespace 提供"挂载视图"
+
根切换决定"哪个位置成为 /"

十三、lxc-ls 与 lxc-info:静态信息和运行时信息要分开

执行:

bash 复制代码
sudo lxc-ls -f

即使刚刚创建完容器、还没有启动,也可以看到:

text 复制代码
test  STOPPED

这说明:

lxc-ls 展示的不只是"当前正在运行的进程",它首先可以枚举已经定义的容器对象。

因此:

text 复制代码
容器已定义
↓
/var/lib/lxc/test/ 存在
↓
即使没有运行,也可以被列出来

但是运行以后,lxc-ls -flxc-info 还能显示:

text 复制代码
RUNNING
PID
IP
TX/RX
...

这些显然不可能全部来自静态 config

因此这里必须建立一个新的二分模型:

text 复制代码
容器信息
    │
    ├── 静态定义
    │     ↓
    │   config / rootfs
    │
    └── 运行时状态
          ↓
        内核中的真实对象

十四、运行时信息最终从哪里来

这里最容易产生一个问题:

容器已经启动以后,PID、CPU、内存、运行状态这些数据究竟从哪里查询?

答案的核心是:

最终权威状态仍然在 Linux Kernel。

但是内核本身并没有一个统一的"test 容器对象"。

内核真正认识的是:

text 复制代码
进程
Namespace
CGroup
网络设备
挂载对象
...

所以 LXC 做的是:

text 复制代码
知道 test 这个逻辑容器
↓
对应哪些进程 / CGroup / Namespace / 网络对象
↓
再通过 Linux 暴露的接口查询这些运行状态

例如:

text 复制代码
进程信息
→ /proc

CGroup 归属与统计
→ /proc + /sys/fs/cgroup

网络状态
→ Linux 网络相关接口

十五、伪文件系统:用户态查询内核运行状态的通道

这正好可以和此前学习的伪文件系统重新串起来。

Linux 中很多内核对象会通过"文件"的形式暴露给用户态。

例如:

text 复制代码
/proc
/sys
/sys/fs/cgroup

这些文件和普通磁盘文件最大的区别是:

它们很多并不是把内容真正持久化存储到磁盘,而是把内核当前状态映射成文件接口。

用户态仍然使用统一接口:

text 复制代码
open
read
write

进入 VFS 以后,根据具体文件对应的操作实现执行真正行为。

可以粗略理解为:

text 复制代码
用户态
↓
read / write
↓
VFS
↓
struct file
↓
f_op / 对应操作函数
↓
具体伪文件系统逻辑
↓
查询或修改内核数据结构

例如 CGroup v2:

text 复制代码
write memory.max
↓
修改资源限制

read memory.current
↓
读取当前内存统计

所以伪文件可以理解成:

用户态与内核状态之间的一种统一操作通道。

这也是 LXC 能够查询很多运行时状态的基础之一。


十六、lxc-start:从"静态定义"变成"运行中的容器"

现在执行:

bash 复制代码
sudo lxc-start -n test

随后:

bash 复制代码
sudo lxc-ls -f
sudo lxc-info -n test

这次可以看到:

text 复制代码
STATE: RUNNING
PID:   36322
IP:    10.0.3.43

同时宿主机还能看到 LXC monitor 等管理进程。

这里就可以把 lxc-start 理解成:

text 复制代码
磁盘上的:
config + rootfs
        ↓
lxc-start
        ↓
读取静态描述
        ↓
准备 Namespace / CGroup / Mount / Network ...
        ↓
启动容器第一个进程
        ↓
形成真正运行中的容器实例

所以:

text 复制代码
容器静态定义
≠
容器运行实例

这个关系非常像:

text 复制代码
磁盘上的 ELF 可执行文件
≠
内存中正在运行的 Linux 进程

十七、容器到底是不是进程

这里很容易说一句:

text 复制代码
"容器本质上就是一个进程"

这句话作为入门直觉有帮助,但是如果严谨一点,应该说:

text 复制代码
容器
=
一组 Linux 进程
+
这些进程所属的 Namespace
+
CGroup
+
rootfs / Mount
+
Network
+
其他运行配置

容器通常会有一个非常核心的第一个进程:

text 复制代码
init

实验中:

bash 复制代码
ps -fp 36322

得到:

text 复制代码
PID 36322
CMD /sbin/init

继续:

bash 复制代码
cat /proc/36322/cgroup

得到:

text 复制代码
0::/lxc.payload.test/init.scope

这就直接验证了:

text 复制代码
容器运行起来以后
↓
宿主机内核中真的存在普通 Linux task
↓
这个 task 又被组织进对应的 CGroup

所以容器不是绕开 Linux 进程模型创建出来的一种"新生命体"。

它最终还是:

Linux 进程,只不过被放进了一套被组织好的运行环境。


十八、为什么容器的 PID 在内外看到不一样

在宿主机中:

text 复制代码
/sbin/init
PID = 36322

但是进入容器以后执行:

bash 复制代码
ps -ef

可以看到:

text 复制代码
/sbin/init
PID = 1

这里就把此前的 PID Namespace 知识完全对应起来了:

text 复制代码
同一个底层进程
        ↓
宿主 PID Namespace 看
        ↓
PID = 36322

同一个底层进程
        ↓
容器 PID Namespace 看
        ↓
PID = 1

也就是说:

Namespace 并不是重新复制出另一个进程,而是让进程在不同 Namespace 视图中拥有不同的资源可见性和标识。


十九、lxc-attach:不是"传送终端",而是在容器里启动新进程

执行:

bash 复制代码
sudo lxc-attach -n test

进入以后:

text 复制代码
root@test:/#

这里很容易误以为:

当前宿主 Bash 被直接"搬进容器"了。

实际上更合适的理解是:

text 复制代码
lxc-attach
    ↓
找到 test 已经存在的运行环境
    ↓
创建新的进程
    ↓
让新进程进入 test 对应的 Namespace / CGroup 等环境
    ↓
没有指定命令时启动一个 shell

这次 ps -ef 中可以看到:

text 复制代码
PID 191  /bin/bash

这个 Bash 就是 lxc-attach 之后新启动出来的交互式 shell。

随后执行:

bash 复制代码
ps -ef

又会短暂创建:

text 复制代码
ps

这个新进程同样继承当前容器的运行环境。

所以可以把两个命令区分成:

text 复制代码
lxc-start
↓
建立容器运行环境,并启动核心 init 进程

lxc-attach
↓
容器已经运行,再在这个既有环境中启动一个新的进程

这和此前学习的 setns() 思想就联系起来了。


二十、运行中的隔离到底是谁在"持续维护"

这里还有一个非常重要的认知。

当 LXC 或 Docker 把 Namespace、CGroup 等规则建立起来以后,不需要某个用户态程序一直:

cpp 复制代码
while (true) {
    检查这个进程能不能看到另一个进程;
    检查这个进程有没有超过内存;
}

不是这样的。

真正持续维护这些规则的是:

text 复制代码
Linux Kernel

例如容器进程申请内存:

text 复制代码
进程申请内存
↓
进入 Kernel
↓
Kernel 查看 task 所属 CGroup
↓
根据 memory controller 的规则处理

进程访问网络:

text 复制代码
进程发起网络操作
↓
进入 Kernel
↓
根据所属 Network Namespace
↓
选择它能够看到的网卡、路由等网络环境

所以 Docker / LXC 的职责可以拆成:

text 复制代码
用户态容器工具
↓
搭环境、配置规则、做生命周期管理

Linux Kernel
↓
真正让隔离与限制在每一次运行过程中生效

当然,用户态仍然可能存在长期运行的管理进程,例如实验中看到的:

text 复制代码
lxc-monitord
[lxc monitor]

它们承担的是:

text 复制代码
监控
状态管理
生命周期协调
停止 / 清理

而不是亲自替代内核维持 Namespace 和 CGroup。


二十一、rootfs 实战:容器中的 / 到底对应宿主哪里

进入容器:

bash 复制代码
sudo lxc-attach -n test

然后执行:

bash 复制代码
pwd
ls /
cat /etc/os-release

实验中看到:

text 复制代码
pwd
→ /

/etc/os-release
→ Ubuntu 24.04.5 LTS (Noble)

此时对于容器中的进程来说:

text 复制代码
/

就是它所看到的根目录。

而宿主机之前明明看到的是:

text 复制代码
/var/lib/lxc/test/rootfs

所以继续做一个最直观的实验。

容器内部:

bash 复制代码
echo "hello lxc" > /tmp/lxc_test.txt
cat /tmp/lxc_test.txt

退出:

bash 复制代码
exit

宿主机执行:

bash 复制代码
sudo cat /var/lib/lxc/test/rootfs/tmp/lxc_test.txt

仍然得到:

text 复制代码
hello lxc

于是可以非常直观地确认:

text 复制代码
容器看到:
/tmp/lxc_test.txt

对应宿主机:
/var/lib/lxc/test/rootfs/tmp/lxc_test.txt

这就把 rootfs 的概念彻底落到了真实文件上。


二十二、LXC 容器也可以通过 SSH 交互

LXC system container 很像一台独立 Linux 用户态环境,因此只要:

text 复制代码
容器处于 RUNNING
+
网络正常
+
容器内部安装并启动 sshd

就可以通过:

bash 复制代码
ssh user@容器IP

进行正常网络登录。

这里要区分:

text 复制代码
lxc-attach
↓
宿主机直接在容器既有运行环境中启动新进程

SSH
↓
SSH Client
→ TCP
→ 容器 IP:22
→ sshd
→ sshd 再启动 shell

所以 SSH 不是 LXC 特有的"进入容器机制",它只是容器内部正常运行的一个网络服务。


二十四、把整个 LXC 心智模型重新串起来

到这里,我们已经可以把这一阶段的知识完整串起来。

首先:

text 复制代码
Linux Kernel

本身提供:

text 复制代码
Namespace
CGroup
Mount
Network
Capability
...

这些都是构建容器需要的底层机制。

但是它们彼此比较零散,如果上层每次都自己组织,使用成本非常高。

于是:

text 复制代码
liblxc

把这些底层机制进一步封装成:

text 复制代码
Container

这一更高层的对象语义。

liblxc 上层,又提供:

text 复制代码
lxc-create
lxc-start
lxc-stop
lxc-attach
lxc-info
...

这些方便用户使用的 CLI 工具。

最终完整链路就是:

text 复制代码
用户
 ↓
Bash
 ↓
LXC CLI
 ↓
liblxc
 ↓
Linux Kernel 接口
 ↓
Namespace / CGroup / Mount / Network ...
 ↓
真正运行的 Linux 进程

而一个 LXC 容器又可以分成两种状态:

text 复制代码
静态状态:
config + rootfs

        ↓ lxc-start

运行状态:
进程
+
Namespace
+
CGroup
+
Mount/rootfs
+
Network
+
其他运行资源

最后,再用一句话概括:

LXC 的核心价值不是重新实现容器隔离,而是把 Linux 已经提供的进程、Namespace、CGroup、Mount、Network 等底层能力,按照"容器"这一高层语义组织起来,并提供可以直接操作容器生命周期的库和命令行工具。

而真正让隔离、资源限制以及进程调度持续生效的,始终还是:

text 复制代码
Linux Kernel

二十五、这一阶段需要真正记住的几个结论

最后不再堆命令,只保留几个最核心的结论。

text 复制代码
1. Namespace 解决"能看到什么",CGroup 解决"能使用多少"。

2. Docker / LXC 是用户态的组织和管理者,真正执行隔离与资源控制的是 Linux Kernel。

3. liblxc 不是简单的一对一系统调用 wrapper,而是把大量底层操作组织成"创建 / 启动 / 停止容器"等高层语义。

4. LXC 是一组 CLI 工具,而不是一个持续等待用户输入的统一交互客户端。

5. lxc-create 创建的是静态容器对象:config + rootfs,此时容器完全可以处于 STOPPED。

6. template 的核心作用是准备容器的用户态 rootfs。

7. lxc-start 把静态定义转化成真正的运行时实例。

8. 运行中的容器最终仍然是一组真实 Linux 进程,只是这些进程处于特定的 Namespace、CGroup、Mount、Network 环境中。

9. lxc-attach 的本质是在已经运行的容器环境中再启动一个新的进程,默认通常是 shell。

10. rootfs 在宿主机中可能只是 /var/lib/lxc/test/rootfs,而在容器进程自己的文件系统视图中,它被组织成了 /。

到这里,LXC 已经不再只是一个"会创建容器的命令行工具",而是成为了连接两层知识的桥梁:

text 复制代码
上层:Docker / 容器平台
        ↓
中间:LXC / 容器运行时抽象
        ↓
底层:Linux Namespace / CGroup / Mount / Network

这也是后面继续理解 Docker 更现代的运行时体系时最重要的前置心智模型。

相关推荐
JavaPub-rodert3 小时前
Docker 多阶段构建:为什么生产环境镜像可以小 90%
运维·docker·容器
2601_962218473 小时前
万象生鲜系统多账套隔离技术适配集团生鲜企业集团化数字化管控
大数据·运维·微服务·云原生·架构
王志来137944730084 小时前
安防监控工控工业服务器配套
运维·服务器·python
hanchenxing4 小时前
自动化脚本“卡住无输出“怎么办: 一次 WebSocket 挂死的排查与自愈模板
运维·javascript·chrome·websocket·自动化·自动化爬虫
吴声子夜歌4 小时前
Shell编程——条件测试与比较
linux·运维·shell
吴声子夜歌5 小时前
Shell编程——函数
linux·运维·shell
PC2005-cloud5 小时前
Qdrant 连接失败排查:localhost 解析到 __1,而 Docker 只绑了 IPv4
运维·docker·容器
电子制造自留地5 小时前
AI服务器PCB的超低损耗材料选型逻辑
运维·服务器·人工智能·科技·制造·科普·pcb工艺
喵喵爱自由5 小时前
Deepseek Harness镜像离线部署
docker·ai·语言模型