如果你想从 0 到 1 理解 Docker,第一步不是安装 Docker,不是背 docker run 命令,也不是研究镜像仓库,而是先回答一个问题:
容器到底是什么?
这个问题如果一开始答歪了,后面所有东西都会变成一堆零散概念;但如果这个问题答对了,后面的 namespace、cgroups、rootfs、overlayfs、runc、containerd 都会顺起来。
这一章的目标,就是建立一个最核心的认识:
容器本质上不是虚拟机,而是宿主机上的进程。
注意,这不是一句比喻,而是接近 Linux 内核现实的说法。
1.1 先从"程序"和"进程"说起
在理解容器之前,你必须先分清"程序"和"进程"。
程序是磁盘上的静态文件,比如:
/bin/bash/usr/sbin/nginxpythonredis-server
它们只是文件,还没有真正运行。
进程则是程序被操作系统加载到内存之后的运行实例。进程有很多运行时属性,比如:
- PID(进程号)
- 内存空间
- 打开的文件描述符
- 当前工作目录
- 环境变量
- 用户身份
所以你可以简单记成:
- 程序 = 静态文件
- 进程 = 运行中的程序实例
这个区分非常重要,因为容器最终一定会落到"某个程序被启动成一个进程"这件事上。
1.2 Docker 的第一性原理:先有进程,再有容器感
很多初学者会自然脑补这样一个过程:
Docker 先创造一台小型 Linux 机器,然后再在里面启动 nginx、bash、python。
这个脑补不完全错,但从底层原理上讲,它不够准确。
更接近真实原理的顺序其实是:
- 先启动一个普通 Linux 进程
- 再给这个进程施加隔离和限制
- 再给它准备一个独立文件系统视角
- 于是这个进程看起来像运行在独立机器里
也就是说,Docker 不是先创造一台机器,再把进程塞进去;而是先有进程,再把这个进程"包装"成容器。
这是整套容器技术最重要的思维转换。
1.3 容器不是一种新物种
从底层看,容器不是 Linux 里突然冒出来的一种神秘对象。
它更像是:
一个普通进程,被放进了特殊的运行环境里。
这个"特殊运行环境"主要包括三类东西:
第一类:隔离视图
让这个进程看到的系统世界,不再是宿主机完整世界,而是一个局部世界。
这主要依赖 namespace。
第二类:资源限制
让这个进程不能无限吃 CPU、内存、IO。
这主要依赖 cgroups。
第三类:独立文件系统视角
让这个进程眼里的 / 看起来像自己的一套系统目录,而不是直接面对宿主机根目录。
这主要依赖 rootfs、mount namespace、overlayfs 等机制。
所以我们可以先写出一个非常重要的公式:
容器 = 进程 + 隔离视图 + 资源限制 + 独立文件系统视角
你后面学的每一章,其实都是在把这个公式拆开讲。
1.4 容器和虚拟机最本质的区别
这是第一章里一定要讲清楚的一件事。
虚拟机做的事情
虚拟机是在硬件抽象层 做隔离。
它会虚拟出:
- CPU
- 内存
- 磁盘
- 网卡
然后在这套虚拟硬件上再运行一个完整的 Guest OS。
所以虚拟机内部通常有自己的内核。
容器做的事情
容器不是虚拟硬件。
容器是在操作系统层 做隔离。
它不自己带一套内核,而是共享宿主机 Linux 内核。
所以可以把两者差异概括成一句话:
- VM 虚拟的是硬件
- Container 隔离的是进程运行环境
这就是为什么容器一般比虚拟机更轻、更快、启动更迅速,但隔离层级通常也没有虚拟机那么重。
1.5 为什么说容器本质上还是宿主机上的进程
现在回到本章最重要的命题。
为什么说容器本质上还是宿主机上的进程?
因为无论你在命令行里敲的是:
bash
docker run nginx
还是:
bash
docker run ubuntu bash
最后一定都会落到一件事上:
宿主机 Linux 内核启动了一个真实进程。
比如:
docker run nginx,本质上会有一个 nginx 主进程跑起来docker run ubuntu bash,本质上会有一个 bash 进程跑起来docker run redis,本质上会有一个 redis-server 进程跑起来
这些进程不是假的,不是模拟出来的,它们就是宿主机内核调度的真实进程,只不过它们被放进了特殊的 namespace、cgroup 和 rootfs 环境里。
所以"容器是进程"这句话,不是说容器和进程完全一模一样,而是说:
容器的存在基础,最终还是一个或一组真实的 Linux 进程。
这是你以后理解一切 Docker 现象的根。
1.6 容器为什么看起来像一台机器
既然容器本质上只是进程,那为什么它又会看起来像一台独立机器?
因为这个进程被精心"布景"了。
它被布景的结果是:
- 看自己的进程列表时,好像只有自己和少量相关进程
- 看自己的 hostname 时,好像自己是一台独立主机
- 看自己的网络时,好像有独立网卡和 IP
- 看自己的根目录
/时,好像自己拥有一整套 Linux 文件系统 - 使用资源时,又像被分配了一台小机器的 CPU / 内存额度
也就是说,容器的"机器感"不是来自一套独立硬件,而是来自多个 Linux 内核机制叠加出的错觉。
后面你会学到:
- namespace 负责"你看到了什么世界"
- cgroups 负责"你最多能用多少资源"
- rootfs 负责"你眼中的
/是什么"
这些机制叠起来,进程就越来越像运行在一台独立机器里。
1.7 为什么容器生命周期通常和主进程绑定
这是第一章另一个必须打牢的理解点。
当你执行:
bash
docker run nginx
Docker 会启动一个入口命令。这个入口命令就是容器的主进程,通常在容器内部被视为 PID 1。
只要这个主进程还活着,容器通常就被认为还在运行。
一旦这个主进程退出,容器通常也就结束。
为什么?
因为容器不像虚拟机那样有一个完整独立操作系统生命周期。
它更像是:
为某个主进程临时搭起的一套运行时上下文。
这个上下文包括:
- 进程隔离
- 网络隔离
- 文件系统视角
- 资源限制
- 日志、元数据、生命周期管理
主进程活着,这套上下文就有意义;主进程退出,这套上下文通常也就该收掉了。
所以不要把容器想成"永远存在的一台小电脑",而要把它想成:
围绕主进程搭起来的一座舞台。
演员退场,舞台通常也就收了。
1.8 镜像不是容器,容器也不是镜像
初学者还很容易把这两个概念混起来。
镜像 Image
镜像是静态模板。
它更像是:
- 一套准备好的文件系统
- 程序及依赖
- 默认启动配置
镜像本身不运行。
容器 Container
容器是镜像启动后的运行实例。
它包括:
- 运行中的主进程
- 容器可写层
- 隔离和限制环境
- 生命周期状态
所以关系更像:
- 镜像:模板
- 容器:模板启动后的实例
你可以把它类比成:
- 类和对象
- 程序安装包和运行中的程序实例
这个区分后面讲 Dockerfile、镜像分层时会非常重要。
1.9 第一章最重要的总结
如果你只记住这一章的 5 句话,那就记这 5 句:
- 程序是静态文件,进程是运行中的程序实例。
- 容器本质上还是宿主机上的真实 Linux 进程。
- Docker 更接近真实原理的顺序是:先启动进程,再给进程施加隔离和限制。
- 虚拟机虚拟的是硬件,容器隔离的是进程运行环境。
- 容器生命周期通常和主进程绑定,因为容器本来就是围绕主进程搭起来的运行时上下文。
如果这 5 句你已经吃透,第一章就算真正过关了。
1.10 第一章的自测题
你可以用这几题检查自己是不是真的理解了,而不是"看着像懂了"。
问题 1
为什么说容器不是轻量虚拟机?
你应该能答出:
因为容器不虚拟硬件,也没有独立 Guest OS 和独立内核;它共享宿主机 Linux 内核,只是在操作系统层隔离进程运行环境。
问题 2
为什么说容器本质上还是进程?
你应该能答出:
因为容器运行时最终落地成宿主机上的一个或一组真实 Linux 进程,这些进程被放进特殊的 namespace、cgroup 和文件系统环境里。
问题 3
为什么容器退出通常和主进程退出有关?
你应该能答出:
因为容器并不是一台独立存在的小机器,而是围绕入口主进程搭建的运行时上下文,主进程退出后,容器的主要运行目标也结束了。