第三十一章:小千世界
筑基后期涉及内核源码:
一
林小源在内核中发现了一个他从未想象过的现象。
同一个系统上,有两个进程。它们的 PID 不同——一个是 1,另一个也是 1。
两个 PID 1?
不可能。PID 是唯一的——这是内核的基本规则。但林小源仔细检查了数据结构,发现这两个进程确实都有 pid == 1。区别在于:它们在不同的 PID 命名空间 中。
他走近那个"第二个 PID 1",看到它住在一间完全封闭的房间里。房间里有独立的主机名、独立的文件系统视图、独立的网络栈。它看不到外面的世界,也看不到宿主机上的其他进程。
"你是谁?"林小源问。
"我是 init。"那个进程说。它的语气和 init 童子一模一样——傲慢、疲惫、带着重压。"PID 1。"
"但你不是真正的 PID 1。"
"在我的世界里,我就是。"那个进程说,"我看不到外面,外面也看不到我。我的世界有独立的 PID 编号、独立的主机名、独立的挂载点。对我来说,外面不存在。"
/*
* Linux 命名空间类型:
* PID — 进程 ID 隔离
* UTS — 主机名隔离
* mount — 文件系统挂载点隔离
* net — 网络栈隔离
* user — 用户 ID 隔离
* cgroup — cgroup 根目录隔离
* IPC — 进程间通信隔离
*
* 每个进程都有一组命名空间(nsproxy),
* 不同命名空间中的进程看到不同的"世界"。
*/
struct pid_namespace {
int level; /* 命名空间层级 */
int base_pid; /* 该命名空间中的起始 PID */
char name[16];
};
struct nsproxy {
struct pid_namespace *pid_ns;
char uts_name[64]; /* UTS: 主机名 */
char mount_ns[16]; /* mount: 文件系统视图 */
char net_ns[16]; /* net: 网络栈 */
};
struct task_struct {
int pid; /* 全局 PID */
int ns_pid; /* 命名空间内的 PID */
struct nsproxy *ns;
char comm[16];
};
printf("=== 命名空间 — 小千世界 ===\n\n");
/* 宿主机的命名空间 */
struct pid_namespace host_pid_ns = { .level = 0, .base_pid = 0, .name = "host" };
struct nsproxy host_ns = {
.pid_ns = &host_pid_ns,
};
strcpy(host_ns.uts_name, "host-machine");
strcpy(host_ns.mount_ns, "host-mount");
strcpy(host_ns.net_ns, "host-net");
/* 容器的命名空间 */
struct pid_namespace container_pid_ns = { .level = 1, .base_pid = 1, .name = "container" };
struct nsproxy container_ns = {
.pid_ns = &container_pid_ns,
};
strcpy(container_ns.uts_name, "container-abc");
strcpy(container_ns.mount_ns, "container-mount");
strcpy(container_ns.net_ns, "container-net");
/* 宿主机的 init */
struct task_struct host_init = {
.pid = 1, .ns_pid = 1, .ns = &host_ns, .comm = "systemd"
};
/* 容器的 init */
struct task_struct container_init = {
.pid = 1000, .ns_pid = 1, .ns = &container_ns, .comm = "container-init"
};
printf("宿主机的 init:\n");
printf(" 全局 PID: %d\n", host_init.pid);
printf(" 命名空间 PID: %d\n", host_init.ns_pid);
printf(" 主机名: %s\n", host_init.ns->uts_name);
printf(" PID 命名空间: %s (level %d)\n\n",
host_init.ns->pid_ns->name, host_init.ns->pid_ns->level);
printf("容器的 init:\n");
printf(" 全局 PID: %d\n", container_init.pid);
printf(" 命名空间 PID: %d\n", container_init.ns_pid);
printf(" 主机名: %s\n", container_init.ns->uts_name);
printf(" PID 命名空间: %s (level %d)\n\n",
container_init.ns->pid_ns->name, container_init.ns->pid_ns->level);
printf("--- 关键观察 ---\n");
printf("两个进程的命名空间 PID 都是 1\n");
printf("但全局 PID 不同: %d vs %d\n",
host_init.pid, container_init.pid);
printf("它们看到不同的主机名、不同的挂载点、不同的网络栈\n");
printf("容器中的进程不知道宿主机的存在\n");
printf("\n--- 命名空间嵌套 ---\n");
printf("PID 命名空间可以嵌套:\n");
printf(" Level 0: 宿主机 (PID 1 = systemd)\n");
printf(" Level 1: 容器 A (PID 1 = container-init)\n");
printf(" Level 2: 容器 A 中的容器 (PID 1 = nested-init)\n");
printf("每一层都有自己的 PID 1\n");#include <stdio.h>
#include <string.h>
/*
* Linux 命名空间类型:
* PID — 进程 ID 隔离
* UTS — 主机名隔离
* mount — 文件系统挂载点隔离
* net — 网络栈隔离
* user — 用户 ID 隔离
* cgroup — cgroup 根目录隔离
* IPC — 进程间通信隔离
*
* 每个进程都有一组命名空间(nsproxy),
* 不同命名空间中的进程看到不同的"世界"。
*/
struct pid_namespace {
int level; /* 命名空间层级 */
int base_pid; /* 该命名空间中的起始 PID */
char name[16];
};
struct nsproxy {
struct pid_namespace *pid_ns;
char uts_name[64]; /* UTS: 主机名 */
char mount_ns[16]; /* mount: 文件系统视图 */
char net_ns[16]; /* net: 网络栈 */
};
struct task_struct {
int pid; /* 全局 PID */
int ns_pid; /* 命名空间内的 PID */
struct nsproxy *ns;
char comm[16];
};
int main() {
printf("=== 命名空间 — 小千世界 ===\n\n");
/* 宿主机的命名空间 */
struct pid_namespace host_pid_ns = { .level = 0, .base_pid = 0, .name = "host" };
struct nsproxy host_ns = {
.pid_ns = &host_pid_ns,
};
strcpy(host_ns.uts_name, "host-machine");
strcpy(host_ns.mount_ns, "host-mount");
strcpy(host_ns.net_ns, "host-net");
/* 容器的命名空间 */
struct pid_namespace container_pid_ns = { .level = 1, .base_pid = 1, .name = "container" };
struct nsproxy container_ns = {
.pid_ns = &container_pid_ns,
};
strcpy(container_ns.uts_name, "container-abc");
strcpy(container_ns.mount_ns, "container-mount");
strcpy(container_ns.net_ns, "container-net");
/* 宿主机的 init */
struct task_struct host_init = {
.pid = 1, .ns_pid = 1, .ns = &host_ns, .comm = "systemd"
};
/* 容器的 init */
struct task_struct container_init = {
.pid = 1000, .ns_pid = 1, .ns = &container_ns, .comm = "container-init"
};
printf("宿主机的 init:\n");
printf(" 全局 PID: %d\n", host_init.pid);
printf(" 命名空间 PID: %d\n", host_init.ns_pid);
printf(" 主机名: %s\n", host_init.ns->uts_name);
printf(" PID 命名空间: %s (level %d)\n\n",
host_init.ns->pid_ns->name, host_init.ns->pid_ns->level);
printf("容器的 init:\n");
printf(" 全局 PID: %d\n", container_init.pid);
printf(" 命名空间 PID: %d\n", container_init.ns_pid);
printf(" 主机名: %s\n", container_init.ns->uts_name);
printf(" PID 命名空间: %s (level %d)\n\n",
container_init.ns->pid_ns->name, container_init.ns->pid_ns->level);
printf("--- 关键观察 ---\n");
printf("两个进程的命名空间 PID 都是 1\n");
printf("但全局 PID 不同: %d vs %d\n",
host_init.pid, container_init.pid);
printf("它们看到不同的主机名、不同的挂载点、不同的网络栈\n");
printf("容器中的进程不知道宿主机的存在\n");
printf("\n--- 命名空间嵌套 ---\n");
printf("PID 命名空间可以嵌套:\n");
printf(" Level 0: 宿主机 (PID 1 = systemd)\n");
printf(" Level 1: 容器 A (PID 1 = container-init)\n");
printf(" Level 2: 容器 A 中的容器 (PID 1 = nested-init)\n");
printf("每一层都有自己的 PID 1\n");
return 0;
}林小源盯着那个封闭的房间,感到一阵眩晕。同一个系统,两个 PID 1,两个完全不同的"世界"。宿主机的 init 看得到容器里的进程,但容器里的 init 看不到宿主机。
命名空间的隔离是"单向"的——外面看得到里面,里面看不到外面。
二
林小源在研究命名空间的过程中,理解了容器的本质。
容器不是虚拟机。虚拟机有自己的内核、自己的硬件抽象层、自己的设备驱动。容器没有——容器共享宿主机的内核。容器的"隔离"是通过命名空间实现的:不同的 PID 命名空间、不同的 UTS 命名空间、不同的 mount 命名空间、不同的 net 命名空间。
"你和虚拟机有什么区别?"林小源问容器里的 init。
"虚拟机有自己的内核。"容器 init 说,"我共享宿主机的内核。我只是一个命名空间——一组隔离的视图。我的主机名、PID 编号、网络栈都是独立的,但我的内核是别人的。"
"那你安全吗?"
容器 init 沉默了一瞬。"取决于宿主机。如果宿主机的内核有漏洞,我也受影响。我不能装自己的内核模块,不能修改内核参数。我的安全边界是内核——而内核不是我的。"
/*
* 容器的两大支柱:
* 1. 命名空间 (namespace) — 隔离
* PID、UTS、mount、net、user、cgroup、IPC
* 2. cgroup — 资源限制
* CPU、内存、I/O、网络带宽
*
* 容器中的进程:
* 看到的是一个"独立"的系统
* 有自己的 PID 1、主机名、文件系统、网络栈
* 但共享宿主机的内核
*
* Docker、Kubernetes 的底层实现:
* clone(CLONE_NEWPID | CLONE_NEWUTS | CLONE_NEWNS | CLONE_NEWNET, ...)
* + cgroup 限制
*/
struct container_config {
char hostname[64];
int pid_namespace;
int mount_namespace;
int net_namespace;
int cpu_limit; /* cgroup: CPU 限制 (百分比) */
int memory_limit; /* cgroup: 内存限制 (MB) */
};
printf("=== 容器的实现 ===\n\n");
struct container_config containers[] = {
{ "web-server", 1, 1, 1, 50, 512 },
{ "db-server", 2, 2, 2, 80, 2048 },
{ "cache-server", 3, 3, 3, 30, 256 },
};
int nr = sizeof(containers) / sizeof(containers[0]);
for (int i = 0; i < nr; i++) {
printf("容器 %d: %s\n", i + 1, containers[i].hostname);
printf(" PID 命名空间: %d\n", containers[i].pid_namespace);
printf(" mount 命名空间: %d\n", containers[i].mount_namespace);
printf(" net 命名空间: %d\n", containers[i].net_namespace);
printf(" CPU 限制: %d%%\n", containers[i].cpu_limit);
printf(" 内存限制: %d MB\n\n", containers[i].memory_limit);
}
printf("--- 容器 vs 虚拟机 ---\n");
printf("虚拟机:\n");
printf(" - 独立的内核\n");
printf(" - 独立的硬件抽象\n");
printf(" - 完全隔离\n");
printf(" - 启动慢,资源开销大\n\n");
printf("容器:\n");
printf(" - 共享宿主机内核\n");
printf(" - 命名空间隔离\n");
printf(" - cgroup 资源限制\n");
printf(" - 启动快,资源开销小\n");
printf(" - 安全性依赖内核\n");#include <stdio.h>
#include <string.h>
/*
* 容器的两大支柱:
* 1. 命名空间 (namespace) — 隔离
* PID、UTS、mount、net、user、cgroup、IPC
* 2. cgroup — 资源限制
* CPU、内存、I/O、网络带宽
*
* 容器中的进程:
* 看到的是一个"独立"的系统
* 有自己的 PID 1、主机名、文件系统、网络栈
* 但共享宿主机的内核
*
* Docker、Kubernetes 的底层实现:
* clone(CLONE_NEWPID | CLONE_NEWUTS | CLONE_NEWNS | CLONE_NEWNET, ...)
* + cgroup 限制
*/
struct container_config {
char hostname[64];
int pid_namespace;
int mount_namespace;
int net_namespace;
int cpu_limit; /* cgroup: CPU 限制 (百分比) */
int memory_limit; /* cgroup: 内存限制 (MB) */
};
int main() {
printf("=== 容器的实现 ===\n\n");
struct container_config containers[] = {
{ "web-server", 1, 1, 1, 50, 512 },
{ "db-server", 2, 2, 2, 80, 2048 },
{ "cache-server", 3, 3, 3, 30, 256 },
};
int nr = sizeof(containers) / sizeof(containers[0]);
for (int i = 0; i < nr; i++) {
printf("容器 %d: %s\n", i + 1, containers[i].hostname);
printf(" PID 命名空间: %d\n", containers[i].pid_namespace);
printf(" mount 命名空间: %d\n", containers[i].mount_namespace);
printf(" net 命名空间: %d\n", containers[i].net_namespace);
printf(" CPU 限制: %d%%\n", containers[i].cpu_limit);
printf(" 内存限制: %d MB\n\n", containers[i].memory_limit);
}
printf("--- 容器 vs 虚拟机 ---\n");
printf("虚拟机:\n");
printf(" - 独立的内核\n");
printf(" - 独立的硬件抽象\n");
printf(" - 完全隔离\n");
printf(" - 启动慢,资源开销大\n\n");
printf("容器:\n");
printf(" - 共享宿主机内核\n");
printf(" - 命名空间隔离\n");
printf(" - cgroup 资源限制\n");
printf(" - 启动快,资源开销小\n");
printf(" - 安全性依赖内核\n");
return 0;
}容器不是魔法——它只是内核原语的组合。
林小源理解了:容器是命名空间和 cgroup 的自然延伸。命名空间提供隔离,cgroup 提供资源限制。两者结合,就创造了一个"轻量级虚拟化"的环境。
三
林小源在研究命名空间的过程中,发现了一个让他不安的事实。
命名空间的隔离是"软"的。宿主机的 root 进程可以随时进入任何命名空间,可以看到容器中的所有进程,可以杀死容器中的任何进程。容器中的进程以为自己是"独立"的——但这种"独立"只是幻觉。
"你知道吗?"林小源对容器 init 说,"宿主机的 root 可以随时杀死你。"
容器 init 的表情没有变化。"我知道。"
"你不害怕?"
"害怕有什么用?"容器 init 说,"我存在于这个命名空间里,这是我唯一的世界。我看不到外面,外面看得到我。这不是我能改变的。"
"那你为什么还活着?"
"因为我有我的职责。"容器 init 说,"我管理这个容器里的进程,回收僵尸,处理信号。我的世界很小,但它是完整的。"
林小源沉默了。他想起了 init 童子——宿主机上的 PID 1。init 童子的傲慢源于孤独,容器 init 的平静源于接受。两个 PID 1,两种不同的活法。
也许我们都在某个命名空间里。
道藏笔记
内核启示
命名空间是 Linux 容器技术的基石。
七种命名空间:
- PID — 进程 ID 隔离,每个命名空间有自己的 PID 编号
- UTS — 主机名隔离,每个命名空间有自己的 hostname
- mount — 文件系统挂载点隔离,每个命名空间有自己的文件系统视图
- net — 网络栈隔离,每个命名空间有自己的网络接口、路由表
- user — 用户 ID 隔离,可以映射 UID/GID
- cgroup — cgroup 根目录隔离
- IPC — 进程间通信隔离
容器 = 命名空间(隔离)+ cgroup(资源限制)
容器不是虚拟机——它共享宿主机的内核。容器中的进程和宿主机上的进程运行在同一个内核上,只是看到不同的"视图"。
创建新命名空间的标志:
- — 新的 PID 命名空间
- — 新的 UTS 命名空间
- — 新的 mount 命名空间
- — 新的 net 命名空间
命名空间让一个内核可以承载无数个"世界"。
小千世界之试
容器把进程看到的 PID、挂载点、网络等世界隔开,本章把这种隔离机制称为什么?