本文为读书摘要(个人认为重要的知识点),穿插课后习题的选取(主要选取考察概念性的习题)
从技术上看,在所有这些情形中,新进程都是由于ー个已存在的进程执行了一个用于创建进程的系统调用而创建的。(系统初始化例外)
三态模型
进程准备好后就可以运行了,但是这个时候其他程序在运行,因此进入就绪状态,当调度选择了这个进程时,它就可以运行了。而正在运行的进程,由于CPU给的时间用完了,但还没执行完毕,这时就会进入就绪转态,等待下次调度进入CPU。如果正在运行的进程由于需要I/O操作或者其他因素,需要等待一段时间才可以继续运行,就会进入阻塞状态,如果I/O执行完毕,就会进入就绪状态,再次等待运行。
与每ーI/O类关联的是ー个称作中断向量(interrupt vector)的位置(靠近内存底部的固定区域)。
它包含中断服务程序的入口地址。假设当一个磁盘中断发生时,用户进程3正在运行,则中断硬件将程序计数器、程序状态字、有时还有一个或多个寄存器压入堆栈,计算机随即跳转到中断向量所指示的地址。 这些是硬件完成的所有操作,然后软件,特别是中断服务例程就接管一切剩余的工作。
硬件压入堆栈程序计数器等。
硬件从中断向量装入新的程序计数器。
汇编语言过程保存寄存器值。
汇编语言过程设置新的堆栈。
C中断服务例程运行(典型地读和缓冲输入)。
调度程序决定下ー个将运行的进程。
C过程返回至汇编代码。
汇编语言过程开始运行新的当前进程。
更好的模型是从概率的角度来看CPU的利用率。假设ー个进程等待 I/O 操作的时间与其停留在内存中时间的比为P。当内存中同时有n个进程时,则所有n个进程都在等待 I/O(此时CPU空转)的概率是 P ^ n。
从完全精确的角度考虑,应该指出此概率模型只是描述了一个大致的状况。它假设所有 n 个进程是独立的,即内存中的5个进程中,3个运行,2个等待,是完全可接受的。但在单CPU中,不能同时运行3个进程,所以当CPU忙时,已就绪的进程也必须等待CPU。因而,进程不是独立的。更精确的模型应该用排队论构建,但我们的模型(当进程就绪时,给进程分配CPU,否则让CPU空转)仍然是有效的,
多线程的含义 : 并行实体拥有共享同一个地址空间和所有可用数据的能力
多线程的特点
进程模型基于两种独立的概念:资源分组处理与执行。
除了共享地址空间之外,所有线程还共享同一个打开文件集、子进程、定时器以及相关信号等,但是每个线程都有寄存器、堆栈、程序计数器、状态等。
第一种方法是把整个线程包放在用户空间中,内核对线程包一无所知。从内核角度考虑,就是按正常的方式管理,即单线程进程。
这种方法第一个也是最明显的优点是,用户级线程包可以在不支持线程的操作系统上实现。
用户线程优缺点:
优点:
1、线程切换不用陷入内核,不需要上下文切换,所以线程切换速度很快;
2、允许每个进程有自己定制的调度算法。
缺点:
1、在如何实现阻塞系统调用上实现有困难,而如果采用非阻塞方案,需要修改原有的操作系统;
2、如果一个线程开始允许,那么该进程中其他线程就无法运行,除非第一个线程放弃CPU,那么线程的调度又是一个问题;
所有能够阻塞线程的调用都以系统调用的形式实现,这与运行时系统过程相比,代价是相当可观的。 **当ー个线程阻塞时,内核根据其选择,可以运行同一个进程中的另一个线程(若有一个就绪线程)或者运行另一个进程中的线程。**而在用户级线程中,运行时系统始终运行自己进程中的线程,直到内核剥夺它的CPU (或者没有可运行的线程存在了)为止。
由于在内核中创建或撤销线程的代价比较大,某些系统采取“环保”的处理方式,回收其线程。**当某个线程被撤销时,就把它标志为不可运行的,但是其内核数据结构没有受到影响。稍后,在必须创建ー个新线程时,就重新启动某个旧线程,从而节省了一些开销。**在用户级线程中线程回收也是可能的,但是由于其线程管理的代价很小,所以没有必要进行这项工作.
编程人员可以决定有多少个内核级线程和多少个用户级线程彼此多路复用。这ー模型带来最大的灵活度。
采用这种方法,内核只识别内核级线程,并对其进行调度。其中一些内核级线程会被多个用户级线程多路复用。如同在没有多线程能力操作系统中某个进程中的用户级线程一样,可以创建、撤销和调度这些用户级线程。在这种模型中,每个内核级线程有一个可以轮流使用的用户级线程集合。
在用户态线程执行调度的一种机制
调度程序激活工作的目标是模拟内核线程的功能,但是为线程包提供通常在用户空间中才能实现的更好的性能和更大的灵活性。
使该机制工作的基本思路是,当内核了解到ー个线程被阻塞之后(例如,由于执行了一个阻塞系统调用或者产生了一个页面故障),内核通知该进程的运行时系统,并且在堆栈中以参数形式传递有问题的线程编号和所发生事件的一个描述。内核通过在ー个已知的起始地址启动运行时系统,从而发出了通知,这是对UNIX中信号的ー种粗略模拟。这个机制称为上行调用(upcall)。
一旦如此激活,运行时系统就重新调度其线程,这个过程通常是这样的:把当前线程标记为阻塞并从就绪表中取出另ー个线程,设置其寄存器,然后再启动之。稍后,当内核知道原来的线程又可运行时(例如,原先试图读取的管道中有了数据,或者已经从磁盘中读入了故障的页面),内核就又一次上行调用运行时系统,通知它这ー事件。
进程间通信是很重要也很常用的的一个概念,主要围绕三个问题:
1、如何把信息传递给另一个进程
2、如何确保两个或多个进程不会交叉(例如多个用户在飞机订票系统同时取买票,该给谁)
3、如何确保按正确的顺序执行:例如B进程要打印A进程的结果,那么肯定是先执行完A,才能执行B。
两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序,称为竞争条件(race condition)。
我们把对共享内存进行访问的程序片段称作临界区域(critical region)或临界区(critical section)。如果我们能够适当地安排,使得两个进程不可能同时处于临界区中,就能够避免竞争条件。
为了避免竞争条件,引入互斥的概念,**设计的方案**应该满足以下四个条件:
1、任何两个进程不能同时处于临界区
2、不应对CPU的速度和数量做任何假设。
3、临界区外的进程不得阻塞其他进程
4、进程不能无限期等待
屏蔽中断 —— 一般单CPU才会这么做
锁变量 —— 理想化,软件难以实现(需要硬件支持)
严格轮换法:需要用到忙等待,非常浪费CPU。导致其他进程可能被阻塞。 连续测试一个值直到某个值出现,称为忙等待, java示例代码如下:
while (TRUE) {
while (turn != 0); /* 循环 */
critical_region();
turn = 1;
noncriticaLregion();
)
while (TRUE) {
while (turn != 1); /* 循环 */
critical_region();
turn = 0;
noncritical_region();
)
实际上,该方案要求两个进程严格地轮流进入它们的临界区
Peterson解法
#define FALSE 0
#define TRUE 1
#define N 2/*进程数量*/
int turn; /* 现在轮到谁 */
int interested[N]; /*所有值初始化为(FALSE) */
void enter_region(int process)
{
int other; /*另一进程号*/
other = 1 - process;
interested[process] = TRUE;
turn = process;
while (turn = process && interested[other] == false){}
}
void leave_region(int process) f产进程:谁离开? */t
{
interested[process] = FALSE;
}
现在考虑两个进程几乎同时调用entejregion的情况。它们都将自己的进程号存入turn,但只有后被保存进去的进程号オ有效,前ー个因被重写而丢失。假设进程1是后存入的,则tum为1。当两个进程都运行到while语句时,进程。将循环。次并进入临界区,而进程1则将不停地循环且不能进入临界区,直到进程。退出临界区为止。
原子指令
TSL指令将 内存自LOCK读到寄存器RX中,然后在该内存地址上存在一个非零值。读字和写字由操作系统保证是不可分割的(原子性),CPU将锁住内存总线,以禁止其他CPU在本指令结束前访问内存。
Perterson解法和TSL/XCHG解法都是正确可行的,他们的本质都是:当一个进程想进入临界区,先检查是否允许进入,若不允许则原地等待,直到允许为止。 但很浪费时间。
本章假设计算机只有一个CPU。
考虑ー台计算机有两个进程,H优先级较高,L优先级较低。调度规则规定,只要H处于就绪态它就可以运行。在某ー时刻,L处于临界区中,此时H变到就绪态,准备运行(例如,一条I/O操作结束)。现在H开始忙等待,但由于当H就绪时L不会被调度,也就无法离开临界区,所以H将永远忙等待下去。这种情况有时被称作优先级反转问题(priority inversion problem)。
改用sleep和wakeup的原语,sleep将进程挂起,wakeup 将进程唤醒
信号量:将检查值、修改值以及可能发生的睡眠操作作为一个单一的、不可分割的原子操作完成,原子性由操作系统保证。在完成前,其他进程不允许访问信号量。
互斥量:信号量的简化版本,不需要计数能力,只需要两种状态:解锁和加锁。一个二进制位就可表示它,不过通常用整型。
futex 是Linux的ー个特性,它实现了基本的锁(很像互斥锁),但避免了陷入内核,除非它真的不得不这样做。 因为来回切换到内核花销很大,所以这样做可观地改善了性能。
ー个futex包含两个部分:ー个内核服务和一个用户库。内核服务提供ー个等待队列,它允许多个进程在ー个锁上等待。它们将不会运行,除非内核明确地対它们解除阻塞。将一个进程放到等待队列需要(代价很大的)系统调用,我们应该避免这种情况。**因此,没有竞争时,futex完全在用户空间工作。特别地,这些进程共享通用的锁变量一个对齐的32位整数锁的专业术语。假设锁初始值为1,即假设这意味着锁是释放状态。**线程通过执行原子操作“减少并检验”来夺取锁(Linux的原子函数包含封装在C语言函数中的内联汇编并定义在头文件中)。
接下来,这个线程检査结果,看锁是否被释放。如果未处于被锁状态,那么一切顺利,我们的线程成功夺取该锁。然而,如果该锁被另ー个线程持有,那么线程必须等待。**这种情况下,futex库不自旋,而是使用ー个系统调用把这个线程放在内核的等待队列上。**可以期望的是,切换到内核的开销已是合乎情理的了,因为无论如何线程被阻塞了。当一个线程使用完该锁,它通过原子操作’增加并检验”来释放锁, 并检査结果,看是否仍有进程阻塞在内核等待队列上。如果有,它会通知内核可以对等待队列里的ー个或多个进程解除阻塞。
如果没有锁竞争,内核则不需要参与其中。
一些其他的同步机制
管程(编译器实现):是编程语言的组成部分,编译器知道他们的特殊性。任意时刻管程中只有一个活跃进程。 管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。管程具有面向对象编程的特点。
屏障: 通常用于进程组,把他们的执行划分了不同阶段,每个阶段末尾设置一个屏障,只有所有进程都到达屏障,才能继续运行下一个阶段。
然而,,某些情况下,我们可以允许写操作来更新数据结构,即便还有其他的进程正在使用它。窍门在于确保毎个读操作要么读取旧的数据版本,要么读取新的数据版本,但绝不能是新旧数据的ー些奇怪组合。
当然,还有一个问题。只要还不能确定没有对B和D更多的读操作,我们就不能真正释放它们。但是应该等待多久呢? 一分钟?或者十分钟?我们不得不等到最后ー个读操作读完这些节点。RCU谨慎地决定读操作持有一个数据结构引用的最大时间。在这段时间之后,就能安全地将内存回收。
多个进程同时竞争CPU,应该选择哪一个进程执行,这就由操作系统的 调度程序完成,它内部实现了调度算法。
进程切换:占用CPU资源的使用者发生了切换。需要保存当前进程在PCB(进程控制块)的执行上下文(寄存器、数据、打开资源文件等),然后恢复下一个进程的上下文。
调度:从就绪进程队列中挑一个进程去CPU上执行。
首先用户态必须切换到内核态,然后要保存当前进程的状态,包括在进程表中存储寄存器值以便以后重新装载。在许多系统中,内存映像(例如,页表内的内存访问位)也必须保存,接着,通过运行调度算法选定一个新进程;之后,应该将新进程的内存映像重新装入MMU,最后新进程开始运行。除此之外,**进程切换还要使整个内存高速缓存失效,强迫缓存从内存中动态重新装入两次(进入内核一次, 离开内核一次)。**总之,如果每秒钟切换进程的次数太多,会耗费大量CPU时间,所以有必要提醒注意。
某些I/O活动可以看作计算。例如,当CPU向视频RAM复制数据以更新屏幕时,因为使用了CPU,所以这是计算,而不是I / O活动。按照这种观点,当ー个进程等待外部设备完成工作而被阻塞时,オ是I/O活动。
按照这种观点,当ー个进程等待外部设备完成工作而被阻塞时,才是I/O活动。
典型的计算密集型进程具有较长时间的CPU集中使用和较小频度的I/O等待。I/O密集型进程具有较短时间的CPU集中使用和频繁的I/O等待。它是 I/O 类的,因为这种进程在I/O请求之间较少进行计算,并不是因为它们有特别长的 I/O 请求。在"I/O 开始后无论处理数据是多还是少,它们都花费同样的时间提出硬件请求读取磁盘块。
有关调度处理的ー个关键问题是何时进行调度决策。存在着需要调度处理的各种情形。
如果硬件时钟提供50Hz、60Hz或其他频率的周期性中断,可以在每个时钟中断或者在每A个时钟中断时做出调度决策。
根据如何处理时钟中断,可以把调度算法分为两类
某些目标取决于环境(批处理、交互式或实时),但是还有一些目标是适用于所有情形的。
运行大量批处理作业的大型计算中心的管理者们为了掌握其系统的工作状态,通常检查三个指标: 吞吐量、周转时间以及CPU利用率。呑吐量(throughout)是系统每小时完成的作业数量。把所有的因素考虑进去之后,每小时完成50个作业好于每小时完成40个作业。周转时间(turnaround time)是指从ー个批处理作业提交时刻开始直到该作业完成时刻为止的统计平均时间。该数据度量了用户要得到输出
所需的平均等待时间。其规则是:小就是好的。
能够使吞吐量最大化的调度算法不一定就有最小的周转时间。例如,对于确定的短作业和长作业的ー个组合,总是运行短作业而不运行长作业的调度程序,可能会获得出色的吞吐性能(每小时大量的短作业),但是其代价是对于长的作业周转时间很差。如果短作业以ー个稳定的速率不断到达,长作业可能根本运行不了,这样平均周转时间是无限长,但是得到了高的呑吐量。
CPU利用率常常用于对批处理系统的度量。尽管这样,CPU利用率并不是ー个好的度量参数。真正有价值的是,系统毎小时可完成多少作业(吞吐量),以及完成作业需要多长时间(周转时间)。
批处理系统中的调度算法:
批处理系统在商业领域仍在广泛应用,用来处理薪水册、存货清单、账目收入、账目支出、利息计算(在银行)、索赔处理(在保险公司)和其他的周期性的作业。在批处理系统中,不会有用户不耐烦地在终端旁等待ー个短请求的快捷响应。因此,非抢占式算法,或对毎个进程都有长时间周期的抢占式算法,通常都是可接受的。
1、先来先服务:按照请求CPU的顺序使用CPU。
2、最短作业优先:谁的运行时间短,谁先执行,好处是每个作业的平均等待时间短。如两个进程,A需要运行20分钟,B两分钟,如果先运行A,则B等待20分钟,总的等待20分钟,平均等待10分钟;如果先运行B,在运行A,则A等待2分钟,总的等待2分钟,平均等待一分钟。
(每天都做类似的工作,可估计)
3、最短剩余时间优先:也是抢占式的,每次找到剩余执行最短的程序执行,给其固定的运行时间,如果到期还没运行完毕,则进入就绪队列等待。
交互式系统的调度算法:
1、轮转调度:也就是大家轮流来,一个进程分配固定时间片,时间到了还没执行完,则移动到就绪队列队尾,下一个进程接着来。
2、优先级调度:优先级高的进程先运行,统一优先级的则按照轮转调度。:
3、多级队列:举个例子,高优先级的进程先运行一个时间片,然后是次高级队列每个进程运行2个时间片,然后再次一级运行四个时间片。每个进程运行一次后,优先级降低一级。
4、最短进程优先
5、保证调度 6、彩票调度 7、公平分享调度
实时系统的调度算法:
硬实时调度:在绝对截止时间前完成。
软实时调度:在某个时间前后完成调度。
调度策略与机制分离:将调度算法以某种形式参数化,具体参数由用户进程写入。调度机制位于内核,调度策略可由用户进程决定。
比如假设内核使用优先级调度算法,并提供了一条可供进程设置(并改变)优先级的系统调用。
应该有一个寄存器包含当前进程表项的指针。当I/O结束时,CPU将把当前的机器状态存入到当前进程表项中。然后,将转到中断设备的中断向量,读取另一个进程表项的指针(服务例程),然后,就可以启动这个进程了。
通常,高级语言不允许访问CPU硬件(不直接操作),而这种访问是必需的。例如,中断处理程序可能需要禁用和启用某个特定设备的中断服务,或者处理进程堆栈区的数据。另外,中断服务例程需要尽快地执行。
要执行信号量操作,操作系统首先禁用中断。 然后它读取信号量的值。 如果它正在执行down并且信号量等于零,则它将调用进程放在与信号量关联的阻塞进程列表中。 如果它正在执行 up,它必须检查信号量是否阻止任何进程。 如果阻止了一个或多个进程,则会从阻塞进程列表中删除其中一个进程并使其可运行。 完成所有这些操作后,可以再次启用中断。