一.进程的切换

进程在CPU上面运行,每个进程在运行的时候都有自己的一个运行时间单位,叫做时间片。

时间片:从进程开始运行到被别的进程抢占的时间当代计算机都是分时操作系统,没有进程都有它合适的时间⽚(其实就是⼀个计数器)。时间⽚到达,进程就被操作系统从CPU中剥离下来。)

举个例子,一个进程只能运行100ms即使进程A没有运行完毕,但它的时间片耗尽了,必须从CPU中抽离开,换成下一个进程运行

进程切换的几种情况:

(1)优先级比之前的高(前提OS支持可以抢占)

(2)时间片的时间用完了

  • 一般来说操作系统可以允许一次性同时运行多个进程。但单独一个CPU对应一个进程,你所看到的多个进程同时运行,实则是通过进程快速切换的方式,在一段时间内,让所有代码都可以得到推进,这叫做并发。但是时间片的时间很多ms甚,我们用户感受不到而已。
  • 对于多的CPU也就意味着允许多个进程同时运行,这叫做并行。

注意:(1)大多数情况下,操作系统都是并行和并发同时进行

           (2) 进程切换最重要的一步就是上下文数据的保存

 补充概念-竞争、独立、并行、并发
• 竞争性: 系统进程数⽬众多,⽽CPU资源只有少量,甚⾄1个,所以进程之间是具有竞争属性的。为
了⾼效完成任务,更合理竞争相关资源,便具有了优先级
• 独⽴性: 多进程运⾏,需要独享各种资源,多进程运⾏期间互不⼲扰
• 并⾏: 多个进程在多个CPU下分别,同时进⾏运⾏,这称之为并⾏
• 并发: 多个进程在⼀个CPU下采⽤进程切换的⽅式,在⼀段时间之内,让多个进程都得以推进,称
之为并发


二.上下文数据

  • 进程在CPU中开始运行的时候,CPU的寄存器上面会保存许多临时的数据,当发生进程的切换的时候,这些数据要肯定被保存的,这些数据被称为进程的上下文数据
  • 可以理解为CPU内寄存的数据,成为进程的上下文数据

举个例子说明,正在上大一的张三,中途想去当兵,但为了不让自己的学业受到影响,他申请保留学籍休学两年,这才能正常离开学校。当兵结束后,给学校申请恢复学籍,才能正常完成学业

有了上下文数据,它是如何恢复和保存的呢?

  • 上下文数据的保存:当一个进程在运行的时候,时间片耗尽了,因此需要暂停,让出CPU给其他进程,此时进程要保护好自己的数据(也就是当前进程的上下文数据)到PCB中,方便下次的恢复
  • 上下文数据的恢复:当这个进程被又被重新切换回来,或者切换到下一个新的进程运行的时候(当前进程 A 的时间片用完了,系统决定运行另一个进程 B(这个 B 可能是之前暂停的,也可能是新创建的)),只需要把此进程PCB上下文数据重新写入到CPU的寄存器中,就可以继续恢复运行。

运行队列:

  • 假设在当前的操作系统,有5个进程在处于可以运行状态中,操作系统就会形成可以与运行队列
  • 每一个进程用链表连接起来,其中有的进程处于可运行状态,有的进程处于可运行队列当中
  • CPU在执行任务的时候就只需要从这个可运行的队列中进行寻找即可。(一个进程因为状态变成“可运行”,所以被放入“可运行队列”;只要在队列里,它的状态就是“可运行”)。
  • 在操作系统中寻找一个新的进程在CPU当中运行,应该选择当前可以运行的状态的进程,如果你整个扫描链表的话效率会很低,因此引入了双向链表来提高可运行状态进程的效率,也称为运行队列(runqueue)
  • 运行队列通过task_struct结构体中的两个指针run_list链表来维持。队列的两个标志:空进程idle_task,队列的长度。
  • 操作系统为每个进程状态管理各种类型的队列,与进程相关的 PCB 也存储在相同状态的队列中。如果进程从一种状态转移到另一种状态,则其 PCB 也从相应的队列中断开,并被添加到进行转换的另一个状态队列中。
  •  PCB 是可以被列入多种数据结构内的。比如 PCB 在被调度的时候,以及在等待某种资源的时候会被从调度队列移入或移出,包括等待某种资源的等待队列。

【补充】

进程有三种常见状态

  • 运行态:正在 CPU 上执行。

  • 就绪态(可运行):在就绪队列中。

  • 阻塞态:在某个等待队列中。

PCB 永远只出现在:

  • 就绪队列(一个或多个,取决于调度策略)

  • 等待队列(可以有多个,例如:等待键盘、等待硬盘、等待锁)不会有一个 PCB 同时存在于就绪队列和等待队列,因为一个进程不可能既“可运行”又“在等某件事”。


三.进程状态

3-1查看进程状态:

ps aux / ps axj

  • a:显⽰⼀个终端所有的进程,包括其他⽤⼾的进程。
  • x:显⽰没有控制终端的进程,例如后台运⾏的守护进程。
  •  j:显⽰进程归属的进程组ID、会话ID、⽗进程ID,以及与作业控制相关的信息
  •  u:以⽤⼾为中⼼的格式显⽰进程信息,提供进程的详细信息,如⽤⼾、CPU和内存使⽤情况等

一个进程的生命周期划分为一种状态,进程的状态即体现一个进程的生命状态

注意:操作系统所说明的进程的状态一般是宏观上面的描述


3-2Linux内核源代码的进程状态:

为了弄明⽩正在运⾏的进程是什么意思,我们需要知道进程的不同状态。⼀个进程可以有⼏个状
态(在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):这个状态只是⼀个返回状态,你不会在任务列表⾥看到这个状态。
  • 僵尸状态(zombie)

3-3状态解释:

3-3-1R状态(running)

定义:可运行状态。(准备就绪,可以被调度)

进程是R状态不一定是在CPU上运行的,进程在运行队列也是R状态。但如果一个进程想被CPU运行就必须是R状态才行;

#include<stdio.h>
  2 #include<unistd.h>                                                                                                                                                     
  3 int main()
  4 {
  5     while(1)
  6     {
  7         printf("I`m a running\n");
  8         sleep(1);
  9     }
 10     return 0;
 11 }

解答:该进程大部分时间都在休眠sleep(1);因为 printf 是往显示器上打印,涉及到 IO(后面会讲是为啥),效率比较低,该进程需要等待操作系统把数据刷新到显示器中。因此,该进程绝大多数时间都在休眠,只有极少数的时间在运行,所以很难看到该进程处在 R 状态。

那要件进程R的状态可以使用这个代码:

int main(){while(1){} };就可以看见R的显示。


3-3-2S(sleep状态)和D(disk sleep)磁盘休眠状态

S:休眠状态(sleeping)(浅度休眠,大部分情况)

S状态:进程虽然是一种休眠状态,但随时可以接受外部的信号,处理外部的请求,被唤醒,一叫就回应开始执行任务。

D:磁盘休眠状态(disk sleep)(深度休眠

属于叫不醒,给你信号没反应

kill -9 都杀不掉 D 状态的进程,只能等着 I/O 完成或重启系统。

举例子说明:

比如:进程 A 想要把些数据写入磁盘中,因为 一些原因,进程 A 需要等待。但因为内存资源不足,在等待期间进程 A 被操作系统 kill 掉,而此时磁盘也因为空间不足,数据写入磁盘失败,却不能把情况汇报给进程 A,那这些数据应该怎么办?这样很可能导致这些数据被丢失,操作系统 kill 掉进程 A 导致了此次事故的发生。所以诞生了D 状态,进程A不可以被杀掉,即便是操作系统。只能等待 D 状态自动醒来,或者是关机重启。


其实S状态和D状态从本质上来说都是一种等待状态,只是各自的触发条件没有被满足。

【补充】

请问S和S+两个状态的区别在哪里?

S+ :表示前台进程。(前台进程一旦运行,bash 就无法进行命令行解释,使用 Ctrl+C 可以终止前台进程)
S :表示后台进程。(后台进程在运行时,bash 可以进行命令行解释,使用 Ctrl+C 无法终止后台进程)

3-3-3T状态(stopped)

kill 指令:可以向目标进程发信号

  • 我们给进程发 19 号信号 SIGSTOP,可以让进程进入 T 停止状态。停止运行。

  • 我们给进程发 18 号信号 SIGCONT,可以让进程停止 T 停止状态。恢复运行。

之前的循环写入这个指令又开始运行


3-3-4僵尸状态--僵尸进程(Zombie)

进程创建的目的,目的是完成任务和工作,当它结束的时候,我们要看他任务和工作完成的怎么样,所以我们得知道它是正常还是异常退出进程的?

假设进程正常退出,那么交给进程的任务有没有正常完成?

进程退出时,会自动将自己的退出的信息,保存到PCB当中,让OS或者父进程进行读取

(1)如果进程退出了,但是父进程是没有读取,此时的进程处于僵尸状态。

(2)假如读取成功后,该进程才是真正的死亡状态,也就是变成X状态。

僵尸进程不属于可运行队列,也不属于等待队列,它是一个孤立的 PCB 残骸。

基本定义:

  • 僵尸状态是当子进程退出时,并且父进程没有读取到子进程退出时返回的数据和代码就会产生僵尸状态(父进程使用系统调用让wait()回收子进程)。
  • 僵尸状态的保持在进程表中一直会等到父进程读取退出代码,说白了就是子进程退出,但是父进程没有读取到子进程的状态,子进程就会进入Z状态。

举例说明

#include<stdio.h>
  2 #include<stdlib.h>
  3 #include<sys/types.h>
  4 #include<unistd.h>
  5 
  6 int main()
  7 {
  8 for(int i = 0;i<5;i++)
  9     {
 10     pid_t ret = fork();
 11     if(ret==0)
 12     {
 13     printf("子进程%d,pid:%u,ppid %u\n",i,getpid(),getppid());                                                                                            
 14     sleep(1);
 15     exit(1);//子进程退出
 16 
 17     }
 18     }
 19 getchar();//父进程无法退出无法回收子进程
 20 return 0;
 21 }

结果是五个进程创建成功,但是这里被卡住了没有自动退出

同时我们也可以看见,子进程的打印顺序并不是按照创建顺序,这是因为操作系统调度进程的执行没有固定的顺序,进程的执行顺序可能会因为多种因素(如调度策略、系统负载等)而有所不同。这展示了进程调度的非确定性特性。 

注意:

  • 父进程通过调用 getchar() 函数来等待用户输入,这样做可以防止父进程过早退出,在大多数情况下,这也意味着父进程不会立即回收结束的子进程资源。
  • 父进程通过 getchar() 等待,但这并不是处理僵尸进程的正确做法。在实际应用中,父进程一般使用 wait / waitpid 函数来等待子进程结束,并回收它们的数据和代码,以避免僵尸进程的产生。
  • getchar() 就像你站在路口发呆等朋友,但你没有接他的电话,他到了也只能在路边干等(僵尸)。wait() 就像你一直盯着到达口,他一来你就把他领走。

僵尸进程的危害:

  • 如果一个父进程创建了很多子进程,但就是不回收,会造成内存资源的浪费。因为数据结构要占用内存,定义一个 task_struct(PCB) 结构体变量要在内存的某个位置开辟空间,僵尸进程会导致内存泄漏。
  • 进程的退出状态必须被维持下去,因为他要告诉关心它父进程,你交给我的任务办的怎么样。假如可父进程如果一直不读取,那子进程就会一直处于 Z 状态。
  • 维护退出状态本身就是要用数据维护,也属于进程的基本信息,所以保存在task_struct(PCB) 中,换句话说,Z 状态一直不退出,PCB 就要一直维护。

僵尸进程的危害不是消耗CPU或内存,而是像“进程ID的漏洞”——只占位置不干活,攒多了系统就“生不出新孩子”了

一般来说避免内存泄漏的方法是:进程等待

【补充】Z状态不能被kill杀掉因为赭红进程已经没了。

3-3-5孤儿进程

父进程先退出,子进程就被称为孤儿进程。一般被1号系统进程领养,孤儿进程退出时,1号系统进程进行回收。

举例说明:

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

总结:

  • 就绪 / 运行:R 状态
  • 阻塞:S / D / T 状态
  • 退出:Z / X 状态

四.进程的优先级

4-1基本概念

  • cpu资源分配的先后顺序,就是指进程的优先权(priority)。
  • 优先权⾼的进程有优先执⾏权利。配置进程优先权对多任务环境的linux很有⽤,可以改善系统性能。
  • 还可以把进程运⾏到指定的CPU上,这样⼀来,把不重要的进程安排到某个CPU,可以⼤⼤改善系统整体性能

进程权限对比进程优先级

  • 优先级:在资源有限的前提下,确立多个进程中谁先访问资源,谁后访问资源。
  • 权限:决定能不能得到某种资源。

最大的区别前者有资源,后者是否可以得到资源

 4-2查看系统进程

在 Linux 或者 Unix 系统中,使用命令 ps -al /ps -l查看当前系统进程的信息:

  • UID : 代表执⾏者的⾝份

在 Linux 中,标识一个用户,不是通过用户名来标识的,而是通过用户的 UID

UID 是给计算机看的,UID 对应的用户名是方便给人看的。

比如 QQ 可以随意更改昵称,那就说明昵称不是唯一标识这个 QQ 用户的,而是通过 QQ 账号,因为账号是不变的,给计算机看的。

  •  PID : 代表这个进程的代号
  •  PPID :代表这个进程是由哪个进程发展衍⽣⽽来的,亦即⽗进程的代号
  •  PRI :代表这个进程可被执⾏的优先级,其值越⼩越早被执⾏

其值越小,优先级越高,越早被执行。
其值越大,优先级越低,越晚被执行。

  • NI :代表这个进程的nice值

nice 值,表示进程可被执行的优先级的修正范围数值:[-20, 19]。

进程新的优先级:PRI(new) = PRI(old, 默认都是 80) + nice

补充:

ps -a 默认排除了会话首进程(如 bash),而 ps -l 显示当前终端的所有进程

4-3查看进程优先级的命令

 top 命令(类似于 Windows 的任务管理器)更改已存在进程的 nice:

   执行 top 命令后,按 r 键,输入进程的 PID,输入 nice 值。

每次输入 nice 值调整进程优先级,都是默认从 PRI = 80 开始调整的(80作为一个基准值,方便调整,在设计上,实现比较简单)。
输入的 nice 值如果超过 [-20, 19] 这个范围,默认是按照最左/最右范围来取的。(在这个范围是一种可控状态,保证了进程的优先级始终在 [60, 99] 这个范围内,保证了 OS 调度器的公平性。但不是绝对公平。根据每个进程的特性尽可能公平的去调度它们,而不是指每个进程的调度时间必须完全一样。

其他调整优先级的命令:nice,renice


五. Linux2.6内核进程O(1)调度队列

方便理解画的数据结构图:

5-1 活动队列

调度流程(工作原理):

  1. CPU 每次只从活动队列中挑选进程运行。

  2. 选中的依据:优先级(O(1) 调度器维护 140 个优先级队列)。

  3. 进程运行时消耗时间片:

    • 时间片未用完 → 放回活动队列(可能调整优先级)

    • 时间片用完 → 移到过期队列

  4. 活动队列为空时:

    • 交换指针:active = expired; expired = active

    • 原来的过期队列变成新的活动队列,开始新的一轮

【补充】

  • 时间⽚还没有结束的所有进程都按照优先级放在该队列
  •  nr_active: 总共有多少个运⾏状态的进程
  •  queue[140]: ⼀个元素就是⼀个进程队列,相同优先级的进程按照FIFO规则进⾏排队调度,所以,数组下标就是优先级!

 从该结构中,选择⼀个最合适的进程,过程是怎么的呢?
1. 从0下表开始遍历queue[140]
2. 找到第⼀个⾮空队列,该队列必定为优先级最⾼的队列
3. 拿到选中队列的第⼀个进程,开始运⾏,调度完成!
4. 遍历queue[140]时间复杂度是常数!但还是太低效了!
bitmap[5]:⼀共140个优先级,⼀共140个进程队列,为了提⾼查找⾮空队列的效率,就可以⽤5*32个⽐特位表⽰队列是否为空,这样,便可以⼤⼤提⾼查找效率!

5-2过期队列

同活动队列长得一模一样

工作原理

  1. 进程创建或唤醒后,先进入活动队列,并获得一个时间片。

  2. CPU 每次从活动队列中选最高优先级的进程运行。

  3. 当进程的时间片用完:

    • 如果还有剩余时间片?→ 放回活动队列(优先级可能调整)

    • 如果彻底用完?→ 从活动队列摘下,放入过期队列

  4. 活动队列为空时:

    • 交换指针:active = expired; expired = active

    • 原来的过期队列变成新的活动队列,开始新一轮调度


 active指针和expired指针

  •  active指针永远指向活动队列
  •  expired指针永远指向过期队列
  •  可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间⽚到期时⼀直都存在的。
  •  没关系,在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了⼀批新的活动进程!

【补充】

活动队列(Active Queue) 存放还有时间片未用完、可以被调度的进程
过期队列(Expired Queue) 存放时间片已用完、需要等待下一轮调度的进程

简单理解:活动队列 = 有票的人;过期队列 = 票用完、等下一轮发票的人。

过期队列是 O(1) 调度器中存放“时间片已用完但还可运行”的进程的地方,通过定期与活动队列交换,实现公平调度。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐