Linux 进程状态详解

有同事问到 top 命令里的 running、sleep、stop、zombie 等进程状态分别是什么意思,于是借这个机会整理一下 Linux 系统里的进程状态。

概述

进程(Process)即当前正在运行的计算机程序的实例。每个进程都有一些属性变量,决定了它所能使用的计算机资源。进程在执行过程中会处在不同状态——正如人会有不同的人生状态。

进程由父进程通过 fork 创建。进程在执行(running)过程中会用到一些资源;当所需的资源暂时不可用时,进程就会进入睡眠(sleeping)状态。和人一样,进程也会”死亡”:可能正常结束,也可能被杀死(kill)。

进程的类型

Linux 系统里有几种不同类型的进程:用户进程(User processes)、守护进程(Daemon processes)和内核进程(Kernel processes)。

用户进程

系统里大多数进程都是用户进程。用户进程由通常的用户账户启动,在用户空间(user space)中执行。在没有获得额外许可的情况下,用户进程通常无法对处理器进行特殊访问,或访问启动它的用户无权访问的文件。

守护进程

守护进程通常是后台程序,往往由一些持续运行的服务来管理。守护进程可以用来监听请求并访问某些服务,例如 httpd 守护进程监听访问网页的请求;也可以自行启动任务,例如 crond 守护进程在预设时间点启动计划任务。

尽管管理守护进程的服务通常由 root 启动,但守护进程本身往往以非 root 用户运行。这符合「只赋予进程运行所必需的权限」的要求,能使系统免于一些攻击——例如,即使 httpd(以 Apache 用户启动)被攻破,黑客也无法访问 root 等其他用户的文件,或影响其他用户启动的守护进程。

守护进程通常由系统在启动时拉起,一直运行到系统关闭;也可以按需启动和终止、在特定系统运行级别执行,或运行中触发重载配置。

内核进程

内核进程仅在内核空间(kernel space)中执行。内核进程与守护进程有些相似,主要区别在于:内核进程对内核数据结构拥有完全的访问权限。此外,内核进程不如守护进程灵活——修改配置文件并重载即可改变守护进程的行为,而修改内核进程的行为则需要重新编译内核。

运行状态

系统在进程启动时赋予其状态,进程的状态由状态描述符来描述。设置进程状态通常对应一个简单的赋值:

p->state = TASK_RUNNING;

其中,p 代表进程,state 是其状态标识,TASK_RUNNING 表示该进程正在运行或可以执行。进程通常处于以下两种状态之一:

  • 在 CPU 上执行(进程正在运行);
  • 不在 CPU 上执行(进程未在运行)。

同一时间同一 CPU 上只能运行一个进程,其他进程只能等待或处于其他状态。未在运行的进程可能处于:

  • 可运行状态;
  • 可中断睡眠状态;
  • 不可中断睡眠状态;
  • 僵死状态。

ps 输出的各状态标识及含义如下:

标识 状态 说明
R 运行 / 可运行 正在运行,或已就绪等待 CPU
S 可中断睡眠 等待事件或资源,可被信号唤醒
D 不可中断睡眠 等待磁盘/网络 I/O,不处理信号
Z 僵尸 已终止,等待父进程收集退出状态
T 停止 / 跟踪 被停止或被跟踪

fork 之初

按 fork(2) 的手册页(man 2 fork),fork 系统调用创建一个与调用进程几乎完全相同的进程:前者称为父进程,后者称为子进程。子进程与父进程几乎完全相同,但有以下差别:

  • 子进程拥有全局唯一的进程 ID(见 setpgid(2));
  • 子进程的父进程 ID 是父进程的进程 ID;
  • 子进程不继承父进程的内存锁(见 mlock(2)、mlockall(2));
  • 子进程的资源使用计数及 CPU 时间计数重置为零(见 getrusage(2)、times(2));
  • 子进程未处理的信号队列重置为空(见 sigpending(2));
  • 子进程不继承父进程的信号量修正(见 semop(2));
  • 子进程不继承父进程的文件区域锁(见 fcntl(2));
  • 子进程不继承父进程的计时器(见 setitimer(2)、alarm(2)、timer_create(2));
  • 子进程不继承父进程未完成的异步 I/O 操作(见 aio_read(3)、aio_write(3));
  • 子进程不继承父进程的异步 I/O 上下文(见 io_setup(2))。

正在运行状态

系统中最珍贵的资源是 CPU,正使用 CPU 的进程处于「正在运行状态」。在 ps 或 top 中,状态标识为 R 的进程即处于该状态。

举例说明进程如何进入「正在运行状态」:在 Shell(以 bash 为例)中执行 ls 时,Shell 会在环境变量 PATH 记录的搜索路径里寻找 ls 对应的可执行文件;找到后,Shell 用 fork 克隆自身,然后在子进程里用 ls 的可执行文件替换虚存空间中 Shell 的内容。此时,系统会设置子进程的运行状态:

p->state = TASK_RUNNING;

CPU 既可在内核模式运行,又可在用户模式运行。当用户初始化一个进程,进程在用户空间运行,对应 CPU 在用户模式运行;在用户空间运行的进程无权访问内核数据结构和算法。各型号 CPU 都提供特定指令,以便在内核模式和用户模式之间切换。如果用户级进程需要访问内核数据结构或算法,就需要使用系统调用与文件子系统或进程控制子系统交互。部分系统调用罗列如下:

  • 文件子系统:open()close()read()write()chmod()chown()
  • 进程控制子系统:fork()exec()exit()wait()brk()signal()

当内核开始处理来自用户级进程的请求,相应进程就进入内核空间,对应 CPU 在内核模式运行。/proc/[pid]/stat 的第 14、15 项分别记录进程在用户空间和内核空间执行的时间。摘录部分 proc(5) 的手册页如下:

utime %lu

Amount of time that this process has been scheduled in user mode, measured in clock ticks (divide by sysconf(_SC_CLK_TCK)). This includes guest time, guest_time (time spent running a virtual CPU, see below), so that applications that are not aware of the guest time field do not lose that time from their calculations.

stime %lu

Amount of time that this process has been scheduled in kernel mode, measured in clock ticks (divide by sysconf(_SC_CLK_TCK)).

top 命令的 CPU 统计行则展示了 CPU 位于用户模式和内核模式的时间占比:

top - 12:27:25 up 2:51, 4 users, load average: 4.37, 3.64, 3.44
Tasks: 194 total, 2 running, 192 sleeping, 0 stopped, 0 zombie
Cpu(s): 57.0%us, 1.3%sy, 0.0%ni, 41.1%id, 0.0%wa, 0.4%hi, 0.1%si, 0.0%st

可运行状态

进程获得了所有所需资源、正等待 CPU 时,就进入可运行状态。处于可运行状态的进程在 ps 输出中同样以 R 标识。例如,一个正在 I/O 的进程并不立即需要 CPU;当它完成 I/O 后,会触发信号通知 CPU 和调度器,将其置于运行队列(内核维护的可运行进程列表);当 CPU 可用时,该进程就进入正在运行状态。和正在运行状态一样,其状态被设置为:

p->state = TASK_RUNNING;

睡眠状态

当进程所需的资源暂不可用时,就会进入睡眠状态。此时,进程要么主动睡眠,要么被内核强制置于睡眠状态。进入睡眠状态的进程会立即交出 CPU 使用权;当所需资源可用时,CPU 会收到信号,调度器下次调度该进程时会将其置为正在运行或可运行状态。

以 login shell 进程为例:

  • 键入命令时,它进入睡眠状态,同时等待一个特定事件(取决于键入的命令);
  • Shell 睡眠时进入一个特定的等待通道(WCHAN,wait channel,同样取决于键入的命令);
  • 当等待的事件发生时(例如收到来自键盘的 ^C),该等待通道上的所有进程都会苏醒。

执行 ps -l 可看到与当前 shell 关联的进程,执行 ps -el 可看到系统上所有进程。进程处于睡眠状态时,ps 输出中的 WCHAN 字段会显示它在等待什么系统调用:

$ ps -l | more
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 R 867 12085 27779 0 80 0 - 27029 - pts/480 00:00:00 ps
0 S 867 12086 27779 0 80 0 - 25779 pipe_w pts/480 00:00:00 more
0 S 867 27779 35146 0 80 0 - 27721 do_wai pts/480 00:00:01 bash

这里执行了 ps -l | more。输出中 more 和 bash 都处于睡眠状态:前者在等待管道输入(pipe_wait),因为 ps 输出时 more 还没接到内容;后者在等待 ps -l | more 执行完毕(等待 do_wait 系统调用)。

除了等待资源,进程也可以主动睡眠一段时间。例如 sleep() 接收一个以秒为单位的时间参数(比如 10 秒),调用它的进程就进入睡眠并持续 10 秒。睡眠结束后,调度器再次调度到该进程时,会将其设为可运行状态;CPU 空闲后,进程重新进入正在运行状态。由此可见,sleep(10) 并不能保证「恰好」睡 10 秒,只保证睡眠时间不少于 10 秒。

部分进程永远不会终止,而是不断在睡眠、唤醒、干活之间循环:每次循环开始时进入睡眠并等待某个事件;事件发生时被唤醒(进入正在运行或可运行状态),然后处理任务。

睡眠状态又分为可中断睡眠状态和不可中断睡眠状态。

可中断睡眠状态

可中断睡眠状态表示进程在等待时间片或某个特定事件;一旦事件发生,进程就会退出该状态。ps 输出中标识为 S。系统为其设置运行状态:

p->state = TASK_INTERRUPTIBLE;

不可中断睡眠状态

不可中断睡眠状态的进程不处理任何信号,仅在等待的资源可用或超时(前提是设置了超时)时退出。它通常与设备驱动等待磁盘或网络 I/O 有关。在内核源码 fs/proc/array.c 中,其文字定义为 “D (disk sleep)”, / 2 /。进程进入不可中断睡眠状态时不会处理信号,而是把信号积累起来,等唤醒后再处理。ps 输出中标识为 D。系统为其设置运行状态:

p->state = TASK_UNINTERRUPTIBLE;

由于不可中断睡眠状态的进程不处理任何信号,kill -9 也杀不掉它。解决这类进程的办法只有两个:

  • 满足它所等待的资源,使其可用;
  • 若无法满足,只能重启系统。

进程的终止和僵尸状态

进程可以主动调用 exit 系统调用来终止,也可以接收信号、由信号处理函数调用 exit 来终止。执行 exit 后,进程释放相应的数据结构,此时进程本身已经终止。不过,操作系统还没有释放进程表中该进程的槽位——为解决这个问题,终止前进程会向父进程发送 SIGCHLD 信号,通知父进程来释放子进程在进程表中的槽位。这个设计是为了让父进程知道子进程退出时的状态。

从子进程终止到父进程释放其进程表槽位,这一过程中子进程处于僵尸状态(zombie state)。如果父进程因为各种原因在释放槽位之前就退出(来不及收集子进程状态),子进程就会一直处于僵尸状态。由于僵尸进程本身已经终止、无法再处理任何信号,它只能停留在进程表中,直到系统重启。

ps 输出中,僵尸状态标识为 Z。系统为其设置运行状态:

p->state = TASK_ZOMBIE;

参考来源:https://liam.page/


   转载规则


《Linux 进程状态详解》 吴杭沉 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
Linux 孤儿进程与僵尸进程 Linux 孤儿进程与僵尸进程
子进程的结束和父进程的运行是异步的——父进程永远无法预测子进程什么时候结束。那么,会不会因为父进程太忙来不及 wait 子进程,或者不知道子进程何时结束,而丢失子进程结束时的状态信息呢?答案是:不会。 UNIX 提供了一种机制来保证:只要父
2020-03-05
下一篇 
JSON/YAML 数据模型 JSON/YAML 数据模型
从结构上看,所有的数据最终都可以分解成三种类型: 标量(scalar):一个单独的字符串或数字,如”北京”。 序列(sequence):若干相关数据按顺序并列,又称数组(array)或列表(list),如”北京,上海”。 映射(mappi
2020-02-06
  目录