Docker 容器状态机 学习

为什么单独抽出 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 字段分类(三个语义层)

|---------|---------------------------------------------------------------------------|-----------------------|--------------------------|
| 语义层 | 字段 | 持久化? | 说明 |
| 进程态 | RunningPausedRestartingPidStartedAtFinishedAtExitCode | ✅ 写入 config.v2.json | 描述容器进程当前在干什么 |
| 结果态 | OOMKilledDeadErrorMsg | ✅ | 进程结束的原因/错误信息 |
| 管理态 | RemovalInProgressRemoved | ❌(json:"-") | 删除流程的内存标记,重启后无意义 |
| 健康态 | Health | ✅(指针) | 健康检查状态,独立 goroutine 异步更新 |
| 句柄 | ctrtask | ❌(私有) | containerd 进程引用,运行时才有意义 |

1.2 为什么这些字段是 json:"-"

RemovalInProgressRemoved:

  • 它们表达的是当前 daemon 进程内的瞬时操作状态
  • 假设 daemon 被 kill -9 杀掉,正在进行的删除操作就丢失了,重启后不应该认为"还在删除中"。
  • 所以这两个字段强制不持久化 ,daemon 重启后 RemovalInProgress=false, Removed=false

1.3 ctrtask 为什么是私有

源码位置: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 psSTATUS 列就是 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 的写入方法分两类:

  • 带锁版本 (SetRemovalInProgressSetRemovalError 等):自己 Lock/Unlock,可在任何上下文调用。
  • 不带锁版本 (SetRunningSetStoppedSetRestarting 等):调用方必须先持有锁。注释明确写 "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 会把 PausedRestartingExitCodeErrorMsgOOMKilled 全部清零。这是为了保证状态机的"干净重置",避免历史脏数据污染新启动周期。

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 rmdocker 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=removedremoved

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:waitCresultC

为什么需要两个?解耦"状态变更通知"和"调用方读取"

  • 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 命令都得等。
  • 粒度太粗 :理论上 NetworkSettingsMountPoints 是独立的,可以分别加锁,但当前设计强制串行。

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 句柄

关键设计 :RemovalInProgressRemoved 因为 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 推荐阅读顺序

  1. state.go顶部 33 行注释 ------ 全局视角的导览。
  1. State结构体定义(54-93) ------ 字段语义。
  1. State.State()方法(154-180) ------ 字段到枚举的映射规则。
  1. State.Wait()方法(190-232) ------ 等待者机制的核心。
  1. SetRunning/ SetStopped/ SetRestarting三个方法(278-344) ------ 状态转移。
  1. notifyAndClear(452-462) ------ 通知机制。
  1. SetRemovalInProgress/ SetRemovalError(394-442) ------ 删除流程。
  1. 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 | 单元测试,验证各种状态组合 |

相关推荐
漫随流水1 小时前
Java——springboot web案例
java·开发语言·spring boot
渣渣灰飞1 小时前
MySQL 系统学习 第六阶段:Redis + 缓存 + 高并发设计 第六章:Redis 实现登录 Token
学习·mysql·缓存
从此以后自律1 小时前
Java Object 类常用方法全讲解
java·开发语言
渣渣灰飞1 小时前
MySQL 系统学习 第六阶段:Redis + 缓存 + 高并发设计 第五章:缓存穿透、缓存击穿、缓存雪崩
学习·mysql·缓存
你有我备注吗2 小时前
SQL学习之查询
java·sql·学习
从零开始的代码生活_2 小时前
C++ stack、queue 与 priority_queue:容器适配器原理与实战
开发语言·c++·后端·学习·算法
铅笔侠_小龙虾2 小时前
Rust 学习(6)-所有权规则、移动语义、Clone 与 Copy
python·学习·rust
kiss strong2 小时前
AI学习-langchain
学习·langchain
xixi09242 小时前
JMeter5.6.3基础使用教程1
java·开发语言