为什么单独抽出 state.go 来学
Container 结构体有 40+ 字段,但真正决定"docker 怎么对待这个容器"的只有 State:
docker ps显示Up 3 seconds还是Exited (0) 5 minutes ago,由State决定。
docker wait <id>阻塞还是立即返回,由State决定。
docker rm能否执行,要先看State.RemovalInProgress。
docker start一个已经在 Running 的容器要报错,判断依据也是State。
所以 state.go是 docker 容器子系统里"逻辑密度最高"的一个文件------481 行代码涵盖了状态字段、转移方法、并发原语、等待者机制和外部状态查询接口。
1. State 结构体定义
源码位置:daemon/container/state.go:54-93
Go
type State struct {
// 这把锁是 exported by design,作为整个 Container 的全局锁使用
sync.Mutex
// ⚠️ 注意:这三个字段 NOT mutually exclusive(非互斥!)
Running bool
Paused bool
Restarting bool
OOMKilled bool
RemovalInProgress bool `json:"-"` // 不持久化(进程重启后这个状态无意义)
Dead bool
Pid int
ExitCode int `json:"ExitCode"`
ErrorMsg string `json:"Error"`
StartedAt time.Time
FinishedAt time.Time
Health *Health
Removed bool `json:"-"`
// 等待者 channel 列表(私有)
stopWaiters []chan<- StateStatus
removeOnlyWaiters []chan<- StateStatus
// libcontainerd 句柄(私有,强制通过 getter 访问)
ctr libcontainerdtypes.Container
task libcontainerdtypes.Task
}
1.1 字段分类(三个语义层)
|---------|---------------------------------------------------------------------------|-----------------------|--------------------------|
| 语义层 | 字段 | 持久化? | 说明 |
| 进程态 | Running、Paused、Restarting、Pid、StartedAt、FinishedAt、ExitCode | ✅ 写入 config.v2.json | 描述容器进程当前在干什么 |
| 结果态 | OOMKilled、Dead、ErrorMsg | ✅ | 进程结束的原因/错误信息 |
| 管理态 | RemovalInProgress、Removed | ❌(json:"-") | 删除流程的内存标记,重启后无意义 |
| 健康态 | Health | ✅(指针) | 健康检查状态,独立 goroutine 异步更新 |
| 句柄 | ctr、task | ❌(私有) | containerd 进程引用,运行时才有意义 |
1.2 为什么这些字段是 json:"-"
RemovalInProgress 和 Removed:
- 它们表达的是当前 daemon 进程内的瞬时操作状态。
- 假设 daemon 被 kill -9 杀掉,正在进行的删除操作就丢失了,重启后不应该认为"还在删除中"。
- 所以这两个字段强制不持久化 ,daemon 重启后
RemovalInProgress=false, Removed=false。
1.3 ctr 和 task 为什么是私有
源码位置:state.go:86-92
Go
// The libcontainerd reference fields are unexported to force consumers
// to access them through the getter methods with multi-valued returns
// so that they can't forget to nil-check
ctr libcontainerdtypes.Container
task libcontainerdtypes.Task
获取必须通过 Task() / C8dContainer():
Go
// state.go:478
func (s *State) Task() (_ libcontainerdtypes.Task, ok bool) {
return s.task, s.task != nil
}
强制多返回值的妙处 :调用方写 task := s.Task() 会编译报错(返回值数量不匹配),必须写 task, ok := s.Task()。这样 nil-check 在编译期就被强制了,避免运行时空指针 panic。
Container.GetRunningTask()(container.go:910)就是用这个机制做的强校验:
Go
func (container *Container) GetRunningTask() (libcontainerdtypes.Task, error) {
if !container.State.Running {
return nil, errdefs.Conflict(...)
}
tsk, ok := container.State.Task()
if !ok {
return nil, errdefs.System(... "in Running state but has no containerd Task set")
}
return tsk, nil
}
2. 七种对外状态:从字段到枚举
源码位置:api/types/container/state.go:12-19
Go
StateCreated ContainerState = "created"
StateRunning ContainerState = "running"
StatePaused ContainerState = "paused"
StateRestarting ContainerState = "restarting"
StateRemoving ContainerState = "removing"
StateExited ContainerState = "exited"
StateDead ContainerState = "dead"
这 7 个枚举是 API 层暴露给用户 的状态(docker inspect / docker ps 看到的就是它们)。
但 State 内部的字段是多个 bool,怎么从字段映射到枚举? 答案是 State.State() 方法。
2.1 State.State() 的优先级判定
源码位置:state.go:154-180
Go
func (s *State) State() container.ContainerState {
if s.Running {
if s.Paused {
return container.StatePaused
}
if s.Restarting {
return container.StateRestarting
}
return container.StateRunning
}
if s.RemovalInProgress {
return container.StateRemoving
}
if s.Dead {
return container.StateDead
}
if s.StartedAt.IsZero() {
return container.StateCreated
}
return container.StateExited
}
判定优先级(从高到低):
Go
1. Running?
├─ Paused → StatePaused
├─ Restarting → StateRestarting
└─ (默认) → StateRunning
2. RemovalInProgress? → StateRemoving
3. Dead? → StateDead
4. StartedAt.IsZero()? → StateCreated (create 后从未 start)
5. 其他 → StateExited
关键洞察 :Running 是优先级最高的判定。即使同时设了 Dead=true,只要 Running=true,对外仍然是 Running 状态。这种设计避免了"容器还在跑但已经被标记 Dead"这种不一致对外暴露。
2.2 为什么字段不互斥
源码位置:state.go:58-68(注释)+ state.go:249-258 等多处
三个组合场景:
|--------------------------------------------------|------------|------------------------------------------------------------------|
| 组合 | 含义 | 触发场景 |
| Running=true && Paused=true | 进程在跑但被冻结 | Linux 上 docker pause 用 freezer cgroup 暂停所有进程,冻结的前提是进程必须先运行 |
| Running=true && Restarting=true | 进程即将被重启替换 | 重启策略触发,旧进程刚退出但新的还没起来 |
| Running=true && Paused=true && Restarting=true | 理论可能但实践中罕见 | 暂停状态下被重启策略触发(通常不会发生) |
对外状态机 :State.State() 用 if/else 优先级屏蔽了内部字段的非互斥性。用户看到的就是 7 个离散状态,内部却是多个 bool 叠加。
2.3 String():CLI 友好输出
源码位置:state.go:116-149
docker ps 的 STATUS 列就是 State.String() 的输出。它的优先级和 State() 略有不同:
Go
func (s *State) String() string {
if s.Running {
if s.Paused {
return fmt.Sprintf("Up %s (Paused)", units.HumanDuration(...))
}
if s.Restarting {
return fmt.Sprintf("Restarting (%d) %s ago", s.ExitCode, ...)
}
if h := s.Health; h != nil {
return fmt.Sprintf("Up %s (%s)", ..., h.String()) // 如 "Up 5 minutes (healthy)"
}
return fmt.Sprintf("Up %s", units.HumanDuration(...))
}
if s.RemovalInProgress { return "Removal In Progress" }
if s.Dead { return "Dead" }
if s.StartedAt.IsZero(){ return "Created" }
if s.FinishedAt.IsZero(){ return "" }
return fmt.Sprintf("Exited (%d) %s ago", s.ExitCode, ...)
}
比 State()多了:
Up X minutes (healthy)------ 健康检查状态嵌入。
Exited (0) 5 minutes ago------ 退出码 + 时间。
Restarting (1) 2 seconds ago------ 重启中显示上一次退出码。
3. 状态转移方法详解
State 的写入方法分两类:
- 带锁版本 (
SetRemovalInProgress、SetRemovalError等):自己Lock/Unlock,可在任何上下文调用。
- 不带锁版本 (
SetRunning、SetStopped、SetRestarting等):调用方必须先持有锁。注释明确写 "without locking"。
3.1 进入运行态:SetRunning / SetRunningExternal
源码位置:state.go:278-309
Go
func (s *State) SetRunning(ctr libcontainerdtypes.Container, tsk libcontainerdtypes.Task, start time.Time) {
s.setRunning(ctr, tsk, &start)
}
func (s *State) SetRunningExternal(ctr libcontainerdtypes.Container, tsk libcontainerdtypes.Task) {
s.setRunning(ctr, tsk, nil) // 不设置 StartedAt
}
func (s *State) setRunning(ctr, tsk, start *time.Time) {
s.ErrorMsg = ""
s.Paused = false
s.Running = true
s.Restarting = false
s.ExitCode = 0
s.ctr = ctr
s.task = tsk
if tsk != nil {
s.Pid = int(tsk.Pid())
} else {
s.Pid = 0
}
s.OOMKilled = false
if start != nil {
s.StartedAt = start.UTC()
}
}
两者的区别(state.go:283 注释):
SetRunning:docker 自己启动容器时用,会更新StartedAt。
SetRunningExternal:docker 接管一个已经在跑的容器 (比如外部用 runc 启动,docker attach)时用,不动StartedAt------因为不知道它的真实启动时间。
字段清理要点 :setRunning 会把 Paused、Restarting、ExitCode、ErrorMsg、OOMKilled 全部清零。这是为了保证状态机的"干净重置",避免历史脏数据污染新启动周期。
3.2 进入停止态:SetStopped
源码位置:state.go:311-325
Go
func (s *State) SetStopped(exitStatus *ExitStatus) {
s.Running = false
s.Paused = false
s.Restarting = false
s.Pid = 0
if exitStatus.ExitedAt.IsZero() {
s.FinishedAt = time.Now().UTC()
} else {
s.FinishedAt = exitStatus.ExitedAt
}
s.ExitCode = exitStatus.ExitCode
s.notifyAndClear(&s.stopWaiters) // ★ 关键:通知所有等待"停止"的调用方
}
注意 Pid 被清零 ,但 ctr / task 没动------因为这两个是 libcontainerd 句柄,即使进程退出,task 对象在 containerd 那边还存在(可以查 exit status、可以 attach stdio 残留)。
3.3 进入重启态:SetRestarting
源码位置:state.go:327-344
Go
func (s *State) SetRestarting(exitStatus *ExitStatus) {
// we should consider the container running when it is restarting because of
// all the checks in docker around rm/stop/etc
s.Running = true // ★ 注意:Running 仍是 true!
s.Restarting = true
s.Paused = false
s.Pid = 0
...
s.notifyAndClear(&s.stopWaiters)
}
什么 Running=true? 注释说得很清楚:重启中的容器对外应该按"运行中"对待 ,这样 docker rm、docker stop 等命令的检查逻辑可以统一("容器还活着,不能直接删")。
3.4 删除流程:SetRemovalInProgress / SetRemoved / SetRemovalError
源码位置:state.go:394-442
Go
// 标记开始删除(带锁,返回是否已经在删除中)
func (s *State) SetRemovalInProgress() bool {
s.Lock()
defer s.Unlock()
if s.RemovalInProgress {
return true // 已经在删了,调用方应报错
}
s.RemovalInProgress = true
return false
}
// 删除成功完成
func (s *State) SetRemoved() {
s.SetRemovalError(nil)
}
// 删除失败:记录错误 + 通知所有 waiter(stop + remove)
func (s *State) SetRemovalError(err error) {
s.SetError(err)
s.Lock()
s.Removed = true
s.notifyAndClear(&s.removeOnlyWaiters)
s.notifyAndClear(&s.stopWaiters) // ★ 也通知 stopWaiters
s.Unlock()
}
为什么 SetRemovalError同时通知两类 waiter? 注释解释(state.go:209-211):
Removal wakes up both removeOnlyWaiters and stopWaiters.
Container could be removed while still in "created" state
in which case it is never actually stopped.
容器可能在 Created 状态下被删(从未 start),此时它永远不会进入 Stopped。但 docker wait 的调用方在等"停止或删除",必须被唤醒,否则永远阻塞。
3.5 状态转移总结表
|--------------------------|----------------------------------------------------------------------------|---------------------------------|-----------------------------------|
| 方法 | 改动的字段 | 唤醒的 waiter | 锁责任 |
| SetRunning | Running=T, Paused=F, Restarting=F, Pid=task.Pid, ExitCode=0, StartedAt=now | 无 | 调用方 |
| SetRunningExternal | 同上但不改 StartedAt | 无 | 调用方 |
| SetStopped | Running=F, Paused=F, Restarting=F, Pid=0, FinishedAt=now, ExitCode=n | stopWaiters | 调用方 |
| SetRestarting | Running=T, Restarting=T, Pid=0, FinishedAt=now, ExitCode=n | stopWaiters | 调用方 |
| SetError | ErrorMsg=err | 无 | 无(纯字段写) |
| SetExitCode | ExitCode=n | 无 | 调用方(注释说 "if state may be shared") |
| SetRemovalInProgress | RemovalInProgress=T | 无 | 方法自带 |
| ResetRemovalInProgress | RemovalInProgress=F | 无 | 方法自带 |
| SetRemoved | Removed=T, ErrorMsg=清空 | stopWaiters + removeOnlyWaiters | 内部走 SetRemovalError |
| SetRemovalError | Removed=T, ErrorMsg=err | stopWaiters + removeOnlyWaiters | 方法自带 |
4. 等待者机制:docker wait 的实现核心
源码位置:state.go:182-244
4.1 三种等待条件
源码位置:api/types/container/waitcondition.go:18-22
Go
WaitConditionNotRunning WaitCondition = "not-running" // 默认:等容器停止
WaitConditionNextExit WaitCondition = "next-exit" // 等下次退出
WaitConditionRemoved WaitCondition = "removed" // 等容器被删除
docker wait 默认是 not-running,docker wait --condition=removed 是 removed。
4.2 Wait 方法逐行解析
源码位置:state.go:190-232
Go
func (s *State) Wait(ctx context.Context, condition container.WaitCondition) <-chan StateStatus {
s.Lock()
defer s.Unlock()
// ★ Buffer so we can put status and finish even nobody receives it.
resultC := make(chan StateStatus, 1)
// 快速路径:条件已满足,立即返回
if s.conditionAlreadyMet(condition) {
resultC <- StateStatus{
exitCode: s.ExitCode,
err: s.Err(),
}
return resultC
}
// 慢路径:注册一个 waiter
waitC := make(chan StateStatus, 1)
if condition == container.WaitConditionRemoved {
s.removeOnlyWaiters = append(s.removeOnlyWaiters, waitC)
} else {
s.stopWaiters = append(s.stopWaiters, waitC)
}
// 用 goroutine 把 waitC 转发给 resultC,同时监听 ctx.Done()
go func() {
select {
case <-ctx.Done():
resultC <- StateStatus{
exitCode: -1,
err: ctx.Err(),
}
case status := <-waitC:
resultC <- status
}
}()
return resultC
}
几个关键设计:
(1) 两个 channel:waitC 和 resultC
为什么需要两个?解耦"状态变更通知"和"调用方读取"。
waitC:State 内部持有引用,当SetStopped被调用时向它发送状态。
resultC:返回给调用方,可以加 ctx 超时/取消。
如果没有 resultC,调用方就没法在 ctx 超时后取消等待,只能等到容器真的退出------这显然不行。
(2) 为什么都是 buffered channel(make(chan StateStatus, 1))
注释 state.go:195 写得很清楚:"Buffer so we can put status and finish even nobody receives it."
如果用 unbuffered channel,发送方(notifyAndClear)会被阻塞,直到所有调用方都来读。但调用方可能已经死了(ctx cancel)、或者根本不读 channel(代码 bug)。
buffered 保证发送永不阻塞,即使没有接收者也能完成状态转移逻辑。
(3) conditionAlreadyMet 的快速路径
Go
func (s *State) conditionAlreadyMet(condition container.WaitCondition) bool {
switch condition {
case container.WaitConditionNotRunning:
return !s.Running
case container.WaitConditionRemoved:
return s.Removed
default:
// TODO(thaJeztah): how do we want to handle "WaitConditionNextExit"?
return false
}
}
坑点 :WaitConditionNextExit 还没实现(返回 false,只能走慢路径)。注释里有个 TODO,说明这是个待办事项。
4.3 notifyAndClear:一次性通知 + 清空
源码位置:state.go:452-462
Go
func (s *State) notifyAndClear(waiters *[]chan<- StateStatus) {
result := StateStatus{
exitCode: s.ExitCode,
err: s.Err(),
}
for _, c := range *waiters {
c <- result
}
*waiters = nil // ★ 清空切片
}
为什么通知完就清空? 因为容器只能 stop 一次(在不 restart 的情况下)。所有的 waiter 都已经被唤醒,继续保留 channel 引用就是内存泄漏。
下次容器再启动 → 再退出时,stopWaiters 会被新的 Wait 调用重新填充。
5. 并发模型:一把锁的取舍
5.1 锁的范围
State 内嵌的 sync.Mutex 是整个 Container 的全局锁。注释 state.go:54-57 明确:
Go
// State contains an exported [sync.Mutex] which is used as a global lock
// for both the State and the Container it's embedded in.
type State struct {
sync.Mutex
...
}
也就是说,Container.Lock() 和 Container.State.Lock() 是同一把锁(Go 的嵌入语义)。
5.2 这种设计的优缺点
优点:
- 简化心智模型:不用记"先锁 A 还是先锁 B",只有一把锁。
- 避免死锁:多把锁才有死锁可能,一把锁不会。
- 状态一致性 :任何修改
Container字段的操作都会同时阻止其他修改,读到的状态一定是 consistent snapshot。
缺点:
- 并发度低:大量操作在同把锁上排队。例如 inspect 一个容器时,start/stop 命令都得等。
- 粒度太粗 :理论上
NetworkSettings和MountPoints是独立的,可以分别加锁,但当前设计强制串行。
docker 团队的取舍:用低并发换正确性。这对 docker 这种"状态错乱代价极高"(可能丢数据、进程异常)的系统是合理的。
5.3 带锁方法 vs 不带锁方法的判定规则
如何知道一个方法是不是自己加锁?看注释里有没有 "Callers must hold ...":
|------------------------|----------------------------------------------------------------|----------------------|
| 方法 | 注释原话 | 加锁责任 |
| SetRunning | (无注释,但和 SetStopped 同类) | 调用方 |
| SetStopped | "sets the container state to 'stopped' without locking" | 调用方 |
| SetRestarting | "sets the container state to 'restarting' without locking" | 调用方 |
| SetRemovalInProgress | (注释没说) | 方法自带 Lock/Unlock |
| SetRemovalError | (注释没说) | 方法内部 Lock/Unlock |
| IsRunning | (无注释) | 方法自带 Lock/Unlock |
| Wait | "Wait must be called without holding the state lock" | 调用方不能持锁(否则死锁) |
Wait的特别约束:注释 state.go:188 写明 "Wait must be called without holding the state lock"。
为什么?因为 Wait 内部要 s.Lock(),如果调用方已经持锁就会死锁 (Go 的 sync.Mutex 不可重入)。
5.4 实际调用示例
来自 daemon/start.go:375:
Go
// 阶段 10:状态更新 + 健康监控 + 事件上报
// container.State.SetRunning 是无锁方法,所以这里必须先 Lock
container.Lock()
container.HasBeenManuallyRestarted = false
container.State.SetRunning(ctr, tsk, startupTime) // ★ 无锁版
container.HasBeenStartedBefore = true
daemon.setStateCounter(container)
container.Unlock()
来自 daemon/monitor.go:134(容器退出事件处理):
Go
if c.State.ShouldRestart() {
c.State.SetRestarting(&ctrExitStatus) // ★ 这里调用方应该已持锁
} else {
c.State.SetStopped(&ctrExitStatus)
if !c.HasBeenManuallyRestarted {
defer daemon.autoRemove(&cfg.Config, c)
}
}
来自 daemon/delete.go:39(rm 命令入口):
Go
// Container state RemovalInProgress should be used to avoid races.
if inProgress := ctr.State.SetRemovalInProgress(); inProgress {
err := fmt.Errorf("removal of container %s is already in progress", name)
return errdefs.Conflict(err)
}
注意 SetRemovalInProgress 是自带锁的,所以 delete.go 这里不用先 Lock。
6. 完整状态机图
把所有转移方法画成一张图:
Go
NewBaseContainer()
│
▼
┌─────[Created]─────────────┐
│ (Running=F,StartedAt=0) │
│ │
│ SetRunning(now) │
│ │
▼ │
┌─────[Running]◄──SetRunning(now)──────┘ (restart)
│ (Running=T,Paused=F,Restarting=F)
│ Pid=task.Pid,StartedAt=now
│
SetRestarting(ec)
│ ▲
▼ │
[Restarting]─────SetRunning(now)─────┘
(Running=T,Restarting=T)
│
(monitor 检测到旧 task 退出后,
根据策略决定走重启还是停止)
│
▼ SetStopped(ec)
┌─────[Exited]─────┐
│ Running=F │
│ ExitCode=ec │
│ FinishedAt=now │
│ Pid=0 │
│ stopWaiters 通知 │
└───────────────────┘
│
│ docker rm
▼
SetRemovalInProgress()
│
▼
[Removing]
│
┌────────┴────────┐
│ │
删除成功 删除失败
SetRemoved() SetRemovalError(err)
│ │
▼ ▼
[Removed] 回到 Removing(状态不变,
(Removed=T) ErrorMsg=err,waiters 被唤醒)
─────────────────────────────────────────────────────
独立通道:pause/unpause(只动 Paused 字段,Running 不变)
─────────────────────────────────────────────────────
[Running] ──pause──> [Running+Paused] ──unpause──> [Running]
(State.State()=StatePaused)
Paused的特殊之处 :pause/unpause 不触发状态转移方法 ,只直接改 Paused 字段(在调用方持锁的前提下)。所以图中 pause 是个"侧通道"。
6.1 状态可达矩阵
|--------------|---------|------------|--------|---------------|--------------------|----------------------|--------|
| 起始状态 \ 目标状态 | Created | Running | Paused | Restarting | Exited | Removing | Dead |
| Created | - | SetRunning | (n/a) | (n/a) | (n/a) | SetRemovalInProgress | (罕见) |
| Running | (n/a) | - | pause | SetRestarting | SetStopped | SetRemovalInProgress | (n/a) |
| Paused | (n/a) | unpause | - | (n/a) | unpause→SetStopped | SetRemovalInProgress | (n/a) |
| Restarting | (n/a) | SetRunning | (n/a) | - | SetStopped | SetRemovalInProgress | (n/a) |
| Exited | (n/a) | SetRunning | (n/a) | (n/a) | - | SetRemovalInProgress | 删除失败标记 |
| Removing | (n/a) | (n/a) | (n/a) | (n/a) | (n/a) | - | 失败后 |
| Dead | (n/a) | (n/a) | (n/a) | (n/a) | (n/a) | (尝试清理) | - |
7. 真实调用链路
7.1 docker run 完整路径里的 State 操作
Go
docker run nginx
│
▼
daemon.create() (daemon/create.go)
│ → NewBaseContainer(id, root)
│ → State 初始所有 bool=false,Pid=0,StartedAt=0
│ → 此时 State.State() = StateCreated
│
▼
daemon.containerStart() (daemon/start.go:165)
│
├─ 阶段 9:tsk.Start() // 真正 fork 容器进程
│
└─ 阶段 10 (start.go:375):
container.Lock()
container.State.SetRunning(ctr, tsk, startupTime) // ★
→ Running=T, Paused=F, Restarting=F
→ Pid=task.Pid, StartedAt=now
→ ExitCode=0, ErrorMsg=""
container.Unlock()
// 此时 State.State() = StateRunning
7.2 容器进程退出的处理
来自 daemon/monitor.go(processEvent 处理 EventExit):
Go
containerd 检测到容器进程退出
│
▼
daemon.processEvent(EventExit)
│
▼
daemon.monitor.go::(容器退出处理函数)
│
├─ c.ShouldRestart() 询问 RestartManager
│
├─ 如果应该重启:
│ c.State.SetRestarting(&exitStatus) // ★
│ → Running=T, Restarting=T, Pid=0
│ → notifyAndClear(stopWaiters) // 唤醒 docker wait
│ 然后走 containerStart() 路径重新启动
│
└─ 如果不该重启:
c.State.SetStopped(&exitStatus) // ★
→ Running=F, Paused=F, Restarting=F
→ Pid=0, FinishedAt=now, ExitCode=ec
→ notifyAndClear(stopWaiters) // 唤醒 docker wait
// 此时 State.State() = StateExited
7.3 docker rm 的删除流程
来自 daemon/delete.go:
Go
docker rm <id>
│
▼
daemon.ContainerRm(name)
│
├─ ctr.State.SetRemovalInProgress() // ★ 自带锁,防并发 rm
│ → RemovalInProgress=T
│ → 返回旧值(false 表示之前没在删)
│ → 如果已经是 true,返回 Conflict 错误
│
├─ cleanupContainer():
│ ├─ 标记 Dead=true
│ ├─ CheckpointTo(持久化 Dead 状态)
│ ├─ 释放 RWLayer
│ │ └─ 失败时:ctr.State.SetRemovalError(err)
│ │ → Removed=T, ErrorMsg=err
│ │ → 通知 stopWaiters + removeOnlyWaiters
│ │ → 返回错误给用户,容器仍可见
│ │
│ ├─ 删除容器根目录
│ │ └─ 失败时:SetRemovalError(err)(同上)
│ │
│ └─ 成功:
│ ctr.State.SetRemoved() // ★
│ → 内部调 SetRemovalError(nil)
│ → Removed=T, ErrorMsg=""
│ → 通知 stopWaiters + removeOnlyWaiters
│
└─ metrics.StateCtr.Delete(ctr.ID)
7.4 daemon 重启时的状态修复
来自 daemon/daemon.go:545:
Go
dockerd 重启
│
▼
遍历 /var/lib/docker/containers/
│
├─ container.FromDisk() // 从 config.v2.json 加载
│ → 此时 Running/Paused 等字段是从磁盘读的旧值
│ → 这些值可能不准确(进程可能已经死了)
│
├─ client.LoadContainer(ctx, id) // 从 containerd 查真实状态
│ ├─ 如果 containerd 里有 → 还活着
│ └─ 如果没有(NotFound):
│ ces.ExitCode = 255
│ c.State.SetStopped(&ces) // ★ 强制改为 stopped
│ daemon.Cleanup(ctx, c)
│ c.CheckpointTo(...) // 持久化修正后的状态
│
└─ RestoreTask:重新接管 task 句柄
关键设计 :RemovalInProgress 和 Removed 因为 json:"-",daemon 重启后永远是 false。所以 daemon 重启能自动从"删除中断"状态恢复------它会重新看到这些容器,但不会以为它们正在被删。
8. Health:独立于运行状态的子系统
源码位置:daemon/container/health.go
State.Health 是个指针 字段,代表"健康检查状态"。它独立于 Running/Paused/Restarting:
Go
type Health struct {
container.Health // 嵌入 API 类型
stop chan struct{} // 停止 monitor goroutine 的信号
mu sync.Mutex // 注意:这是 Health 自己的锁,不是 State 的锁
}
8.1 健康状态值
来自 api/types/container:
Starting------ 容器刚启动,健康检查还没跑够次数
Healthy------ 健康检查通过
Unhealthy------ 健康检查连续失败 N 次
NoHealthcheck------ 容器没配置 HEALTHCHECK
8.2 为什么 Health 有自己的锁
State.Lock() 是粗粒度锁,如果健康检查 worker 每次更新状态都抢这把锁,会和 start/stop 命令严重竞争。所以 Health 有自己的 mu sync.Mutex,只在读写 Health.Status时加自己的锁。
这是 docker 内部少见的"用多把锁细化并发度"的地方------大部分其他场景都是统一用 State 的锁。
8.3 docker ps 里的健康状态显示
回看 state.go:125-127:
Go
if h := s.Health; h != nil {
return fmt.Sprintf("Up %s (%s)", units.HumanDuration(...), h.String())
}
所以 docker ps 看到 Up 5 minutes (healthy) 就是这里来的。
9. StateStatus:不可拷贝的返回值
源码位置:state.go:99-113
Go
// StateStatus is used to return container wait results.
// Implements exec.ExitCode interface.
// This type is needed as State include a sync.Mutex field which make
// copying it unsafe.
type StateStatus struct {
exitCode int
err error
}
func (s StateStatus) ExitCode() int { return s.exitCode }
func (s StateStatus) Err() error { return s.err }
为什么需要这个类型? 注释说得很清楚:State 含 sync.Mutex,拷贝 State 是不安全的(go vet 会报 "assignment copies lock value")。
所以需要一个不含锁的轻量类型 作为 Wait 的返回值,让调用方能拿到退出码和错误,而不需要持锁。
9.1 实现 exec.ExitCode 接口
Go
// Implements exec.ExitCode interface.
这让 StateStatus 可以被 swarm 等子系统当通用的退出结果处理。
10. 学习路径建议
10.1 推荐阅读顺序
state.go顶部 33 行注释 ------ 全局视角的导览。
State结构体定义(54-93) ------ 字段语义。
State.State()方法(154-180) ------ 字段到枚举的映射规则。
State.Wait()方法(190-232) ------ 等待者机制的核心。
SetRunning/SetStopped/SetRestarting三个方法(278-344) ------ 状态转移。
notifyAndClear(452-462) ------ 通知机制。
SetRemovalInProgress/SetRemovalError(394-442) ------ 删除流程。
state_test.go------ 通过测试用例验证理解(尤其是 Wait 的各种组合)。
10.2 配读文件
|----------------------------------------|-----------------------------------------------|
| 文件 | 作用 |
| api/types/container/state.go | ContainerState 枚举定义 |
| api/types/container/waitcondition.go | WaitCondition 枚举定义 |
| daemon/container/container.go | Container 结构(嵌入 State) |
| daemon/container/health.go | Health 子系统 |
| daemon/start.go | SetRunning 的真实调用方 |
| daemon/monitor.go | SetStopped / SetRestarting 的真实调用方 |
| daemon/delete.go | SetRemovalInProgress / SetRemovalError 的真实调用方 |
| daemon/daemon.go | daemon 重启时的状态修复 |
| daemon/wait.go | docker wait 的 API 入口 |
| daemon/container/state_test.go | 单元测试,验证各种状态组合 |