#操作系统
计算机最重要的概念就是抽象。在操作系统中,文件是对I/O的抽象,有了文件这样的概念,I/O这样的外接设备就像虚拟的一种东西一样随时写入输出;虚拟内存是对即程序存储器的抽象;而进程是对一个正在运行的程序的抽象(从英文就可以看出,processor和process的关系)。而在这之上的,是虚拟机,对整个操作系统、处理器和程序的抽象。因此,对于操作系统而言,主要还是要探究四个主要部分:对进程、内存、文件和I/O设备的管理。
程序与进程是两个相关但是不同的概念。程序(program),是静态的,就是个存放在磁盘里的可执行文件,如:QQ.exe。而进程(process),是动态的,是程序的一次执行过程,比如,可你可以同时用多个账号启动QQ程序,这同一个程序多次执行,会对应多个进程。 那么,操作系统作为这些进程的管理者,它要怎么区分各个进程? 跟人的身份证一样,每个人都有一个身份证号。当进程被创建时,操作系统会为该进程分配一个唯一的、不重复的“身份证号”—— PID(Process ID,进程ID)。操作系统会记录PID、进程所属用户ID(UID),记录给进程分配了哪些资源(如:分配了多少内存、正在使用哪些I/O设备、正在使用哪些文件),还要记录进程的运行情况(如:CPU使用时间、磁盘使用情况、网络流量使用情况等)。
进程的相关详细信息都被保存在一个数据结构PCB (Process Control Block)中,即进程控制块。操作系统需要对各个并发运行的进程进行管理,但凡管理时所需要的信息,都会被放在PCB中。Linux
系统中的PCB叫做task struct
。
PCB会存储以下相关信息:
- 进程ID或PID :任何执行阶段中每个进程的唯一整数ID。
- 工艺状态,任何进程当前所处的状态,如就绪、等待、退出等
- 进程权限,对进程所具有的内存或设备的不同资源的特殊访问。
- 指针,指向父进程的指针位置。
- 程序计数器,它将始终具有流程中下一条指令的地址
- CPU 寄存器,在执行程序之前,CPU 注册了需要存储进程的位置。
- 日程安排信息,流程有不同的调度算法,将根据这些算法优先选择它们。本部分包含有关计划的所有信息。
- 内存管理信息,操作系统将使用大量内存,它需要知道诸如页表、内存限制、段表等信息,以执行不同的程序,MIM具有有关此的所有信息。
- 会计信息,顾名思义,它将包含有关所花费时间、执行 ID、限制等的所有信息。
- I/O 状态 – 进程可以使用的所有 I/O 信息的列表。
PCB是进程存在的唯一标志,当进程被创建时,操作系统为其创建PCB,当进程结束时,会回收其PCB。
PCB 是给操作系统用的,程序段、数据段是给进程自己用的。一个进程实体(进程映像)由PCB、程序段、数据段组成。进程是动态的,进程实体(进程映像)是静态的。可以理解成,进程实体(进程映像)是某一种状态下进程动态的快照。
程序段、数据段、PCB三部分组成了进程实体(进程映像) 引入进程实体的概念后,可把进程定义为:进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位。
- 同时挂三个QQ号,会对应三个QQ进程,它们的PCB、数据段各不相同,但程序段的内容都是相同的(都是运行着相同的QQ程序)。
- 程序段、数据段是给进程自己用的,与进程自身的运行逻辑有关。
- 一个进程被“调度”,就是指操作系统决定让这个进程上CPU运行。
程序是静态的,进程是动态的,相比于程序,进程拥有以下特征:
- 动态性(dynamic):进程最基本的特征。动态性进程是程序的一次执行过程,是动态地产生、变化和消亡的;
- 并发性(concurrent):内存中有多个进程实体,各进程可并发执行;
- 独立性(independent):能独立运行、独立获得资源、独立接受调度的基本单位;
- 异步性(asynchronous):各个进程按照各自的独立的、不可预知的速度往前推进。操作系统要提供“进程同步机制”来解决异步问题。
- 结构性(structural):每个进程都会配置1个PCB,进程由程序段、数据段、PCB组成。
进程是动态的,这一特点是进程最基本的特征,那么动态的进程会经历什么状态,又会因为什么条件导致状态发生变化,关于这个问题,我们由简单到复杂,由表象到深入地逐一剖析。
进程状态中最简单的模型是,也是双状态模型,无论进程是否正在执行,都可以随时创建双状态。它仅包含下面给出的两种状态:
- 运行状态(Running State):当前正在执行进程的状态。
- 未运行状态(Not Running State):进程正在等待执行的状态。
在这个过程中,
- 首先,当操作系统创建一个新进程时,它还会为该进程创建一个进程控制块(PCB),以便该进程可以在非运行状态下进入系统。如果任何进程退出/离开系统,则操作系统知道它。
- 接下来,当前正在运行的进程将被中断(interrupted)或闯入(break-in),操作系统的调度程序(dispatcher) 将运行任何其他进程,将处理器从一个进程切换到另一个进程的程序。前一个进程(被中断的进程)从运行状态变为非运行状态,后一个进程从非运行状态变为运行状态,之后它退出系统。
- 未运行(Not Running State)的进程必须保留在某种队列中,并等待轮到它们执行。在操作系统中,有一个队列,其中的队列的内容是指向那些未运行特定进程的进程控制块(PCB)的指针,通过这个指针,能够快速获取进程的状态、标识符、程序计数器、上下文数据等信息存储在数据结构中的块。
随着操作系统的进程管理复杂化,人们开始认识到了进程的异步性需要将进程的非运行状态进行拆分:如果是因为异步性而等待别的程序执行完再执行的情况,就叫阻塞态(blocked),如果是因为进程尚且处于未被调度,或者因为中断(interrupt)仍然在等待队列里面,就叫做就绪态(ready)。在这个基础上,扩充了进程的执行首尾状态:创建态(new),中止态(exit,terminated),至此,进程五状态模型就构建出来了。
现在再详细拆分下各个状态:
- 创建态:进程正在被创建时的状态,在这个阶段操作系统会为进程分配资源、初始化PCB。
- 就绪态:当进程创建完成后,便进入的状态,该阶段进程已经具备运行条件,但由于没有空闲CPU,就暂时不能运行。
- 运行态:CPU会执行该进程对应的程序,即执行指令序列;
- 阻塞态:在进程运行的过程中,可能会请求等待某个事件的发生(如等待某种系统资源的分配,或者等待其他进程的响应)。在这个事件发生之前,进程无法继续往下执行,此时操作系统会让这个进程下CPU,并让它进入这个状态。当CPU空闲时,又会选择另一个“就绪态”进程上CPU运行。
- 终止态:一个进程可以执行 exit 系统调用,请求操作系统终止该进程。此时操作系统会让该进程下CPU,并回收内存空间等资源,最后还要回收该进程的PCB。当终止进程的工作完成之后,这个进程就彻底消失了。
运行、就绪、阻塞,是三个基本状态。进程的整个生命周期中,大部分时间都处于三种基本状态。单CPU情况下,同一时刻只会有一个进程处于运行态,多核CPU情况下,可能有多个进程处于运行态。
这三个基本状态的最本质的区别在于它们对处理机和其他资源的拥有情况:
- 运行态具备处理机和其他资源;
- 就绪态只具备其他资源,而未得到处理机资源;
- 阻塞态均不具备处理机和其他资源,即使给处理机上也无法运行。
这里的其他资源是指临界等待区、I/O资源已经其他需要依赖的进程资源。
进程PCB中,会有一个变量 state 来表示进程的当前状态。例如,1表示创建态、2表示就绪态、3表示运行态…为了对同一个状态下的各个进程进行统一的管理,操作系统会将各个进程的PCB组织起来。
- null -> 创建:为执行进程创建新的进程。
- 创建 -> 就绪:系统会将流程从新状态移动到就绪状态,现在它已准备好执行。在这里,系统可以设置一个限制,以便不能发生多个进程,否则可能会出现性能问题。
- 就绪 -> 运行:操作系统调度现在选择一个进程进行运行,系统只选择一个处于就绪态的进程进行执行。
- 运行 -> 退出:如果进程指示进程现已完成或已中止,则系统将终止该进程。
- 运行 -> 就绪:当正在运行的进程达到其最大运行时间以不间断执行时。例如,在后台运行的进程会定期执行某些维护或其他功能。
- 运行 -> 阻塞:如果进程请求它正在等待的内容,则该进程将处于阻塞状态。例如,进程可能请求一些当时可能不可用的资源,或者它可能正在等待 I/O 操作或等待其他进程完成,然后进程才能继续。
- 阻塞 -> 就绪:当进程一直在等待的事件触发时,进程会从“阻塞”状态变为“就绪”状态。
- 就绪 -> 退出:这种转换只能在某些情况下存在,因为在某些系统中,父级进程可以随时终止子级进程。
五状态模型中可能会出现一个问题,让我们用一个例子来讨论这个问题。 假设,有一种情况,即大量进程都位于阻塞状态,并且都是 I/O 密集型进程,因此他们都会在 I/O 处理器的阻塞状态内的队列中等待。这种阻塞状态和RAM中的就绪队列共享一个公共空间,即这两个队列都驻留在RAM中。假设RAM被阻塞队列填满,在这种情况下,我们的系统将无法将任何新进程加载到就绪队列中,同时,我们的 CPU 也将保持空闲状态,也无法将其他将来可能会来的就绪态进程提供处理资源,这将直接影响系统的性能。为了克服这些问题,使用了“虚拟内存”的概念,并引入了七状态模型,对五状态过程模型略有改动,增加挂起态(Suspend State)。并将挂起态分为就绪挂起状态和阻塞挂起状态,至于具体细节,可以参考进程调度部分。
进程的组织分为链接(linker)方式和索引(index)方式。
按照进程状态将PCB分为多个队列,操作系统持有指向各个队列的指针。按照进程状态将PCB分为多个队列,操作系统持有各个队列的指针。队列由运行队列、就绪队列和阻塞队列。很多操作系统还会根据阻塞原因不同,再分为多个阻塞队列。就绪队列中,通常会把优先级高的进程放在队头。
根据进程状态的不同,建立几张索引表,操作系统持有指向各个索引表的指针。
总体上来说,进程的链接方式经常使用,而索引方式运用较少。
进程控制的主要功能是对系统中的所有进程实施有效的管理,它具有创建新进程、撤销已有进程、实现进程状态转换等功能。总之,进程控制就是要实现特定场景下进程状态转换,并通过操作系统内核的“原语”(primitive)实现。
原语是一种特殊的程序,它的执行具有原子性(atomic)。也就是说,这段程序的运行必须一气呵成,不可中断。
问题来了,为什么原语不可以中断呢?
假设PCB中的变量state
表示进程当前所处状态,state == 1
表示就绪态,state == 2
表示阻塞态。这时候,进程2(PCB2)等待的事件发生,则操作系统中,负责进程控制的内核程序至少需要做这样两件事:
- 将PCB2的
state
设为 1。 - 将PCB2从阻塞队列放到就绪队列。
假设,完成了第一步后收到中断(interrupt)信号,那么PCB 2的
state=1
,但是它却被放在阻塞队列里。这个时候就会产生关键数据结构信息不统一的现象,直接导致系统出现紊乱。因此原语操作必须不可以被中断。
原语的执行具有原子性,即执行过程只能一气呵成,期间不允许被中断。可以用关中断指令和开中断指令这两个特权指令实现原子性。 正常情况:CPU每执行完一条指令都会例行检查是否有中断信号需要处理,如果有,则暂停运行当前这段程序,转而执行相应的中断处理程序。
CPU执行了关中断指令之后,就不再例行检查中断信号,直到执行开中断指令之后才会恢复检查。这样,关中断、开中断 之间的这些指令序列就是不可被中断的,这就实现了“原子性”。其中开中断和关中断的程序,都在内核态运行,绝不可能允许用户程序使用。其实,关中断和开中断就是一种高权限的对中断信号的屏蔽。
关于相关的原语包含进程的创建、进程中止、进程阻塞、进程唤醒、进程切换。
在实际系统中创建一个进程有两种方法: 一是由操作系统建立,O号进程就是由操作系统建立的; 二是由其他进程创建一个新的进程。基本操作都是一样的。 创建进程原语总是先为新建进程申请一空白PCB,并为之分配唯一的数字标识符,使之获得PCB的内部名称,若该进程所对应的程序不在内存中,则应将它从外存储器调入内存,并将该进程有关信息填入PCB中,然后置该进程为就绪状态,并将它排入就绪队列和进程家族队列中。
撤销进程的实质是撤销进程存在标志——进程控制块PCB。一旦PCB被撤销,进程就消亡了。 撤销原语的操作过程大致如下:以调用者提供的标识符为索引,从该进程所在的队列,将它从该队列中消去,并撤销属于该进程的一切“子孙进程”,若有父进程则从父进程PCB中删除指向该进程的指针,并释放撤销进程所占用的全部资源,或将其归还给父进程,或归还给系统。若被撤销的进程处于执行状态,应立即中断该进程的执行,并设置调度标识为真,以指示该进程被撤销后系统应重新调度 。
阻塞原语的大致工作过程如下:开始时,进程正处于执行状态,因此首先应中断CPU执行,并保存该进程的CPU上下文,然后把阻塞状态赋予该进程,并将它插入具有相同实体的阻塞队列中。
进程因为等待事件的发生而处于阻塞状态,当等待的事件完成后,进程又具有了继续执行的条件,这时就要把该进程从阻塞状态转变为就绪状态,这个工作由唤醒原语来完成。唤醒原语执行的操作有:先把被唤醒进程从阻塞队列中移出,设置该进程当前状态为就绪状态,然后再将该进程插入到就绪队列中。
将运行环境信息(进程上下文)存入PCB,PCB移入相应队列,选择另一个进程执行,并更新其PCB,根据PCB恢复新进程所需的运行环境。
对程序的运行从CPU角度,进行剖析:在CPU处理器当中,有以下4个常用的寄存器,用于存储程序运行过程中的中间变量。在执行的过程中,会在寄存器中缓存进程的相关参数。
【问题】如果执行完指令3后,另一个进程开始上CPU运行。另一个进程在运行过程中也会使用各个寄存器,之后还怎么切换回之前的进程?数据等如果被覆盖怎么解决? 【解决办法】在进程切换时先在PCB中保存这个进程的运行环境(保存一些必要的寄存器信息)。 总结,无论哪个进程控制原语,要做的无非三类事情:
- 更新PCB中的信息(state,context);
- 将PCB插入合适的队列;
- 分配/回收资源;