我们编写的代码只是一个存储在硬盘的静态文件,通过编译后会生成二进制可执行文件,当我们运行这个可执行文件后,它会被装载到内存中,接着CPU会执行程序中的每一条指令,那么这个运行中的程序就被称为进程。
现在我们考虑有一个会读取硬盘文件数据的程序执行了,那么当运行到读取文件指令时,就会去从硬盘读取数据,但是硬盘的读写速度相比于CPU的处理速度是非常慢的,那么在这个时候,如果CPU一直等待硬盘返回数据的话,CPU的利用率是非常低的。
类比,当你去烧开水的时候,你不会傻傻等水壶烧开。我们可以在水壶烧开之前去做其他事情。当水壶烧开了,我们自然会听到"滴滴滴"的声音,于是再把烧开的水倒入到水杯中就好了。
所以,当进程要从硬盘读取数据时,CPU不需要阻塞等待数据的返回,而是去执行另外的进程。当硬盘数据返回时,CPU会收到一个中断,于是CPU会再继续运行这个进程。
这种多个程序、交替执行的思想,就有CPU管理多个进程的初步想法。
对于一个支持多进程的系统,CPU会从一个进程快速切换到另一个进程,期间每个进程各运行几十或几百毫秒。
虽然大单核CPU在某个瞬间,只能运行一个进程。但在一秒钟内,他可能会运行多个进程,这样就产生了并行的错觉,实际上这是并发。
并行和并发的区别
进程与程序的关系
到了晚饭时间,一对小情侣肚子都咕咕叫了,于是男生见机行事,就想给女生做晚饭,所以他就在网上找了辣子鸡的菜谱,接着买了一些鸡肉、辣椒、香料等材料,然后边看边学边做这道菜。
突然,女生说她想喝可乐,那么男生只好把做菜的事情暂停一下,并在手机菜谱标记做到哪一个步骤,把状态信息记录了下来。
然后男生听从女生的指令,跑去下楼买了一瓶冰可乐后,又回到厨房继续做菜。
这体现了,CPU 可以从一个进程(做菜)切换到另外一个进程(买可乐),在切换前必须要记录当前进程中运行的状态信息,以备下次切换回来的时候可以恢复执行。
所以,可以发现进程有着「运行 - 暂停 - 运行」的活动规律。
我们知道了进程有着 [运行-暂停-运行] 的活动规律。一般来说,一个进程并不是自始至终连续不停地运行的,它与并发执行中的其他进程的执行是相互制约的。
它有时处于运行状态,有时又由于某种原因而暂停运行处于等待状态,当使他暂停的原因消失后,它又进入准备运行状态。
所以,在一个进程的活动期间至少具备三种基本状态,即运行状态、就绪状态、阻塞状态。
当然,进程还有另外两个基本状态:
于是完整的进程状态图为:
如果有大量处于阻塞状态的进程,进程可能会占用着物理内存空间。显然不是我们所希望的,因为物理内存空间有限,被阻塞状态的进程占用物理内存就是一种浪费物理内存的行为。
所以,在虚拟内存管理的操作系统中,通常会把阻塞状态的进程的物理内存换出到硬盘,等需要再次运行的时候,再从硬盘换入到物理内存。
那么,就需要一个新的状态,来描述进程没有占用实际的物理内存空间的情况,这个状态就是挂起状态。这跟阻塞状态是不一样的,阻塞状态是等待某个事件的返回。
挂起状态可以分为两种:
这两种状态加上前面的五种状态,就变成了七种状态变迁:
导致进程挂起的原因不只是因为进程所使用的内存空间不在物理内存,还包括如下情况:
在操作系统中,是用进程控制块(process control block ,PCB) 数据结构来描述进程的。
PCB是进程存在的唯一标识,这意味着一个进程的存在,必然会有一个PCB,如果进程消失了,那么PCB也会随之消失。
PCB具体包含什么信息呢?
进程描述信息:
CPU状态信息:
进程调度信息:
进程控制信息:
每个PCB是如何组织的?
通常是通过链表的方式进行组织,把具有相同状态的进程链在一起,组成各种队列。比如:
那么,就绪队列和阻塞队列链表的组织形式如下:
除了链接的组织方式,还有索引方式,它的工作原理:将同一状态的进程组织在一个索引表中,索引表项指向相应的PCB,不同状态对应不同的索引表。
一般会选择链表,因为可能面临进程创建,销毁等调度导致进程状态发生变化,所以链表能够更加灵活的插入和删除。
这里我们介绍进程的创建、终止、阻塞、唤醒的过程,这些过程也就是进程的控制
操作系统允许一个进程创建另一个进程,而且运行子进程继承父进程所拥有的资源。
创建进程的过程如下:
进程可以有3种终止方式:正常结束、异常结束以及外界干预(信号 kill 掉)
当子进程被终止时,其在父进程处继承的资源应当还给父进程。而当父进程被终止时,该父进程的子进程就变为孤儿进程,会被 1 号进程收养,并由 1 号进程对它们完成状态收集工作。
终止进程的过程如下:
引起进程阻塞的与唤醒的事件如下:
进程阻塞的步骤如下:
进程由 [运行] 转变为 [阻塞] 状态是由于进程必须等待某一事件的完成,所以处于阻塞状态的进程是绝对不可能唤醒自己的。
当被阻塞进程所期待的事件出现时,如I/O完成获其所期待的数据已经到达,则由有关进程(如用完并释放I/O设备的进程)调用唤醒语句wakeup,将等待该事件的进程唤醒,首先将被阻塞的进程从等待该事件的阻塞队列中移出,将其PCB中的现行状态由阻塞改为就绪,然后再将该PCB插入到就绪队列中。值得注意的是,block原语与wakeup原因应该在不同进程中执行。
唤醒进程的过程如下:
进程的阻塞和唤醒是一对功能相反的语句,如果某个进程调用了阻塞语句,则必有一个与之对应的唤醒语句。
各个进程之间是共享CPU资源的,在不同的时候进程之间需要切换,让不同的进程可以在CPU执行,那么这个一个进程切换到另一个进程运行,称为进程的上下文切换。
CPU的上下文切换
大多数操作系统都是多任务,通常支持大于CPU数量的任务同时运行。实际上,这些任务并不是同时运行的,只是因为系统在很短的时间内让各个任务分别在CPU上运行,于是就造成了同时运行的错觉。
任务是交给CPU运行的,那么在每个任务运行前,CPU需要知道任务从哪里加载,又从哪里开始运行。
所以,操作系统事先需要帮CPU设置好CPU寄存器和程序计数器
CPU寄存器是CPU内部一个容量小,但速度极快的内存(缓存)。
程序计数器则是用来存储CPU正在执行的指令位置,或者即将执行的下一条指令位置。
所以,CPU寄存器和程序计数器是CPU在运行任何任务前,所必须依赖的环境,这些环境就叫做CPU上下文
CPU上下文切换就是把前一个任务的上下文(CPU寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。
系统内核会存储保存下来的上下文信息,当此任务再次被分配给CPU运行时,CPU会重新加载这些上下文,这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。
上面说到所谓的 [任务],主要包含进程、线程和中断。所以,可以根据任务的不同,把CPU上下文切换分为:进程上下文切换、线程上下文切换和中断上下文切换。
进程的上下文切换到底是切换什么呢?
进程是由内核管理和调度的,所以进程的切换只能发生在内核态
所以,进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源
通常,会把交换的信息保存在进程的 PCB,当要运行另外一个进程的时候,我们需要从这个进程的 PCB 取出上下文,然后恢复到 CPU 中,这使得这个进程可以继续执行,如下图所示:
需要注意,进程的上下文开销是很关键的,我们希望它的开销越小越好,这样可以使得进程可以把更多时间花费在执行程序上,而不是耗费在上下文切换。
发生进程上下文切换有哪些场景?
以上,就是发生进程上下文切换的常见场景了。