进程与线程
进程
在某一个瞬间,CPU只能运行一个进程,但在1秒钟,它可能运行多个进程,这样就产生了并行的错误(伪并行),还有多处理系统(该系统有两个或多个CPU共享一个物理内存);
进程是计算机系统资源分配的基本单位
进程模型
在进程模型中,计算机上所有可运行的软件,包括操作系统被组成称若干个顺序进程,简称进程(process),一个进程就是一个正在执行程序的实列,包括程序计数器,寄存器和变量的当前值,从概念上来说‘每个进程拥有它自己的虚拟CPU’,实际上真正的CPU在各个进程之间来回切换,这种切换被称作多道程序设计
抽象的理解下:一个进程是某种类型的一个活动,它有程序,输入,输出以及状态,单个处理器可以被若干进程共享,它使用某种调度算法决定何时停止一个进程的工作,转而为另一个进程提供服务
进程的创建
4种主要事件会导致进程的创建:
- 系统初始化
- 正在运行的程序执行了创建进程的系统调用
- 用户请求创建一个新进程
- 一个批处理作业的初始化
守护进程:停留在后台处理的进程
在UNIX操作系统中系统调用fork 这个系统调用会创建一个与调用进程相同的副本,这两个进程(父子)拥有相同的内存映像,同样的环境字符串和同样的打开文件;
在windows中,一个Win32函数调用CreateProcess即处理进程的创建,复制把正确的程序装入新的进程中;
在windows和Unix中进程创建之后,父进程和子进程有各自不同的地址空间,如果某个进程在其地址空间修改了一个字,这个修改对于其他进程是不可见的;
在Unix实现中,程序正文在两者是共享的,不能被修改,子进程共享父进程的所有内存,这种情况下内存通过写时复制共享,意味着两者之一想要修改部分内存,则这块内存要被明确的复制,确保修改在私有内存区域;
在windows实现中,一开始父进程和子进程的地址空间就不同;
进程的终止
进程创建后,开始工作,完成工作后,这个进程会结束,通常是下列条件组成
- 正常退出(自愿的)完成工作,执行系统调用Exit or ExitProcess
- 出错退出(自愿的)进程发现了严重的错误
- 严重出错(非自愿)进程引起的错误,通常由于程序中的错误所导致
- 被其他进程杀死(非自愿)某个进程执行一个系统调用通知操作系统杀死某个其他进程
进程的层次结构
Unix中可以有一个父进程多个子进程,可以组成进程组
windows没有进程的层次结构概念,所有进程都是地位相同的;在创建进程时,父进程会得到一个句柄,句柄可以控制子进程
进程的状态
- 运行态(该时刻进程实际占用CPU)
- 就绪态(可运行,因为其他进程正在运行时暂时停止)
- 阻塞态(除非某种外部事件发生,否则进程不能运行)
上面两个逻辑是类似的,第二个状态是没有CPU分配给它,第三种完全不同,CPU空闲也不能运行,这三种状态有四种转换关系
- 进程因为等待输入而被阻塞(运行->阻塞)
- 调度程序选择另一个进程(运行->就绪)
- 调度程序选择这个进程(就绪->运行)
- 出现有效输入(阻塞->就绪)
转换2.3是由进程调度程序引起的;
进程的实现
为实现进程模型,操作系统维护着一张进程表,每个进程占用一个表项(即进程控制块,Process Control Block,PCB)。PCB包含了进程状态的重要信息,包括程序计数器(保存了下一条指令的内存地址)、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息,以及其他在进程由运行态转换到就绪态或阻塞态时必须保存的信息,从而保证该进程随后能再次启动,就像从未被中断过一样。
**中断向量(interrupt vector)**是中断服务程序的入口地址。
进程结束时,操作系统等待一个提示符并等待新的命令,一旦接到新的命令,就装入新的程序进内存,覆盖前一个程序。
与每一I/O类关联的是一个称作中断向量的位置,它包含中断服务程序的入口地址,假设当一个磁盘中断发生时,用户进程3正在运行,则中断硬件将程序计数器,程序状态字,有时还有一个或多个寄存器压入堆栈中,计算机随即跳转到中断向量所指示的地址,是由硬件完成的,任何软件中断服务例程将接管一切剩余的工作;
- 硬件压入堆栈程序计数器等
- 硬件从中断向量装入新的程序计数器
- 汇编语言过程保存寄存器值
- 汇报语言过程设置新的堆栈
- C中断服务例程运行
- 调度程序决定下一个将运行的进程
- C过程返回至汇编代码
- 汇编语言过程开始运行新的当前进程
一个进程在执行过程可能被中断数千次,但每次中断后,被中断的进程都返回到与中断发生前完全相同的状态
多道程序设计模型
采用多道程序设计可以提高CPU的利用率
CPU利用率=1-p^n (一个进程等待IO操作的实际与其停留在内存中时间比为p,n多少个进程)
线程
线程(Thread)是CPU调度的基本单位
线程的使用
为什么一个进程中还需要一些迷你进程(线程)??主要原因:
- 应用中同时发生着多种活动,其中某些活动随着时间的偏移会被阻塞,通过这些应用程序分解成可以准并行运行的多个顺序线程,程序模型会变得简单;
- 线程比进程更轻量级,比进程更容易创建也更容易撤销;
- 多个线程都是CPU密集型的,那么并不能获得性能上的提升,如果存在着大量的计算和大量的IO处理,拥有多个线程允许这些获得彼此重叠进行,从而加快执行;
同一进程内的所有线程共享地址空间和数据。
### 经典的线程模型
进程拥有一个执行的线程,通常简写为线程,在线程中有一个程序计数器,用来记录接着要执行那一条指令,线程拥有寄存器,用来保存线程当前的工作变量,线程还拥有一个堆栈,用来记录执行历史,其中每一帧保存了一个已调用的但是还没有从中返回的过程。线程必须在某个进程中执行。
进程中的不同线程不像不同进程之间那样存在很大的独立性,所有的线程都有完全一样的地址空间,这意味着它们也共享同样的全局变量。由于各个线程都可以访问进程地址空间中的每一个内存地址,所以一个线程可以读、写或甚至清除另一个线程的堆栈。线程之间是没有保护的。
和传统进程一样(即只有一个线程的进程),线程可以处于若干种状态的任何一个:运行、阻塞、就绪或终止。
每个线程有其自己的堆栈很重要:每个线程的堆栈都有帧,供各个被调用单数还没有从中返回的过程使用,在该栈帧中存放了相应过程的局部变量以及过程调用完成之后使用的返回地址;
一个进程中所有线程共享的内容 | 每个线程自己的内容 |
---|---|
地址空间,全局变量,打开文件,子进程,即将发生的定时器,信号与信号处理程序,账户信息 | 程序计数器,寄存器,堆栈,状态 |
POSIX线程
IEEE在IEEE标准1003.1c中定义了线程的标准,他定义的线程包叫做pthread,所有pthread线程都有某些特性,每一个都含有一个标识符,一组寄存器(pc),和一组存储在结构中的属性,这些属性包括堆栈大小,调度参数以及其他线程需要的项目;
在用户空间中实现线程
两种主要的方式实现线程包:在用户空间中和在内核中
第一种方法:把整个线程包放在用户空间种,内核对线程包一无所知;在内核角度考虑:就是按正常的方式管理,即单线程进程;在用户空间管理线程时,每个进程需要有其专用的线程表(thread table),用来跟踪进程中的线程;
同一个进程中,有一个线程运行,其他线程也会运行
在用户空间中实现线程中的优点:不需要陷入内核,不需要上下文切换,不需要对内存高速缓存进行刷新,使得线程调度非常快捷,允许每个进程都有自己的调度算法
缺点:如何实现阻塞系统调用?
用户级线程包另一个问题:如果一个线程开始运行,那么该进程中的其他线程就不能运行,除非第一个线程自动放弃CPU,在一个单独的进程内部,没有时钟中断和轮转调度的方法调度线程,除非某个线程都够按照自己的意志进入运行时系统,否则调度程序就没有任何机会;
在内核中实现线程
线程表保存于内核中。
阻塞线程的调用都以系统调用的形式实现。
创建或撤销线程代价较大。
在内核中有用来记录系统中所有线程的线程表,当某个线程希望创建一个新线程或撤销一个已有线程时,他进行一个系统调用,这个系统调用通过对线程表的更新完成线程创建或撤销工作;
内核的线程表保存了每个线程的寄存器,状态和其他信息,这些信息和在用户空间中(在运行时系统中)的线程是由于的,但现在保存在内核中,这些信息是传统内核所围成的每个单线程进程信息的子集,内核还维护了传统的进程表,可以跟踪进程的状态;
混合线程
使用混合线程:使用内核级线程,然后将用户线程与某些或者全部内核线程多路复用起来:采用这样的方法,编程人员可以决定由多少个内核级线程和多少个用户级线程彼此多路复用,这个模型带来最大的灵活度
调度程序激活机制
虽然内核级线程在一些关键的地方优于用户级线程,但内核级线程的速度慢
**调度程序激活(scheduler activation)**机制工作的目标是模拟内核线程的功能,但是为线程包提供通常在用户空间才能实现的更好的性能和更大的灵活性。当使用调度程序激活机制时,内核给每个进程安排一定数量的虚拟的处理机,并且让运行时系统将线程分配到处理机上。
该机制工作的基本思路是,当内核了解到一个线程被阻塞后,内核通知该线程的运行时系统,并且在堆栈中以参数形式传递有问题的现成的编号和所发生时间的一个描述,一旦如此激活,运行时系统就重新调度其线程,这个过程通常是这样的:把当前线程标记为阻塞并从就绪表中取出另一线程,设置其寄存器,然后再启动之,稍后,当内核知道原来的线程又可运行时,内核就又一次上下调用运行时系统,通知它这一事件,此时运行时系统按照自己的判断,或者立即重新启动被阻塞的线程或者把它放入就绪表中稍后继续
弹出式线程
在该处理方式中,一个消息的到达导致系统创建一个处理该消息的线程,称为弹出式线程;
当有新的请求到达时,马上创建一个线程去处理这个请求。
这种线程新,没有历史—没有必须存储的寄存器,堆栈如此类的内容,每个线程从全新开始,每一个线程彼此都完全一样,这样可快速创建这类线程,对该新线程指定所要处理的消息,使用弹出式线程的结果是:消息到达与处理开始之间的时间非常短
使单线程代码多线程化
单线程代码所运行的系统,在内核里认为上层运行的程序是单线程进程,此时改为多线程进程,是用户级多线程。
全面禁止全局变量或为每一个线程赋予其私有的全局变量(解决全局变量问题);为每个过程提供一个包装器,为该包装器设置一个二进制位从而标识某个库处于处于使用中,在先前的调用还没有完成之前,任何试图使用该库的其他线程都会被阻塞(解决库过程不可重入问题);非线程专用问题较复杂;堆栈管理麻烦。
考虑到信号,如果是线程专用该如何解决?
堆栈,当一个进程有多个线程时,就必须有多个堆栈,如果内核不了解所有的堆栈,就不能自动增长,直到造成堆栈出错,实际上,内核有可能还没有意思到内存错误和某个线程栈的增长有关系;
进程间通信
进程经常需要与其他进程通信;
竞争关系
协作的进程可能共享一些彼此都能读写的公共存储区(内存,共享文件等)。
两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序,称为竞争条件(race condition)
临界区
要找出某种途径来阻止多个进程同时读写共享的数据:互斥:即以某种手段确定一个进程在使用一个共享变量或文件时,其他进程不能做同样的操作;
把共享内存进行访问的程序片段称作临界区域或临界区
避免的四个主要条件:
- 任何两个进程不能同时处于其临界区
- 不应对CPU速度和数量做任何假设
- 临界区外运行的进程不得阻塞其他进程
- 不得使进程无限期等待进入临界区
忙等待的互斥
实现互斥的几种方案
- 屏蔽中断:使每个进程在刚刚进入临界区后立即屏蔽所有中断,并在就要离开之前再打开中断。// 当一个进程进入临界区后立即屏蔽所有中断,离开再打开中断;屏蔽中断后,时钟中断也会被屏蔽,CPU只有发生时钟中断时,才会进行进程切换,再屏蔽中断之后CPU将不会被切换到其他进程;一旦某个进程屏蔽中断后,他就可以检查和修改共享内存,不必担心其他进程介入;
- 锁变量:设置一个共享(锁)变量,当一个进程想进入临界区时,先测试这把锁。
- 严格轮换法:连续测试一个变量直到某个值出现为止,称为忙等待。(这种方法浪费CPU时间,通常应该避免,只有在有理由认为等待时间是非常短的情况下才会使用忙等待)用于忙等待的锁,称为自旋锁(spin lock)。两个协作的并发进程轮流进入临界区。
- Peterson解法:一个不需要严格轮换的软件互斥算法,锁变量与警告变量的思想结合
- TSL指令:由硬件支持的一种方案,TSL指令锁住内存总线,禁止其他CPU在本指令结束之前访问内存;
睡眠与唤醒
Peterson解法和TSL或XCHG解法都正确,但是都有忙等待的确定,这些解法的本质是:当一个进程想进入临界区时,要先检查是否允许进入,若不允许,则该进程原地等待,直到允许为止;
**生产者-消费者(producer-consumer)问题,也称作有界缓冲区(bounded-buffer)**问题:两个进程共享一个公共的固定大小的缓冲区,其中的一个是生产者,将信息放入缓冲区,另一个是消费者,从缓冲区中取出信息,生产者向缓冲区内存放信息至满然后唤醒消费者并自我休眠,消费者从缓冲区中取出信息至空然后唤醒生产者并自我休眠。
信号量
使用一个整型变量来累计唤醒次数,供以后使用。引入一个新的变量类型,称作信号量(semaphore)。一个信号量的取值可以为0,也可以为正值。信号量的另一种用途是用于实现同步(synchronization),保证某种事件的顺序发生或不发生
信号量可以解决生产者-消费者之间的信号丢失的问题;
互斥量
互斥量仅仅适用于管理共享资源或一小段代码;
互斥量(mutex)是一个可以处于两态之一的变量:解锁和加锁,仅仅适用于管理共享资源或一小段代码,允许或阻塞进程对临界区的访问。
pthread还提供了称作**条件变量(condition variable)**的同步机制,允许线程由于一些未达到的条件而阻塞,(不像信号量)不会存在内存中,与互斥量经常一起使用。
条件变量于互斥量一起使用,这种模式让一个线程锁住了一个互斥量,然后当他不能获得他期待的结构时等一个条件变量,最后另一个线程会向他发信号;
管程
一个**管程(monitor)**是一个由过程、变量及数据结构等组成的一个集合,它们组成一个特殊的模块或软件包,任一时刻管程中只能有一个活跃进程,这一特性使得管程能有效地完成互斥。
消息传递
**消息传递(message passing)**是一种进程间通信方式,使用send与receive两条原语,send向一个给定的目标发送一条消息,receive从一个给定的源接收一条消息,如果没有消息可用,则接收者可能被阻塞,直到一条消息到达,或者,带着一个错误码立即返回。
屏障
屏障:当一个进程达到屏障时,他就被屏障阻拦,直到所有进程都到达屏障位置,屏障可用于一组进程同步
调度
当计算机系统由多道程序设计系统时,通常就由多个进程或线程同时竞争CPU,只要有两个或更多进程处于就绪状态,这种情况就会发生,如果只有一个CPU可用,就必须选择下一个要允许的进程,在操作系统中完成选择工作的这一部分是调度程序,该程序的算法就是调度算法
调度简介
**计算密集型(compute-bound)**进程具有较长时间的CPU集中使用和较小频度的I/O等待。
**I/O密集型(I/O-bound)**进程具有较短时间的CPU集中使用和频繁的I/O等待,越来越多的进程是这种类型。
-
进程行为:几乎所有进程的(磁盘/网络)IO请求和计算都是交替突发的
-
何时进行调度决策:
- 在创建一个新进程之后,需要决定的是运行父进程还是运行子进程;
- 在一个进程退出时必须做出调度决策;
- 当一个进程阻塞在I/O和信号量上或由于其他原因阻塞时,必须选择另一个进程运行;
- 在一个I/O中断发生时,必须做出调度决策。
-
调度算法分类: 调度程序的环境:(1)批处理 (2) 交互式 (3) 实时
-
调度算法的目标:
-
所有系统:
i. 公平——给每个进程公平的CPU份额
ii. 策略强制进行——看到所宣布的策略执行
iii. 平衡——保持系统的所有部分都忙碌 -
批处理系统:
i. 吞吐量——每小时最大作业数
ii. 周转时间——从提交到终止间的最小时间
iii. CPU利用率——保持CPU始终忙碌 -
交互式系统:
i. 响应时间——快速响应请求
ii. 均衡性——满足用户的期望 -
实时系统:
i. 满足截止时间——避免丢失数据
ii. 可预测性——在多媒体系统中避免品质降低
-
批处理系统中的调度
- 先来先服务:进程按照他们请求CPU的顺序使用CPU,有一个就绪进程的单一队列
- 最短作业优先:适用于运行时间可以预知的另一个非抢占式的批处理调度算法,总是运行时间从短到长的依次运行
- 最短剩余时间优先:最短作业优先的抢占式版本是最短剩余时间优先算法,使用这个算法,调度程序总是选择剩余运行时间最短的那个进程运行
交互式系统中的调度
交互式在个人计算机,服务器和其他类系统中常用
轮转调度
每个进程被分配一个时间段,称为时间片(quantum),即允许该进程在该时间段中运行,如果在时间片结束时该进程还在运行,则将剥夺CPU并分配给另一个进程,如果该进程在时间片结束前阻塞或结束,则CPU立即进行切换。
最古老、最简单、最公平且使用最广的调度算法。
进程切换(process switch)有时称为上下文切换(context switch)。
优先级调度
使相同优先级进程进行轮转,只有较高优先级进程全部运行完毕,才会转入下一优先级进程的轮转,防止高优先级一直运行下去,调度程序可能在每个时钟降低当前进程的优先级,或者给每个进程赋予一个运行运行最大时间片,当用完这个时间片,次高优先级获得运行机会
多级队列
使优先级由高到低的进程进行轮转,高优先级分配的时间片较长,当一个时间片结束,立即转入下一个优先级进行一个时间片的运行
最短进程优先
当前需要运行时间最短的进程优先。
保证调度
向用户作出明确的性能保证,并去实现它。如果 n 个进程同时运行,公平的情况下每进程应该获得处理机时间的 1/n。
彩票调度
基本思想是为进程发放针对系统各种资源(如CPU时间)的彩票。当调度程序需作出决策时,随机选择一张彩票,持有该彩票的进程将获得系统资源。
公平分享调度
针对用户而不是进程,使得每用户获得相同的处理机时间。
实时系统中的调度
实时系统是一种时间起着主导作用的系统,典型的,一种或者多种外部设备发给计算机一个服务请求,而计算机必须在一个确定的时间范围内恰当的做出反应
实时系统还分为硬实时和软实时,前者满足绝对的截至时间,后者不希望偶尔错失截止时间,但是可以容忍
策略和机制
将**调度机制(scheduling mechanism)与调度策略(scheduling policy)**分离,也就是将调度算法以某种形式参数化,而参数可以由用户进程填写。
线程调度
当若干进程都有多个线程时,就存在两个层次的并行:进程和线程。在这样的系统中调度处理有本质差别,这取决于所支持的是用户级线程(或两者都支持)。
用户级线程和内核级线程之间的差别在于性能。
小结
进程可以动态地创建和终止。每个进程都有自己的地址空间。
每个线程有自己的堆栈,但是在一个进程中所有线程共享一个公共地址空间。线程可以在用户空间或内核中实现。
进程间通过进程间通信原语彼此通信。
将调度策略和调度机制清晰地分离,可以使用户对调度算法进行控制