Linux-进程状态

进程状态

并行和并发

对于单核也可以实现多个进程同时运行。

CPU切换和运行的速度非常快,人类感知不到,所以听音乐的时候不会觉得卡顿。

并发:CPU执行进程代码,不是 把进程代码执行完毕,才开始执行下一个,而是给每个进程预分配一个时间片,基于时间片,进行调度轮转(单CPU下)

并行:多个进程在多个CPU下分别,同时进行运行

概念 核心定义 底层实现 本质特征
并发 多个任务在同一时间段内交替执行(宏观上 "同时进行",微观上 "轮流执行") 依赖操作系统的任务调度(如时间片轮转),单个 CPU 即可实现 任务 "交替执行",共享资源需同步
并行 多个任务在同一时刻同时执行(宏观 + 微观均 "同时进行") 依赖多个 CPU 核心或多处理器,每个任务独立占用一个核心 任务 "同时执行",资源竞争较少
  • 并行是并发的 "子集":并行一定满足并发,但并发不一定是并行(单 CPU 只能实现并发,多 CPU 才能实现并行)。
  • 目标一致:均为提高 CPU 利用率,减少任务等待时间(如 IO 操作时切换任务)。

时间片

时间片是操作系统为每个就绪态进程 / 线程 分配的CPU 执行时间片段(通常为 10-100 毫秒),是实现 "并发" 的核心机制。

工作原理(以 Linux 为例)

  1. 调度器触发 :操作系统的进程调度器(如 LinuxCFS 调度器)按优先级为就绪态进程分配时间片。
  2. 执行与切换
    • 进程获得 CPU 后,在时间片内执行指令;
    • 时间片耗尽时,调度器触发上下文切换 (保存当前进程的寄存器、PC 指针等状态,加载下一个进程的状态);
    • 进程切换到就绪态,等待下一次调度,下一个进程获得 CPU 执行权。
  3. 循环往复:通过快速切换(毫秒级),人类感官上认为多个任务 "同时进行"。

时间片大小的影响

  • 太小:上下文切换频繁,切换开销(保存 / 加载状态)占比过高,降低系统效率;
  • 太大:并发响应变慢(如打开多个软件时,某个软件独占 CPU 过久,其他软件卡顿)

Linux和windows民用操作系统是分时操作系统

实时操作系统 :分进程优先级的系统(特定领域)

进程具有独立性

一个进程出现问题不会影响另一个进程,父子进程也是。

等待的本质

进程等待是指进程因等待某个事件完成 (如 IO 操作、资源分配、子进程结束),主动放弃 CPU 执行权,进入阻塞态(非就绪态),直到等待的事件发生后,由操作系统唤醒并回到就绪态,重新等待调度。

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

R运行状态:只要进程在运行队列中,该进程就叫做运行状态,可以被CPU随时调度。并不意味着进程一定在运行中.

对于所有底层硬件,操作系统也需要进行管理,先描述再组织,也就是有自己的结构体。

在每个设备结构体中都有个task_struct类型的wait_queue链表。

作用是管理 "等待该设备完成操作" 的进程,是操作系统实现 "进程阻塞 / 唤醒" 机制的核心组件之一。

比如图中当进程执行scanf()(需要读取键盘输入)时,操作系统会检查键盘设备的状态 ------ 如果键盘还没有输入(设备未就绪),就会把当前进程的task_struct(进程描述符)挂到键盘对应的struct devicewait_queue链表上,同时把进程从 "就绪队列" 移到 "阻塞态"(放弃 CPU)。

等用户按下键盘(设备就绪)后,操作系统会找到键盘设备wait_queue里挂着的task_struct,把这些进程重新移回 "就绪队列",让它们等待 CPU 调度,继续执行scanf()读取输入。

"其他设备需要键盘输入" 这个场景本身不成立------ 键盘是 "输入设备",只有 "进程需要键盘输入",不存在 "设备需要键盘输入"(设备之间是独立的,比如磁盘、网卡不会主动请求键盘的数据)。

如果有其他的设备 需要键盘输入,会接着链入到键盘的wait queue

这就是阻塞状态

操作系统是硬件的管理者,清楚硬件是否有数据。

当有数据,会将wait queue中的task_struct重新链入到run queue中进行运行。

运行和阻塞的本质:是让不同的进程,处在不同的队列中。

挂起状态

操作系统内存不足时,会把在等待的进程代码和数据换入到磁盘的swap分区:

  1. 阻塞状态(Blocked)
  • 原因 :进程等待某个事件完成(如键盘输入、磁盘读写、锁释放),主动放弃 CPU。
  • 资源占用 :进程仍在内存中,只是暂时不参与 CPU 调度。
  • 场景 :比如进程调用scanf()等 IO 操作时,进入阻塞态。
  1. 挂起状态(Suspended)
  • 原因 :进程被操作系统主动 "换出" 内存(通常是因为内存不足,或进程长时间未活动),暂时存放到磁盘的 "交换分区 / 交换文件" 中。
  • 资源占用 :进程的代码、数据不在内存中 ,只有task_struct(进程描述符)留在内存。
  • 场景:比如系统内存不足时,把长期阻塞的进程换出到磁盘,腾出内存给活跃进程。

运行时挂起状态也会存在,这种状态用时间换空间。

如果swap分区也解决不了,操作系统会直接将该进程。就好像有些游戏突然闪退。

维度 阻塞状态(Blocked) 挂起状态(Suspended)
无法执行的原因 等待事件完成(如 IO、锁) 被操作系统换出内存(内存不足)
内存占用情况 进程完整存放在内存中 进程被移到磁盘(仅保留进程描述符在内存)
唤醒 / 恢复条件 等待的事件完成(如键盘输入) 操作系统将进程换入内存(内存有空闲)
与 CPU 的关系 不参与 CPU 调度,但在内存中 "待命" 连内存都不在,无法被 CPU 调度(必须先换入内存)

挂起状态通常分两种:

  1. 阻塞挂起:进程原本是阻塞态,又被换出内存(比如长期等 IO 的进程被换出);
  2. 就绪挂起:进程原本是就绪态,但内存不足被换出(换入内存后直接进入就绪队列)。

进程状态标志

  • 为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。
    下面的状态在kernel源代码里定义:
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 */
};
  • R运行状态running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。

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

  • D磁盘休眠状态Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。

  • T停止状态stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。

  • X死亡状态dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

c++ 复制代码
#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():

sql 复制代码
#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状态在一般系统中很难出现,如果出现了一般就是磁盘出问题了。

暂停进程

c++ 复制代码
#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 + 空格 + &即可以后台运行方式进行·

c++ 复制代码
./proc &

调试断点的进程

shell 复制代码
while:;do ps ajx | head -1 && ps ajx | grep code | grep -v grep;sleep 1;done

调试code

shell 复制代码
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. 阶段 1:运动员正常比赛(进程运行态 R)
  • 场景:运动员在赛道上全力冲刺,正在执行 "比赛" 任务(对应进程执行指令);
  • 状态特征:"活跃中",占用赛道资源(CPU),无任何 "退出迹象"(对应进程无退出状态)。
  1. 阶段 2:运动员受伤倒地(进程终止,生成退出状态)
  • 场景:运动员突然脚下打滑(正常退出:如exit(0)完成任务),或被身后运动员撞倒(异常退出:如SIGSEGV信号),瞬间倒地,无法继续比赛;
  • 核心动作:
    • 运动员停止跑步(进程停止执行);
    • 赛场工作人员(内核)立即赶到,记录 "受伤时间、倒地位置"(对应内核回收进程内存、文件句柄等资源);
    • 关键步骤:工作人员在 "运动员信息卡" 上初步标注 "受伤类型"(生成退出状态)------ 如 "自愿退赛(状态码 0)""碰撞受伤(信号 11,状态码 139)",但需法医确认。
  1. 阶段 3:等待法医鉴定(僵尸态 Z,退出状态待读取)
  • 场景:运动员倒地后,法医还未到场,工作人员守护在旁,不移动运动员(对应内核保留task_structPID);
  • 状态特征:
    • 运动员(僵尸进程):无行动能力,仅保留 "身份信息 + 受伤初步记录"(对应仅保留task_struct含退出状态);
    • 无法 "重新比赛"(进程无法被调度),也无法 "自行离开"(不能被kill终止);
    • 退出状态:已存储在 "信息卡" 中,但未被法医(父进程)读取,处于 "待确认" 状态。
  1. 阶段 4:法医现场检测(父进程读取退出状态)
  • 场景:法医(父进程)赶到,通过 "检查伤口、询问情况"(调用wait()/waitpid()),确认受伤原因:
    • 若运动员是 "体力不支自愿退赛"(正常退出),法医记录 "状态码 0:正常退赛"(WIFEXITED(status)=0);
    • 若运动员是 "被撞倒受伤"(异常退出),法医记录 "信号 2:碰撞终止,状态码 130"(WIFSIGNALED(status)=2);
  • 核心动作:法医读取并确认 "退出状态",完成 "回收信息" 的关键步骤。
  1. 阶段 5:抬离赛场(进程彻底消失)
  • 场景:法医鉴定完成后,工作人员(内核)将运动员抬离赛道(回收 PIDtask_struct),赛道资源释放(PID 可复用);
  • 状态特征:运动员彻底离开赛场(进程消失),"信息卡"(退出状态)被销毁,再也查询不到该运动员的 "比赛状态"。
死亡状态

"X 状态" 是 Linux 进程中的 "死亡态(Dead)" ------ 它是进程从 "僵尸态(Z)" 到 "彻底消失" 之间的瞬时过渡状态 ,出现时间极短(毫秒级甚至更短),用ps命令几乎不可能捕捉到,日常排查中也基本不用关注。

进程退出

1.代码不会执行了---首先可以立即释放的就是对应的程序信息数据

2.进程退出,要有退出信息,保存自己的task_struct内部

3.管理结构task_struct 必须被OS维护起来,方便用户未来进行获取进程退出的信息

一个进程 = 内核数据结构(task_struct) + 代码和数据。

那么进程创建时,是先有数据结构?还是数据呢?答案是先有数据结构,因为得先有对应的管理信息,才能有数据和代码。就好像高考结束后择校,学校得先拿到你的档案,这个档案就是你的数据结构。

这个创建过程很快,

那么进程在释放时,先释放的时代码和数据,释放过程中我们要对数据结构进行维护,这个状态就是僵尸状态。

这个时候就方便父进程或操作系统读取信息。

即退出信息需要被操作系统知道,这样操作系统才能知道任务完成的如何。

内核数据结构是最早产生,最后释放的。

模拟僵尸状态

创建子进程->父子进程同时存在->让子进程退出,父进程还活着,但是父进程什么都不做。

c++ 复制代码
#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之后,子进程变为僵尸状态

修改一下代码:

c++ 复制代码
#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;
}

我们将子进程改为死循环,然后在运行中杀掉子进程观察子进程状态.

删除之后剩下的就是父进程在运行:

shell 复制代码
kill -9 23452

发现子进程的状态是僵尸状态:

为什么子进程会一直存在呢?

僵尸状态的特性:如果没有人管理,它会一直僵尸,task_struct会一直消耗内存,这就造成了内存泄漏

需要父进程读取子进程信息,子进程才会退出

僵尸进程危害

  • 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!

  • 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护?是的!

  • 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!

  • 内存泄漏?是的。

语言层面的内存泄露问题,如果在常驻内存的进程中出现,影响比较大。

父进程在,子进程退掉,子进程可能会有僵尸。

如果父进程退出,子进程在,子进程会被init进程管理,变为孤儿进程。

孤儿进程

父进程在,子进程退出,子进程僵尸。

那么父进程退出,子进程存在呢?

代码和僵尸进程代码一样,父子进程死循环,使用kill杀掉父进程观察子进程。

打开三个终端,一个进行程序的运行,一个进行监控,一个进行程序的关闭:

shell 复制代码
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 命令

进程状态总结

端,一个进行程序的运行,一个进行监控,一个进行程序的关闭:

shell 复制代码
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 命令

进程状态总结

相关推荐
用户97183563346610 小时前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪11 小时前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠1 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush41 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5201 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩1 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
古城小栈1 天前
Unix 与 Linux 异同小叙
linux·服务器·unix
凡人叶枫1 天前
Effective C++ 条款42:了解 typename 的双重意义
java·linux·服务器·c++
2601_961875241 天前
决战申论100题2026|最新|范文
linux·容器·centos·debian·ssh·fabric·vagrant
java_cj1 天前
深入kube-apiserver认证机制:从Bearer Token到mTLS的完整认证链解析
linux·运维·服务器·云原生·容器·kubernetes