为什么需要这篇
后续的 Namespace,Cgroups,OverlayFS 都是 Linux 内核特性.如果你不清楚"进程是怎么创建的""/proc 是什么""网络接口为什么能配 IP",后续章节会频繁出现认知断层.本篇目标是用最短篇幅补齐这些前置概念,不求精通,但求理解"是什么,为什么".
一,进程模型
Linux 中一切工作都由进程完成.你在终端敲的每条命令,运行的每个程序,容器里的每个应用,都是一个进程.理解进程的创建方式是理解容器的第一步.
fork 与 exec - 进程的诞生方式
Linux 创建新进程只有一种途径:fork(分裂).
| 操作 | 含义 | 类比 |
|---|---|---|
fork() |
当前进程复制一份自己,产生一个子进程(内容完全相同) | 细胞分裂--分裂后两个细胞一模一样 |
exec() |
用一个新程序替换 当前进程的代码和数据 | 换魂--壳还是那个壳,里面的灵魂换了 |
典型流程(你在 shell 里输入 ls 时发生的事):
|
为什么不直接"创建"一个全新进程?因为 Linux 的设计哲学是复制比从零构建更简单--先 fork 出一份副本,再按需替换内容(exec).这个"先复制再替换"的模式贯穿整个系统.
PID - 进程的身份证号
PID(Process ID)是内核分配给每个进程的唯一编号:
- PID 1 =
init(或 systemd),系统启动后的第一个用户态进程,是所有进程的祖先 - 每个进程都有一个父进程(PPID,Parent PID)
- 进程退出后,PID 可以被回收复用
|
进程树 - 所有进程的族谱
由于每个进程都由 fork 产生,所有进程形成一棵树:
|
与容器的关系
容器里的进程也在这棵树上--它只是一个普通的子进程.但通过 PID Namespace(第 02 篇),容器内的进程"以为"自己的 PID 是 1,看不到树的其他部分.隔离的是视图,不是进程本身.
二,Linux 文件系统基础
在讨论容器技术之前,必须先把 Linux 文件系统的基本概念理清.很多人对"文件系统"的理解停留在"文件夹和文件"的层面--这在 Windows 的日常使用中够用,但在 Linux 里不够.下面我们从最核心的一条设计哲学讲起,逐层建立认知.
2.1 "一切皆文件":Linux 的核心设计哲学
Linux 有一条贯穿整个系统的设计理念:一切皆文件(Everything is a file).这句话不是比喻,而是字面意义上的事实.
什么是"文件"?
在 Linux 中,"文件"不是一个狭义概念(只有 Word 文档,图片才叫文件).文件是一个抽象接口--只要一个东西支持以下四个基本操作,它在 Linux 眼里就是"文件":
| 系统调用 | 含义 | 类比 |
|---|---|---|
open() |
打开一个文件,获得文件描述符(file descriptor) | 拿起电话听筒 |
read() |
从文件中读取数据 | 听对方说话 |
write() |
向文件中写入数据 | 对着话筒说话 |
close() |
关闭文件,释放文件描述符 | 挂断电话 |
关键是:读一个 .txt 文件,读键盘输入,读网络数据,读进程信息,用的都是同一套 open → read → write → close 系统调用.程序员不需要为"不同类型的 IO"写完全不同的代码,这是巨大的简化.
哪些东西在 Linux 里是"文件"?
| 类型 | 例子 | 你平时就在这里看到它 |
|---|---|---|
| 普通文件(Regular File) | 源代码,日志,图片,二进制程序 | ~/project/main.go |
| 目录(Directory) | 文件夹--本质上是一个存有"文件名→inode 编号"映射关系的特殊文件 | 任何一个文件夹 |
| 设备文件(Device File) | 硬盘,键盘,鼠标,终端 | /dev/sda(硬盘),/dev/tty(终端) |
| 管道(Pipe / FIFO) | 进程间单向数据流 | ls | grep foo |
| 套接字(Socket) | 网络连接端点,也可以是本机进程间通信 | /var/run/docker.sock |
| 符号链接(Symlink) | 指向另一个文件路径的"快捷方式" | /usr/bin/python → python3 |
| 虚拟文件(Virtual File) | 内核实时生成的"假"文件,不存在于磁盘 | /proc/cpuinfo,/sys/class/net/eth0/address |
一切皆文件 = 一切都有统一的读写接口.想象一个万能插座:不管你插的是台灯,电视,电风扇,手机充电器,插座提供的都是 220V 交流电.Linux 的"文件接口"就是这个万能插座--不论底层是硬盘,键盘,网卡还是内核数据,上层都用同一套 open/read/write/close 来操作.
一个具体例子来感受
假设你想获取当前系统有几颗 CPU 核心.在 Linux 里,你不需要调用特殊 API,直接"读文件"就行:
|
再比如,你想限制一个进程的 CPU 使用量(第 03 篇 Cgroups 的核心操作),不需要特殊工具,直接"写文件":
|
读文件 = 查询信息,写文件 = 修改配置. 这就是 Linux 把复杂性隐藏在统一接口背后的方式.
关键认知
"一切皆文件"不是 Linux 的专利(Unix 首创),但 Linux 把这条哲学贯彻得最彻底.理解这一点后,你会发现:在 Linux 里,"探索系统"就是"浏览文件"--你用 ls 和 cat 就能窥探内核的内部状态,不需要特殊工具.这是 Linux 作为学习平台的巨大优势.
2.2 统一目录树:没有 C 盘 D 盘的世界
Windows 用户习惯了 C:\,D:\,E:\ 这样的盘符体系--每个分区是一个独立的"根",各自拥有一棵目录树:
|
Linux 的设计完全不同:整个系统只有一棵目录树,根就是 /.不管你插了几个 U 盘,挂了几块硬盘,它们全部"接入"到这棵树的某个分支上:
|
Windows 盘符 = 独立的岛屿,每个岛有自己的地图系统.C 岛和 D 岛之间没有天然的层次关系.Linux 目录树 = 一棵大树,/ 是树干,所有存储设备和文件系统都是长在这棵树上的树枝.你要访问 U 盘?它就在 /mnt/usb 这个树枝上--和访问 /home/yutong 没有任何语法差异.
这种设计为什么更好?
- 位置透明:程序不需要关心数据在哪个物理设备上.读
/home/yutong/file.txt就行了--它可能在本地 SSD 上,也可能是网络挂载的 NFS 远程文件系统,对程序来说没有区别. - 灵活挂载:任何目录都可以成为"接入点".你可以把一整块硬盘挂到
/home/yutong/projects,让那个目录拥有独立的存储空间--不需要给它单独的盘符. - 容器的基础:容器的 rootfs 就是给容器进程"换一个根"--让容器进程看到的
/不是宿主机的/,而是一个隔离的目录树(通过pivot_root或chroot实现).没有"统一树"的设计,容器文件系统隔离会复杂得多.
2.3 inode:文件在磁盘上的"身份证"
有了"一切皆文件"和"统一目录树"的认知后,还有一个关键概念需要理解:文件名和文件内容是两个不同的东西,它们通过 inode 关联.
什么是 inode?
inode(Index Node,索引节点)是文件系统内部的一个数据结构,每个文件(或目录)都对应唯一一个 inode.inode 里存的是:
| inode 存储的内容 | 说明 |
|---|---|
| 文件类型与权限 | 是普通文件,目录还是符号链接?rwx 权限是什么? |
| 所有者信息 | UID(属主),GID(属组) |
| 大小 | 文件占多少字节 |
| 时间戳 | 最后访问时间(atime),最后修改时间(mtime),最后属性变更时间(ctime) |
| 数据块指针 | 文件的实际数据存在磁盘的哪些位置 |
| 链接计数 | 有多少个"文件名"指向这个 inode |
注意 inode 里不存什么:不存文件名.文件名存在哪里?存在目录里.目录本质上是一个"文件名 → inode 编号"的映射表:
|
inode = 房产证,记录着"这块地(数据块)的面积,位置,产权人".文件名 = 门牌号,只是贴在门口的标签.房产证(inode)只有一份,但一栋房子可以有多个门牌号(多个文件名指向同一个 inode)--这就是 硬链接(hard link) 的本质.
这解释了哪些日常现象?
为什么 mv (移动/重命名)几乎瞬间完成? 因为 mv 只修改了目录里的"文件名 → inode 编号"映射,没有碰实际数据.只要源和目标在同一个文件系统内(inode 编号空间一致),mv 就是改一行记录而已.
|
为什么删除大文件很快,但删除很多小文件很慢? 删除文件本质是减少 inode 的链接计数 + 标记数据块为可用.大文件只有一个 inode 要处理;100 万个小文件有 100 万个 inode 要逐个处理.
为什么硬链接不能跨文件系统? 因为 inode 编号只在单个文件系统内有意义.文件系统 A 的 inode 789012 和文件系统 B 的 inode 789012 是完全不同,毫无关系的两个文件.
|
inode 与容器的关系
OverlayFS(第 04 篇)的分层机制本质上是在不同层之间管理 inode 的指向关系.写时复制(Copy-on-Write)就是:当你要修改 lower 层的一个文件时,先把它的 inode 指向的数据块复制到 upper 层,然后修改 upper 层的副本--所有这些操作对容器进程透明.
2.4 深入理解 mount:把文件系统"接入"目录树
现在你理解了"统一目录树"和"inode",挂载就好理解了.在讲命令之前,先建立直觉.
挂载的本质是什么?
一块磁盘(或一个虚拟文件系统)在被格式化后,上面有一个独立的文件系统--它有自己的目录结构,自己的 inode 编号空间.但在挂载之前,这个文件系统和 Linux 的目录树是彼此隔离的,你无法访问它.
挂载(mount)就是把一个独立的文件系统"接入"到 Linux 目录树的某个节点上,让那个节点成为访问这个文件系统的入口.
|
挂载 = 把 U 盘插到目录上. Linux 的目录树是一个插线板,每个目录节点都可以是一个"插座".当你把 U 盘 mount 到 /mnt/usb,就等于把 U 盘插进了这个插座--之后电流(数据访问)就通了.当你 umount,就是拔掉插头--U 盘还是那个 U 盘,但不再能通过目录树访问到了.
为什么需要挂载?为什么不直接"看到"所有设备?
回到"统一目录树"的设计:因为 Linux 只有一棵树,所以任何存储资源要可访问,就必须成为这棵树的一部分.挂载就是"成为这棵树的一部分"的操作.
Windows 其实也有挂载--只是把挂载点固定在盘符(C:,D:)上,你感知不到"挂载"这个动作.Linux 把这个权力完全交给用户:你想把 U 盘挂到哪个目录都行.
挂载不只是接 U 盘
在前面的"一切皆文件"小节你已经看到:硬盘上的 ext4/xfs 文件系统需要挂载,虚拟文件系统同样需要挂载.实际上,Linux 系统启动时,内核会先挂载一个临时 rootfs(在内存中),然后再把真正的根文件系统挂载到 /:
|
| 文件系统类型 | 存储在哪里 | 典型挂载点 | 容器场景 |
|---|---|---|---|
ext4 / xfs |
磁盘 | /,/home |
容器镜像的底层存储 |
tmpfs |
内存(掉电丢失) | /tmp,/run |
容器的 tmpfs 挂载 |
proc |
内核虚拟生成 | /proc |
每个容器需要独立的 /proc |
sysfs |
内核虚拟生成 | /sys |
Cgroup 路径依赖 /sys 挂载 |
overlay |
基于多个下层目录构建 | 容器的 rootfs | Docker 镜像分层(第 04 篇) |
bind mount |
把现有目录"映射"到另一个位置 | 任意 | docker run -v /host/path:/container/path |
常用挂载命令
|
挂载与容器的关系--这是理解容器文件系统的关键
容器的文件系统隔离通过 Mount Namespace(第 02 篇)实现.每个容器拥有独立的挂载表(mount table):
- 宿主机看不到容器内部的挂载操作
- 容器看不到宿主机的挂载表全貌(除非显式共享)
- Docker 的
-v /host:/container本质上就是一个 bind mount,把宿主机的目录"映射"到容器内部 - Docker 镜像的分层文件系统(OverlayFS)也是通过 mount 实现把多个层合并挂载为容器的
/
一个帮助你记住的直觉
你可以把 Linux 的文件系统想成一棵树 + 很多个"挂件".树干和主要树枝是根文件系统(ext4/xfs 在磁盘上),而 /proc,/sys,U 盘,OverlayFS 都是"挂件"--它们通过 mount 挂在树的某个节点上.内核负责让这一切看起来像是一棵完整的,无缝的树,这就是 VFS(Virtual File System)层的工作.
2.5 虚拟文件系统的实例:/proc 与 /sys
前面讲了很多抽象概念,现在来看两个最重要的虚拟文件系统实例.它们完美体现了"一切皆文件"哲学.
/proc - 进程信息的窗口
/proc 是一个虚拟文件系统--它不占磁盘空间,内容由内核实时生成.每次你读取 /proc 下的文件,内核都会现场计算并返回当前状态.
| 路径 | 内容 | 谁在用 |
|---|---|---|
/proc/[PID]/ |
某个进程的详细信息 | ps,top,htop |
/proc/[PID]/ns/ |
进程所属的各个 Namespace | nsenter,docker exec |
/proc/[PID]/cgroup |
进程所属的 Cgroup | Cgroup 管理工具 |
/proc/meminfo |
系统内存使用情况 | free 命令 |
/proc/cpuinfo |
CPU 信息 | lscpu |
|
关键认知
ps 命令不是"直接问内核有哪些进程",而是遍历 /proc 目录,读取每个数字子目录的信息拼装输出.所以在容器中如果 /proc 没有重新挂载(还是宿主机的),ps 就会显示宿主机所有进程--这就是 Namespace 篇中 --mount-proc 参数的意义.
/sys - 内核子系统的控制面板
/sys(sysfs)也是虚拟文件系统,主要暴露内核子系统和设备 的信息:
/sys/fs/cgroup/- Cgroup 文件系统的入口(第 03 篇核心内容)/sys/class/net/- 所有网络接口的信息/sys/block/- 块设备信息(磁盘)
|
/proc 偏向"进程视角"(每个进程的状态),/sys 偏向"设备/子系统视角"(硬件和内核模块的状态).两者都是内核暴露信息的窗口,容器技术大量依赖它们--Cgroup 通过写文件限制资源,Namespace 通过读文件查看隔离状态.
三,网络基础
网络接口(Network Interface)
网络接口是 Linux 收发网络数据的端点,可以是物理的也可以是虚拟的:
| 接口 | 类型 | 用途 |
|---|---|---|
eth0 |
物理网卡 | 连接外部网络 |
lo |
回环(loopback) | 本机自己跟自己通信(127.0.0.1) |
docker0 |
虚拟网桥 | Docker 容器的默认网关 |
vethXXXX |
虚拟以太网 | 连接容器和网桥 |
对内核来说,物理网卡和虚拟网卡没有本质区别--都是"网络接口"这个抽象的实例,都能配 IP,收发数据包.这是容器网络的基础.
|
IP 地址与子网
私有地址(内网地址)
RFC 1918 规定了三段永远不会出现在公网 的地址,任何人都可以在内网随意使用:
| 网段 | 范围 | 常见用途 |
|---|---|---|
10.0.0.0/8 |
10.0.0.0 ~ 10.255.255.255 | 大型企业内网,云厂商 VPC,容器网络 |
172.16.0.0/12 |
172.16.0.0 ~ 172.31.255.255 | Docker 默认网段(172.17.0.0/16) |
192.168.0.0/16 |
192.168.0.0 ~ 192.168.255.255 | 家庭路由器,小型办公网络 |
CIDR 表示法
IP 地址 32 位,/数字 表示前多少位是网络号,剩余是主机号:
|
类比邮政地址:网络号 = 街道名,主机号 = 门牌号.同一条街(同一子网)的住户可以直接喊话通信,不同街道需要通过邮局(路由器)转发./24 就是说"街道名占了地址的前 24 位".
快速记忆:
/24= 256 个地址(一个"小区"),最常用/16= 65536 个地址(一个"城区")/8= 约 1677 万个地址(一个"城市")- 数字越大,子网越小
veth pair - 虚拟网线
veth= Virtual Ethernet.veth pair 是成对出现的虚拟网络接口--数据从一端进,必定从另一端出:
|
物理世界里,两台电脑用一根网线直连就能通信.veth pair 就是这根网线的虚拟版.Network Namespace 隔离后两个空间完全不通(像两个密封的房间),veth pair 就是在墙上打一个洞穿一根线,让两边能互通数据.
|
网桥(Bridge)- 虚拟交换机
veth pair 是一对一的连接.当有多个容器需要互通时,不可能两两直连(N 个容器需要 N*(N-1)/2 根线).这时需要网桥(Bridge):
|
| 物理世界 | 虚拟世界 |
|---|---|
| 交换机(Switch) | Linux Bridge(如 docker0) |
| 网线插到交换机端口 | veth 一端连到 Bridge |
| 交换机转发数据帧给目标端口 | Bridge 按 MAC 地址转发给目标 veth |
|
Docker 网络的本质
安装 Docker 后系统多出一个 docker0 网桥.每创建一个容器,Docker 就生成一对 veth--一端放进容器的 Network Namespace(容器里看到的 eth0),另一端接到 docker0 网桥.所有容器通过这个网桥互通,再通过 NAT/iptables 规则与外网通信.
小结
本篇要点回顾
| 模块 | 核心认知 | 后续关联 |
|---|---|---|
| 进程模型 | fork 复制 + exec 替换;PID 是进程身份证;所有进程形成树 | PID Namespace 让容器看到独立的进程树(第 02 篇) |
| 文件系统基础 | 一切皆文件(统一 open/read/write/close 接口);统一目录树(单根 /);inode 是文件身份证(文件名≠文件内容) | 容器 rootfs 就是一棵独立的目录树;OverlayFS 的分层本质是 inode 指向的切换(第 04 篇) |
| mount 挂载 | 所有文件系统必须通过 mount 接入目录树才能访问;不只是接 U 盘,proc/sys/overlay 等虚拟文件系统也通过 mount 接入 | Mount Namespace 隔离挂载表;Docker -v 本质是 bind mount;OverlayFS 通过 mount 实现分层合并(第 02,04 篇) |
| /proc 与 /sys | 虚拟文件系统,不占磁盘,内核实时生成;ps 命令读的是 /proc 而非直接问内核 | Namespace 通过 /proc/PID/ns/ 暴露;Cgroups 通过 /sys/fs/cgroup/ 控制资源(第 03 篇) |
| 网络接口 | 物理网卡和虚拟网卡对内核无区别,都能配 IP 收发数据 | Network Namespace 给容器一套独立的网络接口(第 02 篇) |
| IP/子网/CIDR | 10.x / 172.16~31.x / 192.168.x 是私有地址;/24 表示子网大小 | 容器网络全部使用私有地址 |
| veth pair | 成对的虚拟网线,数据从一端进另一端出 | 连接容器 NS 与宿主机网桥(第 02 篇 Network NS) |
| 网桥 | 虚拟交换机,多个 veth 接入后可互通 | docker0 就是一个网桥,所有容器通过它互联 |
动手练习
- 运行
pstree -p观察你的系统进程树,找到当前 shell 的位置 - 创建一个临时文件,用
ls -li查看它的 inode 编号;mv重命名后再次查看,验证 inode 不变;创建一个硬链接,观察两个文件名是否共享同一个 inode - 用
ls /proc/$$/探索你当前 bash 进程的信息,尝试读取cmdline,status,ns/ - 运行
mount | grep -E 'proc|sys|cgroup',看看系统中有哪些虚拟文件系统被挂载 - 用
ip addr查看你系统上有哪些网络接口,哪些是物理的,哪些是虚拟的 - 创建一对 veth,配上 IP,用
ping验证连通性后删除