任何操作系统的一个核心概念都是进程:正在运行的程序的一个抽象,它几乎是一切计算的基础。

2.1 Process

现代计算机往往会同时进行多个操作,就在电脑开机之时,数以百计的进程就会启动来完成一些任务,在多程序系统中,CPU快速地在进程间切换,每个进程运行数十或数百毫秒不等,在任何时间点上,CPU都只能运行一个进程,只不过每个进程运行的时间很短,所以1秒内可能运行多个进程,给我们造成“同一时间多个进程运行”的假象。

2.1.1 Process Model

在进程模型中,计算机中的一切程序(包括操作系统)都会被组织成一系列的顺序进程,一个进程是执行中程序的一个实例,包括程序计数器、寄存器和程序变量的当前值,CPU在进程间快速地切换,这被叫做multiprogramming(多程序运行)。如果一个程序被运行两次,它会被算作两个进程。

2.1.2 Process Creation

操作系统需要有一些创建进程的方式,四种基本事件会导致进程创建:

  • 系统初始化
  • 一个运行中进程执行了创建进程的系统调用
  • 用户请求的新进程创建
  • 批处理工作的开始

一旦操作系统启动,一般来说许多进程就会被创建,一些是前台进程,负责与用户交互,一些是后台进程,不被看到但是运行其特定功能(如接收即将到来的邮件,当收到邮件才发出通知);类似的进程还有接收网站请求的进程,这种进程被称为daemons(守护进程、系统服务),windows系统中的任务管理器能看到它们。

图一 任务管理器中的系统进程

除了开机,新进程在计算机运行过程中也经常被创建。往往一个运行中的进程会提出系统调用以创建一个或多个进程,帮助自己完成任务。在我们的日常使用中,双击鼠标打开某个程序也是再常见不过的场景,在这种场景下我们开启了一个程序,该程序同样会创建一些进程。 在windows系统中,一个CreateProcess函数负责创建进程和将正确的程序装载进进程中,它有10个参数,包括执行哪个程序、需要的命令行参数、一些可选参数等等。在UNIX和WINDOWS系统中,父进程和子进程都有其自己不同的地址空间,也就是每个进程在自己的地址空间中改变一些值对于别的进程都是不可见的。

2.1.3 Process Termination

每个进程都需要终止,出于以下某种原因中的一个:

  • 正常退出
  • 发生错误退出
  • 发生致命错误退出
  • 被其它进程杀死

通常来讲进程结束都是正常退出(如工作结束退出、用户点击退出按钮退出等),但有时也会有错误发生,例如一个进程试图开启一个不存在的文件就会发生错误并退出,但这种错误还属于一般性错误;致命错误是指程序bug,如非法指令(除0)等;当一个进程执行系统调用通知杀死其它进程时,也会导致进程的终结,当然执行该调用的“杀手进程”一定要有足够的权限才行。

2.1.4 Process Hierarchies

进程往往会创建子进程,子进程又创建子进程,这就形成了进程的层次结构。UNIX系统中的进程层次结构非常明显且重要,一个进程和其所有的子进程形成一个进程组,用户发送的键盘信号会传送到所有进程中,每个进程选择对其进行什么样的操作。实际上,UNIX启动时会有一个init进程,所有进程可以说都在init进程树下。相反,windows系统中没有进程阶级的概念,所有进程都是平等的。

2.1.5 Process States

有两种状态模型用来描述进程的不同状态:三态模型和五态模型。在三态模型中,进程被分为:

  • 运行态(当前在使用CPU)
  • 就绪态(可运行,暂时排队让其它进程运行)
  • 阻塞态(直到一些外部事件发生才能运行(比如等待外部输入))

前两种状态都是可运行的状态,只不过第二种进程没有CPU来为它服务,需要稍等片刻。在图二可以看出,进程会在不同的状态间切换,可以看出运行态和就绪态间的转换并不是进程本身能够决定的,操作系统会判断何时调度哪个进程,何时将进程踢出CPU,针对这种调度会产生许多问题(公平问题等),于是也有一系列算法来处理这些问题,在后面章节会有提及。

图二 三态模型中的进程状态及相互转换

另一种描述进程状态的模型是五态模型,它在三态模型的基础上添加了新建态和终止态。

图三 五态模型

2.1.6 Implementation of Processes

为了实现上述的进程三态模型,操作系统维护了一个进程表,并为每个进程保留一个进程表项(process table entry),该入口包括进程状态的一些重要信息,包括程序计数器、栈指针、 内存分配等等一切该进程需要的信息,这些信息在进程从运行态切换到就绪态或阻塞态时必须被保存,这样在恢复运行时才能恢复到切换时状态。

学习了进程表项后,前面提到过的中断(interrupt)的详细过程也可以说明了:首先要明确一点,中断向量(interrupt vector)是IO中断的入口,它包含了中断服务程序的地址。

  1. 硬件将程序指针等压栈
  2. 硬件将新的程序指针从中断向量中读出
  3. 汇编语言程序保存寄存器状态
  4. 汇编语言程序建立新的栈
  5. C语言中断服务程序运行
  6. 调度程序决定下一个要运行的进程
  7. C程序返回到汇编代码
  8. 汇编程序开启新的进程

2.2 Threads

2.2.1 Thread Usage

线程可以看作是轻量级的进程,一个使用线程的最主要原因是在许多应用中,许多活动同时进行,它们中的一些可能时刻都在阻塞状态。通过把这样的应用解构成准并行的线程,程序模型变得更加简单。详细来说,线程带来了进程所无法实现的功能:共享一个地址空间和地址空间中的数据的能力,多个进程是实现不了这种任务的,因此引入了线程的概念;另外,既然线程是轻量级的进程,那么它势必会更好创建/销毁。在一些操作系统中,创建一个进程的时间是创建一个线程的10-100倍。

到目前为止,一个程序可以有多个进程,一个进程又可以有多个线程,进程是资源分配的基本单位,而线程是调度的基本单位。想象一下经常被使用的word,如果它是单线程的话,那么每次它进行自动存储的时候我们的键盘输入都会无效,只能等自动存储结束才能继续输入,同样,只有多线程能完成这种任务(多个进程不能同时操作同一个文件)。同理,WEB服务器也要用到多线程,一种WEB服务器工作结构是:将接收到的用户请求送到分发者线程(dispatcher thread),该线程负责找到空闲的工作线程来完成请求。

2.2.2 Classical Thread Model

像之前提到的,进程是资源管理的基本单位,而线程是调度的基本单位,线程这个概念加入到进程模型中后,使得同一个进程环境中的多个独立操作成为可能,一个进程中的多个平行线程就像一个计算机中的多个平行进程一样,前者共享的是地址空间和其它资源,后者是物理内存、磁盘空间等一些硬件层面资源。同样,如果是在单核单一CPU上,同时能运行的线程只有一个,CPU通过快速切换线程来完成任务。现在的高性能处理器能够达到八核十六线程,也就是说具有同时处理十六个线程的能力,(将每个核心虚拟拓展成两个)。

每个线程独有的属性很少(大多数都是共享的),包括:程序计数器、寄存器、栈和线程状态。像进程一样,线程也有各种状态(运行、阻塞、就绪或是终结),例如当线程执行系统调用来读取键盘输入,它就要阻塞到键盘输入之后再继续运行,线程间状态的转换和进程是一样的。线程的栈需要给每个发起调用但还没收到返回值的程序保存一个帧,这个帧存储着程序的局部变量和返回地址,当程序调用结束后将利用这个地址返回。

线程会调用一些库程序以执行一些操作,这些操作在java的并发编程中非常常见,如:thread_create,创造线程;thread_exit,结束线程;thread_join,让某个线程阻塞到另一个线程执行结束;thread_yield,线程让步。

2.2.3 Implementing Threads

线程实现有两种方式,用户态线程和内核态线程。

  • 第一种方式是用户态线程,将线程包整个放入用户态,内核态对此一无所知。这种方式最大的优点是能够在不支持线程的操作系统上实现(过去的操作系统几乎都是如此)。当采用这种方式时,所有进程都需要维护一个线程表(thread table)来保存线程的最新信息,就像内核态中维护的进程表一样。与进程最大的不同是,当线程结束运行时,它能通知线程调度器来选择一个其它的线程运行,保存线程状态和通知调度器都是局部程序,也就是说免去了内核调用,无需上下文切换,内存缓冲无需重写,这使得内存切换十分迅速。用户态线程的另一个优点是它支持每个进程使用其自定义的调度算法,同时也避免了内核态中维护一个过大的线程表。当然,用户态线程同样存在着问题,其一是如何实现阻塞系统调用,其次,除非线程主动放弃CPU,否则用户态中一个进程的其它线程无法抢夺CPU资源,因为没有时钟中断,无法实现轮询调度。
  • 第二种方式为内核态线程,内核中维护一个线程表,替代了每个进程中的线程表。当线程想要创建新线程或是摧毁一个线程时,它需要进行内核调用,让内核管理此次操作,同时更新线程表。当然内核中依旧会维护进程表来跟踪进程的信息。在这种方法中,所有可能导致线程阻塞的调用都用系统调用实现,这毫无疑问会导致极大的消耗(对比上一种方法用运行时系统程序实现)。当一个线程阻塞时,内核可以选择启动同一个进程中的另一个线程,也可以启动其它进程中的某个线程。在上个方法中,系统会一直执行单一进程中的线程,直到执行完毕或内核将CPU分配给别的进程。

既然两种方式都有各自的缺点,那么有人就提出了混合式线程,将两种方式融合在一起----通过内核态中的线程管理用户态中的线程,在这种方式中内核态只需要关注内核中的线程以及调度方式。

图一 线程的实现方式

2.3 Interprocess communication

进程间经常需要交流,且最好是通过一种不需要中断的方式。这里面有三个问题亟需解决:①进程如何给其它进程发送信息 ②确保进程间不会相互干扰 ③当线程间有依赖关系时,它们需要以正确的先后顺序执行。对于线程来说,第一个问题还比较好解决,因为线程往往共享地址空间,但后两个问题依旧是需要思考的。

在进程通信中,几个概念比较重要:

  • Race Condition(竞争条件):两个或多个进程在读写共享文件时,最终结果取决于其精确的发生顺序。
  • Critical Regions(临界区):解决上述问题的方法无非是禁止进程们同时读取/书写同一个文件,也就是说需要mutual exclusion(互斥),保证当一个进程操作某个共享变量或文件时,其它进程不能做相同的事情。如何实现互斥是所有操作系统中一个共同的重要议题,解决这个问题的思路就是将程序共享的内存区域设为临界区,该区域内不能同时存在两个进程,互斥由此实现。但是这样仅仅是解决了竞争问题,如何让平行进程互相合作并利用共享文件依旧是个问题,要解决这种问题需要以下4个条件,理想状态下的实现情况如图二所示:
  1. 临界区内不能同时有两个进程
  2. CPU数目及速度要被充分利用
  3. 在临界区外运行的进程不能阻塞其它进程
  4. 不能有进程永久等待进入临界区

图二 临界区与互斥

2.3.1 Busy Waiting

除了临界区之外,还有其它的几种方式用来实现互斥:

  • Disabling Interrupts(关中断) :在单处理器系统中,最简单的方法是当一个进程进入临界区后,关闭所有中断直到它退出临界区,这样做的原理是:CPU通过时钟中断在进程间切换,只要禁止所有中断,那么进程的切换也就无从谈起,这样当进程访问共享资源时它就不用担心被打扰了。同时,这种方法也是极其不明智的,因为它给用户级线程关闭中断的权力,如果该线程再也不开启中断,这个系统就不能再运行了。况且,如果是多核环境,关闭一个核的中断不会影响另一个核,另一个核(CPU)依旧会向临界区中调度进程。
  • Lock Variables(锁变量):关中断是从硬件角度出发,而锁变量则是一种软件解决方法。锁变量是一个共享变量初始化为0,当一个进程想要进入临界区时,它首先检查锁变量的值,如果是0则置1后进入临界区,如果是1那么就等待直到变为0为止。但这其实有和进程竞争类似的问题:锁变量的值更改就是一个竞争,如果一个进程读取锁变量为0,在它将其置为1前另一个进程又被调度,这样会导致两个进程进入临界区。
  • Strict Alternation(严格交替):一个初始化为0的变量控制着谁来进入临界区,并且检查更新共享内存,想进入临界区的进程如果发现该变量是0,要一直检查它的值,直到为1才能进入,一直检查变量值的做法又叫做忙等待(busy waiting)它会浪费CPU时间,所以应该尽量避免,用了忙等待的锁又称为自旋锁(spin lock)。但这种方法并不能严格称为一种解决办法,因为它不满足上述的第三个条件:临界区外的进程不能阻塞其它进程。
  • Peterson's Solution:皮特森算法同样用软件来解决问题,通过一个算法来执行不需要严格交替的互斥。
    #define FALSE 0
    #define TRUE 1
    #define N 2     // 进程数量 int own[N]; // 线程自维护数据,初始化为FALSE
    void enter_region( int threadId ) // 线程号,取值为0,1
    { int other = 1 - threadId; // 代表另外的那个线程的号 own[threadId] = TRUE; // 设置当前线程的意向值为TURE while(  own[other] == TURE ); // 当互斥条件不满足即while里面的条件满足时忙等待
    }
    void leave_region( int threadId )
    { own[threadId] = FALSE; // 设置意向值表示不感兴趣
    }

2.3.2 Sleep and Wakeup

Peterson算法、严格交替方法都需要用到忙等待,当一个进程要进入临界区时它都要检查入口是否可进入,如果不可进入那就循环等待直到可以进入为止。这种方法不止浪费CPU时间,而且会导致优先级倒置问题(优先级低的进程永远等待一个优先级高的进程而进不去临界区)。一个最简单的避免忙等待的方法是加入睡眠和唤醒对,它们是一对系统调用,能够唤醒或挂起某个进程。在生产者-消费者实例中,生产者向资源池中添加资源,而消费者取走资源,当资源池为满/空时,生产者和消费者分别会sleep,等待资源池状态变化后由对方唤醒。

2.3.3 Semaphores

    1965年迪杰斯特拉提出了信号量(semaphore),它是一个用来统计唤醒进程数目的整型变量。对信号量可以有两种操作,down和up,down操作会检查信号量,如果其值大于0则将其减一(意为有一个醒着的进程被用了),如果是0那么进程进入睡眠状态,一旦一个进程操作了信号量,在操作结束前其它进程就都不能对其更改。值得注意的是检查信号量、更改信号量,这些操作都是由atomic action(原语)完成的,在原语中的一系列操作要么都完成,要么就都不做,这是在计算机科学中非常重要的概念。up操作会增加信号量的值,如果一个或多个进程在之前的运行中因为某些原因没能完成down操作并sleep在这个信号量上,那么up操作会选一个进程并让其完成down操作。类似down,up操作同样由原语构成,唤醒一个进程和增加信号量的值是不可分割的操作。在迪杰斯特拉的论文中,他用P和V代表down和up。

2.3.4 Mutexes

当不需要信号量的计数功能时,可以使用一个简化版本的信号量--mutex(互斥量),相比起来它更易于实现,在用户态线程中十分实用。互斥量是一个由两个状态组成的变量:锁着的或未上锁的,也就是说一位数字就能代表它,但实际使用中经常用0代表未上锁状态,而其它整数都代表上锁状态。当线程或进程要进入临界区时,它会调用mutex_lock,如果mutex现在是0,那么这次调用成功,进程可以进入临界区,反之若mutex不是0,进程会被阻塞直到临界区中的进程完成任务并执行mutex_unlock。这一过程看起来很像之前说过的使用锁变量的方法,区别在于在锁变量方法中,若一个进程被阻塞那么它会反复查看锁变量的值,直到为0再进入,也就是说一直处于忙等待状态,而使用mutex时,当一个线程或进程被阻塞,它会调用thread_yield来放弃CPU让给其它线程,从而避免忙等待。同时thread_yield是一个用户态中的线程调度器调用,它非常快速,结果上来说mutex_lock和mutex_unlock都不需要内核调用。

  • Mutexes in Pthreads : Pthreads是posix中的线程接口,它提供了许多同步线程的功能。在上述的互斥锁中,上锁解锁等操作都不是规定好的,程序员需要编程来确保线程正确使用这些方法。Pthreads给出了一些与互斥有关的方法,如下图所示。除了互斥量之外,pthreads还给出了另一个控制同步的方法:条件变量(condition variables),一般情况下两种方法都是一起使用的。回到生产者-消费者实例中,生产者发现资源池满时需要被阻塞,过一会再被唤醒,这就是条件变量起作用的地方,与条件变量有关的调用如图四所示。条件变量与互斥量的合作情况往往是:一个线程对互斥量上锁,当它不能满足工作条件时等待条件变量来唤醒它。最终另一个线程会调用signal来将其唤醒。

图三 Pthreads中与互斥有关的调用

图四 Pthreads中与条件变量有关的调用

2.3.5 Monitors

使用信号量管理线程间通信看起来很简单,但在实际使用时必须非常小心,这就好像在使用集成语言编程,稍有不慎就会导致很严重的错误(死锁)。

为了使这种情况变得简单一些,一种高级的同步原语:管程(monitor)被提出。管程是程序、变量和数据结构的集合,它们用一种特殊的单元或包被组织在一起。进程可以在任意时刻调用管程中的程序,但它们不能直接访问管程内部的数据结构。管程有一个很重要的属性,这让它们在实现互斥中十分有用:在任意时刻下只有一个进程能在管程中运行,一般情况下这是由编译器实现的。

尽管管程在实现互斥上很简单,但它仍然需要一种方式来让不能运行的进程阻塞。管程的方法是条件变量(condition variables),在它之上能进行两种操作,wait和signal。当一个管程程序意识到它不能继续运行时(如生产者发现缓冲区已满),则进行一次在某个条件变量上的wait,这个动作导致了调用进程的阻塞,也允许了之前被禁止进入管程的某个进程来进入管程。同理,以消费者为例,它也能唤醒其搭档,通过对其等待的条件变量进行signal操作。signal操作需要一些约束,否则会出现同时在管程中有两个活跃进程的情况。Hoare提出让最后唤醒的进程运行,其它的在外面等待,而Brinch Hansen提出一个进程若执行signal操作则必须马上退出管程。换句话说,signal操作一定是管程程序的最后一条语句。书中采取的是后者。

条件变量并不是计数器,它们不会计算signal的值以备后续使用,因此如果该条件变量上没有wait的进程而来了一条signal操作,则该操作就会丢失。因此为了避免这种情况,wait操作应该永远在signal之前。

到目前为止,管程中的wait和signal和之前的sleep与wakeup很像,后者导致了致命的竞争条件。而它们的一个关键区别就是:sleep与wakeup模型中若一个进程尝试睡眠,另一个就要将其唤醒,而在管程中这是不存在的。管程中的自动互斥保证了如果管程程序中的生产者发现缓存是满的,则它能够实现wait操作而不需要担心在wait完成之前调度器就会切换到消费者,消费者甚至不会被允许进入管程直到wait操作完成。

最初管程提出时是用Pidgin Pascal编写的,这是一种想象中的语言,而如今一些编程语言也支持了管程。以java为例,java中的关键字synchronized保证了一旦某线程启动了以synchronized修饰的方法,则其它线程都不能运行任何该对象的synchronized方法。

管程的一个问题是它只能解决能够有同样内存空间的一个或多个CPU背景下的问题,一旦变成分布式系统下多个CPU有各自的内存空间,它就会失去效力。在这种背景下信号量就过于底层,管程中必须加入一些高级程序语言才能够发挥作用,同时,还有一个问题存在,这个问题也需要一个新的解决方法。

2.3.6 Message Passing

上述提到的另一个问题就是信息传递的问题,没有任何一种原语能够支持机器间的信息传递,为此产生了message passing,这种跨进程沟通方式用两个原语,sendreceive来实现,和信号量一样,它们是用系统调用而不是类似管程的语言结构来实现的,它们的实现很简单,类似send(destination,&message);receive(source,&message)。由于这种方式传递信息往往是跨机器的,因此它就要通过网络,也就增加了网络丢失信息的可能性,为了处理这种情况,信息传递采取的解决方式和计算机网络中的TCP处理方式差不多(超时重传、增加确认号和序号)。

2.3.7 Barriers

一些应用被分成许多不同的阶段,并且有规定要所有进程都准备好进入下一阶段时才允许进入下一阶段。这种情况可能会通过在每一个阶段末尾加入一个Barrier(屏障)来实现。其作用就像一个闸口,等所有进程都到齐后再统一放行。当一个进程运行到barrier时,它阻塞到其它进程都运行到同一阶段。

2.4 Scheduling

当电脑是多道程序设计后,肯定会频繁地发生同一时间多个进程/线程竞争CPU的情况,在这时必须决定哪个线程占有CPU资源运行,操作系统中做出这个决定的部分是scheduler(调度器),它使用调度算法来调度进程线程出入CPU。

2.4.1 Introduction

在过去的批处理系统上,输入通过磁带进行,调度系统十分简单:运行磁带上的下一项工作即可。然后多道程序设计到来,调度变得复杂了起来。一些系统仍然混合批处理和分时系统服务,要求调度器选择哪个批任务或者哪个终端上的用户应该下一个运行。然后是单处理器是个人PC时代,基本上不会有同时两个活跃进程出现,因此CPU不必费心寻找哪个进程要来运行,同时CPU也比过去迅速了很多。

如果是网络服务器,那么情况就又有变化,这里的确会有多个进程竞争CPU资源,调度问题也就应运而生。除了要挑出合适的下一个运行的进程,调度器还要关心如何最大化利用CPU,之前的章节已经提到很多次,进程间的切换是十分浪费时间的,用户态和内核态的切换就占据了一大部分时间,同时还要存储当前进程的状态,写入到寄存器中等等。因此,过多的进程切换肯定会浪费CPU的一大部分时间,调度器聪明的话一定不会这么做。

进程与进程间的行为是有差异的,一些进程大多数时间都在运算,而另一些进程的大部分时间都在等待I/O。前者称为compute-bound(受计算限制),后者为I/O-Bound(受IO限制)。前者大多数时间都要进行CPU运算,IO等待不是很频繁,而后者相反。随着CPU越来越快,进程大多数都趋向于受IO限制,因为CPU处理速度过快导致进程的大部分时间其实都在等待IO,于是对IO限制的进程调度也就慢慢成为了主要问题。

  • 什么时候进行调度 :调度问题中的一个关键问题就是什么时候进行调度。第一,当一个新进程创建时,调度需要决定是否运行其父/子进程。第二,当一个进程退出时需要调度。如果当该进程退出时没有进程处于就绪状态,一个系统提供的空闲进程就会运行。第三,当一个进程阻塞,等待IO/信号量或一些其它元素时,需要有另一个进程运行。往往阻塞的原因会成为下一个被挑选的进程的关键,如进程A在某临界区阻塞,那么另一个等待该临界区的B就会运行。第四,当IO中断发生,也要进行调度。IO设备发出中断时可能是完成了当下的工作,那么一些阻塞并等待该IO设备的进程就可以运行了。

调度算法以它们对待时钟中断的方式分为两种,非抢先(nonpreemptive)调度选择一个进程运行直到其阻塞(或是因为IO,或是等待其它进程)或自愿放弃CPU。相反,抢先(preemptive)调度选择一个进程,并分配给它一个最大时间,当时间耗尽时它就被挂起,调度器选择另一个进程来运行。抢先式调度要求在时间间隔末尾产生一个时钟中断并把CPU控制权交还调度器,如果没有可用时钟,那么非抢先调度是唯一的选择。

同样,不同系统中的调度算法也会有区别,三种大的系统环境分别为:批处理系统、交互系统、实时系统。批处理系统往往适用于大型应用,没有进程很急着拿到自己的结果,因此非抢先调度比较实用。在交互式系统中,抢先调度是必要的,它会防止一个进程“劫持”CPU不让其它进程使用。实时系统与交互式系统的区别在于它只会运行对手头上的应用有用的程序,进程明白他们不会运行太长时间,因此抢先调度足够,甚至过剩。

最后,我们来研究一下调度算法的目的,以总----分的层次。

  • 所有系统:对于所有系统来说,调度算法应该满足公平:每个进程都应该公平占有CPU;策略执行:执行既定政策;平衡:让系统的每一个部分保证在工作。
  • 批处理系统:在上述基础上满足吞吐量:每小时必须完成的最大工作量;周转时间:进程结束和提交间的时间越短越好;CPU利用率:让CPU保持工作状态。
  • 交互系统:响应时间:对请求快速响应;恰当性:满足用户期望。
  • 实时系统:满足截止时间:同时避免丢失数据;可预见性:避免多媒体系统中的质量下降。

但无论如何,公平是最重要的。相近类型的进程就应该得到相近的资源。

2.4.2 Scheduling in Batch Systems

这节会介绍批处理系统中的一些调度算法,要注意有些算法既可以用于批处理系统中同时也能适用交互系统。

1.First-Come First-Served

最简单的调度算法应该就是非抢先式的先到先服务算法了,这种算法的做法是先请求CPU的进程就先被分配。一般来说会有一个就绪状态进程的等待队列,当第一个任务进入系统,它就能立刻启动并运行任意长的时间。这种算法的最大好处当然就是易于理解,对于程序来说也一样,同样它也是公平的,排队机制无论在任何场景下都是公平的。但同时它也有一个致命的缺点,假设有一个受计算限制的进程每次运行一秒,同时有很多受IO限制的进程占用很少的CPU时间但每个都需要进行1000次硬盘读取。受计算限制的进程运行一秒然后读取了一个磁盘块,所有的IO进程随之启动并开始磁盘读取。当受计算限制的进程得到了它的磁盘块时它就会再运行1s,结果是每个IO限制的进程每秒读取一个磁盘块,最终用1000s完成。如果用一个抢先式调度算法每10毫秒运行一次受计算限制进程,那么受IO限制进程就会10s结束而不是1000s结束。一言以蔽之,其缺点就是短时间作业会等待很长时间。

2.Shortest Job First

最短时间工作优先算法是另一种非抢先算法,它假设所有运行时间都已知。在这种条件下,把短时间的工作排在最前就能减少平均周转时间。但该算法只能用于所有进程都同时可用的情况下,若进程A、B提前到达,C、D、E之后到达,该算法就不再适用。

3.Shortest Remaining Time Next

最短时间优先算法的抢占式版本就是最短剩余时间优先,用这种算法的调度器挑选剩余时间最短的进程来运行。如果新到达的进程的剩余时间小于当前进程的剩余时间,则新进程取代当前进程运行。

2.4.3 Scheduling in Interactive Systems

交互式系统的调度算法在个人电脑、服务器和其它的一些系统中都很常见。

1.Round-Robin Scheduling

一个最老、最简单、最公平也是最泛用的算法是固定时间片算法。每个进程都被分配一个时间段,称为quantum,进程在该时间片内允许运行。若在时间片结束时进程仍旧在运行,CPU要抢占式分配给其它进程。该算法的实现也比较简单,调度器只需维护一个可运行进程的列表,让进程用尽其时间片则将其放到列表末尾排队。就像之前好多问题中谈到的,进程切换需要消耗很多时间和资源,因此时间片大小的分配就是一个至关重要的问题,如果设置得太短就会引起过多进程切换浪费CPU性能,如果设置过长那么短时间的交互请求就得不到充分响应。通常20-50ms的时间片是合理的。

2.Priority Scheduling

时间片算法中的一个假设是所有进程都是同等重要的,然而进程间的优先级是不太可能都一样的,比如一个后台的发送邮件的守护进程比起前台播放视频的进程优先级就会低很多,而这是优先数调度的用武之处。其基本思想是:每个进程被分配一个优先级,然后最高优先级的可运行进程开始运行。调度器在每个时钟中断时可以降低当前运行进程的优先级,若其优先级被降低到小于其它进程的优先级,则进程切换发生。每个进程会被分配给一个最大时间片,该时间片用尽时同样次优先进程会开始运行。通常来说,给进程分配进一个优先级类别,然后对每个类别间进行优先数调度,对类别内进行时间片调度是比较方便的。

3.Multiple Queues

多队列算法同样是建立很多优先级,在最高级中的进程运行一个时间片长度,次级运行两个时间片,再次级四个...当一个进程用尽了它的时间片就会被下降一个级别。这样假设一个进程需要100个时间片,第一次它运行一个,然后是两个、四个、八、16、32、64并最终执行完毕。中间会有7次进程切换,这比单纯的时间片算法需要100次切换要好太多。

4.Shortest Process Next

和批处理系统中的最短时间优先同理,在交互式系统中的最短优先更加有其优势,因为交互式系统中的响应时间是很重要的一个指标,唯一的问题就是如何知道每个进程的运行时间,一个方法是根据过去的行为估算并按照估算时间分配。

5.Guaranteed Scheduling

一个完全不同的调度方法是对用户进行承诺并完成这些承诺,比如:如果在你工作时有n个用户登录,你会收到大概n/1的CPU资源。同样的,在单用户多进程系统,每个进程也会被分配给n/1个CPU循环。为了实现这种承诺,系统必须跟踪每个进程自创建起占用了多久CPU,然后计算每个进程应该占用多少CPU,并用二者相除。若得到的值为0.5,则说明该进程得到的资源远远不够,并在之后的分配中上调其时间。

6.Lottery Scheduling

虽然承诺式调度很好,但其实它很难实现。而乐透调度与它近似同时又有较低的实现难度。其基本实现是给进程的各种资源(如CPU时间)分发“彩票”,当调度发生时,从众多的彩票中随机抽取一个,该彩票的持有进程便得到该资源。重要一点的进程会被分配给额外的彩票来增加赢的几率。

2.4.4 Scheduling in Real-Time Systems

实时系统可大致分类为hard real time硬实时和soft real time软实时。前者意为着所有的deadline都必须被满足,而后者则是有些超过deadline的事件虽然不好但也可以接受。但两种系统都是将程序分为许多进程,让进程被调度并最终在deadline前完成任务。实时系统需要响应的事件可分为两种:周期性的和非周期性的。一个系统要处理许多周期性事件流,根据每个事件所需的时间不同,有些事件可能都不能被处理。若周期性事件i要发生p次,每次占用时间为c,则只有在所有的C/P加和≤1的情况下该系统才是可调度的。

2.5 Summary

为了隐藏中断的影响,操作系统提供了一种能够并行执行的概念模型:进程。进程能够动态创建和终结,每个进程都有其自己的地址空间。对于一些应用来说,需要有一些共享地址空间的线程,但它们彼此之间独立并有自己单独的栈,它们可以在用户态或内核态中实现。进程间能够适用通讯原语通信,例如信号量、管程或信息。这些原语用来确保进程不会同时进入临界区。进程的状态可分为运行态、就绪态或阻塞态。进程间的通信原语能够用来解决生产者-消费者问题等等类似问题。进程间需要进行切换和管理,因此进程调度也是很重要的问题,进程调度可分为批处理、交互、实时系统的调度算法。

操作系统笔记(二):进程和线程相关推荐

  1. 操作系统(二): 进程与线程

    操作系统(二): 进程与线程 本章解读 进程管理是操作系统重点中的重点,涵盖了操作系统中大部分的知识和考点.其主要包括四部分:进程与线程,处理器调度,同步与互斥,死锁.所以我准备分四个部分来解释这四个 ...

  2. 操作系统 (二): 进程与线程

    本文为<现代操作系统>的读书笔记 目录 进程 (process) 多道程序设计模型 程序顺序执行与并发执行 前驱图和程序执行 并发执行 进程模型 进程控制 创建进程 进程是何时被创建的? ...

  3. 操作系统原理:进程与线程、进程生命周期、线程的类型

    一.进程定义 进程可以看成程序的执行过程,可以展示在当前时刻的执行状态.它是程序在一个数据集合上的一次动态执行的过程.这个数据集合通常包含存放可执行代码的代码段,存放初始化全局变量和初始化静态局部变量 ...

  4. linux 驱动线程与进程,Linux内核学习之二-进程与线程

    一.操作系统的功能 根据维基百科的解释,一个操作系统大概包括以下几个功能: 进程管理(Processing management) 安全机制(Security) 内存管理(Memory managem ...

  5. 现代操作系统 第二章 进程与线程 习题

    第2章 进程与线程 习题 1. 图2-2中给出了三个进程状态,在理论上,三个状态可以有六种转换,每个状态两个.但是,图中只给出了四种转换.有没有可能发生其他两种转换中的一个或两个? A:从阻塞到运行的 ...

  6. 操作系统学习:进程、线程与Linux0.12初始化过程概述

    本文参考书籍 1.操作系统真相还原 2.Linux内核完全剖析:基于0.12内核 3.x86汇编语言 从实模式到保护模式 ps:基于x86硬件的pc系统 进程 进程是一种控制流集合,集合中至少包含一条 ...

  7. 操作系统中的进程与线程

    简介 在传统的操作系统中,进程拥有独立的内存地址空间和一个用于控制的线程.但是,现在的情况更多的情况下要求在同一地址空间下拥有多个线程并发执行.因此线程被引入操作系统. 为什么需要线程? 如果非要说是 ...

  8. Python学习笔记:进程和线程(承)

    前言 最近在学习深度学习,已经跑出了几个模型,但Pyhton的基础不够扎实,因此,开始补习Python了,大家都推荐廖雪峰的课程,因此,开始了学习,但光学有没有用,还要和大家讨论一下,因此,写下这些帖 ...

  9. Python学习笔记:进程和线程(起)

    前言 最近在学习深度学习,已经跑出了几个模型,但Pyhton的基础不够扎实,因此,开始补习Python了,大家都推荐廖雪峰的课程,因此,开始了学习,但光学有没有用,还要和大家讨论一下,因此,写下这些帖 ...

  10. 操作系统中的进程与线程和java中的线程

    简介 在传统的操作系统中,进程拥有独立的内存地址空间和一个用于控制的线程.但是,现在的情况更多的情况下要求在同一地址空间下拥有多个线程并发执行.因此线程被引入操作系统. 为什么需要线程? 如果非要说是 ...

最新文章

  1. 机器学习模型质量评价标准 — 精准率、召回率
  2. thinkphp 个别字段无法更新_香港华为手机大面积死机?只是个别手机更新出问题...
  3. 插入的表单控制下拉框怎么设置_想要告别表单重复填写?这一个功能就够了
  4. 居民身份证号码组成规则
  5. Spring-SpringMVC父子容器
  6. uboot中的虚拟地址映射
  7. WPF——Expander控件(转)
  8. PDF文档解析,公司公告信息抽取(附数据集)
  9. bzoj 1085: [SCOI2005]骑士精神(IDA*)
  10. 我终于会加载模块了 值得纪念!
  11. JSP项目155套-开发专题-大作业设计-毕业设计【建议在校生收藏】(保持更新)
  12. php require找不到文件,第一次运行Fatal error: require_once找不到文件
  13. 软考网络工程师必过教程---必看
  14. 【php】PHP制作QQ微信支付宝三合一收款码
  15. 基于百度云主机的USDP 2.x 安装详细教程
  16. 农夫过河c语言算法,农夫过河
  17. 树莓派4b学习笔记三--基于Ubuntu搭建Docker 和portainer,基于Docker 搭建Homeassistant、EMQX
  18. win10 远程桌面卡顿_win10远程桌面连接卡如何解决_windows10远程连接桌面很卡怎么处理...
  19. Hadoop+hive+flask+echarts大数据可视化之系统数据收集
  20. 数据结构物理存储方式

热门文章

  1. 电商数据分析指标体系实例。
  2. 如何添加51la代码及隐藏统计图标
  3. 会计基础(一):记账基础 - 复式记账法
  4. 灵异事件之idea和金山词霸
  5. [pytorch]torch.roll函数
  6. 数据完整性、存储过程、函数
  7. (附源码)spring boot大学生综合素质测评系统 毕业设计 162308
  8. 如何查看己连接的Wi-Fi密码
  9. C语言 循环结构打印*号三角形
  10. 如何打开tdms文件?