进程状态
并行和并发
对于单核也可以实现多个进程同时运行。
CPU切换和运行的速度非常快,人类感知不到,所以听音乐的时候不会觉得卡顿。
并发:CPU执行进程代码,不是 把进程代码执行完毕,才开始执行下一个,而是给每个进程预分配一个时间片,基于时间片,进行调度轮转(单CPU下)
并行:多个进程在多个CPU下分别,同时进行运行
| 概念 | 核心定义 | 底层实现 | 本质特征 |
|---|---|---|---|
| 并发 | 多个任务在同一时间段内交替执行(宏观上 “同时进行”,微观上 “轮流执行”) | 依赖操作系统的任务调度(如时间片轮转),单个 CPU 即可实现 | 任务 “交替执行”,共享资源需同步 |
| 并行 | 多个任务在同一时刻同时执行(宏观 + 微观均 “同时进行”) | 依赖多个 CPU 核心或多处理器,每个任务独立占用一个核心 | 任务 “同时执行”,资源竞争较少 |
- 并行是并发的 “子集”:并行一定满足并发,但并发不一定是并行(单 CPU 只能实现并发,多 CPU 才能实现并行)。
- 目标一致:均为提高 CPU 利用率,减少任务等待时间(如 IO 操作时切换任务)。
时间片
时间片是操作系统为每个就绪态进程 / 线程分配的CPU 执行时间片段(通常为 10-100 毫秒),是实现 “并发” 的核心机制。
工作原理(以 Linux 为例)
- 调度器触发:操作系统的进程调度器(如
Linux的CFS调度器)按优先级为就绪态进程分配时间片。 - 执行与切换:
- 进程获得
CPU后,在时间片内执行指令; - 时间片耗尽时,调度器触发上下文切换(保存当前进程的寄存器、
PC指针等状态,加载下一个进程的状态); - 进程切换到就绪态,等待下一次调度,下一个进程获得
CPU执行权。
- 进程获得
- 循环往复:通过快速切换(毫秒级),人类感官上认为多个任务 “同时进行”。
时间片大小的影响
- 太小:上下文切换频繁,切换开销(保存 / 加载状态)占比过高,降低系统效率;
- 太大:并发响应变慢(如打开多个软件时,某个软件独占 CPU 过久,其他软件卡顿)
Linux和windows民用操作系统是分时操作系统
实时操作系统 :分进程优先级的系统(特定领域)
进程具有独立性
一个进程出现问题不会影响另一个进程,父子进程也是。
等待的本质
进程等待是指进程因等待某个事件完成(如 IO 操作、资源分配、子进程结束),主动放弃 CPU 执行权,进入阻塞态(非就绪态),直到等待的事件发生后,由操作系统唤醒并回到就绪态,重新等待调度。

每个PCB(task_struct)运行一段时间后往后接着链入:

R运行状态:只要进程在运行队列中,该进程就叫做运行状态,可以被CPU随时调度。并不意味着进程一定在运行中.
对于所有底层硬件,操作系统也需要进行管理,先描述再组织,也就是有自己的结构体。

在每个设备结构体中都有个task_struct类型的wait_queue链表。
作用是管理 “等待该设备完成操作” 的进程,是操作系统实现 “进程阻塞 / 唤醒” 机制的核心组件之一。

比如图中当进程执行scanf()(需要读取键盘输入)时,操作系统会检查键盘设备的状态—— 如果键盘还没有输入(设备未就绪),就会把当前进程的task_struct(进程描述符)挂到键盘对应的struct device的wait_queue链表上,同时把进程从 “就绪队列” 移到 “阻塞态”(放弃 CPU)。
等用户按下键盘(设备就绪)后,操作系统会找到键盘设备wait_queue里挂着的task_struct,把这些进程重新移回 “就绪队列”,让它们等待 CPU 调度,继续执行scanf()读取输入。

“其他设备需要键盘输入” 这个场景本身不成立—— 键盘是 “输入设备”,只有 “进程需要键盘输入”,不存在 “设备需要键盘输入”(设备之间是独立的,比如磁盘、网卡不会主动请求键盘的数据)。
如果有其他的设备需要键盘输入,会接着链入到键盘的wait queue
这就是阻塞状态。
操作系统是硬件的管理者,清楚硬件是否有数据。
当有数据,会将wait queue中的task_struct重新链入到run queue中进行运行。
运行和阻塞的本质:是让不同的进程,处在不同的队列中。
挂起状态
操作系统内存不足时,会把在等待的进程代码和数据换入到磁盘的swap分区:

- 阻塞状态(Blocked)
- 原因:进程等待某个事件完成(如键盘输入、磁盘读写、锁释放),主动放弃 CPU。
- 资源占用:进程仍在内存中,只是暂时不参与 CPU 调度。
- 场景:比如进程调用
scanf()等 IO 操作时,进入阻塞态。
- 挂起状态(Suspended)
- 原因:进程被操作系统主动 “换出” 内存(通常是因为内存不足,或进程长时间未活动),暂时存放到磁盘的 “交换分区 / 交换文件” 中。
- 资源占用:进程的代码、数据不在内存中,只有
task_struct(进程描述符)留在内存。 - 场景:比如系统内存不足时,把长期阻塞的进程换出到磁盘,腾出内存给活跃进程。
运行时挂起状态也会存在,这种状态用时间换空间。
如果swap分区也解决不了,操作系统会直接将该进程。就好像有些游戏突然闪退。
| 维度 | 阻塞状态(Blocked) | 挂起状态(Suspended) |
|---|---|---|
| 无法执行的原因 | 等待事件完成(如 IO、锁) | 被操作系统换出内存(内存不足) |
| 内存占用情况 | 进程完整存放在内存中 | 进程被移到磁盘(仅保留进程描述符在内存) |
| 唤醒 / 恢复条件 | 等待的事件完成(如键盘输入) | 操作系统将进程换入内存(内存有空闲) |
| 与 CPU 的关系 | 不参与 CPU 调度,但在内存中 “待命” | 连内存都不在,无法被 CPU 调度(必须先换入内存) |
挂起状态通常分两种:
- 阻塞挂起:进程原本是阻塞态,又被换出内存(比如长期等 IO 的进程被换出);
- 就绪挂起:进程原本是就绪态,但内存不足被换出(换入内存后直接进入就绪队列)。
进程状态标志
- 为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。
下面的状态在kernel源代码里定义:
/*
* 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 */
};
-
R运行状态(
running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。 -
S睡眠状态(
sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep) -
D磁盘休眠状态(
Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。 -
T停止状态(
stopped): 可以通过发送SIGSTOP信号给进程来停止(T)进程。这个被暂停的进程可以通过发送SIGCONT信号让进程继续运行。 -
X死亡状态(
dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
#include <iostream>
#include <unistd.h>
int main()
{
int cnt = 0;
while(1)
{
printf("hello linux,cnt: %d\n",cnt++);
sleep(1);
}
return 0;
}
查看该进程运行的状态:

发现为什么是S状态?(+号表示在前台运行,先不用了解)
将printf代码给注释掉,再次查看状态:

会发现状态变为了运行状态。
因为printf一直在运行,一直在进行IO操作,引起了等待状态。
IO的速度非常慢。
- S:进程处于可中断睡眠状态,也就是阻塞态—— 进程正在等待某个事件完成(比如 IO 操作、信号),此时不参与 CPU 调度,直到等待的事件触发或收到信号被唤醒。
再将代码修改为scanf():
#include <iostream>
#include <unistd.h>
int main()
{
int cnt = 0;
while(1)
{
//printf("hello linux,cnt: %d\n",cnt++);
scanf("%d",&cnt);
sleep(1);
}
return 0;
}
依旧是S+:

则说明S是阻塞状态。即睡眠状态—可中断睡眠(浅睡眠)。
D – 不可终端睡眠,深度睡眠:

例如进程A有100w数据,需要磁盘进行存储,由于磁盘较慢,存储需要时间,这时进程A在做什么呢?
进程A会被run queue链接到对应设备的wait queue中,即变为等待状态。
但是这时如果操作系统的内存不足,OS要对A进程进行删除,同时磁盘也不足,磁盘询问进程A告诉上层返回错误值,是要进行重写还是终止。由于磁盘较慢,存储需要时间。
但是进程A已经被OS干掉了,最后,磁盘不知道怎么办,就不管了。
假设磁盘只保留了80w数据,这就造成了数据丢失。
为了防止这样的情况,我们规定了一个新的状态D,即当OS发现内存不足时,要进行删除进程,但是不能删除正在向磁盘写入数据的进程(如进程A),这时就只好向其他进程下手。磁盘就可以询问进程A该怎么做。
D状态的进程就不可被干掉。OS只好去干掉其他状态的进程。
D状态在一般系统中很难出现,如果出现了一般就是磁盘出问题了。
暂停进程
#include <iostream>
#include <unistd.h>
int main()
{
int cnt = 0;
while(1)
{
printf("hello linux,cnt: %d\n",cnt++);
//scanf("%d",&cnt);
sleep(1);
}
return 0;
}
运行进程,使用kill -19即可暂停进程



T表示暂停状态

要使它开始则使用kill -18即可重新开始。
但是我们重新开始之后,发现进程无法被ctrl+c终止掉,是因为进程被放到后台运行了:

这时只能用kill -9 xxx进行删除。
T状态一般是进程做了非法但不致命的操作,被OS暂停了。
能使用ctrl+c取消的都是前台进程。

像这种+后缀开头都是前台进程。
在运行可执行文件:./xxx + 空格 + &即可以后台运行方式进行·
./proc &
调试断点的进程
while:;do ps ajx | head -1 && ps ajx | grep code | grep -v grep;sleep 1;done
调试code:
gdb code
在7行打断点:

当我们打断点后,实际上是让进程暂停了:

暂停状态:
1.进程做了非法但是不致命的操作,被OS暂停,
2.当进程被追踪的时候,断点停下,就是暂停状态T。
进程退出状态
进程被创建出来
进程终止时通过 exit(status)、_exit(status) 或返回值传递的状态码,存储在进程描述符(task_struct)中,仅父进程可通过 wait()/waitpid() 读取。
获取退出码:
| 宏 | 作用 | 示例场景 |
|---|---|---|
WIFEXITED(status) | 判断子进程是否正常退出(状态码 0~127) | 正常退出返回非 0,否则返回 0 |
WEXITSTATUS(status) | 提取正常退出的状态码(需先通过 WIFEXITED 判断) | 若子进程 exit(5),则 WEXITSTATUS(status)=5 |
WIFSIGNALED(status) | 判断子进程是否被信号终止(状态码 128~255) | 被信号终止返回非 0,否则返回 0 |
WTERMSIG(status) | 提取终止子进程的信号编号(需先通过 WIFSIGNALED 判断) | 若子进程被 SIGSEGV 终止,则 WTERMSIG(status)=11 |
$?表示最近一个进程退出的信息:

而这个信息则是进程函数的返回值。例如main函数返回值,用于告诉我们是否正确的。
父进程是要关心子进程的执行任务的结果的,是否成功。
僵尸状态
僵尸进程与退出状态:子进程退出后,若父进程未调用 wait()/waitpid() 读取状态,子进程会变成僵尸进程(Z 状态),其退出状态会一直保留在内存中,直到父进程回收或父进程退出后由 init 进程回收。
用 “运动员比赛→受伤倒下→等待鉴定→法医检测→抬离赛场” 的场景,完美对应进程退出状态的完整生命周期,每个环节一一映射,直观易懂:
| 进程状态流程 | 运动员场景(形象类比) | 核心角色 / 动作 | 退出状态的对应含义 |
|---|---|---|---|
| 进程运行态(R) | 运动员在赛道上正常比赛 | 运动员(进程):执行 “跑步” 任务 | 进程正在执行指令,无退出状态 |
| 进程终止(触发退出) | 运动员突然受伤,倒地无法继续比赛 | 受伤事件(退出触发):如肌肉拉伤(正常退出)、被撞倒(异常信号终止) | 进程停止执行,开始生成退出状态 |
| 僵尸态(Z)→ 等待回收 | 运动员倒地后,等待法医到场(未鉴定) | 运动员(僵尸进程):失去行动能力,仅保留 “身份信息”;赛场工作人员(内核):保护现场,等待法医 | 退出状态(受伤原因)已记录在 “运动员信息卡”(task_struct),等待父进程(法医)读取 |
| 法医检测(状态读取) | 法医到场,检查运动员受伤原因(鉴定) | 法医(父进程):通过 “检查”(wait ()/waitpid ())读取受伤原因 | 父进程解析退出状态(正常 / 异常原因) |
| 进程彻底消失 | 鉴定完成,工作人员将运动员抬离赛场 | 赛场工作人员(内核):回收 “运动员占用的赛道资源”(PID、task_struct) | 退出状态被销毁,所有资源释放 |
- 阶段 1:运动员正常比赛(进程运行态 R)
- 场景:运动员在赛道上全力冲刺,正在执行 “比赛” 任务(对应进程执行指令);
- 状态特征:“活跃中”,占用赛道资源(CPU),无任何 “退出迹象”(对应进程无退出状态)。
- 阶段 2:运动员受伤倒地(进程终止,生成退出状态)
- 场景:运动员突然脚下打滑(正常退出:如
exit(0)完成任务),或被身后运动员撞倒(异常退出:如SIGSEGV信号),瞬间倒地,无法继续比赛; - 核心动作:
- 运动员停止跑步(进程停止执行);
- 赛场工作人员(内核)立即赶到,记录 “受伤时间、倒地位置”(对应内核回收进程内存、文件句柄等资源);
- 关键步骤:工作人员在 “运动员信息卡” 上初步标注 “受伤类型”(生成退出状态)—— 如 “自愿退赛(状态码 0)”“碰撞受伤(信号 11,状态码 139)”,但需法医确认。
- 阶段 3:等待法医鉴定(僵尸态 Z,退出状态待读取)
- 场景:运动员倒地后,法医还未到场,工作人员守护在旁,不移动运动员(对应内核保留
task_struct和PID); - 状态特征:
- 运动员(僵尸进程):无行动能力,仅保留 “身份信息 + 受伤初步记录”(对应仅保留
task_struct含退出状态); - 无法 “重新比赛”(进程无法被调度),也无法 “自行离开”(不能被
kill终止); - 退出状态:已存储在 “信息卡” 中,但未被法医(父进程)读取,处于 “待确认” 状态。
- 运动员(僵尸进程):无行动能力,仅保留 “身份信息 + 受伤初步记录”(对应仅保留
- 阶段 4:法医现场检测(父进程读取退出状态)
- 场景:法医(父进程)赶到,通过 “检查伤口、询问情况”(调用
wait()/waitpid()),确认受伤原因:- 若运动员是 “体力不支自愿退赛”(正常退出),法医记录 “状态码 0:正常退赛”(
WIFEXITED(status)=0); - 若运动员是 “被撞倒受伤”(异常退出),法医记录 “信号 2:碰撞终止,状态码 130”(
WIFSIGNALED(status)=2);
- 若运动员是 “体力不支自愿退赛”(正常退出),法医记录 “状态码 0:正常退赛”(
- 核心动作:法医读取并确认 “退出状态”,完成 “回收信息” 的关键步骤。
- 阶段 5:抬离赛场(进程彻底消失)
- 场景:法医鉴定完成后,工作人员(内核)将运动员抬离赛道(回收
PID、task_struct),赛道资源释放(PID可复用); - 状态特征:运动员彻底离开赛场(进程消失),“信息卡”(退出状态)被销毁,再也查询不到该运动员的 “比赛状态”。
死亡状态
“X 状态” 是 Linux 进程中的 “死亡态(Dead)” —— 它是进程从 “僵尸态(Z)” 到 “彻底消失” 之间的瞬时过渡状态,出现时间极短(毫秒级甚至更短),用ps命令几乎不可能捕捉到,日常排查中也基本不用关注。
进程退出:
1.代码不会执行了—首先可以立即释放的就是对应的程序信息数据
2.进程退出,要有退出信息,保存自己的task_struct内部
3.管理结构task_struct 必须被OS维护起来,方便用户未来进行获取进程退出的信息
一个进程 = 内核数据结构(task_struct) + 代码和数据。
那么进程创建时,是先有数据结构?还是数据呢?答案是先有数据结构,因为得先有对应的管理信息,才能有数据和代码。就好像高考结束后择校,学校得先拿到你的档案,这个档案就是你的数据结构。
这个创建过程很快,
那么进程在释放时,先释放的时代码和数据,释放过程中我们要对数据结构进行维护,这个状态就是僵尸状态。
这个时候就方便父进程或操作系统读取信息。
即退出信息需要被操作系统知道,这样操作系统才能知道任务完成的如何。
内核数据结构是最早产生,最后释放的。
模拟僵尸状态
创建子进程->父子进程同时存在->让子进程退出,父进程还活着,但是父进程什么都不做。
#include <iostream>
#include <unistd.h>
int main()
{
printf("父进程运行:pid:%d,ppid:%d\n",getpid(),getppid());
pid_t id = fork();
if(id == 0)
{
//子进程
int cnt = 10;
while (cnt)
{
printf("我是子进程,我的pid:%d,ppid:%d\n",getpid(),getppid());
sleep(2);
cnt--;
}
}
else
{
//父进程
while(1)
{
printf("我是父进程,我的pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
}
return 0;
}
编译运行之后,我们每隔一秒监测:
while :;do ps ajx | head -1 && ps ajx | grep myprocess; sleep 1; done
开始一段时间父子进程状态都是S:

之后当子进程中的cnt到达10之后,子进程变为僵尸状态


修改一下代码:
#include <iostream>
#include <unistd.h>
int main()
{
printf("父进程运行:pid:%d,ppid:%d\n",getpid(),getppid());
pid_t id = fork();
if(id == 0)
{
//子进程
int cnt = 10;
while (1)
{
printf("我是子进程,我的pid:%d,ppid:%d\n",getpid(),getppid());
sleep(2);
cnt--;
}
}
else
{
//父进程
while(1)
{
printf("我是父进程,我的pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
}
return 0;
}
我们将子进程改为死循环,然后在运行中杀掉子进程观察子进程状态.
删除之后剩下的就是父进程在运行:
kill -9 23452
发现子进程的状态是僵尸状态:

为什么子进程会一直存在呢?
僵尸状态的特性:如果没有人管理,它会一直僵尸,task_struct会一直消耗内存,这就造成了内存泄漏。

需要父进程读取子进程信息,子进程才会退出。
僵尸进程危害
-
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
-
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在
task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护?是的! -
那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
-
内存泄漏?是的。
语言层面的内存泄露问题,如果在常驻内存的进程中出现,影响比较大。
父进程在,子进程退掉,子进程可能会有僵尸。
如果父进程退出,子进程在,子进程会被init进程管理,变为孤儿进程。
孤儿进程
父进程在,子进程退出,子进程僵尸。
那么父进程退出,子进程存在呢?
代码和僵尸进程代码一样,父子进程死循环,使用kill杀掉父进程观察子进程。
打开三个终端,一个进行程序的运行,一个进行监控,一个进行程序的关闭:
while :;do ps ajx | head -1 && ps ajx | grep myprocess; sleep 1; done

此时父子进程都存在
kill -9 4247之后发现,只剩下了子进程,而ppid变为1

为什么父进程没有僵尸呢?
因为父进程的ppid是-bash,知道父进程退出了,将父进程回收了。
那子进程ppid为1是为什么呢?我们找一下:
输入top查看:

发现这个pid为1的是systemd,实际上它叫做操作系统的 “兜底管家”。
像这种父进程被回收,子进程被操作系统管理的就是叫做孤儿进程。
被系统领养的会被放到后台运行,使用kill -9 xxx才能被删掉.
进程状态查看
ps aux / ps axj 命令
进程状态总结

端,一个进行程序的运行,一个进行监控,一个进行程序的关闭:
while :;do ps ajx | head -1 && ps ajx | grep myprocess; sleep 1; done
[外链图片转存中…(img-5LvUk7Mp-1765005907169)]
此时父子进程都存在
kill -9 4247之后发现,只剩下了子进程,而ppid变为1
[外链图片转存中…(img-PxkELyAq-1765005907169)]
为什么父进程没有僵尸呢?
因为父进程的ppid是-bash,知道父进程退出了,将父进程回收了。
那子进程ppid为1是为什么呢?我们找一下:
输入top查看:
[外链图片转存中…(img-xQxFYiJ1-1765005907169)]
发现这个pid为1的是systemd,实际上它叫做操作系统的 “兜底管家”。
像这种父进程被回收,子进程被操作系统管理的就是叫做孤儿进程。
被系统领养的会被放到后台运行,使用kill -9 xxx才能被删掉.
进程状态查看
ps aux / ps axj 命令
进程状态总结

7444

被折叠的 条评论
为什么被折叠?



