理解操作系统------(2)进程原理

        计算机是用来进行计算的,或者说计算机的基本功能是计算功能。而进行计算的关键部件是计算机的芯片,即CPU。CPU能够按照一定的顺序进行正确计算是在一个指挥者的控制之下完成的。这个指挥者就是操作系统。操作系统对CPU进行管理的重要手段就是进程模型。
        进程是操作系统演化过程中的一个里程碑,由于进程的出现,人类希望的并发从理想变为了现实。从根本上说,进程出现的动机是人类渴望的并发。进程的出现也让操作系统的复杂性大为增加:由于需要对进程进行分离存储而导致出现内存管理;由于需要让不同进程有条不紊地往前推进而导致进程调度的出现。显然,理解进程对理解操作系统十分重要,对其进行管理也就理所当然地成为操作系统的一个关键职责。

1. 进程

        当人们面临困境时通常的做法就是:发明新的概念、新的术语或新的机制来解脱困境。

1.1 进程概论

        进程管理、内存管理和文件管理是操作系统的三大核心功能。那么什么是进程呢?顾名思义,进程就是进展中的程序,或者说进程是执行中的程序。就是说,一个程序加载到内存后就变为进程。即:

进程=程序+执行
        单一操作员单一控制终端、批处理均存在效率低下的问题,即CPU使用率不高。为了提高CPU利用率,人们想起将多个程序同时加载到计算机里,并发执行。这些同时存在于计算机内存的程序就称为进程。进程让每个用户感觉到自己独占CPU。因此,进程就是为了在CPU上实现多道编程而出现的概念,如下图所示

进程让每个用户感觉到自己独占CPU

1.2 进程模型

        那么进程到底是什么呢?什么是进展中的程序呢?从物理内存的分配来看,每个进程占用一片内存空间,从这点上说,进程就是内存的某片空间。由于在任意时刻,CPU只能执行一条指令,因此任意时刻在CPU上执行的进程只有一个,而到底执行哪条指令由物理程序计数器指定。也就是说,在物理层面上,所有进程共用一个程序计数器。
        而从逻辑层面上来看,每个进程可以执行,也可以暂时挂起让别的进程执行,之后又可以接着执行。这样,进程就需要某种办法记住每次挂起时自己所处的执行位置,这样才能在下次接着执行时从正确的地点开始。因此,从这个角度看,每个进程有着自己的计数器,记录其下一条指令所在的位置。从逻辑上说,程序计数器可以有很多个。
        而从时间上看,每个进程都必须往前推进。在运行一定的时间后,进程都应该完成了一定的工作量,即每次进程返回,它都处在上次返回点之后。这就像古希腊哲学家赫拉克里特说过的:“一个人不能两次踏入同一条河流。”进程的这3种概念可由下图所示。
在这里插入图片描述        这里需要注意的是,进程不一定必须终结。事实上,许多系统进程(用来为别的进程提供系统服务的进程)是不会终结的,除非强制终止或计算机关机。
进程模型的实现
        对于操作系统来说,进程是其提供的一种抽象,目的是通过并发来提高系统利用率,同时还能缩短系统响应时间。这种抽象听上去很不错。但这种抽象是如何实现的呢?或者说,操作系统如何实现进程呢?
        首先,任何抽象都需要有一个物理基础。对于进程来说,其物理基础就是程序。程序运行在计算机上,而在计算机上运行首先需要解决的问题是进程的存储:给进程分配合适的内存,让其有一个安身之处。由于多个进程可能同时并存,因此进程的存储需要考虑如何让多个进程共享同一个物理内存而不发生冲突。操作系统解决这个问题的手段是内存管理。
        此外,进程运行实际上是指进程在CPU上执行。那么如何将CPU在多个进程之间进行交接或切换,这就是进程实现需要解决的另一个问题。操作系统解决这个问题的手段就是进程调度:决定在什么时候让什么进程使用CPU。即进程调度,后续会看到。

1.3 多道编程的好处

        进行多道编程的目的则是提高计算机CPU的效率,或者说系统的吞吐量。
多道编程度数和CPU利用率的关系(I/O时间为80%)        这里需要注意的是,上图图描述的是一种非常简单的模型:所有的进程都进行同样的I/O工作量(80%),且进程切换时间忽略不计。随着度的增加,CPU利用率增加,直到一个临界点。
响应时间改善
        除了提高CPU利用率外,多道编程更大的好处是改善系统响应时间,即用户等待时间。毕竟,对于大多数用户来说,关心的是计算机的响应速度,至于CPU利用率到底有多高,除了系统管理员或者某些有偏执狂的人外,一般人是不会在意的。
        当然了,多道编程带来的好处到底有多少与每个程序的性质、多道编程的度数、进程切换消耗等均有关系。但一般来说,只要度数适当,多道编程总是利大于弊。

1.4 进程的产生与消亡

来说,主要的事件有:

  • 系统初始化(神创造人)。
  • 执行进程创立程序(人生子)。
  • 用户请求创立新进程(试管婴儿)。

        在一个系统初始化时,将有许多进程产生。产生的这些进程是系统正常运行必不可少的。这些进程的存在使得新的进程和用户程序的执行成为可能。
        在系统初始化后,系统就等待用户输入命令。如果这个用户启动一个程序,如双击一个可执行文件,那么系统将为这个可执行文件创建一个进程。除此之外,用户也可以在程序里面通过系统调用如fork或者CreateProcess直接生成新的进程。
        造成进程消亡的事件则可以分为四种情况:
寿终:进程运行完成而退出。
自杀:进程因错误而自行退出。
他杀:进程被其他进程所终止。
处决:进程因异常而被强行终结。
        前两种情况均为自愿退出,后两种情况均为非自愿退出。在程序设计时我们追求的是前面两种退出,也算是我们在虚拟世界里面追求人权(进程权)的努力吧。第3种情况通常是一个父进程发出命令终止一个子进程。当然,一个用户也可以“杀死”自己的进程,但不能“杀死”别人的进程。但一个超级用户(具有系统管理员权限)则可以“杀死”任何进程。第4种情况在一个进程进行某种非法操作,如访问出界或者除以0后发生,而这种非法操作将被操作系统捕捉。操作系统捕捉到这种异常后将终结造成异常的进程。

1.5 进程的层次结构

        一个进程在执行过程中可以通过系统调用创建新的进程。这个新创建的进程就称为子进程,而创建子进程的进程则称为父进程。子进程又可以再创建子进程,这样子子孙孙创建下去就形成了所谓的进程树。UNIX称这个进程树里面的所有进程为一个进程组,进程组里面的进程分布在不同的层次上,从而形成一个层次架构。
        Windows没有进程组的概念,而是所有进程均地位平等。

1.6 进程的状态

        进程可以在CPU上执行,也可以处于挂起状态。显然,一个进程至少有这么两种状态。那么进程还有别的状态吗?
        如果进程在CPU上执行,自然就是执行状态。而如果是挂起状态呢?那就得看是什么原因挂起的。因为操作系统在进行进程调度时要从挂起的进程中选择一个来执行,所以清楚一个进程挂起的原因对调度的有效推进十分重要。
        那么进程挂起有哪些原因呢?首先是一个进程在运行过程中执行了某种阻塞操作,如读写磁盘。由于阻塞操作需要等待结果后才能继续执行,因此操作系统将把这个进程挂起,让其他进程运转。另外一种情况是一个进程执行的时间太长了,为了公平,操作系统将其挂起,让其他进程也有机会执行
        这两种挂起的原因十分不同:第一种挂起是进程自身的原因。这个时候,即使把CPU控制权交给它,它也无法运行。第二种挂起是操作系统的原因。进程自己并无问题。只要把CPU交给进程,它就可以立即运行。这样,如果将挂起进程分为这样两类,操作系统在进程调度时就只需要查看第二类进程,而无须浪费时间查看第一类进程。
        因此,进程分为3种状态:执行、阻塞和就绪。如下图所示:
在这里插入图片描述        在3种状态之间可以进行各种转换。如果每个状态都可以转换为另外一种状态,则一共有6种转换:

  • 执行→就绪
  • 执行→阻塞
  • 阻塞→就绪
  • 就绪→执行
  • 阻塞→执行 (没有这种状态)
  • 就绪→阻塞 (这种状态也没有)

        一个进程在执行时,因为运行时间太长,操作系统可以将其挂起,转换为就绪状态,因此第1种转换是可以的。在进程执行过程中如果执行了某种阻塞操作,则进入阻塞状态,因此第2种转换也是可以的。一个阻塞的进程在其等待的资源到达后,就可以随时执行,进入就绪状态,因此第3种转换也是可以的。最后,就绪进程由操作系统调度到CPU上就进入执行状态,因此第4种转换也是可以的。
        但是第5、第6两种转换是不可以的。我们前面讲过,阻塞进程即使被给予CPU,也无法执行,因此操作系统在调度时并不会在阻塞队列里挑选。因此,阻塞状态无法转换为执行状态。对于处于就绪状态的进程来说,因为它并没有执行,自然无法进入阻塞状态。这就像一个人停滞不前,自然就不会有任何人成为其障碍。因此,就绪状态无法转换为阻塞状态。
        这里需要注意的是,第5、第6两种转换虽然都不存在,但其原因不同。第5种转换不可以是因为我们不让它发生。如果我们乐意,完全可以让操作系统在阻塞队列里挑选一个进程予以执行,只不过这个进程在执行第1条指令时就会又发生阻塞(因为其等待的数据尚不可用或者发生异常)。因此,从理论上说,阻塞到执行是可以的,只不过这种状态转换没有任何实际价值而被操作系统禁止。而第6种状态转换则在理论上是不可以的。一个进程只能在执行时才可能阻塞,没有执行的进程无法直接转换到阻塞状态。
        这里阐述的进程的3种典型状态并不是唯一的分类方式。事实上,许多商业操作系统的进程状态不止3个,例如,Windows的进程有7个状态。其目的都是便于操作系统管理进程。只要细分对管理有利,我们就细分。否则就维持3个状态。

1.7 进程与地址空间

        进程空间也称为地址空间。简单来说,地址空间就是进程要用的所有资源。于是所有资源就构成了状态的划分。由于不可能有两个进程状态完全一样,因此每个进程对应计算机的一种状态,而计算机状态就是所有存储单元的内容。
        地址空间的特点就是被动,自己不能做什么,只提供支持。打个比方。看过演出吗?如话剧、芭蕾、歌剧等。京剧总看过吧?有个舞台,那些道具和舞台就是地址空间。这些空间本身不能发生任何动作。做动作的只能是演员。而那些演员就是我们将要讲述的线程。跳上来一个演员就是一个线程,
        进程与地址空间研究的主要内容是如何让多个进程空间共享一个物理内存。具体来说,就是高效、安全地让所有进程共享这片物理内存。

1.8 进程管理

        那么谁管理进程的资源?是操作系统。
        那么如何掌控呢?操作系统要掌控一切状态,就必须拥有某些手段或资源。那需要什么手段或资源呢?如果让你监视一群人,要你掌握他们的一切情况,你第一件要做的事是什么?装监视器?不是!而是要知道这群人到底是哪些人!即你需要知道并维持关于这群人的各种信息!
1.8.1 进程管理所需要的手段
        操作系统要管理进程就要维护关于进程的一些信息。当一个进程产生时,操作系统也需要为其创建记录。操作系统用于维护进程记录的结构就是进程表或进程控制块(Process Control Block,PCB)。这个进程表或PCB中存放的就是有关该进程的资料。
在这里插入图片描述1.8.2 进程的创建过程
        对于一个计算机系统来说,不断创建和终结进程,创建进程和终结进程各有4种方法。但操作系统是如何创建一个进程的呢?一般来说,创建进程的步骤如下所示:

  1. 分配进程控制块。
  2. 初始化机器寄存器。
  3. 初始化页表。
  4. 将程序代码从磁盘读进内存。
  5. 将处理器状态设置为“用户态”。
  6. 跳转到程序的起始地址(设置程序计数器)。
            这里一个最大的问题是,跳转指令是内核态指令,而在第5步时处理器状态已经被设置为用户态,但在用户态下是不能执行内核态指令的。这个问题如何解决?当然了,这就需要硬件帮忙了。硬件必须将第5和第6两步作为一个步骤一起完成。
            进程创建在不同的操作系统里方法也不一样。例如,UNIX将进程创建分为两个步骤:第1步是fork,创建一个与自己完全一样的新进程;第2步是exec,将新的进程的地址空间用另一个程序的内容覆盖,然后跳转到新程序的起始地址,从而完成新程序的启动。而Windows使用一个系统调用就可以完成进程创建。这个系统调用就是CreateProcess。在调用该函数时我们把欲执行的程序名称作为参数传过来,创建新的页表,而不需要复制别的进程。
            UNIX和Windows的进程创建过程各有优缺点。UNIX的创建过程要灵活一些,因为我们既可以自我复制,也可以启动新的程序。而自我复制在很多情况下是很有用的。例如,Web服务器在每收到一个用户请求后,就创建一个新的一模一样的进程来服务用户请求。而在Windows下,复制自我就要复杂一些了。而且,共享数据只能通过参数传递来实现。(Windows不好,linux无敌)
    1.8.3 进程管理要处理的问题
    进程管理的最大问题是资源分配。那么如何分配资源呢?
    除了公平之外,还有一个问题要考虑:效率,也就是最优。每个进程分配同样的资源肯定不行。以前32个终端连到一台计算机上,慢得不行,结果没有任何人高兴。不如让部分人先富起来,给他们使用资源的优先权。
    这样,公平与效率就成了进程管理中永恒的主题。到底是公平重要,还是效率重要?天平的不同倾斜将引出十分不同的进程管理模式。
1.9 进程的缺陷

        看上去,进程的抽象或模型似乎很好:既提高了系统利用率,又缩短了系统响应时间。它通过支持多道编程,让我们感觉每个人都拥有自己的CPU和其他资源,似乎皆大欢喜。
        难道进程模型就没有任何问题了吗?当然不是。
        如果仔细观察,就会发现,进程有个很严重的问题。假定现在有两部很好的电影,都只放映一次,以后再也不放映了。而且,这两部电影同时放映,当然了,是在不同的两个房间放映。而你很想将这两部电影都看了,有什么办法吗?假定没有光碟刻录机也没有录像机等。
        当然,我们没有办法同时看两部电影。这也是进程的缺点。它只能在一个时间做一件事情。如果想同时做两件或多件事情,进程就不够用了。
        另外,更为重要的是,如果进程在执行的过程中阻塞,例如等待输入,整个进程就将挂起(暂停),而无法继续执行。这样,即使进程里面有部分工作不依赖于输入数据,也无法推进。
        而为了解决上述两个问题,人们就发明了线程。
        见下一篇博客。理解操作系统------(3)线程原理

2. 进程调度
2.1 进程调度的定义

        在多进程并发的环境里,虽然从概念上看,有多个进程在同时执行,但在单个CPU下,实际上在任何时刻只能有一个进程处于执行状态。而其他进程则处于非执行状态。那么这就有一个需要解决的问题:我们是如何确定在任意时刻到底由哪个进程执行,哪些不执行呢?或者说,我们是如何进行进程调度的呢?
        进程的调度的任务是选择下一个要运行的进程。那么如何进行选择呢?要探明这一点,首先需要确定操作系统进程调度的目标是什么。有了目标,我们就知道选择什么进程最合适了。
        那么操作系统进程调度的目标是什么呢?这需要对进程使用CPU的模式进行分析。那么进程在执行时有什么样的模式呢?
        一般来说,程序使用CPU的模式有3种:一种是程序大部分时间在CPU上执行;另一种是程序大部分时间在进行输入输出;还有一种是程序介于前两种模式之间。
        第1种程序运行的模式是在CPU上执行较长时间,接着进行短暂的输入,然后又在CPU上进行较长的运算,之后又进行短暂的输入输出操作,就这样循环往复。这种程序由于使用CPU的时间远远长于其用于输入输出上的时间,因此称为CPU导向(CPU-bound)或计算密集型程序。计算密集型程序通常是科学计算方面的程序。计算宇宙大爆炸各种参数的程序、矩阵乘法程序等就都是CPU导向的程序。
        第2种程序则与第1种相反,这种程序的大部分时间用来I/O,每次I/O后进行短暂的CPU执行,因此称为I/O导向(I/O-bound)或输入输出密集型程序。一般来说,人机交互式程序均属于这类程序。如游戏程序以及讲课时使用的PPT程序,都属于I/O导向的程序。
        第3种程序自然介乎二者之间,既有长时间的CPU执行部分,又有长时间的I/O部分。或者说,这种程序使用CPU和I/O的时间相差不大。这种程序称为平衡型程序。例如,网络浏览或下载、网络视频等就属于此类程序。
        自然,对于不同性质的程序,调度所要达到的目的也有所不同。例如,对于I/O导向的程序来说,响应时间非常重要;而对于CPU导向的程序来说,周转时间(turnaround)就比较重要;对于平衡型程序来说,进行某种响应和周转之间的平衡就显得重要。

2.2 进程调度的目标

        CPU调度就是要达到极小化平均响应时间、极大化系统吞吐率、保持系统各个功能部件均处于繁忙状态和提供某种貌似公平的机制。
        极小化平均响应时间就是要极小化用户发出命令和看到某种结果之间所花费的时间,即减少做一件工作平均等待的时间;极大化系统吞吐率就是要在单位时间内完成尽可能多的程序,就是单位时间内能完成的工作数量,即整个系统运行效率高;保持系统各个功能部件繁忙就是要让CPU和输入输出设备均处于忙碌状态。由于CPU非常昂贵,让其闲置显然是一种浪费,因此保持CPU繁忙十分重要。就像生命非常珍贵,因此要一直保持学习繁忙状态,才能不浪费生命。
        提供公平就是要让各个程序感到某种“平等”,即在CPU面前“人人平等”。公平是任何系统都应该努力达到的目标。因为没有公平,该系统对用户的吸引力就会急剧下降。
        对于交互式系统来说,由于用户在等待计算机,因此响应时间要很快。但在这里要注意的是适度性(proportionality)。适度性就是响应时间要和期望值相匹配。这里是说你不要超越用户的期望。比如,用户期待1秒钟的响应时间,你就给他1秒钟的响应时间,而不必提供0.1秒钟的响应时间。这是因为,提供超出用户期望的响应会增加系统设计的难度,而又不会提高用户的满意度(对于一个人来说,1秒钟和0.1秒钟的差别并不是很大)。
        对于实时系统来说,调度就是要达到在截止时间前完成所应该完成的任务和提供性能可预测性。

2.3 先来先服务算法

        先来先服务调度算法缩写为FCFS(First Come First Serve)。谁先来,就先服务谁。
先来先到的一个隐含条件就是不能抢占,一个程序一旦启动就一直运行到结束或者受阻塞为止。这是因为一旦允许抢占,就破坏了先来先到的原则。先来先到的优点就是简单,人人都能理解,实现起来容易。而缺点则是短的工作有可能变得很慢,因为其前面有很长的工作。这样就造成用户的交互式体验也比较差。

2.4 时间片轮转算法

        时间片轮转算法是对FCFS算法的一种改进,其主要目的是改善短程序的响应时间。其方法就是周期性地进行进程切换。
        如果选择的时间片过大,时间片轮转将越来越像FCFS,当选择的时间片超过任何一个程序所需要的执行时间长度时,则完全退化为FCFS。而如果选择的时间片过小,则进程切换所用的系统消耗将太多,使得系统的大部分时间花在进程的上下文切换上,而用来真正执行程序的有用时间很少,从而降低系统效率,并造成浪费。

2.5 短任务优先算法

        短任务优先(Shorted time to Completion First,STCF)算法。这种算法的核心是所有的程序并不都一样,而是有优先级的区分。具体来说,就是短任务的优先级比长任务的高,而我们总是安排优先级高的程序先运行。就像晚辈在公交汽车上见到长辈需要让座一样。
        短任务优先算法有两个变种:一种是非抢占,一种是抢占。非抢占短任务优先算法的原理是让已经在CPU上运行的程序执行到结束或阻塞,然后在所有候选的程序中选择需要执行时间最短的进程来执行。抢占式短任务优先算法则是每增加一个新的进程就需要对所有进程(包括正在CPU上运行的进程)进行检查,谁的时间短,就运行谁。
        显然,由于短任务优先总是运行需要执行时间最短的程序,因此其系统平均响应时间在目前已经讨论过的几种调度算法里面是最优的。这就是STCF算法的优点。事实上,在所有非抢占调度算法中,STCF算法的响应时间最优。而在所有抢占调度算法中,抢占式STCF算法的响应时间最优。
        STCF调度算法也有缺点。第一是可能造成长程序无法得到CPU时间而导致“饥饿”。除此之外,还有一个重大缺点,就是我们怎么知道每个进程还需要运转多久?难道我们能够预测将来不成?就好像你第一次做某种事情时怎么知道需要多长时间呢?
        这个时候就需要做研究!我们可以用一些启发式(heuristic)方法来进行估算,例如,根据程序大小来推测一个程序所需CPU执行时间。但这个方法并不可靠。另外一个办法就是先将每个程序运行一遍,记录其所用CPU时间,这样在以后的运行中,即可根据这个实测数据来进行STCF调度了。

2.6 优先级调度算法

        前面介绍的STCF算法有一个缺点是可能造成长进程“饥饿”。但这个问题比较容易解决,使用优先级即可。优先级调度算法的原理是给每个进程赋予一个优先级,每次需要进程切换时,找一个优先级最高的进程进行调度。这样,如果赋予长进程一个高优先级,则该进程就不会再“饥饿”。事实上,STCF算法本身就是一种优先级调度,只不过它给予短进程高优先级而已。
        优先级调度的优点是可以赋予重要的进程以高优先级以确保重要任务能够得到CPU时间。其缺点则与STCF算法一样,低优先级的进程可能会“饥饿”。不过,这个问题在优先级调度算法里比在STCF里好解决:只要动态地调节优先级即可。例如,在一个进程执行特定CPU时间后将其优先级降低一个级别,或者将处于等待进程的优先级提高一个级别。这样,一个进程如果等待时间很长,其优先级将因持续提升而超越其他进程的优先级,从而得到CPU时间。这样,“饥饿”现象就可以防止。
        不过,优先级调度还有一个缺点,就是响应时间不能保证,除非将一个进程的优先级设置为最高。即使将优先级设置为最高,但如果每个人都将自己进程的优先级设为最高,则响应时间还是无法保证。

2.7 混合调度算法

        之前介绍的所有算法都存在缺点,我们自然想设计一个算法合并它们的优点,摒弃它们的缺点。这就是所谓的混合调度算法。该算法的原理是将所有进程分成不同的大类,每个大类为一个优先级。如果两个进程处于不同的大类,则处于高优先级大类的进程优先执行;如果两个进程处于同一个大类,则采用时间片轮转来执行。混合调度算法的示意图如图所示。
在这里插入图片描述

2.8 实时调度算法

        实时系统是一种必须提供时序可预测性的系统。由于其应用范围广和特性不同于一般计算机系统,因此其调度算法也别出一格,和前面讲过的所有调度算法均有所不同。前面的算法主要考虑的是平均响应时间和系统吞吐率的问题,而实时系统则必须考虑每个具体任务的响应时间必须符合要求,即每个任务必须在什么时间之前完成,而无须考虑如何降低整个系统的响应时间或吞吐率。
        比如,计算来袭导弹轨迹的进程,其计算时间是非常有限的。如果该进程不能在规定时间内计算出来袭导弹的轨迹,则结果毫无意义。但如果能够在截止时间前完成,那么提前多少则无关紧要。这就是说,只要达到一定响应时间后,再提升响应时间并不能获得任何好处。比如,视频输出,在NTSC制式下(美国、日本、中国台湾使用的电视视频制式),只要每33毫秒发出一个图像帧,所看到的视频就是连贯的。而即使发送得比这更快,也不会获得任何额外的好处。其他实时系统还有物理控制系统(如核反应堆控制温度的系统、汽车测速机制等)。
        论述一下其最主要或者说最经典的两种算法:动态优先调度和静态优先级调度。动态优先级调度又称为最早截止任务优先(EarliestDeadline First,EDF)算法,而静态优先级调度又称为最短周期优先(Rate MonotonicScheduling,RMS)算法。
EDF调度算法
        EDF调度算法就是最早截止的任务先做。如果新的工作来了,比正在运行的程序的截止时间更靠前,那么就抢占当前进程。EDF调度算法是实时调度里面的最优算法。如果一组任务可以被调度的话(指所有任务的截止时间在理论上能够满足),则EDF可以满足。一批任务如果不能全部满足,那么EDF能满足的任务数最多。这就是它最优的体现。
        例如,任务A需要15毫秒执行时间,截止时间在进入到系统后第20毫秒,B需要执行10毫秒,截止时间为进入系统后第30毫秒,C需要5毫秒执行时间,截止时间为进入到系统后第10毫秒。使用EDF调度算法的结果就是先运行C,再运行A,最后运行B。如图所示。
在这里插入图片描述        EDF调算算法就是STCF算法变化来的。如果将STCF算法的任务所需执行时间变为截止时间,则抢占式STCF算法就是EDF调度算法。

RMS调度算法
        EDF算法是一种动态调度算法。意思是该算法动态地计算每个任务的截止时间并动态调节优先级。如果需要,还会对当前进程进行抢占。虽然EDF算法在理论上是最优的,但动态计算截止时间和动态抢占CPU均要消耗系统资源,因此EDF算法实际效果比其理论效果要差一截。与EDF算法相对的是所谓的RMS调度算法。该算法在进行调度前先计算出所有任务的优先级,然后按照计算出来的优先级进行调度,任务执行中间既不接收新的进程,也不进行优先级的调整或进行CPU抢占。因此这种算法的优点是系统消耗小,缺点是不灵活。一旦该系统的任务决定了,就不能再接收新的任务。
        对于RMS算法来说,一个重要的任务是判断一个任务组能否调度。而这个判断并不是容易做的。Liu和Kayland在1973年证明了如果一个系统里所有任务的CPU的利用率低于ln 2,则这些任务的截止时间均可以得到满足。具体来说,一个系统里所有任务的截止时间如果想都得到满足,则这些任务必须满足下面的条件:
在这里插入图片描述
        这里,n为任务的数量,ci为第i个任务的执行时间,pi为第i个任务的释放周期。而当n趋向无穷时,U的值变为ln2。
根据上述公式,如果CPU利用率在ln2以下时,所有任务的截止时间均可满足。因为ln2约等于0.693147,此时系统还剩下约30%的CPU时间。这个时间可以用来处理一些非实时任务。
RMS算法为静态最优算法。即如果任何静态优先级算法可以满足一组任务的截止时间,则RMS算法也必能满足。

2.9 进程调度的过程

        前面的内容讨论了进程调度的含义及各种调度算法,但调度一个进程对于操作来说意味着什么却并没有解释。那么在更换进程的时候到底有哪些操作需要完成呢?首先,当然需要将当前进程的状态予以保护,以便将来能够重新执行。然后是将选中的进程的环境布置好,这包括设置寄存器、栈指针、状态字等操作。最后是跳转到选中的进程,也就是设置或恢复其程序计数器。下面给出的是调度进程时操作系统所执行的操作概览:

  • 因时序或外部中断或进程挂起而导致操作系统获得CPU控制权。
  • 操作系统在所有就绪的进程中按照某种算法遴选进程。
  • 如果选中的是非当前进程,则操作系统将当前进程(中断或挂起的进程)状态予以保护。
  • 将选中的进程的环境布置好(设置寄存器、栈指针、状态字等)。
  • 跳转到选中的进程。
高级议题:调度异常之优先级倒挂

        优先级倒挂(priority inversion)将在第8章介绍,在这里仅简单介绍。
        其所指的是一个低优先级任务持有一个被高优先级认为所需要的共享资源。这样高优先级任务因资源缺乏而处于受阻状态,一直到低优先级任务释放资源为止。这样实际上造成了这两个任务的优先级倒挂。如果此时有其他优先级介于二者之间的任务,并且其不需要这个共享资源,则该中级优先级的进程将获得CPU控制,从而超越这两个任务,导致高优先级进程被临界区外的低优先级进程阻塞。
        在某些时候,优先级倒挂并不会造成损害。高优先级任务的延迟并不会注意到。因为低优先级进程最终会释放资源。但在其他一些时候,优先级倒挂则可能引起严重后果。如果一个高优先级进程一直不能获得资源,有可能造成系统故障,或激发事先定义的纠正措施,如系统复位。例如,美国的火星探测器Mars Path-finder就是因为优先级倒挂而出现故障。
        如果高优先级进程在等待资源时不是阻塞等待,而是循环(繁忙)等待,则它将永远无法获得所需资源。因为此时的低优先级进程无法与高优先级进程争夺CPU时间,从而无法执行,进而无法释放资源。而这将造成高优先级进程无法获得资源而继续推进。
        优先级倒挂还可能造成系统性能降低。低优先级进程之所以优先级低是因为其所执行的任务并不重要。例如,它们可能是批处理任务或其他非交互式任务。而高优先级任务执行的则是较为重要的任务,如为交互用户提供数据或实时任务。由于优先级倒挂造成低优先级任务在高优先级任务之前执行,因此系统的响应将降低,甚至实时系统的响应时间保证都有可能违反。
倒挂的解决方案
        那么如何防止优先级倒挂呢?首先我们应该看到,优先级倒挂有两种形式:

  • 不持有资源的低优先级进程阻碍需要资源的高优先级进程的执行。
  • 持有资源的低优先级进程阻碍需要资源的高优先级进程的执行。

        第1种情况为优先级倒挂的正常定义。这种倒挂的出现需要3种优先级别的存在:一个低优先级进程持有资源,一个不需要该资源的中优先级进程和一个需要使用该资源的高优先级进程。由于中优先级进程不需要使用低优先级进程占用的资源,因此它将能够抢占低优先级进程而获得CPU,从而在高优先级进程前面运行。
        要防止这种优先级倒挂,只需要将系统的优先级别限制为两种即可解决。这就可以得出我们的第1种防止办法:使用中断禁止。

  • 使用中断禁止

        这种办法的核心是通过禁止中断来保护临界区。在采用此种策略的系统中只有两个优先级:可抢占优先级和中断禁止优先级。前者为一般进程运行时的优先级,后者为运行于临界区进程的优先级。由于不存在第3种优先级,因此第1种优先级倒挂无法发生。由于系统里只存在一把锁(禁止中断操作在任何时候只能由一个进程执行),不能发生乱序,因此也不会发生死锁。又由于进入临界区的进程总是能够不被打断而一直运行到结束,因此也不会发生悬挂(hang)。此时,即使高优先级进程采用繁忙等待也不会发生死锁。
        这里需要注意的是,所有中断都必须禁止。如果禁止的仅仅是一个特定的硬件设备的中断,则硬件的中断优先处理机制将再次引入优先级倒挂。
        该方法普遍应用于简单的嵌入式系统。这种系统的特点是可靠性、简易性和资源需求低。不过这种方法对程序员的要求较高,因此在程序设计时需要将临界区设计得很短(中断禁止时间过长会造成系统响应时间增加而导致错误或灾难),通常应该在100微秒以下。而这个时间对于通用计算机来说很不现实。
        在多CPU环境下,由于不能使用中断禁止,因此使用一个简单的变种:单一共享标志锁
        该方法在共享内存里面提供一个单一标志。所有CPU在进入跨CPU临界区时都必须先获得该标志。这个标志在有的操作系统里面称为旋锁。本书将在第七篇对旋锁进行讨论。

  • 优先级上限(priority ceiling)

        使用中断禁止可以防止第1种优先级倒挂,但无法避免第2种情况的倒挂。而要防止第2种优先级倒挂,就不能让低优先级进程持有高优先级进程所需要的资源。而我们的思路就是让共享的临界区有自己的优先级,并让访问临界区的进程获得临界区的优先级。这样,只要临界区的优先级设置得足够高,就可以避免优先级倒挂。(因为获得资源的进程具有的将是高优先级!)在实际实现的时候,共享的mutex进程(操作系统代码)有其自身的高优先级。一个程序如果进入mutex保护的临界区,将获得该临界区所具有的高优先级别。此时如果其他试图访问mutex的进程的优先级都低于mutex的优先级,则将不会发生优先级倒挂。但如果试图访问mutex的程序具有高于mutex的优先级,则仍可能发生倒挂。

  • 优先级继承(priority inheritance)

        如果要完全杜绝优先级倒挂,则需要在任何时候都确保等待资源的进程所具有的优先级必须低于持有资源的进程的优先级。而实现这种目标的机制就是优先级继承。在此种方式下,当一个高优先级进程等待一个低优先进程所持有的资源时,这个低优先级进程将暂时获得高优先级进程的优先级别。这样就能防止一个中间优先级的进程抢占低优先级进程而延长资源占用时间,同时也防止了因高优先级进程自身进行繁忙等待可能导致的死锁。即此种方法对两种优先级倒挂都有效果。在释放共享资源后,低优先级进程回到其原来的优先级别。

3. 进程通信
3.1 为什么要通信

        如果进程之间不进行任何通信,那么进程所能完成的任务就要大打折扣。例如,父进程在创建子进程后,通常需要监督子进程的状态,以便在子进程没有完成给定的任务时,可以再创建一个子进程来继续。这就需要父子进程间通信。
        进程之间的交互称为进程间通信(Inter-Process Communication,IPC)。那么进程之间的通信是如何进行的呢?
        由于进程是人类的创造,我们只要看看人类是如何通信的就知道了。
        人类通信的方式无外乎对白(通过声音沟通)、打手势、写信、发电报、拥抱等方法。类似,进程也可以同样的方式进行通信。

3.2 进程对白:管道、记名管道、套接字

        人们最常使用的通信手段就是对白。对白的特点就是一方发出声音,另一方接收声音。而声音的传递则通过空气(当面或无线交谈)、线缆(有线电话)进行传递。类似,进程对白就是一个进程发出某种数据信息,另外一方接收数据信息,而这些数据信息通过一片共享的存储空间进行传递。
        在这种方式下,一个进程向这片存储空间的一端写入信息,另一个进程从存储空间的另外一端读取信息。这看上去像什么?管道。管道所占的空间既可以是内存,也可以是磁盘。就像两人对白的媒介可以是空气,也可以是线缆一样。要创建一个管道,一个进程只需调用管道创建的系统调用即可。该系统调用所做的事情就是在某种存储介质上划出一片空间,赋给其中一个进程写的权利,另一个进程读的权利即可。

3.2.1 管道

        从根本上说,管道是一个线性字节数组,类似文件,可以使用文件读写的方式进行访问。但却不是文件。因为通过文件系统看不到管道的存在。另外,我们前面说了,管道可以设在内存里,而文件很少设在内存里(当然,有研究人员在研发基于内存的文件系统,但这个还不是主流)。
        在程序里面,创建管道需要使用系统调用popen()或者pipe()。popen()需要提供一个目标进程作为参数,然后在调用该函数的进程和给出的目标进程之间创建一个管道。这很像人们打电话时必须提供对方的号码,才能创建连接一样。
在这里插入图片描述        创建时还需要提供一个参数表明管道类型:读管道或者写管道。而pipe()调用将返回两个文件描述符(文件描述符是用来识别一个文件流的一个整数,与句柄不同),其中一个用于从管道进行读操作,一个用于写入管道。也就是说,pipe()将两个文件描述符连接起来,使得一端可以读,另一端可以写。通常情况下,在使用pipe()调用创建管道后,再使用fork产生两个进程,这两个进程使用pipe()返回的两个文件描述符进行通信。
例如,下面的代码段创建一个管道并利用它在父子进程间通信。
在这里插入图片描述        管道的一个重要特点是使用管道的两个进程之间必须存在某种关系,例如,使用popen需要提供另一端进程的文件名,使用pipe()的两个进程则分别隶属于父子进程。

3.2.2 记名管道

        如果要在两个不相关的进程(如两个不同进程里面的进程)之间进行管道通信,则需要使用记名管道。顾名思义,命名管道是一个有名字的通信管道。记名管道与文件系统共享一个名字空间,即我们可以从文件系统中看到记名管道。也就是说,记名管道的名字不能与文件系统里的任何文件名重名。
在这里插入图片描述        一个进程创建一个记名管道后,另外一个进程可使用open来打开这个管道(无名管道则不能使用open操作),从而与另外一端进行交流。
        记名管道的名称由两部分组成:计算机名和管道名,例如\\[主机名]\管道\[管道名]\。对于同一主机来讲,允许有多个同一命名管道的实例并且可以由不同的进程打开,但是不同的管道都有属于自己的管道缓冲区而且有自己的通信环境,互不影响。命名管道可以支持多个客户端连接一个服务器端。命名管道客户端不但可以与本机上的服务器通信也可以同其他主机上的服务器通信。
        管道和记名管道虽然具有简单、无需特殊设计(指应用程序方面)就可以和另外一个进程进行通信的优点,但其缺点也很明显。首先是管道和记名管道并不是所有操作系统都支持。主要支持管道通信方式的是UNIX和类UNIX(如Linux)的操作系统。这样,如果需要在其他操作系统上进行通信,管道机制就多半会力不从心了。其次,管道通信需要在相关的进程间进行(无名管道),或者需要知道按名字来打开(记名管道),而这在某些时候会十分不便。

3.2.3 虫洞:套接字

        套接字(socket)是另外一种可以用于进程间通信的机制。套接字的功能非常强大,可以支持不同层面、不同应用、跨网络的通信。使用套接字进行通信需要双方均创建一个套接字,其中一方作为服务器方,另外一方作为客户方。
        服务器方必须先创建一个服务区套接字,然后在该套接字上进行监听,等待远方的连接请求。欲与服务器通信的客户则创建一个客户套接字,然后向服务区套接字发送连接请求。服务器套接字在收到连接请求后,将在服务器方机器上创建一个客户套接字,与远方的客户机上的客户套接字形成点到点的通信通道。之后,客户方和服务器方就可以通过send和recv命令在这个创建的套接字通道上进行交流了。
        套接字由于其功能强大而获得了很大发展,并出现了许多种类。不同的操作系统均支持或实现了某种套接字功能。例如按照传输媒介是否为本地,套接字可以分为本地(UNIX域)套接字和网域套接字。而网域套接字又按照其提供的数据传输特性分为几个大类,分别是:

  • 数据流套接字(stream socket):提供双向、有序、可靠、非重复数据通信。
  • 电报流套接字(datagram socket):提供双向消息流。数据不一定按序到达。
  • 序列包套接字(sequential packet):提供双向、有序、可靠连接,包有最大限制。
  • 裸套接字(raw socket):提供对下层通信协议的访问。

        套接字从某种程度上来说非常繁杂,各种操作系统对其处理并不完全一样。因此,如要了解某个特定套接字实现,读者需要查阅关于该套接字实现的具体手册或相关文档。

3.3 进程电报:信号

        如果使用管道和套接字方式来通信,必须事先在通信的进程间建立连接(创建管道或套接字),这需要消耗系统资源。其次,通信是自愿的。即一方虽然可以随意向管道或套接字发送信息,但对方却可以选择接收的时机。即使对方对此充耳不闻,你也奈何不得。再次,由于建立连接消耗时间,一旦建立,我们就想进行尽可能多的通信。而如果通信的信息量微小,如我们只是想通知一个进程某件事情的发生,则用管道和套接字就有点“杀鸡用牛刀”的味道,效率十分低下。
        因此,我们需要一种不同的机制来处理如下通信需求:

  • 想迫使一方对我们的通信立即做出回应。
  • 我们不想事先建立任何连接,而是临时突然觉得需要与某个进程通信。
  • 传输的信息量微小,使用管道或套接字不划算。

应付上述需求,我们使用的是信号(signal)。
        那么信号是什么呢?在计算机里,信号就是一个内核对象,或者说是一个内核数据结构。发送方将该数据结构的内容填好,并指明该信号的目标进程后,发出特定的软件中断。操作系统接收到特定的中断请求后,知道是有进程要发送信号,于是到特定的内核数据结构里查找信号接收方,并进行通知。接到通知的进程则对信号进行相应处理。

3.4 进程旗语:信号量

        在计算机里,信号量实际上就是一个简单整数。一个进程在信号变为0或者1的情况下推进,并且将信号变为1或0来防止别的进程推进。当进程完成任务后,则将信号再改变为0或1,从而允许其他进程执行。

3.5 进程拥抱:共享内存

        管道、套接字、信号、信号量,虽然满足了多种通信需要,但还是有一种需要未能满足。这就是两个进程需要共享大量数据。
        共享内存就是两个进程共同拥有同一片内存。对于这片内存中的任何内容,二者均可以访问。要使用共享内存进行通信,一个进程首先需要创建一片内存空间专门作为通信用,而其他进程则将该片内存映射到自己的(虚拟)地址空间。这样,读写自己地址空间中对应共享内存的区域时,就是在和其他进程进行通信。
在这里插入图片描述        乍一看,共享内存有点像管道,有些管道不也是一片共享内存吗?这是形似而神不似。首先,使用共享内存机制通信的两个进程必须在同一台物理机器上;其次,共享内存的访问方式是随机的,而不是只能从一端写,另一端读,因此其灵活性比管道和套接字大很多,能够传递的信息也复杂得多。
        共享内存的缺点是管理复杂,且两个进程必须在同一台物理机器上才能使用这种通信方式。共享内存的另外一个缺点是安全性脆弱。因为两个进程存在一片共享的内存,如果一个进程染有病毒,很容易就会传给另外一个进程。
        这里需要注意的是,使用全局变量在同一个进程的进程间实现通信不称为共享内存。

3.6 信件发送:消息队列

        消息队列是一列具有头和尾的消息排列。新来的消息放在队列尾部,而读取消息则从队列头部开始,如图所示。
在这里插入图片描述乍一看,这不是管道吗?一头读、一头写?没错。这的确看上去像管道,但它不是管道。首先,它无需固定的读写进程,任何进程都可以读写(当然是有权限的进程)。其次,它可以同时支持多个进程,多个进程可以读写消息队列。即所谓的多对多,而不是管道的点对点。另外,消息队列只在内存中实现。最后,它并不是只在UNIX和类UNIX操作系统中实现。几乎所有主流操作系统都支持消息队列。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值