进程(下)【Linux操作系统】

发布于:2025-03-14 ⋅ 阅读:(27) ⋅ 点赞:(0)

进程的状态

在内核中,进程状态的表示,就是进程PCB中的一个int类型的成员变量,状态不同就存储着不同的数字


R状态:

就是运行状态


S状态:

就是普通的阻塞状态,进程处于浅睡眠
处于S状态的进程可以被操作系统杀死


D状态:

特殊的阻塞状态,进程处于深度睡眠
处于D状态的进程不可以被操作系统杀死

为什么需要 D 状态?
不可中断的 I/O 操作
当进程执行某些必须完成的底层操作(如磁盘写入网络数据包接收与硬件设备的交互)时,会进入 D 状态。这些操作通常由内核发起,进程必须等待其完成。
不能被信号(如 kill -9)中断。若强行中断,可能导致数据损坏或硬件状态不一致。


进程A的任务是向磁盘中写入银行的100万条交易数据
因为数据量较大,而且磁盘速度慢,所以会花费一些时间
所以A会在磁盘的等待队列中等待磁盘写完,获取磁盘进行写入的结果(即写入成功还是失败)

此时如果操作系统因为一些原因(内存不足或者用户使用指令杀进程A等
把进程A杀死了
如果刚好磁盘也写入失败了,就会告知进程A:写入失败,询问是否重新写入
但是此时进A已经没了,自然就没人给磁盘应答,此时磁盘就可能会丢弃这些数据,就可能导致数据丢失

所以为了保护重要数据,正在与硬件交互重要数据的进程就不能被杀死
也就有了状态D


T状态

暂停状态
进入暂停状态的两种情况:
进程做了非法但是不致命的操作
操作系统就会暂停进程,并把情况汇报给用户,让用户决定
用户操作,让进程进入暂停状态(比如:双击屏幕暂停视频的播放)


t状态

可追踪的暂停状态


使用gdb调试器,调试可执行程序test,当在gdb中打了断点,再r了一下之后
gdb就会启动test,生成进程
此是gdc的调试进度就会停在打的断点处,此时由test生成的进程就处于t状态
因为我们如果在gdb上按一下n或者s,test生成的进程就会被执行一下(因为要跑一下那句代码,看会怎么样)
此时这个进程的状态会非常短暂地变成运行状态(因为只有运行能被CPU执行代码),然后又会变成t状态
这就是可追踪的含义


Z状态:

当进程任务完成后
需要向父进程[一般是父进程,也有可能是操作系统]汇报任务情况(是否正确完成/错误码)
这个过程进程就处于Z状态

进程创建的时候,是先创建它对应的内核数据结构[PCB]再加载它的代码和数据
就像考进一个学校,再拿到录取通知书之前你的信息就已经被学校记录在学生管理系统里面了,所以在你人进入学校之前,就有了你对应的数据对象了

而进程退出是则是反过来,即先释放代码和数据,再处理PCB
因为进程退出后,就不会再执行代码了,所以代码和数据没有存在的必要了
但是由于要在PCB里面存储退出信息(描述任务执行状态),给父进程/操作系统读取,所以PCB还不能直接释放


处于Z状态的进程不能再被杀死,因为它本质上已经死了
所以只能让父进程去回收它

如果父进程一直不去回收处于Z状态的子进程就会出现僵尸问题
在这里插入图片描述

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

维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task. struct(PCB)中, 换句话说,Z状态一直不退出,内核就必须一直维护它的PCB

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

如何避免?后面会讲。


孤儿进程

如果父进程创建子进程之后,子进程还在运行时,父进程就挂了

此时因为必须要有人在子进程退出时,获取子进程的退出信息以及对子进程进行回收

所有子进程必须再找一个爹,这个爹就是操作系统

所以子进程运行时父进程挂了,子进程被操作系统领养的
此时,这个子进程就叫孤儿进程


X状态:

进程完成Z状态的汇报任务,并且父进程回收完成之后
进程真正死亡时,就处于X状态


进程的优先级

什么是优先级,为什么要有优先级?
在这里插入图片描述
需要进程竞争,但是所有进程一拥而上是肯定不行的
这个时候就得排队,那么谁排在前面就是优先级的体现


进程优先级的实现:
在这里插入图片描述


如果我们要修改一个进程的优先级

一般修改的是它的nice(NI)值,通过这个间接修改进程的优先级

为什么?
因为如果没有nice值,直接修改PRI值
如果该进程正在运行,就不好判断是否要继续执行

但是修改nice值,就可以等运行暂时结束之后(时间片耗尽等),操作系统就会自动加上NI中存储的值,借此改变优先级


重置进程优先级

在这里插入图片描述

为什么每次修改进程的优先级时,pri固定是80?
因为这样可以让用户更方便地修改,就不需要在修改前查看进程之前的优先级了,直接就可以修改

为什么用户可以修改的nice值拥有一个范围?
这是因为我们使用的操作系统绝大部分都是分时操作系统
分时操作系统追求的就是进程调度尽量公平
如果一个用户把自己的一个进程的优先级调的特别高,那么其他用户的进程就有可能一直无法调度
这显然并不公平


进程切换

切换过程的理解
在这里插入图片描述

注意:
上图中被红色半圈圈起来的临时数据,指的就是进程的上下文数据
它们在这个进程运行到时候,存储在CPU里面的各种各样的寄存器中,并且随着进程运行不断地发生变化

eip(pc):是一个寄存器
里面存储了正在运行的那句代码(指令)的下一句代码
方便CPU获取

ir:也是寄存器,存储的就是正在CPU中运行的代码(指令)

所以CPU运行进程的简单过程就是:
①把进程接下来要运行的代码的地址,加载到eip寄存器中
②CPU通过eip中的地址找到汇编代码
③把找到的汇编代码放进ir寄存器中
④让eip存储下一条汇编代码的地址
⑤把ir中的汇编代码加载到控制器中运行

第①步只在进程刚开始被CPU调度时执行,②③④⑤则是不断循环,借此实现进程运行

进程切换的时候,把CPU中相关寄存器中的上下文数据保存在了自己的PCB中
切换回来的时候,直接再把上下文数据加载到CPU中对应的寄存器中,就可以继续运行了


进程的调度

运行队列(runqueue)中的调度队列(runqueue中的queue中的queue[140])的结构:
在这里插入图片描述

如上图所示,调度队列是一个哈希桶的结构相同优先级的进程会被放进同一个桶中

CPU调用进程的时候从左往右进行调用,如果一个桶中有进程,就直接拿着桶里的第一个元素调度(插入进程也是头插)
如果没有就继续向后遍历
这样调用就可以体现出优先级了


而runqueue中有两个类型为queue的结构体变量
如图:
请添加图片描述


这两个结构体变量中的
task_struct* queue[140],就是上面提到的存放进程的PCB的哈希桶

这两个结构体在runqueue中被放在一个数组中
并且定义了两个同类型的指针(active和expired)指向分别指向这两个结构体变量

具体示意图如下:
在这里插入图片描述

所以进程调度只需要从active指向的queue里面的哈希桶中从上到下遍历调用即可


调度过程中,会出现如下3种情况
①进程的代码被全部执行完,进程退出
那么这个进程的PCB当然也就不会再回到调度队列中

②进程的时间片耗尽

③新来了进程

②和③情况的进程肯定是要进入调度队列的,但是它们都只会进入expired指向queue里面的哈希桶中

为什呢?
如果②和③情况的进程再进入active指向的queue里面的哈希桶中
此时如果②和③情况的进程的优先级很高,而且这样的进程很多
那它们就会一直插在哈希桶的最前面
那CPU每次调用的时候就都只会调用到它们
比它们优先级更低的进程就根本没有机会被调度了

这不符合分时操作系统的调度原则:进程调度尽可能公平

所以新进程和时间片耗尽的进程就根据自己的优先级,插入expired指向queue里面的哈希桶中


所以
active指向的queue里面的进程,因为没有补充,所以会一直减少,直到减到0

此时只需要交换active和expired的指向的队列,之前队列里面积压的进程就给了active
就可以实现调度轮转[因为CPU只从active指向的queue里面的哈希桶中选进程]

这样调度就既可以保证公平,也体现了进程的优先级