Linux 进程概念 (三) (进程状态,僵尸进程,孤儿进程,进程优先级)

目录

一、操作系统学科上的进程状态

运行状态

阻塞状态

运行队列和阻塞队列

挂起状态

运行挂起和阻塞挂起

二、linux中对应的进程状态

R运行状态

S睡眠状态

D磁盘休眠状态

T停止状态

kill

X死亡状态

前台进程和后台进程

三、僵尸进程(Z僵尸状态)

什么是僵尸进程?

僵尸进程的危害

问题

四、孤儿进程

什么是孤儿进程?

查看1号进程

孤儿进程为什么会被领养?

为什么会被1号进程领养?

六、进程优先级

什么是进程优先级?

为什么要有进程优先级?

查看系统进程

PRI和NI

修改进程优先级

[改法 : 使用top](#改法 : 使用top)

PRI(new)=PRI(old)+NI

问题:

实时操作系统与分时操作系统

七、进程之间的关系

八、总结


一、操作系统学科上的进程状态

上述为操作系统学科上的进程状态,这里我们仅针对进程状态特定的介绍一下运行,阻塞,挂起三种状态

运行状态

  1. 每个 CPU 都维护一个运行队列,该队列通过结构体实现,包含指向进程控制块(PCB)队列头部和尾部的两个指针。运行队列本质上是大量进程的排队机制,实际排队的是进程的 PCB。PCB 通过指针关联其对应的进程代码和数据。虽然称为"运行队列",但并非所有进程都在执行,"运行"指的是这些进程已准备就绪,可随时被调度器选中执行。
  2. 调度器会优先选择队列首部的 PCB 进行调度。当头部进程在 CPU 上执行一段时间后,会被重新放回队列尾部继续排队。这种频繁将进程调入调出 CPU 的操作称为进程切换
  3. CPU 的运算速度极快,达到纳秒级别,远超人类感知。以日常电脑为例,同时运行多个应用程序(如音乐播放器、浏览器、绘图软件等),看似这些进程在并行执行,实则不然。由于 CPU 的超高速度,它实际上是在极短时间内依次调度各个进程执行:从队列头部取出进程运行片刻后放回尾部,如此循环往复。同一时刻,CPU 只能执行一个进程。
  4. 每个 PCB 都记录有时间片信息,用于限制进程占用 CPU 的时间(如 10ms)。这确保了不会有进程独占 CPU 直至执行完毕。在极短时间周期内,CPU 能完成对整个运行队列的多次轮询,使得所有进程代码都能得到执行,这种现象称为并发执行
  5. 处于运行队列中的所有进程都处于运行状态,简称为 R(run)状态。

阻塞状态

阻塞的一种现象 : 比如scanf,在等待输入时,等待键盘上面的待输入数据,其实就处于阻塞状态

  1. 我们知道计算机系统由各类外部设备(外设)组成,操作系统作为管理软硬件资源的软件,对外设的管理也遵循"先描述后组织"的原则。具体实现方式是为每个设备创建一个硬件设备控制块(struct dev),然后通过数据结构(如单链表)将这些控制块组织起来,便于统一管理。
  2. 在这些硬件设备控制块中,包含指向进程控制块(PCB)的指针 。当进程需要访问外设(如键盘)时,若设备尚未就绪(例如用户未输入数据),进程将进入等待状态。此时,该进程的PCB会被链接到设备控制块(DCB)的PCB指针上,形成一个专门的队列,在操作系统中称为"阻塞队列"。
  3. 若后续有其他进程也需要访问同一设备,它们将被依次添加到阻塞队列的末尾继续等待。处于阻塞队列中的所有进程,其状态均被标记为"阻塞状态"。这种机制确保了设备资源的合理分配和进程的有序等待。
运行队列和阻塞队列

内核里的"运行队列"和"阻塞队列",本质是两种存放PCB(进程控制块)的"容器 " ------所有进程的状态变化,底层都是PCB在这两个队列之间的移动,没有例外。

  1. 运行队列 (Run Queue):内核维护的"待CPU执行"PCB集合,里面的每个PCB都满足"资源全就绪,只差CPU"。队列按进程优先级排序 ,Linux里每个CPU核心都有自己的运行队列(避免竞争)。

  2. 阻塞队列 (Block Queue):不是单一个队列,而是按"等待的资源类型"分的多个队列 (如磁盘IO阻塞队列、网络阻塞队列等外设设备的队列)。里面的PCB都在"等资源"(比如等磁盘读完数据、等网卡收到网络包)。
    判断处于运行状态和阻塞状态的核心判断依据就是进程在内核的哪个队列中,直接对应运行/阻塞状态。

  3. 运行状态:进程在就绪队列中,要么正在用CPU,要么排队等CPU,随时能执行。

  4. 阻塞状态:进程不在就绪队列,而是在阻塞队列 (如等待磁盘IO的队列、等待网络的队列等外设相关的队列)中,必须等资源就绪(如scanf输入语句)才能回到运行队列。

挂起状态

  1. 当操作系统内存资源严重不足时,为确保系统安全、稳定和高效运行,必须采取措施释放内存。由于计算机外设状态可能未就绪,导致大量进程处于阻塞等待状态,操作系统会优先处理这些阻塞进程。
  2. 从技术角度看,加载到内存中 的PCB(进程控制块)和程序对应的代码数据组成称为进程。当大量进程同时存在时,会占用可观的内存空间。处于阻塞状态的进程只需保留PCB即可,因其当前无法执行操作,代码数据可以暂时移出内存。考虑到磁盘空间远大于内存,操作系统会将阻塞进程的代码数据移至磁盘交换区(swap分区) 即swap out**,这一过程称为"换出"或"进程挂起",这个状态叫做挂起状态。**
  3. 当外设就绪后,进程结束等待状态,操作系统会将其代码数据从磁盘"换入"内存即swap in,并将进程加入CPU运行队列,使其恢复运行状态。
  4. 需要注意的是,这种内存资源严重不足的情况较为罕见,进程挂起并非常态。只有当内存极度紧张时才会触发,此外还可能因调试需求、系统管理或进程优先级策略等其他因素引发挂起操作。
运行挂起和阻塞挂起
  1. 运行挂起 : 进程原本在运行/就绪队列(等CPU),因系统资源紧张(如内存不足)内核将其从运行队列移出,状态标记为"运行挂起",暂时不参与CPU调度。
  2. 阻塞挂起 : 进程原本已在阻塞队列(等磁盘/网络资源),且内存极度紧张,内核会把它的PCB和数据从物理内存换出到磁盘(swap分区),状态变为"阻塞挂起",连阻塞队列都暂时退出。

核心是挂起的原因不同:运行挂起是"进程主动放弃CPU,暂离运行队列";阻塞挂起是"进程等资源时,连阻塞队列都暂离,被换出内存",二者都是进程暂时无法执行,但触发场景和内核处理逻辑完全不同。

二、linux中对应的进程状态

上面只是操作系统学科上的对进程状态的理解,接下来我们着重看一下在Linux中对进程状态的理解,掌握Linux中的进程状态,可以更好深入我们对进程状态的理解!
下面是Linux内核中定义进程状态的代码片段,它列出了进程的核心状态:

bash 复制代码
//linux源代码如下
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调度,属于活跃的可执行状态。
  2. S(sleeping):睡眠状态 ------ 进程因等待I/O操作或信号等资源而进入阻塞队列,可被信号唤醒,属于可中断的休眠状态。
  3. D(disk sleep):深度睡眠状态 ------ 进程正在等待磁盘I/O等关键资源,无法被信号中断,强制终止可能导致数据损坏,仅在资源就绪时自动恢复。
  4. T(stopped):停止状态 ------ 进程被主动暂停(如使用Ctrl+Z),处于运行挂起状态,可通过fg/bg命令恢复执行。
  5. t(tracing stop):跟踪停止状态 ------ 进程因调试器(如gdb)跟踪而暂停,仅出现在调试场景中。
  6. X(dead):死亡状态 ------ 进程已完成终止并释放资源,属于瞬时状态,通常无法通过监控工具观察到。
  7. Z(zombie):僵尸状态 ------ 进程已终止但父进程尚未读取其退出状态,仅保留进程控制块(PCB),需父进程处理后才能完全释放资源。

R运行状态

R运行状态(running):并不意味着进程一定在运行中,它表明进程要么在CPU运行中,要么在CUP对应的运行队列里**(运行+就绪)**

为什么我们的进程处于运行状态?

代码中是 while(1) 的无限循环(没有 sleep 、等待输入等阻塞操作),进程会持续占用CPU执行循环逻辑------因此显示其状态为 R+ (前台运行/就绪),符合"无阻塞、持续执行"的进程特征。,

S睡眠状态

S睡眠状态(sleeping):意味着进程在等待事件完成,同时也叫做可中断睡眠,浅度睡眠,即睡眠期间可以被唤醒,对应操作系统学科中的阻塞状态

为什么我们的进程会处于睡眠状态?

代码里的 scanf("%d", &a) 会让进程进入"等待终端输入"的阻塞状态------此时进程不需要占用CPU,会被系统调度到阻塞队列,因此 ps 显示其状态为 S+ (前台可中断睡眠)。

S状态可使用kill被中断 , 也能说明其是浅度休眠

D磁盘休眠状态

D磁盘休眠状态(disk sleep):D状态是Linux特有的一个进程状态, 但它是不可被中断的休眠状体 , 也叫做深度休眠状态,处于这个状态的进程通常会等待IO的结束,不响应任何请求,处于深度睡眠的进程,不响应操作系统的任何请求,也无法被 kill -9杀死

让我们以进程向磁盘写入数据为例进行说明(以下场景为假设情况,并非真实案例)。

当进程尝试向磁盘写入数据时(假设磁盘空间不足),磁盘接收数据并开始写入操作,此时进程会等待磁盘返回写入结果。恰巧此时操作系统面临内存资源严重不足 的情况,急需释放内存空间。操作系统发现该进程处于等待状态 ,便会认为:"系统内存即将耗尽,运行效率严重下降,而这个进程却还在悠闲等待 "。于是操作系统直接终止了该进程

当磁盘尝试写入数据时,发现剩余空间确实不足,便会判断:"这个数据量太大无法写入,但剩余空间足够处理其他较小的数据"。于是磁盘直接丢弃该进程的数据,转而处理其他进程的较小写入请求 。完成操作后,磁盘准备向原进程返回响应,却发现该进程已不存在。

此时,**进程已被操作系统终止,其对应的代码和数据随之释放;同时磁盘也因空间不足丢弃了数据,导致数据永久丢失。**从系统角度来看:

  • 操作系统为维护内存资源而终止进程,这是合理的;
  • 进程在等待磁盘响应时被终止,属于受害者;
  • 磁盘因空间不足选择优先处理较小数据,也是合理行为。

这种情况下很难界定责任归属,但频繁的数据丢失对用户而言是不可接受的。

为解决该问题,系统会将此类进程设置为"深度睡眠"状态:

  • 进程不会响应操作系统的任何请求,因此不会被强制终止;
  • 进程的代码和数据得以保留;
  • 只有收到磁盘的明确响应后,进程才会恢复运行。

通过这种机制,上述数据丢失问题就能得到有效避免。

T停止状态

停止状态(stopped/tracing stop):

  • 进程可通过信号暂停或恢复运行
  • 在学习阶段通常不区分stopped和tracing stop状态

追踪停止状态(tracing stop):

  • 特指调试过程中的进程暂停状态
  • 例如使用gdb调试时,进程会在断点处暂停
  • 此时调试器可查看变量信息并控制进程执行

T暂停状态的主要作用:

  • 实现对进程的调试控制
  • 便于检查程序运行时的状态信息

当程序运行到第8行的断点处之后,使用ps查看进程状态,进程对应进程状态为t,即当前进程处于追踪停止状态

kill

kill指令可以发送信号进行控制进程

  • kill -19 进程标识符 将进程暂停
  • kill -18 进程标识符 让进程继续运行
  • kill -9 进程标识符 杀死进程,终止进程

X死亡状态

X状态(彻底死亡)是Z状态(僵尸)是的前一步,二者是"先僵尸、再彻底死亡消失"的先后关系:

  1. 进程先终止→进入Z状态(留着PCB等父进程回收,是"死了但没清干净");
  2. 父进程回收PCB后→Z状态转为X状态(瞬时过渡,"彻底清干净了");
  3. X状态结束→进程从系统中完全消失,生命周期结束。

简单说:Z是"死了没埋",X是"埋了的最后一瞬间",先Z后X,X之后啥都没了。

关于Z僵尸状态和僵尸进程 , 我们在后面再展开讲解.

前台进程和后台进程

什么是前台进程和后台进程?

进程状态后面带 + 的就是前台进程 , 不带 + 就是后台进程

怎么删除?

  1. 删除前台进程 : 前台进程正在占用终端,直接按 Ctrl+C 即可终止
  2. 删除后台进程 : 先通过 ps axj | grep myprocess 找到后台进程的PID;再执行 kill PID (如 kill 1234 ),若进程无响应,用 kill -9 PID 强制终止。

应用场景:

  1. 前台进程:用在需要实时交互的场景
  • 比如运行需要你手动输入、实时看结果的程序------像执行 ./myprocess 后要通过键盘输参数( scanf )、实时看程序打印的日志( printf ),或用 vim myprocess.c 编辑文件(必须前台,得手动敲键盘操作)。核心是进程要和你实时互动,离不开你的操作。
  1. 后台进程:用在无需手动干预、要长期跑的场景
  • 比如运行耗时的任务------像编译大项目( make & )、后台统计数据( ./count_data & )、启动服务程序(如后台挂着的小脚本)。核心是进程自己能跑完,不用你盯着,你还能在终端干别的事,不耽误用电脑。
    所以区分前后台进程的核心就是 : 是否能从键盘中获取数据输入 ;

能直接接收键盘输入的,就是前台进程;不能接收键盘输入的,就是后台进程
并且同一终端里同一时间只能有1个前台进程 ------它会独占终端的输入响应(比如Ctrl+C、键盘输入),你得等它结束或切到后台,才能再运行新的前台进程;
而后台进程可以同时有多 个------它们不抢终端输入,能在后台并行运行,你还能正常在终端输新命令。

比如你可以同时让3个程序在后台跑, 但是同一时间,只有1个能在前台"霸占"你的键盘操作。

三、僵尸进程(Z僵尸状态)

什么是僵尸进程?

僵尸进程(zombies):子进程终止的时候,如果父进程没有主动回收子进程的信息,那么子进程会让自己一直处于Z僵尸状态,即对应子进程相关资源尤其是task_struct结构体不能释放

首先我们在代码中先让父进程 fork() 创建子进程 , 然后使父子进程都进入 while(1) 死循环,每隔1秒打印一次信息。当开始运行时 , 父子进程都执行 sleep(1) 时,都会进入S+状态( 可中断睡眠+前台进程组),这是因为 sleep 本质是"等待时间资源",符合"S状态是等待资源"的逻辑。当我们用 kill -9 27250 杀死子进程后,子进程变成了Z+状态(僵尸+前台进程组),并且标注了 <defunct> (无效)------这是因为父进程还在死循环里,没调用 wait() 去回收子进程的PCB,所以子进程进入了僵尸状态。

核心结论:

  1. 进程执行 sleep 会进入S状态;
  2. 子进程被杀死后,若父进程不回收,会变成Z(僵尸)状态。

僵尸进程的危害

  1. 子进程的退出状态必须被维持下去,因为它要告诉关心它的父进程,父进程交给自己的任务,自己完成的怎么样了,可是如果父进程一直不进行读取,那么此时子进程就要一直维持这个Z僵尸状态
  2. 那么这个退出状态维护的其实是子进程的进程信息,即进程的PCB即task_struct结构体,那么子进程对应的未进行释放的task_struct结构体会一直会得不到释放,就会一直占用空间,那么就会造成内存泄露

问题

关于进程退出与僵尸进程的4个问题:

1. 进程退出了 , 退出信息是什么?

进程退出时通常会记录以下关键信息:

  • 退出状态码:0表示成功退出,非零值表示异常退出
  • 终止信号:若进程被强制终止(如通过kill -9命令),会记录相应的信号编号
  • 资源使用情况:部分系统会记录CPU时间、内存占用等资源使用统计信息

2. 进程退出了 , 退出的信息保存在哪里?

这些信息存储在task_struct(即进程控制块PCB)中。PCB是内核为每个进程维护的数据结构,即使进程终止,其PCB仍会暂时保留退出状态,直到被父进程读取后才会释放。

3. 检测Z状态进程 , 回收Z状态进程 , 本质是在做什么?

  • 检测机制:通过遍历系统进程表,筛选出"已终止但进程控制块(PCB)未被释放"的进程记录
  • 回收机制:当父进程获取子进程终止信息后,系统将清除该进程的PCB记录,使其从进程表中永久移除

4. 具体怎么回收?谁来回收?

  • 回收主体

    默认由父进程负责回收子进程;若父进程先终止,子进程将由 init 进程(PID=1)接管并完成回收。

  • 回收方式

    1. 主动回收 :父进程调用 wait() 或 waitpid() 函数获取子进程退出状态,并释放其 PCB 资源;
    2. 被动回收:若父进程未处理,终止父进程后,子进程由 init 进程自动回收;
    3. 工具辅助:部分系统工具(如 wait 类命令)可协助触发回收流程。

5. 如果进程处于僵尸状态 , 如果我一直不回收 , 会造成什么结果?

若父进程不回收子进程 → 子进程会长期处于Z(僵尸)状态 → 其对应的 task_struct (即PCB)不会被释放 → task_struct 占用内核内存空间 → 大量堆积时会引发"内存泄漏"的资源占用问题。

四、孤儿进程

什么是孤儿进程?

孤儿进程:父子进程中,父进程先退出 ,子进程的父进程会被改为1号进程(操作系统),我们称该子进程被操作系统领养了,那么像这种父进程是1号进程的子进程我们称为孤儿进程

此时这个子进程就变成了后台进程,普通的ctrl+c无法进行退出,只能使用kill -9 进程标识符,将子进程杀死
孤儿进程不是进程状态,它只是对"父进程已退出、自己还存活的进程"的一种"描述"

运行:

代码中,父进程执行5次循环后就 return 0 退出了,但子进程是 while(1) 死循环(一直运行)------这时候子进程还活在,但它的父进程已经没了,所以子进程就变成了孤儿进程。

查看1号进程

这里不能直接说1号进程是操作系统,它是操作系统用户态的核心进程 , 负责初始化系统服务、管理孤儿进程、回收僵尸进程 等。

简单总结:父进程先死→子进程变孤儿→内核让1号进程接管(避免无人管理),1号进程是系统用户态的核心管理进程。

孤儿进程为什么会被领养?

当一个进程的父进程先退出,这个进程就会变成孤儿进程。为了避免孤儿进程无人管理(比如退出后变成僵尸进程),Linux内核会自动把它"托付"给一个"兜底"进程------也就是1号进程。

为什么会被1号进程领养?

1号进程是操作系统启动后创建的第一个用户态进程,它的核心职责之一就是管理孤儿进程 ,相当于系统的"进程管家"。所以所有孤儿进程都会被1号进程接管(领养),后续由1号进程负责回收它的资源。

为什么我们按 ctrl+c 怎么退不出呢?

并且为什么这个孤儿进程从前台进程变为了后台进程?

因为父进程先于子进程退出,导致子进程成为孤儿进程;此时内核会将孤儿进程从原前台进程组移出,自动转为后台进程。由于后台进程本身不能从键盘获取输入,无法响应 Ctrl + C 这类依赖终端的快捷键,因此无法通过快捷键终止,只能通过 kill 进程PID 命令手动杀死该孤儿进程。
所以区分前后台进程的核心就是 : 是否能从键盘中获取数据输入 ;

能直接接收键盘输入的,就是前台进程;不能接收键盘输入的,就是后台进程

六、进程优先级

(理论很重要)

什么是进程优先级?

优先级:对于资源的访问,谁先访问,谁后访问

进程的优先级 : CPU资源分配的先后顺序,优先级高的进程有优先执行访问权限的权利!

也可以理解为 : 进程在已经得到某种资源的前提下,得到某种资源的先后顺序!

为什么要有进程优先级?

  1. 因为CPU的资源是有限 的,进程有多个,所以就进程之间就必然有竞争关系
  2. 操作系统必须保证大家良性竞争,所以就要确定优先级
  3. 如果非良性竞争,那么就会导致某些进程长时间得不到CPU资源,该进程的代码长时间得不到推进------进程的饥饿问题
  4. 通常来讲,非必要情况用户不需要调整优先级,进程的优先级的调整由调度器管控
    调度队列的本质就是确定优先级

查看系统进程

我们在前已经使用过ps axj查看了系统中进程信息的核心命令------通过ps可以获取进程的ID、状态、优先级、所属用户等关键信息,是管理和调试进程的常用工具。

bash 复制代码
ps aux / ps axj / ps ajx
  1. a:显示一个终端所有的进程,包括其他用户的进程。
  2. x:显示没有控制终端的进程,例如后台运行的守护进程。
  3. j:显示进程归属的进程组ID、会话ID、父进程ID,以及与作业控制相关的信息
  4. u:以用户为中心的格式显示进程信息,提供进程的详细信息,如用户、CPU和内存使用情况等

下面我们再来介绍另一个指令:

ps -la

  1. F :进程标志,代表进程的权限/状态标识(如 4 表示进程拥有root权限)
  2. S :进程状态,常见值包括 S (睡眠)、 R (运行)、 Z (僵尸)等
  3. UID进程所属用户的ID, 0 对应root用户, 1001 是普通用户ID
  4. PID进程的唯一标识ID
  5. PPID当前进程的父进程ID
  6. C :进程的CPU使用率(百分比)
  7. PRI进程实际优先级,数值越小优先级越高,默认值通常为80
  8. NI进程的nice值(优先级调整参数),范围-20~19,默认值为0;NI越小,PRI越低(优先级越高)
  9. ADDR :进程在内存中的地址,显示 - 表示进程处于运行状态
  10. SZ :进程占用的内存大小(单位为页)
  11. WCHAN :进程等待的内核函数,显示 - 表示进程无等待、正在运行
  12. TTY :进程关联的终端设备,如 pts/0 对应当前SSH终端
  13. 最后一列(时间):进程累计占用的CPU时间

PRI和NI

其实进程优先级可以看成task-struct里面的两个整型变量PRI和NI (proirity和nice)

  1. PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
  2. 那NI呢? 就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
  3. PRI值越小越快被执行,那么加入nice值后,将会使得 PRI 变为:PRI(new)=PRI(old)+NI
  4. 这样,当nice值为负值的时候,那么该程序将会优先级值变小,即其优先级会变高,则其越快被执行
  5. 所以,调整进程优先级,在Linux下,就是调整进程nice值

修改进程优先级

改法 : 使用top
  1. 将程序运行起来,复制SSH渠道,在复制的渠道中输入top,按下回车
  2. 之后继续输入r,按下回车
  3. 那么此时它会提示要你输入要调整进程对应的PID,我们这时就输入进程的PID,按下回车
  4. 然后会显示需要设置的NI值,我们就输入需要调整的NI值

按R键+输入进程PID

输入需要调整的NI值

此时我们给NI值输入的是10,根据PRI(new)=PRI(old)+NI这个式子,新的PRI值就变成了90,PRI值由80变成了90,就代表着进程优先级被修改了,这里改为90后进程的优先级是被降低了


PRI(new)=PRI(old)+NI

在Linux系统中,每次修改进程优先级时, PRI(old) 默认都是80,这是系统的设计规则

Linux为了简化优先级计算、统一优先级调整逻辑,将 PRI(old) 固定为默认值80 ------无论进程当前实际优先级是多少,再次调整时都会以80作为基准,结合新的 nice 值计算新优先级 (即 PRI(new) = 80 + NI )。

例如:若进程当前PRI是90(之前设置了 nice=10 ),再次调整 nice=-5 时,新PRI是 80 + (-5) = 75 ,而非 90 + (-5) = 85 。

问题:

NI值的范围是多少?

在Linux系统的进程调度里, NI (Nice值)的范围是 -20 到 +19,共40个梯度。

NI值为什么是这个范围?

如果NI值范围过大(比如允许更低的负值),高优先级进程可能长期占用CPU,导致低优先级进程"饿死";若正值过大,低优先级进程可能永远无法获得资源。-20到+19的区间能平衡"高优先级进程优先"和"低优先级进程不被忽略"的需求。

进程优先级的变化范围是多少?

PRI (进程优先级)的范围取决于系统的计算规则,常见有两种情况:

  1. 基于 PRI = 20 + NI 的场景:范围是0~39(对应NI的-20~+19)。
    2. 基于 PRI = 80 + NI 的场景:范围是60~99(或100)(同样对应NI的-20~+19)。

可以高频更改优先级吗?

不建议高频更改进程优先级的变化范围(也就是NI值范围)。

  1. 高频修改优先级范围会让系统调度逻辑频繁"混乱",可能导致进程调度失控,比如高优先级进程持续抢占资源,低优先级进程长期无法运行,甚至引发系统卡顿、无响应。
  2. 原本的-20到+19(对应PRI的60到100这类范围)已经能满足绝大多数场景的优先级区分需求,高频调整范围属于过度操作,反而会破坏系统原本的调度平衡。
  3. 修改优先级范围通常需要特殊权限(比如root),高频操作不仅麻烦,还容易因误操作设置极端范围,造成不可逆的系统问题。

实时操作系统与分时操作系统

  1. 实时操作系统(RTOS): 核心是任务响应的时间确定性------对任何任务请求,系统必须在预先规定的"死线"(确定时间,通常毫秒/微秒级)内完成响应和处理,绝不能超时。它的目标不是追求资源利用率最大化,而是确保高优先级任务的实时执行,哪怕牺牲部分非关键任务的资源。
  2. 分时操作系统 : 核心是多任务/多用户的公平性------将CPU时间分割成固定的"时间片",轮流分配给多个并发任务或用户,让每个任务都能"分时"使用CPU。它的目标是让多个任务看起来"同时运行",保证资源分配的公平性,对响应时间没有严格的确定性要求,延迟可能因任务数量变化而波动。

七、进程之间的关系

进程具有竞争性,独立性,并行性和并发性

  1. 竞争性:系统进程数目众多,几十成百上千都有可能,而CPU资源只有少量,甚至只有一个,所以进程之间是具有竞争属性的。为了更高效完成任务,合理竞争资源,于是便有了优先级
  2. 独立性 :多进程运行,需要独享各种资源,多进程运行期间互不干扰
  3. 并行 :在同一时间内,多个进程在多个CPU下分别同时进行运行,这称之为并行
  4. 并发 :多个进程在一个CPU下,采用进程快速切换的方式,在一段时间内,让多个进程都得以推进,称为并发

有个疑问,既然进程之间具有独立性,那么在fork之后父子进程共享代码和数据,那父子进程之间又是如何保持独立性的呢?

fork创建的父子进程在逻辑上是独立的,但默认共享代码段(采用写时拷贝机制),而数据段则是"表面共享,实际独立"。其独立性保障机制如下:

  1. 核心机制:**写时拷贝(**COW)

    • fork调用后,父子进程的代码段、数据段、堆栈等内存区域会先共享物理内存
    • 仅复制页表而不复制实际数据,提高效率
    • 当任一进程尝试修改数据时,系统会立即为该进程复制独立的物理内存页
    • 确保修改操作不会影响另一个进程,从而维护数据独立性
  2. 独立资源

    • 除内存外,以下资源也是独立的:
      • 进程ID
      • 文件描述符表(共享文件描述符但偏移量独立)
      • 信号处理函数(默认继承但可独立修改)
      • 资源限制

关键点:

  • fork后的"共享"是高效的临时性共享,旨在节省内存
  • 写时复制机制与独立进程资源共同确保父子进程的逻辑独立性
  • 表面上看似共享,实际运行时互不干扰

进程之间如何实现并发呢?

并发指的是多个进程在同一个CPU下,采用切换进程的方式,在一段时间内,让多个进程都得以推进,称为并发

进程的并发是通过操作系统的"时间片调度+进程切换"机制实现的:

  • 在单CPU环境下,系统把CPU时间分割成短时间片,轮流分配给多个进程;每个进程只运行一个时间片,时间到了就触发进程切换(保存当前进程的上下文、加载下一个进程的上下文),让多个进程在"一段时间内"交替执行------从用户视角看,就像多个进程在同时运行。

如何实现进程之间的切换?

  1. 切换触发 :切换由特定事件启动,常见情况有时间片耗尽(系统定时器中断触发)、高优先级进程进入就绪态、进程因等待I/O操作主动阻塞、硬件中断(如键盘输入)以及进程执行完毕或异常终止等。
  2. 调度器选定目标进程 :触发事件发生后,内核中的调度器会暂停当前流程,从就绪队列里依据调度算法(如优先级调度、时间片轮转)挑选下一个要运行的进程,同时标记当前进程状态为就绪或阻塞态。
  3. 保存当前进程完整上下文操作系统会把当前进程的上下文全量存入其PCB(进程控制块)。一方面保存硬件上下文,包括通用寄存器、程序计数器、栈指针等,通过 push 指令压入内核栈再同步到PCB;若用到浮点指令,还会保存浮点寄存器数据。另一方面保存软件上下文,像进程的虚拟内存映射信息、打开的文件列表、信号处理配置等也会更新到PCB中。
  4. 切换地址空间:这是进程切换的关键步骤。每个进程有独立虚拟地址空间,切换时内核会将新进程页全局目录的物理地址,加载到对应CPU寄存器(如ARM64架构的 TTBR0_EL1 ),同时更新CR3寄存器切换页表,让CPU后续访问的是新进程的虚拟内存地址。此外还会处理TLB(转换后备缓冲器),避免旧进程地址映射干扰,部分系统用PCID技术减少TLB全量刷新的开销。
  5. 恢复目标进程上下文:从新进程的PCB中读取之前保存的信息。先将硬件上下文(寄存器值、程序计数器等)重新加载到CPU对应寄存器,再切换到新进程的内核栈,同时恢复其软件上下文,比如重新关联打开的文件、信号处理规则等运行环境配置。
  6. 跳转执行新进程:程序计数器会指向新进程上次暂停时的下一条指令地址,CPU自此开始执行新进程,新进程状态也被标记为运行态,整个切换流程完成。
  1. 即CPU寄存器中保存的是进程需要访问或修改的数据,即进程的相关数据
  2. CPU寄存器里保存的是进程的临时数据,我们把这些进程的临时数据成为进程的上下文
    寄存器有很多,例如

通用寄存器:eax,ebx,ecx,edx

栈帧:ebp,esp,eip

状态寄存器:status

八、总结

本文系统介绍了操作系统中的进程状态及其管理机制。主要内容包括:1. 进程三大核心状态:运行(R)、阻塞(S/D)、挂起状态及其转换关系;2. Linux特有的进程状态实现,如深度睡眠(D)、僵尸(Z)等特殊状态;3. 进程优先级管理(PRI/NI)和调度机制;4. 进程间关系(竞争/独立/并发)及实现原理;5. 特殊进程(僵尸/孤儿)的产生原因和处理方法。重点阐述了进程状态转换与资源管理的关系,以及Linux如何通过调度队列、写时复制等技术实现进程的高效管理。

相关推荐
敲上瘾2 小时前
Docker镜像构建优化指南:CMD/ENTRYPOINT、多阶段构建与缓存优化
运维·缓存·docker·容器·架构
是小胡嘛7 小时前
C++之Any类的模拟实现
linux·开发语言·c++
口袋物联7 小时前
设计模式之工厂模式在 C 语言中的应用(含 Linux 内核实例)
linux·c语言·设计模式·简单工厂模式
qq_479875438 小时前
X-Macros(1)
linux·服务器·windows
笨笨聊运维9 小时前
CentOS官方不维护版本,配置python升级方法,无损版
linux·python·centos
ζั͡山 ั͡有扶苏 ั͡✾9 小时前
EFK 日志系统搭建完整教程
运维·jenkins·kibana·es·filebeat
jun_bai10 小时前
python写的文件备份网盘程序
运维·服务器·网络
Warren9810 小时前
Python自动化测试全栈面试
服务器·网络·数据库·mysql·ubuntu·面试·职场和发展
HIT_Weston10 小时前
39、【Ubuntu】【远程开发】拉出内网 Web 服务:构建静态网页(二)
linux·前端·ubuntu