进程的概念、状态、优先级、切换

文章详细介绍了进程的概念,强调进程是程序的动态实例,具有多种状态,如运行、阻塞、挂起等。在Linux环境下,通过`ps`命令查看进程,`kill`命令管理进程。文章还讨论了进程控制块(PCB)、进程的创建(`fork`)以及进程的优先级。此外,还阐述了进程的四种特性:竞争性、独立性、并行和并发,以及进程切换的过程和上下文保护的重要性。
摘要由CSDN通过智能技术生成

目录

进程概念

Linux下的进程

进程状态(抽象理论)

Linux下的进程状态(具体)

进程优先级

进程切换


进程概念

什么是进程?————一个程序运行起来加载到内存就是进程。只不过进程有多种不同状态,可能并不在执行。

进程与程序的区别在于进程具有动态属性

根据冯诺依曼体系结构,一个程序要想被执行,首先要从磁盘加载到内存,再被CPU读取调度。但是整个过程就这么简单吗?显然不是,因为每时每刻都有很多程序需要加载到内存,并且内存中也已经有很多进程了,所以操作系统要管理这些进程。

怎么管理?先描述,再组织。将进程的代码、属性用结构体\类描述起来,再将其作为节点放到对应数据结构中。

先说描述,这里就要提到一个概念PCB(Process Control Block) 进程控制块。其实就是一个结构体,我叫它task_struct,里面存程序的所有属性、代码属性的地址以及下一个节点。

struct  task_struct
{
    //程序所有属性
    //程序代码属性的地址
    struct task_struct* next;
};
    
struct task_struct* p1 = new ...
p1->data = ...
p1->addr = ...

 用这个进程控制块标识对应进程的信息存到链表中。每个进程控制块连接一个进程和下一个节点控制块:

 当一个进程死亡时,操作系统根据进程控制块信息,将对应进程及控制块从链表删除,有新的程序要加载进内存也是一样,都是操作系统遍历链表,根据进程控制块筛选节点进行增删查,或者选择优先级。

struct task_struct称为内核结构体,task_struct对象称为内核对象,

进程 = 内核数据结构(task_strcut) + 进程对应的磁盘代码。

Linux下的进程

查看进程

首先写个小程序,此时mytest并不是进程,对应上图在磁盘中的程序,./mytest 运行起来加载到内存后才是进程。

ps ajx 指令可以看到系统当前所有进程:

 ps ajx | grep 'mytest' : 通过管道过滤mytest进程

 ps ajx | head -1:会将输出结果的标题打印出来。

 

 PPID:父进程ID             PID:子进程ID            PGID:祖进程ID       SID:会话ID

 TTY:终端                     STAT:状态                 UID:用户ID             COMMAND:当前进程

ps ajx | head -1 && ps ajx | grep 'mytest'  : 先打印标题,再显示对应进程:

 杀死进程

要想杀掉该进程,kill -9 29333(PID)

 这样就杀掉了该进程。

见一下系统调用接口

 打开man手册,查看一下getpid这个系统调用接口的用法。

在我们上面写的程序中加入getpid:



另一种查看进程的方式(了解)

在Linux下有一个内存级的目录proc,ls /proc可查看根目录的进程属性信息:

 可以看到有很多数字目录,这些目录对应的就是进程PID,进程也可以被看做文件,Linux下一切皆文件!

ls /proc/17225 -d 显示当前运行进程文件

 此时结束右边运行程序,该进程文件也被系统回收:

 再做一个实验,运行刚刚写的程序加载到内存。

 进入该进程目录,找到exe文件,这就是磁盘可执行程序mytest对应的进程路径。

如果我们此时删除可执行程序mytest,会不会将进程退出?

 可以看到,进程仍在跑,但是进程文件被删除了,这说明当可执行程序被加载到内存后就和它本身没关系了,这是普遍现象但也有特例,后面再提。

进程的常见调用(2个)

fork命令是创建进程的:

 

 查看进程信息,我们发现这里第一个创建的进程29661其实就是bash(shell命令行解释器)

一般来说,第一个进程的父进程就是bash。

现在我们知道了fork可以创建进程,那么怎么使用呢?像上面那样使用肯定不能满足。

打开man手册查询一下:

 来看返回值,子进程ID作为父进程的返回值,0作为子进程的返回值;如果创建失败,-1作为父进程的返回值,没有子进程。

根据手册我们尝试来写创建父子进程的代码:

 2190是父进程,2191是子进程,父进程返回值是子进程PID,子进程返回是0.

我们发现一个不可思议点:同一段代码,在后续没有改变的情况下,变量val(ID)居然会有不同值! 在C语言的学习过程中,函数返回值都只有一个,但是这里居然出现了两个。这就是系统与语言的差别,多线程的影子在此体现。

 父子进程同时运行,fork函数执行前只有一个父进程,执行后,父进程+子进程。

既然fork是用返回值来判断父子进程的,并且执行后父子进程一起跑,那么:

 由此得出,fork函数后续代码,被父子进程共享,当然要根据 if判断返回值。

进程状态(抽象理论)

之前说了进程不一定都是在运行,也存在不同状态,如就绪、阻塞、挂起、死亡...

这里说一下,不同操作系统书籍上对状态的说法描述有所不同,或者叫法不同。

那首先要明白为什么会存在这么些状态。一般我们的电脑只有个位数的CPU,总之肯定比进程数来的少,那么就不可能一个CPU专门管一个进程,通常是单核一个CPU操作所有进程数据。那么一定就有正在执行的进程,其他管不过来的就处于其他状态。

运行(R)

操作系统有一个运行队列runqueue,在里面的进程状态被标识为运行(R),但是不一定真正在运行,只是状态为R。

之前在讲进程控制块PCB时说了,程序被加载到内存成为进程然后被CPU读取,操作系统调度进程时是根据PCB来的,不是程序代码。所以这里,进程状态会被保存到PCB中,也就是结构体对象task_struct->int(1:run  2:stop  3:hup  4:dead) 所以让进程入运行队列,实际上就是将PCB的task_struct结构体对象放入队列。

阻塞

当一个进程想要使用外设资源,但是其前面已经有其他进程占用外设时,它就会进入等待队列,操作系统看它暂时使用不了外设资源,就不会让它留在runqueue,当然操作系统很忙速度很快不会陪它一起等外设空闲,就把它塞到等待队列,当前面进程完事后再把它拉入runqueue。

所以进程不止会占用CPU资源,也会随时随地需要动用外设资源。

所谓的进程不同状态,就是进程在不同的队列中。

挂起

当进程标识阻塞状态后,可能会很长时间都在等待,不仅要等硬件资源,还要再进runqueue排队,这时它加载到内存的数据就会一直卡在那,对于内存来说是个负担,操作系统要不断给内存腾空间,万一内存不够了就会无法响应卡住。所以这时操作系统会将阻塞进程的代码置换到磁盘中,等资源准备好了再把代码重新加载到内存运行。这部分节省的空间给其他进程使用,阻塞进程的PCB没有被换出去,因为体积小并且还需靠PCB重新加载对应代码。当资源准备好,其他进程也会给该阻塞进程腾空间让其进入runqueue,操作系统是一视同仁的。所以挂起实际上是对进程数据的换入换出。

所以该场景下阻塞不一定挂起,挂起一定阻塞。注意是该场景下,还有其他情况,比如阻塞挂起状态,进程既阻塞又挂起,还有就绪挂起等各种情况。阻塞就是单纯在等待自己的资源,挂起会和其他状态各种组合,要注意。

Linux下的进程状态(具体)

Linux下的状态有以下这些,运行,浅度睡眠,深度睡眠,暂停,t停止,僵尸,死亡。

与上面提到的状态说法不同,具体操作系统下有自己的状态规则,比如S和T状态都是阻塞。

 先写个代码见见状态:

 此时a.out 处于R运行状态。此时程序是计算密集型,查看进程状态为R。

改一下代码:

此时进程变成了S睡眠状态,也就是阻塞。为什么呢,就是多加了一句printf?————这里就印证了CPU与硬件之间速度的差距。CPU可能一瞬间就跑完了这段代码,但是要打印到显示器 ,显示器当然刷新要慢得多,CPU不会一直等待,该进程就进入睡眠状态等到资源加载完毕CPU再执行打印,其实大部分时间都在等IO,这种程序就是IO密集型,查看状态基本为S。

然后这个+是什么意思呢?———— 带+的是前台进程,不带的是后台进程。

之前我们学过一个指令kill ,当时只说了kill -9杀死进程。

 说一下kill -19 暂停信号和kill -18继续信号。

对刚刚的进程,kill -19暂停,查看进程:

 这时进程状态变为了T,暂停。暂停也是阻塞,但是不是挂起呢要看操作系统的决定,这就是为什么挂起状态不好说的原因,完全取决于操作系统,而且它不会把挂起状态暴露给用户,也就是说你不知道进程是不是挂起的。

此时再kill -18继续进程:

 然后进程会再度运行起来,此时S+变成了S,也就是前台进程变成了后台进程。前台进程可以Crtl+C退出,但是后台进程不可以,需要kill -9杀死进程退出。

深度睡眠

刚刚说的S是浅度睡眠,D状态是深度睡眠,这个不常见到,只有公司里高IO的情况下容易见到。

举个例,进程A有个任务。向磁盘写入1000条重要数据,因为数据量大,磁盘慢慢去执行了,进程A就挂着在等待。此时操作系统内存吃紧,于是检查内存的闲杂人员,将大量暂时不用的进程挂起,但还是不够,再继续下去操作系统将崩溃,于是它杀死了很多挂起进程包括A。等磁盘写完1000条数据回来发现进程A死亡,于是数据就被丢弃了。为了防止重要数据被该情况丢弃,给部分特定进程挂上免死金牌,告诉操作系统不要杀死他,这种状态就是深度睡眠,理论上只有该进程自动醒来或者断电能杀死它,否则无法杀死。

当一个机器挂满大量D状态的进程时,机器离崩溃也不远了。

t (tracing stop) 暂停(进程正在被追踪)

当我们编译完一个程序,让它跑起来进入调试,此时会显示暂停状态,并且进程显示被追踪。

 可以看到此时进程处于t 状态,此时可以查看程序代码上下文。

Z状态(僵尸进程)

一个进程执行完任务后,需要回报任务信息,虽然是否关心该任务信息不一定,但是需要回报。随后进程正常退出,按理应该进入死亡状态。这时如果没有人来回收它(它的父进程没回收,操作系统也没回收),那么它就会进入Z状态。

我们来写个程序看看僵尸进程是什么样的。

 创建进程,运行一段时间让子进程退出,让父进程不要去回收它。

写个监控脚本方便查看:

while :; do ps ajx | head -1 && ps ajx | grep myproc | grep -v grep; sleep 1; done

 可以看到,子进程状态由S+变成了Z+,因为父进程没有回收它。  defunct意思是死者,死亡的。

 如果父进程一直不回收僵尸子进程,那么它就会一直占用系统的内存。僵尸进程的磁盘代码可能被释放了,但是PCB数据会保留下来一直占着空间,这会造成内存泄漏,所以僵尸进程是一个要解决的问题。

当进程标记为X(dead)状态时,所有进程资源包括PCB都释放了/

孤儿进程

上面的僵尸进程是父子进程运行时子进程先退出的情况,这里的孤儿进程则是父亲先退出的情况。

改一下代码,让父子进程一同运行下去:

 此时杀掉父进程:

我们发现父进程是没了,子进程状态从S+变成了S。并且此时的子进程的PPID变成了1

 查一下系统进程,发现PID为1的进程就是bash,也就是说失去父进程的子进程,它被操作系统领养了。

问题:为什么操作系统要领养孤儿进程?————如果没人回收,该进程会变成僵尸进程,长时间占用内存,所以由操作系统直接接手。

进程优先级

首先明确一点,优先级和权限不是一个概念。权限是系统是否让你执行,优先级是在允许执行的前提下排队。

为什么要有进程优先级?————CPU个数有限,匹配不了大量等待运行的进程,大量的人要抢占少量的资源,所以必须要有优先级,必须要排队。

Linux下的优先级

Linux下优先级是怎么设置的呢?其实也不是什么高大上的东西。就是PCB里的一项属性,用整数来标识优先级,Linux下使用PRI和NI两个整数。 

ps -la查看进行优先级;

 初始PRI是80,NI不一定,范围在[-20,19]。

最终优先级 = PRI(老的优先级)+NI 。(优先级[60,99])

 sudo top可以更改进程优先级,通过调整NI值做到,注意超出NI范围会去到NI边界值,不会超出。

sudo top --->  r --->  输入进程PID--->输入要更改的NI值,就可以改变优先级了。PRI始终是80,不会因为上一次改变而变化。

进程切换

进程的4个特性:竞争性、独立性、并行、并发。

1、竞争性。 上面说了,计算机中CPU数量一定是少于进程的,不可能一个CPU运行一个进程,所以大量待运行的进程要抢占少量的CPU资源,就存在竞争。

2、独立性。 多进程运行期间不会互相干扰。一个进程的退出不会影响其他进程,比如QQ进程无响应了,不会影响浏览器的运行。子进程退出,父进程依然会跑下去。

3、并行和并发。  多个进程在多个CPU下同时运行,这叫并行;多个进程在单个CPU下采用进程切换的方式运行,使得多个进程都能推进,这叫并发。

比如一台电脑上有2个CPU,那么就能有2个进程同时在运行(注意不是在runqueue中,而是真正在运行)。只有一个CPU那么同一时间只能有一个进程在运行!为什么我的电脑只有一个CPU却好像也同时运行着许多进程呢?————因为采用了进程切换的方式。CPU是纳秒级别的芯片,它的执行速度非常快,一个进程不会让它从头执行到尾再换下去,CPU要兼顾其他进程,所以可能一个进程CPU给几毫秒的时间运行,时间一到就换下去,让其他进程上来,没执行完继续排队等待下一次,否则要是必须执行完一个进程再切换那么死循环就出不来了。CPU实在太快了,瞬间能跑大量代码,所以在我们看来CPU好像同时在执行多个进程。

进程切换的执行

首先要知道CPU上有很多寄存器,可见的、不可见的,这些寄存器帮助存储临时数据、完成计算...

CPU调度进程,存储进程的数据也是通过寄存器。

 CPU上有些关键寄存器,其中一个叫pc/eip,pc指针。它专门存储当前进程执行代码的下一条指令地址。

我们知道CPU不会主动执行什么命令,只会被动的接受发送的指令,然后根据内部指令集分析指令,最后执行。当进程在运行的时候会产生非常多的临时数据,而这份数据属于当前进程。

虽然CPU中有非常多的寄存器,但是寄存器硬件只有一套!寄存器数据是属于当前进程的,不是寄存器属于当前进程!一定要分请两个概念,寄存器硬件 != 寄存器数据。

上面说了进程都有自己的时间片,一到时间就会从CPU上剥离下来等待再运行。假如一个进程没执行完,它的数据会怎样呢?————一定会被操作系统保存下来,否则下一次上来运行的时候就不知道执行到哪了。

在任何时刻我们看到寄存器里存储的数据,都是只属于当前进程的,寄存器硬件被所有进程共享,但是寄存器数据只属于当前进程。所以进程切换要保存数据,也就是上下文保护,这个上下文就是指寄存器数据。当前进程下去了,进行上下文保护,回来继续运行了,进行上下文恢复。

我们知道寄存器是很小的,一个寄存器可能只有几字节,如果我写的代码开辟了很多变量数组,它要存储进程数据存的下吗?————不用担心,寄存器对进程代码是一部分一部分获取的,对于已经执行过的代码和尚未执行的代码也不会加载到CPU,所以不需要有容量考虑。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值