【Linux】理解进程状态与优先级:操作系统中的调度原理

发布于:2025-06-29 ⋅ 阅读:(16) ⋅ 点赞:(0)

在这里插入图片描述

Linux 相关知识点 可以通过点击 以下链接进行学习 一起加油!
初识指令 指令进阶 权限管理 yum包管理与vim编辑器 GCC/G++编译器
make与Makefile自动化构建 GDB调试器与Git版本控制工具 Linux下进度条 冯诺依曼体系与计算机系统架构 进程概念与 fork 函数

操作系统通过进程调度来有效管理系统资源,其中进程状态和优先级是关键因素。进程状态反映了进程的执行阶段,而优先级决定了进程获取资源的顺序。本文将简要探讨这两者在操作系统调度中的作用,及其如何影响系统性能。

请添加图片描述

Alt

🌈个人主页:是店小二呀
🌈C/C++专栏:C语言\ C++
🌈初/高阶数据结构专栏: 初阶数据结构\ 高阶数据结构
🌈Linux专栏: Linux
🌈算法专栏:算法
🌈Mysql专栏:Mysql

🌈你可知:无人扶我青云志 我自踏雪至山巅 请添加图片描述

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

在这里插入图片描述

为了更好地理解进程的行为,我们需要了解其不同的状态。主要有三种状态:运行、阻塞和挂起。

1.1 运行状态

在这里插入图片描述

在多核系统中,每个 CPU 配备有一个运行队列,该队列本质上是一个数据结构。运行队列中的进程并不一定正在执行,而是处于已准备好、随时可以被调度的状态。因此,运行状态和就绪状态可以看作是一个整体的执行状态。

需要注意的是,运行状态并不特指正在执行的进程,而是指那些已在运行队列中,能够随时被调度到 CPU 上执行的进程

1.1.2 并发执行与进程切换

当一个进程获取到 CPU 时,它会一直运行到结束吗?答案是否定的。即使某个进程进入死循环,CPU也不会一直被这个进程占用

在这里插入图片描述

操作系统通过 时间片轮转调度 来管理 CPU 的使用。每个进程会被分配一个时间片,在时间片结束后,CPU 会切换到下一个进程,从而实现多个进程的交替执行。通过这种快速的切换方式,多个进程可以在同一时间段内共同推进代码执行,这种机制称为 并发。当时间片用完后,当前进程会被移到调度队列的尾部,等待下一轮调度。

在单核 CPU 上,尽管某一时刻只能有一个进程真正运行,但通过时间片轮转实现了“伪并行”效果,给用户一种进程同时运行的假象。

并发与并行的区别

  • 并发】:多个进程共享 CPU,通过快速切换来推进多个任务的执行。适用于单核 CPU。

  • 并行】:多个进程在不同 CPU 核心上同时执行,是真正意义上的同时运行。适用于多核 CPU。

虽然时间片轮转是常见的调度算法之一,但现代操作系统(如 Linux)采用了更复杂的调度策略,如 完全公平调度器(CFS),以更高效地管理进程。

在多核 CPU 的环境下,真正的并行(多个进程在多个核心上同时运行)和伪并行(单核通过快速切换实现的并发)可以同时存在,从而大幅提高系统的整体性能

1.2 阻塞状态

阻塞状态是进程等待外部资源(如设备)状态。在这种状态下,进程无法继续执行,只能在等待队列中停留,直到资源准备就绪。

无论S状态和D状态都是属于阻塞状态,进程等待外设资源准备,进程只能在阻塞队列中等待

1.2.1 设备与等待队列

在这里插入图片描述

操作系统不仅管理 CPU 的运行队列,还为各种设备维护独立的 等待队列(wait_queue),每个设备都有自己的等待队列,通过指针链接形成 设备列表,统一管理所有设备资源。

1.2.2 阻塞状态的机制

其中核心逻辑:进程状态的切换本质上是 PCB(进程控制块)task_struct 的队列切换。加入或移出队列的是进程的控制信息,而不是代码或数据本身

【当进程需要等待设备时】

  • 操作系统将进程从 运行队列 移除,并加入设备的 等待队列
  • 此时,进程的状态切换为 阻塞状态

当设备资源准备就绪时】:

  • 操作系统将进程从 等待队列 移回 运行队列
  • 这一过程称为 唤醒

1.3 挂起状态

挂起状态指的是当操作系统内存紧张时,处于阻塞状态的进程(通常是等待设备资源)无法被调度执行。此时,操作系统会将该进程的代码和数据换出到磁盘的swap分区。用户层通常无法察觉这一过程。

在这里插入图片描述

其中系统通过合理利用内存资源,通常与其他状态配合,不能仅仅通过I/O将数据交换到外设以释放内存空间。

如果出现频繁的换入换出会导致性能下降,因为它增加了I/O操作,磁盘I/O通常比内存访问要慢得多,进程调度周期变长。虽然通过交换数据可以暂时解决内存不足的问题,但这种做法会消耗更多的时间,降低系统的效率。

1.3.1 swap分区不应过大

如果swap分区过大,操作系统会过度依赖它,一旦内存不足,就会频繁进行交换。这样,swap分区的使用频率会显著增加,可能影响系统性能

我们的电脑现在大多数使用的都是SSD固态硬盘,磁盘一般只有大公司的后端在使用,虽然比较慢但是便宜且容量更大

二、Linux内核管理进程状态

在操作系统中,进程状态通常用于描述进程在生命周期中的不同阶段。为了搞清楚正在运行的进程是什么意思,需要知道进程的不同状态。一个进程可以有多个状态(在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 */
};

在这里插入图片描述

我们知道每当启动进程,就会创建对应PCB存储进程相关信息,这样表示所谓的进程状态就是tark_struct对象内部的一个属性,实际上操作系统更改一个进程的状态,原理其实就是在更改描述进程PCB内部状态属性,仅此而已。

进程状态列表】:

  • R运行状态(running)】:并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里
  • S睡眠状态(sleeping)】: 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))
  • D磁盘休眠状态(Disk sleep)】:有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待I0的结束。
  • T停止状态(stopped)】: 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
  • X死亡状态(dead)】:这个状态只是一个返回状态,*你不会在任务列表里看到这个状态。

2.1 进程状态查看

指令:ps aux / ps axj 命令

具体参数含义如下:

  • ps:显示当前系统中的进程信息。
  • a:显示所有用户的进程(不仅仅是当前用户的进程)
  • j:显示进程的控制终端、进程组等信息。
  • x:显示没有控制终端的进程(包括后台进程)

在这里插入图片描述

2.2 R状态

R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中,要么在运行队列中

在这里插入图片描述

在这里插入图片描述

2.3 S状态

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

在这里插入图片描述

通过监视脚本,发现了进程基本处于S状态休眠状态,什么都没有做。

进程大部分处于S状态解释

首先,printf 的本质是将信息输出到显示器,而显示器作为一种外设,需遵循冯·诺依曼体系架构中的资源访问机制。打印的信息实际上是通过内存缓冲区传递,并最终刷新到显示器。由于显示器硬件的处理速度远低于 CPU,进程在访问显示器资源时,经常需要等待外设的就绪状态。

printf 执行过程中,大多数时间进程处于等待状态(S),因为显示器资源的响应速度较慢,而 CPU 处理 printf 指令的时间可能仅需几纳秒,远快于显示器完成打印的几毫秒。因此,在查看进程状态时,99%以上的概率会发现进程处于等待(S)状态,只有在执行真正的打印操作瞬间,才会短暂地显示为运行(R)状态。

如果我们注释掉 printf,进程不再涉及外设交互,仅依赖 CPU 资源。这时,只要进程被调度运行,状态会显示为 R(运行状态),因为没有外设延迟造成的等待。

在这里插入图片描述

printf 看起来涉及多行操作,但由于外设(如显示器)的刷新速度较慢,与 CPU 的执行速度相差巨大。CPU 在极短时间内执行了大量指令,而数据刷新却赶不上 CPU 的处理速度。因此,显示器上显示的数据可能是较早之前的结果,而非实时更新的内容。

当我们启动进程时,可以通过加上取地址符(&)将进程设置为后台运行。在查看进程状态时,如果状态为 S 且后面没有加号(+),表示该进程运行在后台。相反,如果状态后带有加号,说明进程运行在前台。加号标识的作用是区分前台与后台进程。

2.4 D状态

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

不同睡眠状态:

  • S是浅度睡眠(可以被唤醒)
  • D是深度睡眠 (不相应任何需求)

场景理解

进程有1GB重要数据需要写入到磁盘当中,由于内存需要跟磁盘建立联系,磁盘在被写入前需要判断该行为是否可被执行,进程需要等待磁盘资源准备(内存向磁盘发出申请、磁盘完成后将结果返回内存)

假设目前存在大量进程处于阻塞队列,内存出现严重不足,Linux操作系统有权限杀掉认为不重要的进程释放空间。

当内存向磁盘发出请求时,在磁盘还没有将结果进行返回时进程就被操作系统杀死了。这就导致了磁盘写入失败,无法找到该进程,此时进程需要写入到磁盘中重要数据就会丢失(不同操作系统有不同的做法)

解决措施

在我们的系统中,所有进程都需要进行数据I/O操作。当进程等待外部设备(如磁盘资源)时,需要将自身状态设置为 D 状态
D 状态表示进程处于不可中断的深度睡眠中,此时进程无法被终止(无法被杀死)。相比之下,S 状态是浅度睡眠,进程可以更容易被唤醒。

一般情况下,进程会在外部条件满足后自行唤醒或重启;如果系统完全失去响应,则可能需要断电重启。

值得注意的是,D 状态的进程通常较少见。如果系统中存在大量 D 状态进程,通常意味着系统已经接近崩溃,需立即排查问题。

2.5 T状态

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

指令:kill -l( 其中9是杀进程,19是暂停进程,18是重启进程)。这里不是指令,而是信号

在这里插入图片描述

【T 状态的意义】

T 状态表示进程被暂停,通常用于以下两种情况:

  1. 需要等待某种资源。
  2. 主动暂停进程,不希望它继续运行。

【典型应用场景】

调试工具如 gdb。当程序运行到用户设置的断点时,会暂停下来进入 T 状态,此时调试器(gdb 进程)接管并控制被调试的进程。

2.6 X状态

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

三、僵尸进程

3.1 僵尸进程概念

在 Linux 系统中,当一个进程退出时,它不会立即彻底释放,而是会将退出信息保留在其 PCB(进程控制块)中。如果没有进程(通常是父进程)读取该退出信息,该进程就会一直占用 PCB 资源,进入所谓的僵尸状态

一般情况下,进程退出时,其资源(如内存和文件描述符)会被释放,但 PCB 数据结构会保留,用于记录进程的退出信息,等待父进程进行读取或回收。如果父进程调用相关操作(如 wait 系列系统调用)读取了该退出信息,那么该进程的状态才会彻底清除,所有资源将完全释放。

因此,一个进程退出后并不会立刻完全消失,而是先处于僵尸状态,维护退出状态本身就是数据维护,属于进程基本信息。如果父进程未对其进行回收,僵尸状态会一直持续下去,占用系统资源。正确处理僵尸进程是保持系统健康的必要步骤。

模拟场景】:子进程先退出,父进程还在运行

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 int main()
  5 {
  6   pid_t id =fork();
  7   if(id==0)
  8   {
  9     int cnt=5;
 10     while(cnt)
 11     {
 12       printf("child,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);                    13         --cnt;
 14       sleep(1);
 15     }
 16     exit(0);
 17   }
 18   else
 19   {
 20     while(1)
 21     {
 22       printf("parent,pid:%d,ppid:%d\n",getpid(),getppid());
 23       sleep(1);
 24     }
 25   }
 26   return 0;
 27 }

image-20241202212907248

Z 状态表示进程进入了僵尸状态,即 “defunct”(失效/不存在)。虽然进程的主体已终止,但其退出信息仍保留在系统中,直到被父进程读取或处理。

3.2 僵尸进程特点

在操作系统中,资源十分有限。如果一个进程不再运行,其数据和代码可以被释放,但其 PCB 结构体(进程控制块)会一直保留,直到被处理。这种情况会占用系统内存,如果长期不清理,可能引发内存泄漏问题。

僵尸进程的特点

  1. 退出信息保留:僵尸进程需要保留其退出信息,用于反馈给父进程。
  2. 只读状态:进程具有独立性,其退出信息可被读取,但不可修改。
  3. 无法直接杀掉:处于僵尸状态的进程无法被终止,因为它已经完成运行,只剩下信息等待父进程回收。

四、孤儿进程

如果父进程先退出而子进程仍在运行,子进程则成为孤儿进程。孤儿进程PPID会变成1,此时孤儿进程被操作系统托管,由其接管和管理。

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 int main()
  5 {
  6   pid_t id =fork();
  7   if(id!=0)
  8   {
  9     int cnt=5;
 10     while(cnt)
 11     {
 12       printf("parent,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);
 13         --cnt;
 14       sleep(1);
 15     }
 16     exit(0);
 17   }
 18   else
 19   {
 20     while(1)
 21     {
 22       printf("children,pid:%d,ppid:%d\n",getpid(),getppid());
 23       sleep(1);
 24     }
 25   }                                                                                     
 26   return 0;
 27 }

在这里插入图片描述

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

孤儿进程最终也会被系统管理进程接管并管理,确保其资源能够在运行结束后被正确释放

为什么 Ctrl+C 无法中止孤儿进程?

本质原因:孤儿进程的 PPID(父进程ID) 被更改为系统管理进程(如 init),而不再与原父进程相关。因此,无法通过终止原父进程的方式中止孤儿进程。

为什么不是由 bash 回收,而是由系统进程回收?

  • bash 无法回收孙子进程(孤儿进程),因为它不是这些进程的直接创建者,因而没有权限进行管理。
  • 系统管理进程具备托管孤儿进程的权限,能够确保所有孤儿进程被正确处理和释放。

不同操作系统可能对孤儿进程的处理方式有所不同,但核心逻辑是一致的。

五、进程优先级

5.1 理解优先级

进程优先级是指在竞争系统资源(如 CPU 或其他资源)时,进程的调度顺序。优先级较高的进程将优先获取资源。

通过以下四个问题,进行更加充分理解优先级

【第一个问题】:优先级vs权限

权限决定操作是否可执行,优先级则决定在拥有权限的前提下,资源访问的先后顺序。权限回答‘能不能做’,优先级决定谁先谁后。

【第二个问题】:为什么存在优先级

由于进程访问的资源(如 CPU)是有限的,系统中多个进程之间不可避免地会出现竞争。通常,优先级较高的进程会被赋予更高的执行权,优先执行。合理配置进程的优先级对于多任务环境下的 Linux 系统尤为重要,它可以有效地改善系统的资源分配与性能,确保高优先级任务得到及时处理,同时避免系统资源的浪费

【第三个问题】:如何决定优先级

操作系统通过进程控制块(task_struct)中的多个字段来决定进程的优先级。task_struct 结构体包含了与进程相关的各类信息,其中一些字段直接影响进程的调度与优先级。

优先级本质是一个数字,Linux中优先级数字越小,优先级越高,即系统会优先调度优先级较低数字的进程。通过合理设置这些字段优化多任务执行的效率

【第四个问题】:如何运转进程

操作系统会根据自身一套规则尽可能保持良性竞争。虽然通过优先级可以根据系统需求合理进行安排,但是可能会出现部分进程长时间没有被调度出现"饥饿问题"。操作系统秉持为了保证基本的公平性,使用了分时操作系统,基于时间片来进行调度,避免了进程长时间占用资源。

5.2 Linux的优先级的特点 && 查看方式

用于查看进程的优先级指令:ps -la

在这里插入图片描述

  • UID : 执行者的身份
  • PID : 表示该进程的编号
  • PPID :表示目前运行进程的父进程编号
  • PRI :表示进程优先级,其值越小越早被执行
  • NI :进程的nice值

5.3 PRI和NI

对优先级进行调整本质不是直接修改优先级数据,而是通过nice值间接调整优先级数据。如果直接将数据进行修改,有可能会影响当前进程的调度。

  • PRI:进程优先级

  • NI:进程优先级的修正数值,nice值

  • 新的优先级 = 默认优先级(系统默认) + 优先级

  • 调整优先级就是在调整进程nice值,达到对于进程优先级动态修改的过程

5.3.1 top更改nice值

具体的流程:

  1. top
  2. r(renice)
  3. 输入进程PID
  4. 输入nice数值

其中,toptop 命令类似于我们的任务管理器,可用于调整和优化进程的优先级。

虽然Linux当前及其进行中的优先级可以调整,但是调整是有范围的并且不能随意地进行调整。每一次调整进程优先级可能会影响整个操作的系统进程的平衡问题。

在这里插入图片描述
image-20241105151556887

结论】:

  1. nice并不能让你任意调整,而是存在范围【-20,19】,一共40个级别。
  2. 其实这方面的知识无需过于深入,因为大多数情况下我们不会手动修改优先级

六、进程相关名词概念

  • 竟争性】: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
  • 独立性】: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
  • 并行】: 多个进程在多个CPU下分别,同时进行运行,这称之为并行(大多数配置的电脑都是只有一个cpu)
  • 并发】: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发 (常见)

七、调度算法

在这里插入图片描述

7.1 维护两个队列

在这里插入图片描述

我们需要维护运行队列和等待队列。当进程因等待资源或时间片耗尽被暂停时,它会被移到等待队列或就绪队列,等待资源或下一次调度。这样可以避免不公平的调度顺序。

运行队列和等待队列通常通过链表或其他合适的数据结构实现,通过指针将各个队列的进程链接起来。

问题一】:为什么进程范围[0 - 99]?

因为这类进程可能非常重要!无论当前运行什么进程,这类进程都会优先被调度。比如,当电脑出故障并发出警报时,警报进程就会被优先处理,确保及时响应。

问题二】:维护两个队列意义

因为需要一个队列来调度进程,但在运行过程中,准备好的进程会被放入另一个等待队列,以确保按顺序执行。否则,当前运行的进程可能会被打断,导致需要回头处理前面插入的新进程。

问题三】:同等优先级的怎么办?

因为维护的是一个指针数组,所以比如说我当前所处的优先级是100,那么下一个优先级100的进来之后就会被链接在后面,本质上是一个开散列

7.2 快速定位进程位置

在进程创建时,我们无法立即知道哪个位置存在进程,需要遍历整个队列来查找。

为此,可以使用位图(bitmap)来优化查找过程。由于等待队列中只有40个可能的位置(即进程ID范围为【100 - 139】),我们可以用一个包含40个比特位的位图来标记每个位置是否已有进程。

每个比特位对应一个队列位置,当某个位为1时,表示该位置有进程,位为0时,表示该位置没有进程。通过位运算,我们可以快速确定队列中哪些位置有进程,并高效地找到空闲位置。当位图中所有比特位都为0时,说明队列为空。

7.3 需要维护两个指针

在这里插入图片描述

通过维护运行队列和等待队列,我们不仅能够有效地管理进程排队问题,还能通过合理的队列衔接优化进程调度,提高整个系统的效率。

我们使用两个指针来分别指向运行队列和等待队列的头部,当运行队列为空时,我们通过交换这两个队列的指针,使得等待队列的进程被调度到运行队列,从而保持系统的高效运转。

八、进程切换

8.1知识铺垫

问题一】:函数的返回值存储在哪里?

返回值通常存储在CPU的寄存器中,通常是通过 move eax, 10 指令来实现。

问题二】:系统如何得知进程执行到哪一步?

系统通过**程序计数器(PC,Program Counter)**来跟踪进程的执行位置。程序计数器保存了下一条将被执行的指令的内存地址,每次指令执行后,程序计数器会自动更新,指向下一条指令。如果发生上下文切换,系统会保存当前进程的程序计数器值,以便下次恢复时能继续从正确的位置执行。

问题三】:寄存器作用是什么?

寄存器是CPU内部的高速存储单元,用于存储指令、数据和控制信息。它们在程序执行过程中临时保存操作数、指令地址以及运算结果,显著提高了CPU的执行效率。

进程切换是操作系统中的一个重要概念。当进程的时间片用完,需要从CPU中剥离出来时,操作系统必须保存与该进程相关的所有寄存器数据。

总结:

  1. 寄存器用于保存进程的临时(高频)数据,即进程的上下文信息。
  2. 由于寄存器存储的是临时数据,新数据可能会覆盖旧数据。因此,重要数据应存放在离CPU更近、更安全的地方,例如避免将关键数据放在通用寄存器中,以降低丢失风险。

8.2 进程切换数据问题

通常,这些数据会被保存在任务控制块(PCB)中。可以理解为,进程的上下文信息就是该进程在CPU中运行时产生的所有临时数据,主要是寄存器中的内容。

在这里插入图片描述

进程切换的核心任务】:保护和恢复上下文数据

CPU内的寄存器硬件是有限的,只有一套寄存器。但每个进程都有一套与其相关的上下文数据。当进程切换时,操作系统会将当前进程的寄存器内容保存到task_struct中,以便后续恢复

需要注意的是,寄存器本身是硬件资源,而寄存器的内容是保存在上下文中的数据。
在这里插入图片描述

以上就是本篇文章的所有内容,在此感谢大家的观看!这里是Linux笔记,希望对你在学习Linux旅途中有所帮助!


网站公告

今日签到

点亮在社区的每一天
去签到