【Linux】Linux进程状态深度解析

【往期Linux回顾】:

/------------Linux入门篇-----------/

【 Linux 历史溯源与指令入门 】

【 Linux 指令进阶 】

【 Linux 权限管理 】


/------------Linux工具篇------------/

【 yum + vim 】

【 sudo白名单配置 + GCC/G++ 】

【 自动化构建:make + Makefile 】

【 倒计时 + 进度条 】

【 Git + GDB调试器 】


/------------Linux进程篇-------------/

【 冯诺依曼体系 + 操作系统 】

【 进程概念 + PID + fork函数 】


目录

一、什么是进程状态

1、进程状态概念

2、进程状态有哪些?

【进程状态转换示意图(理论模型)】

[< 1 > 运行状态](#< 1 > 运行状态)

[< 2 > 进程转换(运行态 -> 就绪态)](#< 2 > 进程转换(运行态 -> 就绪态))

[< 3 > 阻塞状态](#< 3 > 阻塞状态)

[< 4 > 挂起状态](#< 4 > 挂起状态)

[① 阻塞挂起](#① 阻塞挂起)

[② 就绪挂起](#② 就绪挂起)

二、Linux进程状态

1、R(运行状态,running)

2、S(睡眠状态,sleeping)

问题:为啥bash始终处于S状态?

[3、D(磁盘休眠状态,disk sleep)](#3、D(磁盘休眠状态,disk sleep))

【不可中断睡眠小故事】

【浅睡眠和深度睡眠对比】

4、T(停止状态,stoped)

【kill场景演示】

【gdb场景演示】

[【t(跟踪停止状态,tracing stop)】](#【t(跟踪停止状态,tracing stop)】)

5、X(死亡状态,dead)

6、Z(僵尸状态,zombie)

【僵尸状态由来小故事】

【问题】:为什么进程终止后不直接清理,反而会进入僵尸状态?

【僵尸进程演示】

【问题】:为什么父进程不会进入僵尸状态?

【僵尸进程危害】

7、孤儿进程

【孤儿进程演示】


咱们今天来聊聊 Linux 进程的状态 ------ 这是理解系统运行的基础,但其实这些状态就像进程的 "工作模式":有的在全力运行,有的在等待资源,有的暂时 "待命"。

本文会从实际场景出发,拆解 Linux 中常见的进程状态(比如 R、S、D 等),以及它们背后的内核逻辑,帮你搞懂进程在系统里到底是怎么 "干活" 的。

一、什么是进程状态

1、进程状态概念

进程状态操作系统内核对进程当前活动与执行状态的描述,是内核调度资源的管理依据。

  • 它既反映进程的实时状态(如是否在使用 CPU、是否等待硬件 / 文件资源、是否被暂停),也帮助内核判断如何高效分配 CPU 等系统资源,是进程管理的核心基础之一。

2、进程状态有哪些?

【进程状态转换示意图(理论模型)】

(注:不同操作系统的实际实现会有差异,以下是通用理论状态)

  • 新建:进程刚被创建,正在加载到内存的过程中(完成内存分配、代码 / 数据加载),待加载完成后会进入内存并加入就绪队列。
  • 就绪:进程已准备好执行,等待 CPU 调度
  • 运行:进程正在 CPU 上执行指令
  • 阻塞:进程因等待事件(如硬件资源、信号)暂停,释放 CPU
  • 挂起就绪:就绪进程被换出内存,暂存到外存(需 "换入" 回内存才能继续等待调度)
  • 挂起阻塞:阻塞进程被换出内存,暂存到外存(事件发生后先 "换入" 回内存,再转为就绪)
  • 结束:进程执行完成,资源被系统回收

【状态转换逻辑】:

  1. 新建 → 就绪:进程加载完成
  2. 就绪 ↔ 运行:CPU 调度(就绪进程获得 CPU 转为运行;运行进程时间片超时 / 被抢占,回到就绪)
  3. 运行 → 阻塞:进程等待事件(如请求 IO)
  4. 阻塞 → 就绪:等待的事件发生
  5. 就绪 ↔ 挂起就绪:内存不足时,就绪进程(代码和数据被换出,PCD对象还在内存中)被 "换出" 到外存;外存中进程被 "换入" 回内存
  6. 阻塞 ↔ 挂起阻塞:阻塞进程被 "换出" 到外存;外存中阻塞进程等待的事件发生后,先 "换入" 回内存
  7. 运行 → 结束:进程执行完成

【说明】:

上述是操作系统理论中的全量进程状态,而在 Linux 系统中,运行、阻塞、挂起是最 核心的三种状态(实际会通过更具体的状态标识,如 R、S、D 等,来对应这些逻辑状态)。

接下来,我们就基于进程状态的通用理论模型,来详细解析这三种状态的具体定义与特点

< 1 > 运行状态

进程的运行状态,是衡量它在 CPU 调度环节活跃程度的核心状态。

当进程处于这一状态时,会呈现两种具体情形:

  1. CPU 执行中:已经获取到 CPU 的使用权,正在借助 CPU 完成运算、逻辑处理等实际任务;
  2. 就绪队列待命 :所有运行前的资源准备都已完成,只要操作系统的调度器为它分配 CPU 时间,就能马上切换到 CPU 上开始执行(这个时候实际上进程处于就绪态)。

运行状态(CPU 执行中)是进程 "正在使用 CPU 资源" 的状态,而就绪态(队列待命)是进程 "具备执行条件、等待获取 CPU 资源" 的状态,二者共同构成了进程活跃性的核心体现。


< 2 > 进程转换(运行态 -> 就绪态)

进程被调度到 CPU 上运行后,不会一直执行到完毕 (比如while(1);这类无限循环代码,也不会独占 CPU),核心原因是 "时间片" 机制:

  1. 时间片的定义:每个进程会被分配一个固定时长的 CPU 使用权(比如10ms);
  2. 时间片的作用:时间片耗尽后,当前运行态的进程会被从 CPU 上 "拿下来",回到就绪队列(对应 "我已经准备好了,可以随时被调度" 的状态);
  3. 最终效果:通过 "进程切换"(频繁将进程在 CPU 和就绪队列间切换),在一个时间段内,所有进程的代码都会被轮流执行,实现并发执行

< 3 > 阻塞状态

*阻塞状态:*进程因等待某类资源 / 事件(如 I/O 操作、信号、锁)而无法继续执行的状态:

  1. 进入条件:运行态的进程在执行中需要等待资源(比如读取磁盘文件、等待网络数据),会主动放弃 CPU,由运行态转为阻塞态;
  2. 状态特征:处于阻塞态的进程,即使 CPU 空闲也无法被调度执行(因为缺少必要资源),其 PCB 会被移至 "阻塞队列"(而非就绪队列);
  3. 退出条件:当进程等待的资源 / 事件就绪(比如文件读取完成),会从阻塞态转为就绪态,PCB 被移回就绪队列,等待 CPU 调度。

简言之,阻塞状态是进程 "不具备执行条件(缺资源)、无法使用 CPU" 的状态,是进程非活跃性的典型体现。


< 4 > 挂起状态
① 阻塞挂起

阻塞挂起是挂起状态的核心子状态之一,指进程因等待资源 / 事件处于阻塞态时,又因系统内存不足,其代码和数据被换出到外存(如磁盘)的状态:

  1. 进入条件:系统内存紧张时,会将阻塞队列中的进程(移动的是代码和数据,并且会在PCB中标记移到的位置)换出到外存,转为阻塞挂起状态;
  2. 状态特征:进程既缺少执行所需的资源(符合阻塞态的条件),同时代码和数据也不在内存,双重 "无法执行";
  3. 退出条件
    • 先等进程等待的资源 / 事件就绪:阻塞挂起态转为就绪挂起态;
    • 再等系统内存充足:由就绪挂起态换入内存,恢复为就绪态。

swap 分区是磁盘上的存储空间,在阻塞挂起场景中,它会暂存进程的代码和数据(此时进程 PCB 仍留在阻塞队列),是内存不足时实现进程挂起与恢复的关键载体。


② 就绪挂起

就绪挂起是挂起状态的子状态,指进程本身已具备执行条件(符合就绪态要求),但因系统内存不足(且阻塞进程已被优先挂起),其代码和数据被换出到外存(如 swap 分区),PCB 仍保留在就绪队列的状态:

  1. 进入逻辑:系统会优先将阻塞态进程挂起(释放内存);若内存仍不足,才会将就绪队列中暂未被调度的进程(就绪态)的代码和数据换出到外存,转为就绪挂起状态(PCB 留在就绪队列,标记数据外存位置);
  2. 状态特征:进程 "能执行但没内存",需等系统内存充足后,代码和数据从外存换入内存,恢复为就绪态;
  3. 核心逻辑:"先阻塞挂起、后就绪挂起" 是因为阻塞进程本就无法执行,挂起它们的内存成本更低,是系统优先保障资源利用率的策略。

二、Linux进程状态

下面的状态是在Linux内核源代码中定义的:

cpp 复制代码
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = 
{
    "R (running)",       /* 0:运行状态 */    
    "S (sleeping)",      /* 1:睡眠状态 */
    "D (disk sleep)",    /* 2:磁盘休眠状态 */
    "T (stopped)",       /* 4:停止状态 */
    "t (tracing stop)",  /* 8:跟踪停止状态 */
    "X (dead)",          /* 16:死亡状态 */
    "Z (zombie)",        /* 32:僵尸状态 */
};

1、R(运行状态,running)

**定义:**进程已具备执行条件,正在等待 CPU 调度,或正在 CPU 上执行

  • R 状态 = 运行状态 + 就绪状态
  • Linux 中用 "R 状态(运行状态)" 统一指代 "可执行的进程状态"(不管是正在执行还是等待调度),不再单独区分 "运行态" 和 "就绪态",而是通过 "是否在 CPU 上执行" 来区分 R 状态的两种场景。

【测试1】:

​​​​​​

启动这个程序后,发现它的进程状态一直是阻塞类状态(对应 Linux 中的 S 态)。这是为什么呢?

因为我们一直在用printf函数(向显示器输出内容),进程会进入显示器的等待队列,等待 IO 资源就绪。这个操作会让进程处于 "等待 IO 资源" 的阻塞状态,而 CPU 处理速度极快,进程实际占用 CPU 的时间极短,所以大多数时间都处于阻塞状态,呈现出持续阻塞的状态。


  • S(Sleeping) 进程处于可中断休眠状态,此时它没在运行,而是等待某个条件满足(比如等待键盘输入、磁盘读写完成、sleep定时器到期),这个状态下不会占用 CPU 资源。
  • +(Foreground) 表示进程是当前终端的前台进程:
    • 终端的所有输入(包括按Ctrl+C发送中断信号)都会直接作用于它,能直接中断进程;
    • 它会占据当前终端,你需要等它运行结束,或者按Ctrl+Z将其暂停并切换到后台(切换后进程状态会变成S且不带+),才能在同一个终端里执行其他命令。
  • 无+(Background) 表示进程是当前终端的后台进程:
    • 终端的输入(包括Ctrl+C)不会影响它,无法直接用Ctrl+C中断;
    • 若要操作它,需要用fg命令将其切回前台,或用kill命令发送信号中断;
    • 也可以在启动进程时直接加&(例如./test &),让进程一开始就在后台运行。

【测试2】:

去掉printf后,进程状态始终是R+,这是因为代码里没有了等待 IO 的操作,进程会一直占用 CPU 执行无限循环,所以持续处于运行态(R) ;同时它是前台进程(+),所以状态显示为R+

2、S(睡眠状态,sleeping)

定义: 意味着进程在 等待事件完成 (这里的睡眠有时候也叫做 可中断睡眠 (interruptible sleep ))

运行上述程序,你会发现该进程几乎一直处于休眠状态 ------ 这是因为代码里循环执行的printfsleep操作,都会让进程进入阻塞(休眠)状态:

  • printf需要向终端输出内容,进程会阻塞等待 IO 资源就绪;
  • sleep会让进程主动休眠指定时长,期间同样处于阻塞状态。

这两种操作都会让进程暂时让出 CPU,所以它大部分时间都处于休眠(阻塞)状态。

问题:为啥bash始终处于S状态?

bash 始终处于 S 状态,是因为它在等待用户输入命令,此时处于 IO 阻塞的休眠状态。

3、D(磁盘休眠状态,disk sleep)

D 状态 (磁盘休眠状态,disk sleep)是 Linux 进程的一种阻塞状态,指进程因等待磁盘 IO 操作完成而进入的休眠状态。

  • 进程在等待磁盘读写(比如从硬盘读取文件、向硬盘写入数据),此时会暂停执行、不占用 CPU;
  • 属于不可中断睡眠(与 S 态的 "可中断" 不同)------ 进程在等待磁盘 IO 期间,不会响应普通的信号(比如Ctrl+C),只能等磁盘操作完成后才会被内核唤醒。
【不可中断睡眠小故事】

进程凑到磁盘跟前,递过一摞数据:"劳烦帮存 1000M,存好存坏都跟我说声哈!" 磁盘擦了擦磁头应下,进程闲着没事,往旁边一躺就睡了 ------ 这是能被叫醒的 "可中断休眠"(S 态)。

存到一半,操作系统攥着内存统计单急慌慌跑过来,瞅见睡着的进程就皱了眉:"内存都快挤爆了,腾地方!" 话没说完,直接把进程 "清" 没了。

这边磁盘吭哧吭哧存到 900M,突然弹出 "空间不足" 的提示 ------ 存失败了。它扭头就喊进程报信,可喊破喉咙,进程早没了踪影。1000M 数据没处交代,只能跟着 "蒸发" 了。


这数据要是银行一天的转账记录,损失可就大了!行长把进程、磁盘、系统都叫到办公室:磁盘先摆手:"我按进程说的存了,最后找不着它,我有啥办法?"进程委屈巴巴:"我睡得好好的,莫名其妙就没了,我才是冤大头!"系统也喊冤:"内存都不够撑到下一秒了,不杀它系统崩了,损失更大!"


行长听完叹了口气,拍板定了规矩:"以后进程等磁盘干活时,就睡'叫不醒的觉'------ 叫'不可中断休眠'(D 态)。这时候谁都不能动它,必须等磁盘把活儿干完、把信儿传给它,才能接着来!"

打这儿起,进程再等磁盘这类关键设备干活时,就会钻到 D 态的 "保护壳" 里 ------ 既能安心等结果,也能护着数据不丢啦。
不可中断睡眠(D 态)是系统里比较 "特殊" 的进程状态:一旦进程进入这个状态,通常只能等它完成磁盘 IO 后自行唤醒,哪怕重启系统都未必能终止它,极端情况下甚至得靠断电才能强制结束。

而且这个状态在正常系统里很少见 ------ 要是你的进程长时间(比如持续几秒)卡在 D 态,基本是计算机出问题的信号,大概率是磁盘老化(比如用了 5 年以上)导致 IO 操作异常,才让进程陷在这个 "醒不来" 的状态里。

【浅睡眠和深度睡眠对比】
维度 浅睡眠(S 态) 深度睡眠(D 态)
触发场景 等待 IO、定时器等(非关键操作) 等待磁盘等关键 IO 操作
信号响应 能被普通信号(如 Ctrl+C)唤醒 不响应普通信号,只能等 IO 完成
系统操作 可被系统杀死(如内存不足时) 系统无法杀死,必须等 IO 结束
作用 灵活等待,可中断 保护关键数据,避免操作中断

4、T(停止状态,stoped)

T状态(停止状态,stopped)是 Linux 进程的一种暂停状态,指进程因接收到暂停信号(如 Ctrl+Z)** 而暂时停止执行,但并未终止

核心特点:


场景 1:gdb 调试暂停

在 gdb 调试中,通过设置断点让进程进入 T 态:

  • 执行break 行号/函数名(比如break main.c:10)设置断点,当程序运行到断点处时,会自动暂停执行,进入 T 态;
  • 此时进程的代码、数据等资源仍保留在内存中,可在 gdb 中进行查看变量、单步调试等操作;
  • 调试完成后,执行 gdb 的continue命令,进程会从断点处恢复运行。

场景 2:信号暂停(如 Ctrl+Z)

用户可通过kill命令向进程发送暂停信号(SIGSTOP),让进程进入 T 态:

  • 执行kill -SIGSTOP 进程ID,进程会暂停运行,内存中的代码、数据等资源保持不释放;
  • 后续若要恢复进程,可执行kill -SIGCONT 进程ID(发送继续信号),进程会从暂停处恢复执行

简单说:T 状态是进程 "被暂停但没结束" 的状态,随时可以恢复运行。

【kill场景演示】

此时进程处于S 态(可中断睡眠),因为它在持续等待 IO 设备输出。

我用kill -19(对应SIGSTOP信号)暂停了进程,此时进程进入T 态(停止状态)

使用kill -18(对应SIGCONT信号)让进程恢复运行,它回到了之前的S 态(可中断睡眠),同时不再保持前台运行状态。

【gdb场景演示】

此时 gdb 进程处于S 态(可中断睡眠),因为它在等待用户输入调试指令。

通过b main打主函数断点、r运行程序后,程序会在断点处暂停 ------ 此时被调试的./test 进程进入 t 态(跟踪停止状态),gdb 进程仍保持 S 态(等待后续调试操作)。

这里的t态是停止状态的细分场景,它专门对应 "进程被调试器(如 gdb)跟踪暂停" 的情况,和普通的 T 态(比如用 Ctrl+Z 挂起的停止状态)在触发场景上有所区别。

【t(跟踪停止状态,tracing stop)】
  • 核心定义: 进程因为被调试器(如 gdb)跟踪而暂停执行的状态。

  • 常见触发方式:

    1. 程序运行到断点 处(break 命令)。
    2. 使用单步执行 命令(next, step)后。
    3. 在程序运行时按下 Ctrl + C 主动中断。
  • 如何恢复:

    • 在调试器中使用 continue, next, step 等命令让程序继续执行。

5、X(死亡状态,dead)

X(dead)是进程彻底终止前的瞬时状态,你不会在ps/top里看到它。

  • 核心特点:仅存在于内核回收进程资源的瞬间,无交互性,资源回收完成后进程直接消失。

6、Z(僵尸状态,zombie)

【僵尸状态由来小故事】

下午三点,你坐在咖啡馆靠窗的位置,刚喝完最后一口拿铁,起身拿起包准备离开。

就在你推门的瞬间,邻桌的客人突然捂住胸口,趴在桌上没了动静。你吓了一跳,立刻喊来店员,同时拨了急救电话。

店员的第一反应不是直接把人抬走 ------ 而是先守在桌边,不让其他客人碰桌上的物品(手机、药瓶),同时等医生和警察到现场。毕竟得先搞清楚:是突发疾病?还是有其他情况?这些 "现场信息" 得留着等专业人员确认。


这时候,这位客人虽然已经没了意识,但还在原地等着 "后续处理"------ 就像进程的僵尸状态: 自身运行已经停止,但 "父进程(系统)" 还没回收它的状态信息,不能直接清理。

等医生做完初步检查、警察记录完现场信息,工作人员才把人抬上救护车 ------ 这个 "信息交接完、彻底离场" 的瞬间,就是进程的死亡状态(X) :所有收尾工作完成,彻底从系统里消失。


总而言之:

  • 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲) 没有读取到子进程退出的返回代码时就会产生僵尸进程
  • 僵尸进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
  • 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
【问题】:为什么进程终止后不直接清理,反而会进入僵尸状态?

核心原因是父进程需要获取子进程的 "退出状态":

进程终止时,会先将自身的代码、数据、堆栈等内存资源彻底清理,但会把 "运行结果"(正常结束 / 异常崩溃)存在 PCB(进程控制块)中;而父进程需要通过wait()/waitpid()等系统调用读取这个状态,才能判断子进程的工作是否完成。


这就像 "人意外去世后,身体(对应进程的代码 / 数据)会被处理,但现场的证据(对应 PCB 里的退出状态)会被保留"------ 要等警方(对应父进程)来调查取证(读取退出状态),确认死亡原因后,才会彻底收尾(清理 PCB)。


若内核直接清理 PCB,父进程就会丢失子进程的退出信息,相当于 "子进程的工作结果没完成交接"。因此僵尸态是子进程留着 PCB 等父进程 "查收结果" 的临时状态------ 父进程读取状态后,内核才会彻底清理 PCB。

【僵尸进程演示】

进程退出时,若父进程未主动回收子进程信息(未调用wait()/waitpid()),子进程会持续处于僵尸态(Z 状态),其对应的task_struct结构体(进程控制块)无法被内核释放,仅保留退出状态等必要信息,直到父进程处理或自身终止后由 1 号进程接管清理。

【问题】:为什么父进程不会进入僵尸状态?

父进程的父进程通常是终端 Shell(比如 bash)。当我们运行程序时,父进程由 Shell 创建并作为其子进程存在。一旦父进程执行结束或被终止,Shell 会主动调用wait()类函数回收父进程的退出状态和 PCB 资源,不会让父进程残留为僵尸进程。此外,即使父进程意外退出,系统的 1 号进程(init/systemd)也会接管并清理其资源,因此父进程不会像未被回收的子进程那样成为僵尸。

【僵尸进程危害】

僵尸进程的定义

处于僵尸状态(Z 状态)的子进程 ,即子进程已退出,但父进程未调用wait()/waitpid()读取其退出状态时的进程。


僵尸进程的 "内存泄漏" 本质

僵尸进程不会造成常规程序的内存泄漏 (如动态分配内存未释放导致堆内存占用),但从系统资源管理角度,会引发类似内存泄漏的不良影响:

  • 子进程进入僵尸状态时,代码段、数据段、堆 / 栈等用户空间资源会被系统释放
  • 进程控制块(PCB,存储在task_struct结构体中)会被保留,用于存储子进程的退出状态(如正常结束、信号终止等);
  • 单个 PCB 占用内存较小(几十~几百字节),但大量僵尸进程的 PCB 会持续积累,占用的内存无法被其他进程利用,宏观上造成内存资源的 "泄漏"。

不同进程的影响差异

  • 常驻内存进程(如后台服务、守护进程):若产生僵尸进程,会长期占用资源,逐渐拖垮系统;
  • 短生命周期进程:退出时系统会自动回收大部分资源,僵尸进程问题相对不突出。

系统不主动回收 PCB 的原因

task_struct中保存了子进程的退出状态关键信息 ,操作系统需要将这些信息完整交付给父进程;因此必须维护PCB直到父进程主动调用wait()获取信息 ------ 回收僵尸进程的责任由开发者承担。

7、孤儿进程

父进程先于子进程退出 时,子进程会变成孤儿进程(原父进程已消失,子进程 "无父")。

此时,操作系统会将这个孤儿进程的父进程重新设置为 1 号进程(init/systemd,系统初始化进程),由 1 号进程 "领养" 它。

当孤儿进程后续退出并进入僵尸状态(Z 状态)时,1 号进程会主动调用wait()类函数回收它的退出状态和 PCB 资源,避免其残留为僵尸进程。

简单说:父进程提前退 → 子进程变孤儿 → 1 号进程领养 → 子进程退出后由 1 号进程回收,不会成为僵尸进程。

【孤儿进程演示】

从图中可以看出父进程结束后,子进程的PPID就变成1了,这也刚好验证了我们上面说的没问题

相关推荐
小嵌同学3 分钟前
Linux 内核中的 Per-CPU 机制详解
linux·运维·arm开发·嵌入式
云飞云共享云桌面8 分钟前
自动化设备厂 10 人研发团队,如何共用一台服务器做 SolidWorks 建模?
运维·服务器·网络·3d·自动化·制造
筝筝ba28 分钟前
Configure IP Address Via Different User Roles And Verify(1)
服务器
QH1392923188037 分钟前
NVIDIA H200 H300服务器
运维·服务器·开发语言·网络·信息与通信
xixiaoyunya38 分钟前
中小微企业轻量组网方案的技术选型与实践:从传统专线到软定义隧道的成本与效能分析
服务器·php·apache
Hello-FPGA38 分钟前
嵌入式LINUX 设备 ip配置后怎么保存才能掉电不消失?
linux·运维·服务器
奥莱维1 小时前
KNX酒店方案_KNX专用线与高端酒店技术逻辑
java·服务器·前端·数据库
工业一体机老司机1 小时前
Linux工业一体机自启动服务配置:systemd服务单元编写与开机优化实战
java·linux·服务器
zhixingheyi_tian1 小时前
Linux 之 ssh
linux·服务器·ssh
艾伦_耶格宇2 小时前
【DOCKER容器实战】-2MySQL
运维·docker·容器