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