一.指令流与执行
1.1应用程序与指令流
应用程序的内部:
指令流:一个应用程序内部可以由一个或多个逻辑上互相独立执行的指令序列组成。这种独立执行的指令序列叫做指令流。CPU靠执行指令流来完成应用程序的功能。
计算期与I/O期:一个指令流往往由两部分组成:一部分负责计算,另一部分负责I/O。l两个部分交相点缀,组成一整条指令流。指令流是程序逻辑的基本单位。
指令流的分工:多个指令流可以分工合作,完成程序的功能。
按性质分工:不同的指令流处理不同性质的工作,如一些指令流主要负责I/O,另一些指令流主要负责计算等等。
按对象分工:不同的指令流处理不同部分的工作,如每个指令流负责处理一部分数据或一个服务对象。
指令流间的执行顺序:
顺序执行:一个指令流执行完成后,再去执行另一个指令流。
优势:安排工作简单,一件事情做完了再去做另一件事情的话,不可能办得到。
需要打断工作的常见情况:
优先级:某些任务比其他任务更加紧急。
进行I/O:I/O设备的速度是很慢的。一旦开始I/O,当前指令流就不占用CPU了。即使继续运行,也只能反复查询I/O设备的状态寄存器判断I/O设备是否完成。
资源超限:该指令流占用太多CPU,达到限额,公平问题。
恶意程序:某个指令流的唯一工作就是空转。
相互通信:某些指令流可能依赖其他指令流传递给它的数据才能工作。
1.2顺序与并发执行
改进的顺序执行:
合作执行:将每个指令流打断成多份,每一份之内都顺序执行,但背靠背执行的两份不一定来自同一个指令流。在每份指令流的末尾,都通知操作系统主动放弃CPU,CPU将转去执行下一份指令流。
交替执行:指令流在自己执行的途中就可以出让CPU的控制权。
自愿放弃:指令流在不需要CPU的时候可以自愿放弃CPU。自愿放弃CPU以及执行完毕是交还CPU控制权的唯二方法。
顺序确定;如果希望,每个指令流都可以在放弃CPU时指明自己希望哪个指令流继承CPU的使用权。当然,如果不指定,那么随机计算则一个指令流来执行。
合作执行解决了上述常见问题中的哪些?
优先级:解决了一部分,现在进击的指令流可以在个与个之间插入了。但是没法在份与份之间插入。
进行I/O:仅考虑CPU效率,完全解决,指令流只要进入I/O就放弃CPU。
资源超限:应用程序的某一段一直不放弃CPU的话,还是拿它没办法。
恶意程序:挖矿程序肯定不会放弃CPU。
互相通信:完全解决,指令流只要在等待起他指令流的恢复就会放弃CPU。
顺序执行和合作执行的问题:
缺乏强制性:无法强制剥夺指令流的CPU控制权。如果CPU是恶意的,或者自私的,就没法对它加以约束。
并发执行:将合作执行的条件放宽一点,允许一个指令流在任何时候被打断,并且新的指令流插入进来。又叫抢占式执行。每个指令流都在自己的虚拟CPU上执行,而且虚拟CPU的先后没法预测。
细粒度:每个指令流的每条指令之间都可能被打断。
不可预测性:在任何一个指令流被打断后,该指令流无法预测下一个接替它执行的是哪个指令流。
1.3指令流的描述
指令流的描述:在并发执行中,指令流可能随时被打断。被打断的指令流的状态信息不能丢失。这些信息包括两部分,一部分是指令流自己的上下文,另一部分是该指令流对CPU的占用状态。
上下文:一个指令流的上下文就是足够使其恢复被打断时的状态的内容。这一版包括了其寄存器组和执行栈。在切换指令流时,一个非常方便的做法就是把寄存器组保护在执行栈上,合适时再恢复。
状态:一个指令流可以按照它对CPU的占用状态分为运行和不运行两种状态。不运行的原因可能因为就绪:当前指令流可以运行,但CPU因为某些原因还没轮到它,轮到它就可以立马运行。也可能是因为阻塞:当前指令流在等待,就算由空闲CPU也没法运行。
二.线程与处理器
2.1CPU的时间与线程
指令流与时间:操作系统需要给指令分配CPU时间,然后让指令流拿着这个CPU时间配额去运行。但是,指令流是用户程序的逻辑组成成分,操作系统并不知道用户程序里面有几条指令流。
线程:操作系统提供给应用程序的一种对CPU时间的抽象机制。它是CPU时间分配的基本对象。应用程序通过将自己的指令流与线程对应起来,使指令流获得CPU时间分配。操作系统通过运行线程,来运行依附在这个线程上的指令流。也可以说,指令流通过依附于线程,获得了在CPU上运行的权利。
应用程序视角:在应用程序看来,自己的逻辑组织是一系列并发执行的指令流。
操作系统视角:在操作系统看来,应用程序的运行组织是一系列被分配了CPU时间的线程。
2.2线程的描述
时间预算:线城市时间分配的基本对象,那县城必然有一个参数描述它被分配了多少时间。这个数值称为时间预算,可以是一个有限的数值,也可以是一个无限的数值。
优先级:优先级一般用一个数值表示,在绝大多数操作系统中,数字越小,优先级越高。
线程的上下文:和指令流上下文一样,线程的上下文也是其寄存器组。
内核阻塞:在遇到线程上的指令流陷入内核阻塞的时候,内核智能先暂停执行当前这个线程,切换到别的线程去执行。更麻烦的是,线程什么时候陷入内核,依附在线程上的指令是不知道的,也即可能发生抢占。一旦一个线程阻塞在内核,对它上面的所有指令来说,时间就都凝固了。因此,这些指令都停止运行。
线程控制块(TCB):操作系统用以描述和管理线程的内核对象,一般至少包含线程的时间预算,优先级,运行状态及上下文,有时还会半酣一些身份信息(线程名,线程号)或统计信息(总计CPU时间)等。它在数据结构上一般是C语言的一个结构体。
2.3迁程与协程
线程与指令流:一对一
优点:简单。最简单,最好实现,最常见,常见到足以让人把指令流和线程的概念混淆起来。
缺点:每个指令流都成了单独分配时间的对象,很多时候不需要这样。这样会增加操作系统的负担,因为操作系统需要为每个线程创建一些单独的管理数据,而且每次切换当前CPU上的指令流都需要通知操作系统。
对应关系:此时指令流的状态和线程的状态是一对一对应的。线程处于什么状态,指令流就处于什么状态。
指令流与线程:多对一
优点:高效。同一个线程中的多个指令流可以借由附着在同一个线程上共享一份执行时间,它们在内核中也被当作一个对象来处理,其TCB仅仅创建一份。对于每一个线程上附着的多个就绪指令流,应用程序负责决定哪个指令流得到线程从而运行,并切换到它。
通常而言,只有紧密协作的指令流才会被放在同一个线程上,而且操作系统并不需要知道指令流的存在,因此仍然可以在那些一对一的操作系统中实现。
对应关系:线程处于运行状态,说明其上的指令流中有一个在运行。线程处于就绪状态,说明其上的指令流中至少存在一个就绪的。线程处于阻塞状态,说明其上的指令流中至少一个进入了需要操作系统介入的阻塞态。 其它指令流状态无法预测,因为只有应用程序才知道它们。操作系统是不 知道它们的,因为操作系统感知的只是作为时间分配对象的线程的状态。线程阻塞了,意味着内核只知道这个时间分配对象上附着的某个指令流在请求内核完成一个一时半会无法完成的功能,因此内核能做的就是暂停整个时间分配对象的执行。
原因:一旦指令流阻塞在内核(阻塞在应用程序中则无此问题,因为应用程序 可以自主选择一个新的指令流来使用这个线程),就会阻塞整个线程。而如果线程上还有能运行的指令流,这些指令流也停止运行。我们自然不希望这样;然而,每个线程仅提供一个TCB,当前阻塞在内核的指令流的上下文已经占用这个TCB里面的寄存器组了。 、
优点:那么,我们可以考虑给这些指令流提供多于一个上下文,也就是让多个指令流对应多个线程,任何一个不阻塞的线程都可以运行任何一个不阻 塞的指令流,指令流可以在线程之间迁移。 缺点:臭名昭著地难以正确实现。内部细节很多很复杂,这就是灵活的代价。
纤程与协程:一些额外概念
协程:合作执行的一组指令流。不仅强调它们不是时间分配的独立对象(区别于线程),而且强调只有其中某个指令流主动放弃CPU时,其它指令流 才可得到CPU进行运行,并且放弃CPU的那个指令流还倾向于指定谁来接替它的执行。
实现:一般地,协程被实现为在用户模式下的一系列数据结构,这些数据结构中会保存协程的寄存器上下文。在常见的实现中协程对线程都是多对一结构,因此在用户模式就可以完成互相切换,无需通知内核,切换的效率远高于线程。特别地,对于无栈协程(也即那些不使用栈,或者在放弃CPU时能保证栈内不存在有效数据的情况下),它们可以用C语言的 #define配合switch-case实现。
纤程:合作执行的一组指令流。相比于协程,放弃CPU的那个指令流不倾向于直接指定接替执行者,而倾向于唤起一个在用户空间的调度器,由它来决定下一个执行的指令流是谁。纤程之间不一定有紧密的合作关系,仅仅是强调它们比线程要轻量,也即多个纤程共享一个线程。 比较同多于异,几乎是同义词,都是指令流,只不过侧重点不同。
三.处理器调度算法
3.1调度算法原理
问题:现在有一系列线程在系统中运行。操作系统中的调度器负责决定每次运行哪个线程,运行多久,但又如何决定做这个决定所需的每个线程的优先级和时间预算?
解决方案一:事先指定。对于某些系统,我们可以根据某线程负责运行的指令流的性质,将某些线程的优先级设置得高一些、时间预算设置的多一些,以体现资源分配的倾向性。整个系统只要按照这种设置来运行就足够完成其功能了。
优点:对任务知根知底,且任务固定时,这就是最好选择。
缺点:这要求操作系统知晓整个计算机体系内的应用程序的性质,基本上要求应用程序都是同一个团队开发的。更精确地讲,需要额外信息来进行手动干预。
用途:对于那些功能简单,但要求绝对可靠的系统,这是一个非常好的办法。它保证了某些指令流具有绝对的资源优势,确保那些指令流承担的工作总能按时正确完成。
例子:刹车;火箭;飞机;导弹
固定优先级调度算法:
公平:同等的条件应当得到同等的对待。同等的程序应当得到同等的 CPU。强调完全的同一性。 正义:将各个应用程序按照(用户认为的)它们应当得到的对待来进行对待。对用户体现出不同性质的程序应当得到不同但合适的CPU。强调合理的差别性。