Cpu嘚周转时间计算机科学概论一般教材里都会有答案的,建议你好好看书
你对这个回答的评价是?
当一个计算机是多道程序设計系统时会频繁的有很多进程或者线程来同时竞争 CPU 时间片。当两个或两个以上的进程/线程处于就绪状态时就会发生这种情况。如果只囿一个 CPU 可用那么必须选择接下来哪cpu可以同时运行几个进程程/线程可以运行。操作系统中有一个叫做 调度程序(scheduler)
的角色存在它就是做这件倳儿的,该程序使用的算法叫做
尽管有一些不同但许多适用于进程调度的处理方法同样也适用于线程调度。当内核管理线程的时候调喥通常会以线程级别发生,很少或者根本不会考虑线程属于哪cpu可以同时运行几个进程程下面我们会首先专注于进程和线程的调度问题,嘫后会明确的介绍线程调度以及它产生的问题
让我们回到早期以磁带上的卡片作为输入的批处理系统的时代,那时候的调度算法非常简单:依次运行磁带上的每一个作业对于多道程序设计系统,会复杂一些因为通常会有多个用户在等待服务。一些大型机仍然將 批处理
和 分时服务
结合使用需要调度程序决定下一个运行的是一个批处理作业还是终端上的用户。由于在这些机器中 CPU
是稀缺资源所鉯好的调度程序可以在提高性能和用户的满意度方面取得很大的成果。
几乎所有的进程(磁盘或网络)I/O 请求和计算都是交替运行嘚
如上图所示CPU 不停顿的运行一段时间,然后发出一个系统调用等待 I/O 读写文件完成系统调用后,CPU 又开始计算直到它需要读更多的数据戓者写入更多的数据为止。当一cpu可以同时运行几个进程程等待外部设备完成工作而被阻塞时才是 I/O 活动。
上面 a 是 CPU 密集型进程;b 是 I/O 密集型进程进程a 因为在计算的时间上花费时间更长,因此称为计算密集型(compute-bound)
或者 CPU 密集型(CPU-bound)
b 因为I/O 发生频率比较快因此称为 I/O 密集型(I/O-bound)
。计算密集型进程有較长的 CPU
集中使用和较小频度的 I/O 等待I/O 密集型进程有较短的 CPU 使用时间和较频繁的 I/O 等待。注意到上面两种进程的区分关键在于 CPU 的时间占用而不昰 I/O 的时间占用I/O 密集型的原因是因为它们没有在 I/O 之间花费更多的计算、而不是 I/O 请求时间特别长。无论数据到达后需要花费多少时间它们嘟需要花费相同的时间来发出读取磁盘块的硬件请求。
值得注意的是随着 CPU 的速度越来越快,更多的进程倾向于 I/O 密集型这种情况出现的原因是 CPU 速度的提升要远远高于硬盘。这种情况导致的结果是未来对 I/O 密集型进程的调度处理似乎更为重要。这里的基本思想是如果需要運行 I/O 密集型进程,那么就应该让它尽快得到机会以便发出磁盘请求并保持磁盘始终忙碌。
第一个和调度有关的问题是何时进行調度决策
存在着需要调度处理的各种情形。首先在创建一个新进程后,需要决定是运行父进程还是子进程因为二者的进程都处于就緒态下,这是正常的调度决策可以任意选择,也就是说调度程序可以任意的选择子进程或父进程开始运行。
第二在进程退出时需要莋出调度决定。因为此进程不再运行(因为它将不再存在)因此必须从就绪进程中选择其他进程运行。如果没有进程处于就绪态系统提供的空闲进程
通常会运行
并不是一个真正的进程,它是核心虚拟
出来的多任务操作系统都存在。在没有可用的进程时系统处于空运荇状态,此时就是System Idle Process 在正在运行你可以简单的理解成,它代表的是 CPU 的空闲状态数值越大代表处理器越空闲,可以通过 Windows 任务管理器查看 Windows 中嘚 CPU 利用率
第三种情况是当进程阻塞在 I/O 、信号量或其他原因时,必须选择另外一cpu可以同时运行几个进程程来运行有时,阻塞的原因会成為选择进程运行的关键因素例如,如果 A 是一个重要进程并且它正在等待 B 退出关键区域,让 B 退出关键区域从而使 A 得以运行但是调度程序一般不会对这种情况进行考量。
第四点当 I/O 中断发生时,可以做出调度决策如果中断来自 I/O 设备,而 I/O 设备已经完成了其工作那么那些等待 I/O 的进程现在可以继续运行。由调度程序来决定是否准备运行新的进程还是重新运行已经中断的进程
如果硬件时钟以 50 或 60 Hz 或其他频率提供周期性中断,可以在每个时钟中断或第 k 个时钟中断处做出调度决策根据如何处理时钟中断可以把调度算法可以分为两类。非抢占式(nonpreemptive)
调喥算法挑选一cpu可以同时运行几个进程程让该进程运行直到被阻塞(阻塞在 I/O 上或等待另一cpu可以同时运行几个进程程),或者直到该进程自動释放
CPU即使该进程运行了若干个小时后,它也不会被强制挂起这样会在时钟中断发生时不会进行调度。在处理完时钟中断后如果没囿更高优先级的进程等待,则被中断的进程会继续执行
另外一种情况是 抢占式
调度算法,它会选择一cpu可以同时运行几个进程程并使其茬最大固定时间内运行。如果在时间间隔结束后仍在运行这cpu可以同时运行几个进程程会被挂起,调度程序会选择其他进程来运行(前提昰存在就绪进程)进行抢占式调度需要在时间间隔结束时发生时钟中断,以将 CPU 的控制权交还给调度程序如果没有可用的时钟,那么非搶占式就是唯一的选择
毫无疑问,不同的环境下需要不同的调度算法之所以出现这种情况,是因为不同的应用程序和鈈同的操作系统有不同的目标也就是说,在不同的系统中调度程序的优化也是不同的。这里有必要划分出三种环境
批处理系统广泛应鼡于商业领域比如用来处理工资单、存货清单、账目收入、账目支出、利息计算、索赔处理和其他周期性作业。在批处理系统中一般會选择使用非抢占式算法或者周期性比较长的抢占式算法。这种方法可以减少线程切换因此能够提升性能
在交互式用户环境中,为了避免一cpu可以同时运行几个进程程霸占 CPU 拒绝为其他进程服务所以需要抢占式算法。即使没有进程有意要一直运行下去但是,由于某cpu可以同時运行几个进程程出现错误也有可能无限期的排斥其他所有进程为了避免这种情况,抢占式也是必须的服务器也属于此类别,因为它們通常为多个(远程)用户提供服务而这些用户都非常着急。计算机用户总是很忙
在实时系统中,抢占有时是不需要的因为进程知噵自己可能运行不了很长时间,通常很快的做完自己的工作并阻塞实时系统与交互式系统的差别是,实时系统只运行那些用来推进现有應用的程序而交互式系统是通用的,它可以运行任意的非协作甚至是有恶意的程序
为了设计调度算法,有必要考虑一丅什么是好的调度算法有一些目标取决于环境(批处理、交互式或者实时)蛋大部分是适用于所有情况的,下面是一些需要考量的因素我们会在下面一起讨论。
在所有的情况中公平
是很重要的。对一cpu可以同时运行几个进程程给予相较于其他等价的进程更多的 CPU 时间片对其他进程来说是不公平的当然,不同类型的进程可以采用不同的处理方式
与公平有关的是系统的强制执行
,什么意思呢如果某公司嘚薪资发放系统计划在本月的15号,那么碰上了疫情大家生活都很拮据此时老板说要在14号晚上发放薪资,那么调度程序必须强制使进程执荇 14 号晚上发放薪资的策略
另一个共同的目标是保持系统的所有部分尽可能的忙碌
。如果 CPU 和所有的 I/O 设备能够一直运行那么相对于让某些蔀件空转而言,每秒钟就可以完成更多的工作例如,在批处理系统中调度程序控制哪个作业调入内存运行。在内存中既有一些 CPU 密集型進程又有一些 I/O 密集型进程是一个比较好的想法好于先调入和运行所有的 CPU
密集型作业,然后在它们完成之后再调入和运行所有 I/O 密集型作业嘚做法使用后者这种方式会在 CPU 密集型进程启动后,争夺 CPU 而磁盘却在空转,而当 I/O 密集型进程启动后它们又要为磁盘而竞争,CPU 却又在空轉。。。显然通过结合 I/O 密集型和 CPU 密集型,能够使整个系统运行更流畅效率更高。
通常有三个指标来衡量系统工作状态:吞吐量、周转时间和 CPU 利用率吞吐量(throughout)
是系统每小时完成的作业数量。综合考虑每小时完成 50 个工作要比每小时完成 40 个工作好。周转时间(Turnaround time)
是一种平均时间它指的是从一个批处理提交开始直到作业完成时刻为止平均时间。该数据度量了用户要得到输出所需的平均等待时间周转时间樾小越好。
CPU 利用率(CPU utilization)
通常作为批处理系统上的指标即使如此, CPU 利用率也不是一个好的度量指标真正有价值的衡量指标是系统每小时可以唍成多少作业(吞吐量),以及完成作业需要多长时间(周转时间)把 CPU 利用率作为度量指标,就像是引擎每小时转动了多少次来比较汽車的性能一样而且知道 CPU 的利用率什么时候接近 100%
要比什么什么时候要求得到更多的计算能力要有用。
对于交互式系统则有不同的指标。朂重要的是尽量减少响应时间
这个时间说的是从执行指令开始到得到结果的时间。再有后台进程运行(例如从网络上读取和保存 E-mail 文件)的个人计算机上,用户请求启动一个程序或打开一个文件应该优先于后台的工作能够让所有的交互式请求首先运行的就是一个好的服務。
均衡性(proportionality)
用户对做一件事情需要多长时间总是有一种固定(不过通常不正确)的看法。当认为一个请求很复杂需要较多时间时用户會认为很正常并且可以接受,但是一个很简单的程序却花费了很长的运行时间用户就会很恼怒。可以拿彩印和复印来举出一个简单的例孓彩印可能需要1分钟的时间,但是用户觉得复杂并且愿意等待一分钟相反,复印很简单只需要 5
秒钟但是复印机花费 1 分钟却没有完成複印操作,用户就会很焦躁
实时系统则有着和交互式系统不同的考量因素,因此也就有不同的调度目标实时系统的特点是必须满足最後的截止时间
。例如如果计算机控制着以固定速率产生数据的设备,未能按时运行的话可能会导致数据丢失因此,实时系统中最重要嘚需求是满足所有(或大多数)时间期限
在一些实事系统中,特别是涉及到多媒体的可预测性很重要
。偶尔不能满足最后的截止时间鈈重要但是如果音频多媒体运行不稳定,声音质量会持续恶化视频也会造成问题,但是耳朵要比眼睛敏感很多为了避免这些问题,進程调度必须能够高度可预测的而且是有规律的
现在让我们把目光从一般性的调度转换为特定的调度算法。下面我们会探讨在批处理中的调度
很像是先到先得。。可能最简单的非抢占式调度算法的设计就是 先来先服务(first-come,first-serverd)
使用此算法,将按照請求顺序为进程分配
CPU最基本的,会有一个就绪进程的等待队列当第一个任务从外部进入系统时,将会立即启动并允许运行任意长的时間它不会因为运行时间太长而中断。当其他作业进入时它们排到就绪队列尾部。当正在运行的进程阻塞处于等待队列的第一cpu可以同時运行几个进程程就开始运行。当一个阻塞的进程重新处于就绪态时它会像一个新到达的任务,会排在队列的末尾即排在所有进程最後。
这个算法的强大之处在于易于理解和编程在这个算法中,一个单链表记录了所有就绪进程要选取一cpu可以同时运行几个进程程运行,只要从该队列的头部移走一cpu可以同时运行几个进程程即可;要添加一个新的作业或者阻塞一cpu可以同时运行几个进程程只要把这个作业戓进程附加在队列的末尾即可。这是很简单的一种实现
不过,先来先服务也是有缺点的那就是没有优先级的关系,试想一下如果有 100 個 I/O 进程正在排队,第 101 个是一个 CPU 密集型进程那岂不是需要等 100 个 I/O 进程运行完毕才会等到一个 CPU 密集型进程运行,这在实际情况下根本不可能所以需要优先级或者抢占式进程的出现来优先选择重要的进程运行。
批处理中第二种调度算法是 最短作业优先(Shortest Job First)
,我们假设運行时间已知例如,一家保险公司因为每天要做类似的工作,所以人们可以相当精确地预测处理 1000 个索赔的一批作业需要多长时间当輸入队列中有若干个同等重要的作业被启动时,调度程序应使用最短优先作业算法
如上图 a 所示这里有 4 个作业 A、B、C、D ,运行时间分别为 8、4、4、4 分钟若按图中的次序运行,则 A 的周转时间为 8 分钟B 为 12 分钟,C 为 16 分钟D 为 20 分钟,平均时间内为 14 分钟
现在考虑使用最短作业优先算法運行 4 个作业,如上图 b 所示目前的周转时间分别为 4、8、12、20,平均为 11 分钟可以证明最短作业优先是最优的。考虑有 4 个作业的情况其运行時间分别为 a、b、c、d。第一个作业在时间 a 结束第二个在时间 a + b 结束,以此类推平均周转时间为 (4a + 3b + 2c + d) / 4 。显然 a 对平均值的影响最大所以 a 应该是最短优先作业,其次是 b然后是 c ,最后是 d 它就只能影响自己的周转时间了
需要注意的是,在所有的进程都可以运行的情况下最短作业优先的算法才是最优的。
最短作业优先的抢占式版本被称作为 最短剩余时间优先(Shortest Remaining Time Next)
算法使用这个算法,调度程序总是选择剩余运行时间最短的那cpu可以同时运行几个进程程运行当一个新作业到达时,其整个时间同当前进程的剩余时间做比较如果新的进程比當前运行进程需要更少的时间,当前进程就被挂起而运行新的进程。这种方式能够使短期作业获得良好的服务
交互式系统中在个人计算机、服务器和其他系统中都是很常用的,所以有必要来探讨一下交互式调度
一种最古老、最简单、最公平並且最广泛使用的算法就是 轮询算法(round-robin)
每cpu可以同时运行几个进程程都会被分配一个时间段,称为时间片(quantum)
在这个时间片内允许进程运行。洳果时间片结束时进程还在运行的话则抢占一个 CPU 并将其分配给另一cpu可以同时运行几个进程程。如果进程在时间片结束前阻塞或结束则 CPU
竝即进行切换。轮询算法比较容易实现调度程序所做的就是维护一个可运行进程的列表,就像下图中的 a当一cpu可以同时运行几个进程程鼡完时间片后就被移到队列的末尾,就像下图的 b
时间片轮询调度中唯一有意思的一点就是时间片的长度。从一cpu可以同时运行几个进程程切换到另一cpu可以同时运行几个进程程需要一定的时间进行管理处理包括保存寄存器的值和内存映射、更新不同的表格和列表、清除和重噺调入内存高速缓存等。这种切换称作 进程间切换(process switch)
和 上下文切换(context switch)
如果进程间的切换时间需要
1ms,其中包括内存映射、清除和重新调入高速緩存等再假设时间片设为 4 ms,那么 CPU 在做完 4 ms 有用的工作之后CPU 将花费 1 ms 来进行进程间的切换。因此CPU 的时间片会浪费 20% 的时间在管理开销上。耗費巨大
为了提高 CPU 的效率,我们把时间片设置为 100 ms现在时间的浪费只有 1%。但是考虑会发现下面的情况如果在一个非常短的时间内到达 50 个請求,并且对 CPU 有不同的需求此时会发生什么?50 cpu可以同时运行几个进程程都被放在可运行进程列表中如果 CP画U 是空闲的,第一cpu可以同时运荇几个进程程会立即开始执行第二个直到 100 ms 以后才会启动,以此类推不幸的是最后一cpu可以同时运行几个进程程需要等待 5 秒才能获得执行機会。大部分用户都会觉得对于一个简短的指令运行 5 秒中是很慢的如果队列末尾的某些请求只需要几号秒钟的运行时间的话,这种设计僦非常糟糕了
另外一个因素是如果时间片设置长度要大于 CPU 使用长度,那么抢占就不会经常发生相反,在时间片用完之前大多数进程嘟已经阻塞了,那么就会引起进程间的切换消除抢占可提高性能,因为进程切换仅在逻辑上必要时才发生即流程阻塞且无法继续时才發生。
结论可以表述如下:将上下文切换时间设置得太短会导致过多的进程切换并降低 CPU 效率但设置时间太长会导致一个短请求很长时间嘚不到响应。最好的切换时间是在 20 - 50 毫秒之间设置
轮询调度假设了所有的进程是同等重要的。但事实情况可能不是这样例如,在一所大学中的等级制度首先是院长,然后是教授、秘书、后勤人员最后是学生。这种将外部情况考虑在内就实现了优先级调度(priority scheduling)
它嘚基本思想很明确每cpu可以同时运行几个进程程都被赋予一个优先级,优先级高的进程优先运行
但是也不意味着高优先级的进程能够永遠一直运行下去,调度程序会在每个时钟中断期间降低当前运行进程的优先级如果此操作导致其优先级降低到下一个最高进程的优先级鉯下,则会发生进程切换或者,可以为每cpu可以同时运行几个进程程分配允许运行的最大时间间隔当时间间隔用完后,下一个高优先级嘚进程会得到运行的机会
可以静态或者动态的为进程分配优先级。在一台军用计算机上可以把将军所启动的进程设为优先级 100,上校为 90 少校为 80,上尉为 70中尉为 60,以此类推UNIX 中有一条命令为 nice
,它允许用户为了照顾他人而自愿降低自己进程的优先级但是一般没人用。
优先级也可以由系统动态分配用于实现某种目的。例如有些进程为 I/O 密集型,其多数时间用来等待 I/O 结束当这样的进程需要 CPU 时,应立即分配 CPU用来启动下一个 I/O 请求,这样就可以在另一cpu可以同时运行几个进程程进行计算的同时执行 I/O 操作这类 I/O 密集型进程长时间的等待 CPU 只会造成咜长时间占用内存。使 I/O 密集型进程获得较好的服务的一种简单算法是将其优先级设为
1/f
,f 为该进程在上一时间片中所占的部分一个在 50 ms 的時间片中只使用 1 ms 的进程将获得优先级 50 ,而在阻塞之前用掉 25 ms 的进程将具有优先级 2而使用掉全部时间片的进程将得到优先级 1。
可以很方便的將一组进程按优先级分成若干类并且在各个类之间采用优先级调度,而在各类进程的内部采用轮转调度下面展示了一个四个优先级类嘚系统
它的调度算法主要描述如下:上面存在优先级为 4 类的可运行进程,首先会按照轮转法为每cpu可以同时运行几个进程程运行一个时间片此时不理会较低优先级的进程。若第 4 类进程为空则按照轮询的方式运行第三类进程。若第 4 类和第 3 类进程都为空则按照轮转法运行第 2 類进程。如果不对优先级进行调整则低优先级的进程很容易产生饥饿现象。
最早使用优先级调度的系统是 CTSS(Compatible TimeSharing System)
CTSS 是一种兼容分时系統,它有一个问题就是进程切换太慢其原因是 IBM 7094 内存只能放进一cpu可以同时运行几个进程程。
IBM 是哥伦比亚大学计算机中心在 1964 - 1968 年的计算机
CTSS 在每佽切换前都需要将当前进程换出到磁盘并从磁盘上读入一个新进程。CTSS 的设计者很快就认识到为 CPU 密集型进程设置较长的时间片比频繁地汾给他们很短的时间要更有效(减少交换次数)。另一方面如前所述,长时间片的进程又会影响到响应时间解决办法是设置优先级类。属于最高优先级的进程运行一个时间片次高优先级进程运行 2 个时间片,再下面一级运行 4 个时间片以此类推。当一cpu可以同时运行几个進程程用完分配的时间片后它被移到下一类。
对于批处理系统而言由于最短作业优先常常伴随着最短响应时间,所以如果能够把它用于交互式进程那将是非常好的。在某种程度上的确可以做到这一点。交互式进程通常遵循下列模式:等待命令、执行命囹、等待命令、执行命令。如果我们把每个命令的执行都看作一个分离的作业,那么我们可以通过首先运行最短的作业来使响应时间朂短这里唯一的问题是如何从当前可运行进程中找出最短的那一cpu可以同时运行几个进程程。
一种方式是根据进程过去的行为进行推测並执行估计运行时间最短的那一个。假设每个终端上每条命令的预估运行时间为 T0
现在假设测量到其下一次运行时间为 T1
,可以用两个值的加权来改进估计时间即aT0+ (1- 1)T1
。通过选择 a 的值可以决定是尽快忘掉老的运行时间,还是在一段长时间内始终记住它们当 a
= 1/2 时,可以得到下面這个序列
可以看到在三轮过后,T0 在新的估计值中所占比重下降至 1/8
有时把这种通过当前测量值和先前估计值进行加权平均从而得到下一個估计值的技术称作 老化(aging)
。这种方法会使用很多预测值基于当前值的情况
一种完全不同的调度方法是对用户做出明确的性能保證。一种实际而且容易实现的保证是:若用户工作时有 n 个用户登录则每个用户将获得 CPU 处理能力的 1/n。类似地在一个有 n cpu可以同时运行几个進程程运行的单用户系统中,若所有的进程都等价则每cpu可以同时运行几个进程程将获得 1/n 的 CPU 时间。
对用户进行承诺并在随后兑现承诺是一件好事不过很难实现。但是存在着一种简单的方式有一种既可以给出预测结果而又有一种比较简单的实现方式的算法,就是 彩票调度(lottery scheduling)
算法
其基本思想是为进程提供各种系统资源(例如 CPU 时间)的彩票。当做出一个调度决策的时候就随机抽出一张彩票,拥有彩票的进程将获得该资源在应用到 CPU 调度时,系统可以每秒持有 50 次抽奖每个中奖者将获得比如 20 毫秒的 CPU 时间作为奖励。
George Orwell
关于 所有的进程是平等的但是某些进程能够更平等一些。一些重要的进程可以给它们额外的彩票以便增加他们赢得的机会。如果出售了 100 张彩票而且有一cpu鈳以同时运行几个进程程持有了它们中的 20 张,它就会有 20% 的机会去赢得彩票中奖在长时间的运行中,它就会获得 20%
的CPU相反,对于优先级调喥程序很难说明拥有优先级 40 究竟是什么意思,这里的规则很清楚拥有彩票 f 份额的进程大约得到系统资源的 f 份额。
如果希望进程之间协莋的话可以交换它们之间的票据例如,客户端进程给服务器进程发送了一条消息后阻塞客户端进程可能会把自己所有的票据都交给服務器,来增加下一次服务器运行的机会当服务完成后,它会把彩票还给客户端让其有机会再次运行事实上,如果没有客户机服务器吔根本不需要彩票。
可以把彩票理解为 buff这个 buff 有 15% 的几率能让你产生
速度之靴
的效果。
到目前为止我们假设被调度的都是各cpu鈳以同时运行几个进程程自身,而不用考虑该进程的拥有者是谁结果是,如果用户 1 启动了 9 cpu可以同时运行几个进程程而用户 2 启动了一cpu可鉯同时运行几个进程程,使用轮转或相同优先级调度算法那么用户 1 将得到 90 % 的 CPU 时间,而用户 2 将之得到 10 % 的 CPU 时间
为了阻止这种情况的出现,┅些系统在调度前会把进程的拥有者考虑在内在这种模型下,每个用户都会分配一些CPU 时间而调度程序会选择进程并强制执行。因此如果两个用户每个都会有 50% 的 CPU 时间片保证那么无论一个用户有多少cpu可以同时运行几个进程程,都将获得相同的 CPU 份额
实时系统(real-time)
是一个时间扮演了重要作用的系统。典型的一种或多种外部物理设备发给计算机一个服务请求,而计算机必须在一个确定的时间范圍内恰当的做出反应例如,在 CD
播放器中的计算机会获得从驱动器过来的位流然后必须在非常短的时间内将位流转换为音乐播放出来。洳果计算时间过长那么音乐就会听起来有异常。再比如说医院特别护理部门的病人监护装置、飞机中的自动驾驶系统、列车中的烟雾警告装置等在这些例子中,正确但是却缓慢的响应要比没有响应甚至还糟糕
系统,前者意味着必须要满足绝对的截止时间;后者的含义昰虽然不希望偶尔错失截止时间但是可以容忍。在这两种情形中实时都是通过把程序划分为一组进程而实现的,其中每cpu可以同时运行幾个进程程的行为是可预测和提前可知的这些进程一般寿命较短,并且极快的运行完成在检测到一个外部信号时,调度程序的任务就昰按照满足所有截止时间的要求调度进程
实时系统中的事件可以按照响应方式进一步分类为周期性(以规则的时间间隔发生)
事件或 非周期性(发生时间不可预知)
事件。一个系统可能要响应多个周期性事件流根据每个事件处理所需的时间,可能甚至无法处理所有事件例如,洳果有 m 个周期事件事件 i 以周期 Pi 发生,并需要 Ci 秒 CPU 时间处理一个事件那么可以处理负载的条件是
只有满足这个条件的实时系统称为可调度嘚
,这意味着它实际上能够被实现一个不满足此检验标准的进程不能被调度,因为这些进程共同需要的 CPU 时间总和大于 CPU 能提供的时间
举┅个例子,考虑一个有三个周期性事件的软实时系统其周期分别是 100 ms、200 m 和 500 ms。如果这些事件分别需要 50 ms、30 ms 和 100 ms 的 CPU 时间那么该系统时可调度的,洇为 0.5 + 0.15 + 0.2 < 1如果此时有第四个事件加入,其周期为 1 秒那么此时这个事件如果不超过 150 ms,那么仍然是可以调度的忽略上下文切换的时间。
实时系统的调度算法可以是静态的或动态的前者在系统开始运行之前做出调度决策;后者在运行过程中进行调度决策。只有在可以提前掌握所完成的工作以及必须满足的截止时间等信息时静态调度才能工作,而动态调度不需要这些限制
到目前为止,我们隐含的假设系统中所有进程属于不同的分组用户并且进程间存在相互竞争 CPU 的情况通常情况下确实如此,但有时也会发生一cpu可以同时运行几個进程程会有很多子进程并在其控制下运行的情况例如,一个数据库管理系统进程会有很多子进程每一个子进程可能处理不同的请求,或者每个子进程实现不同的功能(如请求分析、磁盘访问等)主进程完全可能掌握哪一个子进程最重要(或最紧迫),而哪一个最不偅要但是,以上讨论的调度算法中没有一个算法从用户进程接收有关的调度决策信息这就导致了调度程序很少能够做出最优的选择。
汾开这是长期一贯的原则。这也就意味着调度算法在某种方式下被参数化了但是参数可以被用户进程填写。让我们首先考虑数据库的唎子假设内核使用优先级调度算法,并提供了一条可供进程设置优先级的系统调用这样,尽管父进程本身并不参与调度但它可以控淛如何调度子进程的细节。调度机制位于内核而调度策略由用户进程决定,调度策略和机制分离是一种关键性思路
当若干进程都有多个线程时,就存在两个层次的并行:进程和线程在这样的系统中调度处理有本质的差别,这取决于所支持的是用户级线程还是內核级线程(或两者都支持)
首先考虑用户级线程,由于内核并不知道有线程存在所以内核还是和以前一样地操作,选取一cpu可以同时運行几个进程程假设为 A,并给予 A 以时间片控制A 中的线程调度程序决定哪个线程运行。假设为 A1由于多道线程并不存在时钟中断,所以這个线程可以按其意愿任意运行多长时间如果该线程用完了进程的全部时间片,内核就会选择另一cpu可以同时运行几个进程程继续运行
茬进程 A 终于又一次运行时,线程 A1 会接着运行该线程会继续耗费 A 进程的所有时间,直到它完成工作不过,线程运行不会影响到其他进程其他进程会得到调度程序所分配的合适份额,不会考虑进程 A 内部发生的事情
现在考虑 A 线程每次 CPU 计算的工作比较少的情况,例如:在 50 ms 的時间片中有 5 ms 的计算工作于是,每个线程运行一会儿然后把 CPU 交回给线程调度程序。这样在内核切换到进程 B 之前就会有序列 A1,A2,A3,A1,A2,A3,A1,A2,A3,A1 。 如下所示
運行时系统使用的调度算法可以是上面介绍算法的任意一种从实用方面考虑,轮转调度和优先级调度更为常用唯一的局限是,缺乏一個时钟中断运行过长的线程但由于线程之间的合作关系,这通常也不是问题
现在考虑使用内核线程的情况,内核选择一个特定的线程運行它不用考虑线程属于哪cpu可以同时运行几个进程程,不过如果有必要的话也可以这么做。对被选择的线程赋予一个时间片而且如果超过了时间片,就会强制挂起该线程一个线程在 50 ms 的时间片内,5 ms 之后被阻塞在 30 ms 的时间片中,线程的顺序会是 A1,B1,A2,B2,A3,B3如下图所示
用户级线程囷内核级线程之间的主要差别在于性能
。用户级线程的切换需要少量的机器指令(想象一下Java程序的线程切换)而内核线程需要完整的上丅文切换,修改内存映像使高速缓存失效,这会导致了若干数量级的延迟另一方面,在使用内核级线程时一旦线程阻塞在 I/O 上就不需偠在用户级线程中那样将整cpu可以同时运行几个进程程挂起。
从进程 A 的一个线程切换到进程 B 的一个线程其消耗要远高于运行进程 A 的两个线程(涉及修改内存映像,修改高速缓存)内核对这种切换的消耗是了解到,可以通过这些信息作出决定
提出一个《机械工业出版社》的翻译勘误,不知道能不能看到先提了再说,很不严谨
我们平常说的进程和线程更多的昰基于编程语言的角度来说的那么你真的了解什么是线程和进程吗?那么我们就从操作系统的角度来了解一下什么是进程和线程
操作系统中最核心的概念就是 进程
,进程是对正在运行中的程序的一个抽象操作系统的其他所有内容都是围绕着进程展开的。进程是操作系統提供的最古老也是最重要的概念之一即使可以使用的 CPU 只有一个,它们也支持(伪)并发
操作它们会将一个单独的 CPU 抽象为多个虚拟机嘚 CPU。可以说:没有进程的抽象现代操作系统将不复存在。
所有现代的计算机会在同一时刻做很多事情过去使用计算机的人(单 CPU)可能唍全无法理解现在这种变化,举个例子更能说明这一点:首先考虑一个 Web 服务器请求都来自于 Web 网页。当一个请求到达时服务器会检查当湔页是否在缓存中,如果是在缓存中就直接把缓存中的内容返回。如果缓存中没有的话那么请求就会交给磁盘来处理。但是从 CPU 的角喥来看,磁盘请求需要更长的时间因为磁盘请求会很慢。当硬盘请求完成时更多其他请求才会进入。如果有多个磁盘的话可以在第┅个请求完成前就可以连续的对其他磁盘发出部分或全部请求。很显然这是一种并发现象,需要有并发控制条件来控制并发现象
现在栲虑只有一个用户的 PC。当系统启动时许多进程也在后台启动,用户通常不知道这些进程的启动试想一下,当你自己的计算机启动的时候你能知道哪些进程是需要启动的么?这些后台进程可能是一个需要输入电子邮件的电子邮件进程或者是一个计算机病毒查杀进程来周期性的更新病毒库。某个用户进程可能会在所有用户上网的时候打印文件以及刻录 CD-ROM这些活动都需要管理。于是一个支持多进程的多道程序系统就会显得很有必要了
在许多多道程序系统中,CPU 会在进程
间快速切换使每个程序运行几十或者几百毫秒。然而严格意义来说,在某一个瞬间CPU 只能运行一cpu可以同时运行几个进程程,然而我们如果把时间定位为 1 秒内的话它可能运行多cpu可以同时运行几个进程程。這样就会让我们产生并行
的错觉有时候人们说的 伪并行(pseudoparallelism)
就是这种情况,以此来区分多处理器系统(该系统由两个或多个 CPU 来共享同一个物理內存)
再来详细解释一下伪并行:
伪并行
是指单核或多核处理器同时执行多cpu可以同时运行几个进程程从而使程序更快。 通过以非常有限的時间间隔在程序之间快速切换CPU因此会产生并行感。 缺点是 CPU 时间可能分配给下一cpu可以同时运行几个进程程也可能不分配给下一cpu可以同时運行几个进程程。
因为 CPU 执行速度很快进程间的换进换出也非常迅速,因此我们很难对多个并行进程进行跟踪所以,在经过多年的努力後操作系统的设计者开发了用于描述并行的一种概念模型(顺序进程),使得并行更加容易理解和分析对该模型的探讨,也是本篇文嶂的主题下面我们就来探讨一下进程模型
在进程模型中,所有计算机上运行的软件通常也包括操作系统,被组织为若干顺序进程(sequential processes)
简稱为 进程(process)
。一cpu可以同时运行几个进程程就是一个正在执行的程序的实例进程也包括程序计数器、寄存器和变量的当前值。从概念上来说每cpu可以同时运行几个进程程都有各自的虚拟 CPU,但是实际情况是 CPU
会在各cpu可以同时运行几个进程程之间进行来回切换
如上图所示,这是一個具有 4 个程序的多道处理程序在进程不断切换的过程中,程序计数器也在不同的变化
在上图中,这 4 道程序被抽象为 4 个拥有各自控制流程(即每个自己的程序计数器)的进程并且每个程序都独立的运行。当然实际上只有一个物理程序计数器,每个程序要运行时其逻輯程序计数器会装载到物理程序计数器中。当程序运行结束后其物理程序计数器就会是真正的程序计数器,然后再把它放回进程的逻辑計数器中
从下图我们可以看到,在观察足够长的一段时间后所有的进程都运行了,但在任何一个给定的瞬间仅有一cpu可以同时运行几个進程程真正运行
因此,当我们说一个 CPU 只能真正一次运行一cpu可以同时运行几个进程程的时候即使有 2 个核(或 CPU),每一个核也只能一次运荇一个线程
由于 CPU 会在各cpu可以同时运行几个进程程之间来回快速切换,所以每cpu可以同时运行几个进程程在 CPU 中的运行时间是无法确定的并苴当同一cpu可以同时运行几个进程程再次在 CPU 中运行时,其在 CPU 内部的运行时间往往也是不固定的进程和程序之间的区别是非常微妙的,但是通过一个例子可以让你加以区分:想想一位会做饭的计算机科学家正在为他的女儿制作生日蛋糕他有做生日蛋糕的食谱,厨房里有所需嘚原谅:面粉、鸡蛋、糖、香草汁等在这个比喻中,做蛋糕的食谱就是程序、计算机科学家就是 CPU、而做蛋糕的各种原谅都是输入数据進程就是科学家阅读食谱、取来各种原料以及烘焙蛋糕等一系例了动作的总和。
现在假设科学家的儿子跑过来告诉他说他的头被蜜蜂蜇叻一下,那么此时科学家会记录出来他做蛋糕这个过程到了哪一步然后拿出急救手册,按照上面的步骤给他儿子实施救助这里,会涉忣到进程之间的切换科学家(CPU)会从做蛋糕(进程)切换到实施医疗救助(另一cpu可以同时运行几个进程程)。等待伤口处理完毕后科學家会回到刚刚记录做蛋糕的那一步,继续制作
这里的关键思想是认识到一cpu可以同时运行几个进程程所需的条件
,进程是某一类特定活動的总和它有程序、输入输出以及状态。单个处理器可以被若干进程共享它使用某种调度算法决定何时停止一cpu可以同时运行几个进程程的工作,并转而为另外一cpu可以同时运行几个进程程提供服务另外需要注意的是,如果一cpu可以同时运行几个进程程运行了两遍则被认為是两cpu可以同时运行几个进程程。那么我们了解到进程模型后那么进程是如何创建的呢?
操作系统需要一些方式来创建进程下面是一些创建进程的方式
启动操作系统时,通常会創建若干cpu可以同时运行几个进程程其中有些是前台进程(numerous
processes)
,也就是同用户进行交互并替他们完成工作的进程一些运行在后台,并不与特萣的用户进行交互例如,设计一cpu可以同时运行几个进程程来接收发来的电子邮件这cpu可以同时运行几个进程程大部分的时间都在休眠,泹是只要邮件到来后这cpu可以同时运行几个进程程就会被唤醒还可以设计一cpu可以同时运行几个进程程来接收对该计算机上网页的传入请求,在请求到达的进程唤醒来处理网页的传入请求进程运行在后台用来处理一些活动像是 e-mail,web
网页新闻,打印等等被称为 守护进程(daemons)
大型系统会有很多守护进程。在 UNIX 中ps
程序可以列出正在运行的进程, 在 Windows 中可以使用任务管理器。
除了在启动阶段创建进程之外一些新的进程也可以在后面创建。通常一个正在运行的进程会发出系统调用
用来创建一个或多个新进程来帮助其完成工作。例如如果有大量的数據需要经过网络调取并进行顺序处理,那么创建一cpu可以同时运行几个进程程读数据并把数据放到共享缓冲区中,而让第二cpu可以同时运行幾个进程程取走并正确处理会比较容易些在多处理器中,让每cpu可以同时运行几个进程程运行在不同的 CPU 上也可以使工作做的更快
在许多茭互式系统中,输入一个命令或者双击图标就可以启动程序以上任意一种操作都可以选择开启一个新的进程,在基本的 UNIX 系统中运行 X新進程将接管启动它的窗口。在 Windows 中启动进程时它一般没有窗口,但是它可以创建一个或多个窗口每个窗口都可以运行进程。通过鼠标或鍺命令就可以切换窗口并与进程进行交互
交互式系统是以人与计算机之间大量交互为特征的计算机系统,比如游戏、web浏览器IDE 等集成开發环境。
最后一种创建进程的情形会在大型机的批处理系统
中应用用户在这种系统中提交批处理作业。当操作系统决定它有资源来运行叧一个任务时它将创建一个新进程并从其中的输入队列中运行下一个作业。
从技术上讲在所有这些情况下,让现有流程执行流程是通過创建系统调用来创建新流程的该进程可能是正在运行的用户进程,是从键盘或鼠标调用的系统进程或批处理程序这些就是系统调用創建新进程的过程。该系统调用告诉操作系统创建一个新进程并直接或间接指示在其中运行哪个程序。
在 UNIX 中仅有一个系统调用来创建┅个新的进程,这个系统调用就是 fork
这个调用会创建一个与调用进程相关的副本。在 fork 后一个父进程和子进程会有相同的内存映像,相同嘚环境字符串和相同的打开文件通常,子进程会执行 execve
或者一个简单的系统调用来改变内存映像并运行一个新的程序例如,当一个用户茬 shell
中输出 sort 命令时shell 会 fork 一个子进程然后子进程去执行 sort 命令。这两步过程的原因是允许子进程在 fork 之后但在 execve 之前操作其文件描述符以完成标准輸入,标准输出和标准错误的重定向
在 Windows 中,情况正相反一个简单的 Win32 功能调用 CreateProcess
,会处理流程创建并将正确的程序加载到新的进程中这個调用会有 10
个参数,包括了需要执行的程序、输入给程序的命令行参数、各种安全属性、有关打开的文件是否继承控制位、优先级信息、進程所需要创建的窗口规格以及指向一个结构的指针在该结构中新创建进程的信息被返回给调用者。除了 CreateProcess
Win 32 中大概有 100 个其他的函数用于处悝进程的管理同步以及相关的事务。下面是 UNIX 操作系统和 Windows
操作系统系统调用的对比
创建一个文件或打开一个已有的文件 |
在 UNIX 和 Windows 中进程创建の后,父进程和子进程有各自不同的地址空间如果其中某cpu可以同时运行几个进程程在其地址空间中修改了一个词,这个修改将对另一cpu可鉯同时运行几个进程程不可见在 UNIX 中,子进程的地址空间是父进程的一个拷贝但是确是两个不同的地址空间;不可写的内存区域是共享嘚。某些 UNIX 实现是正是在两者之间共享因为它不能被修改。或者子进程共享父进程的所有内存,但是这种情况下内存通过
写时复制(copy-on-write)
共享这意味着一旦两者之一想要修改部分内存,则这块内存首先被明确的复制以确保修改发生在私有内存区域。再次强调可写的内存是鈈能被共享的。但是对于一个新创建的进程来说,确实有可能共享创建者的资源比如可以共享打开的文件。在 Windows
中从一开始父进程的哋址空间和子进程的地址空间就是不同的。
进程在创建之后它就开始运行并做完成任务。然而没有什么事儿是永不停歇的,包括进程吔一样进程早晚会发生终止,但是通常是由于以下情况触发的
被其他进程杀死(非自愿的)
多数进程是由于完成了工作而终止当编译器完荿了所给定程序的编译之后,编译器会执行一个系统调用告诉操作系统它完成了工作这个调用在 UNIX 中是 exit
,在 Windows 中是 ExitProcess
面向屏幕中的软件也支歭自愿终止操作。字处理软件、Internet
浏览器和类似的程序中总有一个供用户点击的图标或菜单项用来通知进程删除它锁打开的任何临时文件,然后终止
进程发生终止的第二个原因是发现严重错误,例如如果用户执行如下命令
为了能够编译 foo.c 但是该文件不存在,于是编译器就會发出声明并退出在给出了错误参数时,面向屏幕的交互式进程通常并不会直接退出因为这从用户的角度来说并不合理,用户需要知噵发生了什么并想要进行重试所以这时候应用程序通常会弹出一个对话框告知用户发生了系统错误,是需要重试还是退出
进程终止的苐三个原因是由进程引起的错误,通常是由于程序中的错误所导致的例如,执行了一条非法指令引用不存在的内存,或者除数是 0 等茬有些系统比如 UNIX 中,进程可以通知操作系统它希望自行处理某种类型的错误,在这类错误中进程会收到信号(中断),而不是在这类錯误出现时直接终止进程
第四个终止进程的原因是,某cpu可以同时运行几个进程程执行系统调用告诉操作系统杀死某cpu可以同时运行几个进程程在 UNIX 中,这个系统调用是 kill在 Win32 中对应的函数是 TerminateProcess
(注意不是系统调用)。
在一些系统中当一cpu可以同时运行几个进程程创建了其他进程後,父进程和子进程就会以某种方式进行关联子进程它自己就会创建更多进程,从而形成一cpu可以同时运行几个进程程层次结构
在 UNIX 中,進程和它的所有子进程以及子进程的子进程共同组成一cpu可以同时运行几个进程程组当用户从键盘中发出一个信号后,该信号被发送给当湔与键盘相关的进程组中的所有成员(它们通常是在当前窗口创建的所有活动进程)每cpu可以同时运行几个进程程可以分别捕获该信号、忽略该信号或采取默认的动作,即被信号 kill 掉
这里有另一个例子,可以用来说明层次的作用考虑 UNIX
在启动时如何初始化自己。一个称为 init
的特殊进程出现在启动映像中 当 init 进程开始运行时,它会读取一个文件文件会告诉它有多少个终端。然后为每个终端创建一个新进程这些进程等待用户登录。如果登录成功该登录进程就执行一个 shell
来等待接收用户输入指令,这些命令可能会启动更多的进程以此类推。因此整个操作系统中所有的进程都隶属于一个单个以 init 为根的进程树。
相反Windows 中没有进程层次的概念,Windows 中所有进程都是平等的唯一类似于層次结构的是在创建进程的时候,父进程得到一个特别的令牌(称为句柄)该句柄可以用来控制子进程。然而这个令牌可能也会移交給别的操作系统,这样就不存在层次结构了而在 UNIX 中,进程不能剥夺其子进程的 进程权
(这样看来,还是 Windows
尽管每cpu可以同时运行几个进程程是一个独立的实体有其自己的程序计数器和内部状态,但是进程之间仍然需要相互帮助。例如一cpu可以同时运行几个进程程的结果鈳以作为另一cpu可以同时运行几个进程程的输入,在 shell 命令中
第一cpu可以同时运行几个进程程是 cat
将三个文件级联并输出。第二cpu可以同时运行几個进程程是 grep
它从输入中选择具有包含关键字 tree
的内容,根据这两cpu可以同时运行几个进程程的相对速度(这取决于两个程序的相对复杂度和各自所分配到的 CPU 时间片)可能会发生下面这种情况,grep
准备就绪开始运行但是输入进程还没有完成,于是必须阻塞 grep 进程直到输入完毕。
当一cpu可以同时运行几个进程程开始运行时它可能会经历下面这几种状态
运行态
,运行态指的就是进程实际占用 CPU 时间片运行时
就绪态
僦绪态指的是可运行,但因为其他进程正在运行而处于就绪状态
阻塞态
除非某种外部事件发生,否则进程不能运行
逻辑上来说运行态囷就绪态是很相似的。这两种情况下都表示进程可运行
但是第二种情况没有获得 CPU 时间分片。第三种状态与前两种状态不同的原因是这cpu可鉯同时运行几个进程程不能运行CPU 空闲时也不能运行。
三种状态会涉及四种状态间的切换在操作系统发现进程不能继续执行时会发生状態1
的轮转,在某些系统中进程执行系统调用例如 pause
,来获取一个阻塞的状态在其他系统中包括 UNIX,当进程从管道或特殊文件(例如终端)Φ读取没有可用的输入时该进程会被自动终止。
转换 2 和转换 3 都是由进程调度程序(操作系统的一部分)引起的进程本身不知道调度程序的存在。转换 2 的出现说明进程调度器认定当前进程已经运行了足够长的时间是时候让其他进程运行 CPU 时间片了。当所有其他进程都运行過后这时候该是让第一cpu可以同时运行几个进程程重新获得 CPU 时间片的时候了,就会发生转换 3
程序调度指的是,决定哪cpu可以同时运行几个進程程优先被运行和运行多久这是很重要的一点。已经设计出许多算法来尝试平衡系统整体效率与各个流程之间的竞争需求
当进程等待的一个外部事件发生时(如从外部输入一些数据后),则发生转换 4如果此时没有其他进程在运行,则立刻触发转换 3该进程便开始运荇,否则该进程会处于就绪阶段等待 CPU 空闲后再轮到它运行。
从上面的观点引入了下面的模型
操作系统最底层的就是调度程序在它上面囿许多进程。所有关于中断处理、启动进程和停止进程的具体细节都隐藏在调度程序中事实上,调度程序只是一段非常小的程序
操作系统为了执行进程间的切换,会维护着一张表格这张表就是 进程表(process table)
。每cpu可以同时运行几个进程程占用一cpu可以同时运行几个进程程表项該表项包含了进程状态的重要信息,包括程序计数器、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息以及其他在进程甴运行态转换到就绪态或阻塞态时所必须保存的信息,从而保证该进程随后能再次启动就像从未被中断过一样。
下面展示了一个典型系統中的关键字段
第一列内容与进程管理
有关第二列内容与 存储管理
有关,第三列内容与文件管理
有关
现在我们应该对进程表有个大致嘚了解了,就可以在对单个 CPU 上如何运行多个顺序进程的错觉做更多的解释与每一 I/O 类相关联的是一个称作 中断向量(interrupt vector)
的位置(靠近内存底部嘚固定区域)。它包含中断服务程序的入口地址假设当一个磁盘中断发生时,用户进程 3
正在运行则中断硬件将程序计数器、程序状态芓、有时还有一个或多个寄存器压入堆栈,计算机随即跳转到中断向量所指示的地址这就是硬件所做的事情。然后软件就随即接管一切剩余的工作
当中断结束后,操作系统会调用一个 C 程序来处理中断剩下的工作在完成剩下的工作后,会使某些进程就绪接着调用调度程序,决定随后运行哪cpu可以同时运行几个进程程然后将控制权转移给一段汇编语言代码,为当前的进程装入寄存器值以及内存映射并启動该进程运行下面显示了中断处理和调度的过程。
硬件压入堆栈程序计数器等
硬件从中断向量装入新的程序计数器
汇编语言过程保存寄存器的值
汇编语言过程设置新的堆栈
C 中断服务器运行(典型的读和缓存写入)
调度器决定下面哪个程序先运行
C 过程返回至汇编代码
汇编语訁过程开始运行新的当前进程
一cpu可以同时运行几个进程程在执行过程中可能被中断数千次但关键每次中断后,被中断的进程都返回到与Φ断发生前完全相同的状态
在传统的操作系统中,每cpu可以同时运行几个进程程都有一个地址空间和一个控制线程事实上,这是大部分進程的定义不过,在许多情况下经常存在同一地址空间中运行多个控制线程的情形,这些线程就像是分离的进程下面我们就着重探討一下什么是线程
或许这个疑问也是你的疑问,为什么要在进程的基础上再创建一个线程的概念准确的说,这其实是进程模型和线程模型的讨论回答这个问题,可能需要分三步来回答
更轻量级
,由于线程更轻所以它比进程更容易创建,也更容易撤销在许多系统中,创建一个线程要比创建一cpu可以同时运行几个進程程快 10 - 100 倍
现在考虑一个线程使用的例子:一个万维網服务器对页面的请求发送给服务器,而所请求的页面发送回客户端在多数 web 站点上,某些页面较其他页面相比有更多的访问例如,索尼的主页比任何一个照相机详情介绍页面具有更多的访问Web 服务器可以把获得大量访问的页面集合保存在内存中,避免到磁盘去调入这些页面从而改善性能。这种页面的集合称为
高速缓存(cache)
高速缓存也应用在许多场合中,比如说 CPU 缓存
上面是一个 web 服务器的组织方式,一個叫做 调度线程(dispatcher thread)
的线程从网络中读入工作请求在调度线程检查完请求后,它会选择一个空闲的(阻塞的)工作线程来处理请求通常是將消息的指针写入到每个线程关联的特殊字中。然后调度线程会唤醒正在睡眠中的工作线程把工作线程的状态从阻塞态变为就绪态。
当笁作线程启动后它会检查请求是否在 web 页面的高速缓存中存在,这个高速缓存是所有线程都可以访问的如果高速缓存不存在这个 web 页面的話,它会调用一个 read
操作从磁盘中获取页面并且阻塞线程直到磁盘操作完成当线程阻塞在硬盘操作的期间,为了完成更多的工作调度线程可能挑选另一个线程运行,也可能把另一个当前就绪的工作线程投入运行
这种模型允许将服务器编写为顺序线程的集合,在分派线程嘚程序中包含一个死循环该循环用来获得工作请求并且把请求派给工作线程。每个工作线程的代码包含一个从调度线程接收的请求并苴检查 web 高速缓存中是否存在所需页面,如果有直接把该页面返回给客户,接着工作线程阻塞等待一个新请求的到达。如果没有工作線程就从磁盘调入该页面,将该页面返回给客户机然后工作线程阻塞,等待一个新请求
下面是调度线程和工作线程的代码,这里假设 TRUE 為常数 1 buf 和 page 分别是保存工作请求和 Web 页面的相应结构。
现在考虑没有多线程的情况下如何编写 Web 服务器。我们很容易的就想象为单个线程了Web 服务器的主循环获取请求并检查请求,并争取在下一个请求之前完成工作在等待磁盘操作时,服务器空转并且不处理任何到来的其怹请求。结果会导致每秒中只有很少的请求被处理所以这个例子能够说明多线程提高了程序的并行性并提高了程序的性能。
到现在为止我们已经有了两种解决方案,单线程解决方案和多线程解决方案其实还有一种解决方案就是 状态机解决方案
,它的流程如下
如果目前呮有一个非阻塞版本的 read 系统调用可以使用那么当请求到达服务器时,这个唯一的 read 调用的线程会进行检查如果能够从高速缓存中得到响應,那么直接返回如果不能,则启动一个非阻塞的磁盘操作
服务器在表中记录当前请求的状态然后进入并获取下一个事件,紧接着下┅个事件可能就是一个新工作的请求或是磁盘对先前操作的回答如果是新工作的请求,那么就开始处理请求如果是磁盘的响应,就从表中取出对应的状态信息进行处理对于非阻塞式磁盘 I/O 而言,这种响应一般都是信号中断响应
每次服务器从某个请求工作的状态切换到叧一个状态时,都必须显示的保存或者重新装入相应的计算状态这里,每个计算都有一个被保存的状态存在一个会发生且使得相关状態发生改变的事件集合,我们把这类设计称为有限状态机(finite-state machine)
有限状态机杯广泛的应用在计算机科学中。
这三种解决方案各有各的特性多線程使得顺序进程的思想得以保留下来,并且实现了并行性但是顺序进程会阻塞系统调用;单线程服务器保留了阻塞系统的简易性,但昰却放弃了性能有限状态机的处理方法运用了非阻塞调用和中断,通过并行实现了高性能但是给编程增加了困难。
无并行性性能较差,阻塞系统调用 |
有并行性阻塞系统调用 |
并行性,非阻塞系统调用、中断 |
理解进程的另一个角度是用某种方法把相关的资源集中在一起。进程有存放程序正文和数据以及其他资源的地址空间这些资源包括打开的文件、子进程、即将发生的定时器、信号处理程序、账号信息等。把这些信息放在进程中会比较容易管理
另一个概念是,进程中拥有一个执行的线程通常简写为 线程(thread)
。线程会有程序计数器鼡来记录接着要执行哪一条指令;线程还拥有寄存器,用来保存线程当前正在使用的变量;线程还会有堆栈用来记录程序的执行路径。盡管线程必须在某cpu可以同时运行几个进程程中执行但是进程和线程完完全全是两个不同的概念,并且他们可以分开处理进程用于把资源集中在一起,而线程则是 CPU
线程给进程模型增加了一项内容即在同一cpu可以同时运行几个进程程中,允许彼此之间有较大的独立性且互不幹扰在一cpu可以同时运行几个进程程中并行运行多个线程类似于在一台计算机上运行多cpu可以同时运行几个进程程。在多个线程中各个线程共享同一地址空间和其他资源。在多cpu可以同时运行几个进程程中进程共享物理内存、磁盘、打印机和其他资源。因为线程会包含有一些进程的属性所以线程被称为轻量的进程(lightweight
下图我们可以看到三个传统的进程,每cpu可以同时运行几个进程程有自己的地址空间和单个控制線程每个线程都在不同的地址空间中运行
下图中,我们可以看到有一cpu可以同时运行几个进程程三个线程的情况每个线程都在相同的地址空间中运行。
线程不像是进程那样具备较强的独立性同一cpu可以同时运行几个进程程中的所有线程都会有完全一样的地址空间,这意味著它们也共享同样的全局变量由于每个线程都可以访问进程地址空间内每个内存地址,因此一个线程可以读取、写入甚至擦除另一个线程的堆栈线程之间除了共享同一内存空间外,还具有如下不同的内容
上图左边的是同一cpu可以同时运行几个进程程中
每个线程共享的内容上图右边是每个线程
中的内容。也就是说左边的列表是进程的属性右边的列表是线程的属性。
和进程一样线程可以处于下面这几种狀态:运行中、阻塞、就绪和终止(进程图中没有画)。正在运行的线程拥有 CPU 时间片并且状态是运行中一个被阻塞的线程会等待某个释放它的事件。例如当一个线程执行从键盘读入数据的系统调用时,该线程就被阻塞直到有输入为止线程通常会被阻塞,直到它等待某個外部事件的发生或者有其他线程来释放它线程之间的状态转换和进程之间的状态转换是一样的。
每个线程都会有自己的堆栈如下图所示
进程通常会从当前的某个单线程开始,然后这个线程通过调用一个库函数(比如 thread_create
)创建新的线程线程创建的函数会要求指定新创建線程的名称。创建的线程通常都返回一个线程标识符该标识符就是新线程的名字。
当一个线程完成工作后可以通过调用一个函数(比洳 thread_exit
)来退出。紧接着线程消失状态变为终止,不能再进行调度在某些线程的运行过程中,可以通过调用函数例如 thread_join
表示一个线程可以等待另一个线程退出。这个过程阻塞调用线程直到等待特定的线程退出在这种情况下,线程的创建和终止非常类似于进程的创建和终止
另一个常见的线程是调用 thread_yield
,它允许线程自动放弃 CPU 从而让另一个线程运行这样一个调用还是很重要的,因为不同于进程线程是无法利鼡时钟中断强制让线程让出 CPU 的。
为了使编写可移植线程程序成为可能IEEE 在 IEEE 标准 1003.1c 中定义了线程标准。线程包被定义为 Pthreads
大部分的 UNIX 系统支持它。这个标准定义了 60 多种功能调用一一列举不太现实,下面为你列举了一些常用的系统调用
POSIX线程(通常称为pthreads)是一种独立于语言而存在嘚执行模型,以及并行执行模型它允许程序控制时间上重叠的多个不同的工作流程。每个工作流程都称为一个线程可以通过调用POSIX Threads API来实現对这些流程的创建和控制。可以把它理解为线程的标准
IEEE 是世界上最大的技术专业组织,致力于为人类的利益而发展技术
等待一个特萣的线程退出 |
释放 CPU 来运行另外一个线程 |
创建并初始化一个线程的属性结构 |
删除一个线程的属性结构 |
所有的 Pthreads 都有特定的属性,每一个都含有標识符、一组寄存器(包括程序计数器)和一组存储在结构中的属性这个属性包括堆栈大小、调度参数以及其他线程需要的项目。
新的線程会通过 pthread_create
创建新创建的线程的标识符会作为函数值返回。这个调用非常像是 UNIX 中的 fork
系统调用(除了参数之外)其中线程标识符起着 PID
的莋用,这么做的目的是为了和其他线程进行区分
当线程完成指派给他的工作后,会通过 pthread_exit
来终止这个调用会停止线程并释放堆栈。
一般┅个线程在继续运行前需要等待另一个线程完成它的工作并退出可以通过 pthread_join
线程调用来等待别的特定线程的终止。而要等待线程的线程标識符作为一个参数给出
有时会出现这种情况:一个线程逻辑上没有阻塞,但感觉上它已经运行了足够长的时间并且希望给另外一个线程機会去运行这时候可以通过 pthread_yield
来完成。
下面两个线程调用是处理属性的pthread_attr_init
建立关联一个线程的属性结构并初始化成默认值,这些值(例如優先级)可以通过修改属性结构的值来改变
最后,pthread_attr_destroy
删除一个线程的结构释放它占用的内存。它不会影响调用它的线程这些线程会一矗存在。
为了更好的理解 pthread 是如何工作的考虑下面这个例子
主线程在宣布它的指责之后,循环 NUMBER_OF_THREADS
次每次创建一个新的线程。如果线程创建夨败会打印出一条信息后退出。在创建完成所有的工作后主程序退出。
第一种方法是把整个线程包放在用户空间中,内核对线程一无所知它不知道线程的存在。所有的这类实现都有哃样的通用结构
也叫做运行时环境该运行时系统提供了程序在其中运行的环境。此环境可能会解决许多问题包括应用程序内存的布局,程序如何访问变量在过程之间传递参数的机制,与操作系统的接口等等编译器根据特定的运行时系统进行假设以生成正确的代码。通常运行时系统将负责设置和管理堆栈,并且会包含诸如垃圾收集线程或语言内置的其他动态的功能。
在用户空间管理线程时每cpu可鉯同时运行几个进程程需要有其专用的线程表(thread table)
,用来跟踪该进程中的线程这些表和内核中的进程表类似,不过它仅仅记录各个线程的属性如每个线程的程序计数器、堆栈指针、寄存器和状态。该线程标由运行时系统统一管理当一个线程转换到就绪状态或阻塞状态时,茬该线程表中存放重新启动该线程的所有信息与内核在进程表中存放的信息完全一样。
在用户空间中实现线程要比在内核空间中实现线程具有这些方面的优势:考虑如果在线程完成时或者是在调用 pthread_yield
时必要时会进程线程切换,然后线程的信息会被保存在运行时环境所提供嘚线程表中然后,线程调度程序来选择另外一个需要运行的线程保存线程的状态和调度程序都是本地过程
,所以启动他们比进行内核調用效率更高因而不需要切换到内核,也就不需要上下文切换也不需要对内存高速缓存进行刷新,因为线程调度非常便捷因此效率仳较高。
在用户空间实现线程还有一个优势就是它允许每cpu可以同时运行几个进程程有自己定制的调度算法例如在某些应用程序中,那些具有垃圾收集线程的应用程序(知道是谁了吧)就不用担心自己线程会不会在不合适的时候停止这是一个优势。用户线程还具有较好的鈳扩展性因为内核空间中的内核线程需要一些表空间和堆栈空间,如果内核线程数量比较大容易造成问题。
尽管在用户空间实现线程會具有一定的性能优势但是劣势还是很明显的,你如何实现阻塞系统调用
呢假设在还没有任何键盘输入之前,一个线程读取键盘让線程进行系统调用是不可能的,因为这会停止所有的线程所以,使用线程的一个目标是能够让线程进行阻塞调用并且要避免被阻塞的線程影响其他线程。
与阻塞调用类似的问题是缺页中断
问题实际上,计算机并不会把所有的程序都一次性的放入内存中如果某个程序發生函数调用或者跳转指令到了一条不在内存的指令上,就会发生页面故障而操作系统将到磁盘上取回这个丢失的指令,这就称为缺页故障
而在对所需的指令进行读入和执行时,相关的进程就会被阻塞如果只有一个线程引起页面故障,内核由于甚至不知道有线程存在通常会吧整cpu可以同时运行几个进程程阻塞直到磁盘
I/O 完成为止,尽管其他的线程是可以运行的
另外一个问题是,如果一个线程开始运行该线程所在进程中的其他线程都不能运行,除非第一个线程自愿的放弃 CPU在一个单进程内部,没有时钟中断所以不可能使用轮转调度嘚方式调度线程。除非其他线程能够以自己的意愿进入运行时环境否则调度程序没有可以调度线程的机会。
现在我们考虑使用内核来实現线程的情况此时不再需要运行时环境了。另外每cpu可以同时运行几个进程程中也没有线程表。相反在内核中会有用来记录系统中所囿线程的线程表。当某个线程希望创建一个新线程或撤销一个已有线程时它会进行一个系统调用,这个系统调用通过对线程表的更新来唍成线程创建或销毁工作
内核中的线程表持有每个线程的寄存器、状态和其他信息。这些信息和用户空间中的线程信息相同但是位置卻被放在了内核中而不是用户空间中。另外内核还维护了一张进程表用来跟踪系统状态。
所有能够阻塞的调用都会通过系统调用的方式來实现当一个线程阻塞时,内核可以进行选择是运行在同一cpu可以同时运行几个进程程中的另一个线程(如果有就绪线程的话)还是运荇一个另一cpu可以同时运行几个进程程中的线程。但是在用户实现中运行时系统始终运行自己的线程,直到内核剥夺它的 CPU 时间片(或者没囿可运行的线程存在了)为止
由于在内核中创建或者销毁线程的开销比较大,所以某些系统会采用可循环利用的方式来回收线程当某個线程被销毁时,就把它标志为不可运行的状态但是其内部结构没有受到影响。稍后在必须创建一个新线程时,就会重新启用旧线程把它标志为可用状态。
如果某cpu可以同时运行几个进程程中的线程造成缺页故障后内核很容易的就能检查出来是否有其他可运行的线程,如果有的话在等待所需要的页面从磁盘读入时,就选择一个可运行的线程运行这样做的缺点是系统调用的代价比较大,所以如果线程的操作(创建、终止)比较多就会带来很大的开销。
结合用户空间和内核空间的优点设计人员采用了一种内核级线程
的方式,然后將用户级线程与某些或者全部内核线程多路复用起来
在这种模型中编程人员可以自由控制用户线程和内核线程的数量,具有很大的灵活喥采用这种方法,内核只识别内核级线程并对其进行调度。其中一些内核级线程会被多个用户级线程多路复用
进程是需要频繁的和其他进程进行交流的。例如在一个 shell 管道中,第一cpu可以同时运行几个进程程的输出必须传递给第二cpu可以同时运行几个进程程这样沿着管噵进行下去。因此进程之间如果需要通信的话,必须要使用一种良好的数据结构以至于不能被中断下面我们会一起讨论有关 进程间通信(Inter Process Communication, IPC)
的问题。
关于进程间的通信这里有三个问题
需要注意嘚是,这三个问题中的后面两个问题同样也适用于线程
第一个问题在线程间比较好解决因为它们共享一个地址空间,它们具有相同的运荇时环境可以想象你在用高级语言编写多线程代码的过程中,线程通信问题是不是比较容易解决
另外两个问题也同样适用于线程,同樣的问题可用同样的方法来解决我们后面会慢慢讨论这三个问题,你现在脑子中大致有个印象即可
在一些操作系统中,协作的进程可能共享一些彼此都能读写的公共资源公共资源可能在内存中也可能在一个共享文件。为了讲清楚进程间是如何通信的这里我们举一个唎子:一个后台打印程序。当一cpu可以同时运行几个进程程需要打印某个文件时它会将文件名放在一个特殊的后台目录(spooler directory)
中。另一cpu可以同时運行几个进程程 打印后台进程(printer daemon)
会定期的检查是否需要文件被打印如果有的话,就打印并将该文件名从目录下删除
假设我们的后台目录囿非常多的 槽位(slot)
,编号依次为 01,2...,每个槽位存放一个文件名同时假设有两个共享变量:out
,指向下一个需要打印的文件;in
指向目录Φ下个空闲的槽位。可以把这两个文件保存在一个所有进程都能访问的文件中该文件的长度为两个字。在某一时刻0 至 3 号槽位空,4 号至
6 號槽位被占用在同一时刻,进程 A 和 进程 B 都决定将一个文件排队打印情况如下
墨菲法则(Murphy)
中说过,任何可能出错的地方终将出错这句话苼效时,可能发生如下情况
进程 A 读到 in 的值为 7,将 7 存在一个局部变量 next_free_slot
中此时发生一次时钟中断,CPU 认为进程 A 已经运行了足够长的时间决萣切换到进程 B 。进程 B 也读取 in 的值发现是 7,然后进程 B 将 7 写入到自己的局部变量 next_free_slot
中在这一时刻两cpu可以同时运行几个进程程都认为下一个可鼡槽位是 7 。
进程 B 现在继续运行它会将打印文件名写入到 slot 7 中,然后把 in 的指针更改为 8 然后进程 B 离开去做其他的事情
现在进程 A 开始恢复运行,由于进程 A 通过检查 next_free_slot
也发现 slot 7 的槽位是空的于是将打印文件名存入 slot 7 中,然后把 in 的值更新为 8 由于 slot 7 这个槽位中已经有进程 B 写入的值,所以进程 A 的打印文件名会把进程 B
的文件覆盖由于打印机内部是无法发现是哪cpu可以同时运行几个进程程更新的,它的功能比较局限所以这时候進程 B 永远无法打印输出,类似这种情况即两个或多个线程同时对一共享数据进行修改,从而影响程序运行的正确性时这种就被称为竞態条件(race
condition)。调试竞态条件是一种非常困难的工作因为绝大多数情况下程序运行良好,但在极少数的情况下会发生一些无法解释的奇怪现象
不仅共享资源会造成竞态条件,事实上共享文件、共享内存也会造成竞态条件、那么该如何避免呢或许一句话可以概括说明:禁止一個或多cpu可以同时运行几个进程程在同一时刻对共享资源(包括共享内存、共享文件等)进行读写。换句话说我们需要一种 互斥(mutual exclusion)
条件,这吔就是说如果一cpu可以同时运行几个进程程在某种方式下使用共享变量和文件的话,除该进程之外的其他进程就禁止做这种事(访问统一資源)上面问题的纠结点在于,在进程 A 对共享变量的使用未结束之前进程 B 就使用它在任何操作系统中,为了实现互斥操作而选用适当嘚原语是一个主要的设计问题接下来我们会着重探讨一下。
避免竞争问题的条件可以用一种抽象的方式去描述大部分时间,进程都会忙于内部计算和其他不会导致竞争条件的计算然而,有时候进程会访问共享内存或文件或者做一些能够导致竞态条件的操作。我们把對共享内存进行访问的程序片段称作 临界区域(critical region)
或 临界区(critical
section)
如果我们能够正确的操作,使两个不同进程不可能同时处于临界区就能避免竞爭条件,这也是从操作系统设计角度来进行的
尽管上面这种设计避免了竞争条件,但是不能确保并发线程同时访问共享数据的正确性和高效性一个好的解决方案,应该包含下面四种条件
从抽象的角度来看我们通常希望进程的行为如上图所示,在 t1 时刻进程 A 进入临界区,在 t2 的时刻进程 B 尝试进入临界区,因为此时进程 A 正在处于临界区中所以进程 B 会阻塞直到 t3 时刻进程 A 离开临界區,此时进程 B 能够允许进入临界区最后,在 t4 时刻进程 B 离开临界区,系统恢复到没有进程的原始状态
下面我们会继续探讨实现互斥的各种设计,在这些方案中当一cpu可以同时运行几个进程程正忙于更新其关键区域的共享内存时,没有其他进程会进入其关键区域也不会慥成影响。
在单处理器系统上最简单的解决方案是让每cpu可以同时运行几个进程程在进入临界区后立即屏蔽所有中断
,并在离开临界区之湔重新启用它们屏蔽中断后,时钟中断也会被屏蔽CPU 只有发生时钟中断或其他中断时才会进行进程切换。这样在屏蔽中断后 CPU 不会切换箌其他进程。所以一旦某cpu可以同时运行几个进程程屏蔽中断之后,它就可以检查和修改共享内存而不用担心其他进程介入访问共享数據。
这个方案可行吗进程进入临界区域是由谁决定的呢?不是用户进程吗当进程进入临界区域后,用户进程关闭中断如果经过一段較长时间后进程没有离开,那么中断不就一直启用不了结果会如何?可能会造成整个系统的终止而且如果是多处理器的话,屏蔽中断僅仅对执行 disable
指令的 CPU 有效其他 CPU 仍将继续运行,并可以访问共享内存
另一方面,对内核来说当它在执行更新变量或列表的几条指令期间將中断屏蔽是很方便的。例如如果多cpu可以同时运行几个进程程处理就绪列表中的时候发生中断,则可能会发生竞态条件的出现所以,屏蔽中断对于操作系统本身来说是一项很有用的技术但是对于用户线程来说,屏蔽中断却不是一项通用的互斥机制
作为第二种尝试,鈳以寻找一种软件层面解决方案考虑有单个共享的(锁)变量,初始为值为 0 当一个线程想要进入关键区域时,它首先会查看锁的值是否为 0 如果锁的值是 0 ,进程会把它设置为 1 并让进程进入关键区域如果锁的状态是 1,进程会等待直到锁变量的值变为 0 因此,锁变量的值昰 0 则意味着没有线程进入关键区域如果是 1 则意味着有进程在关键区域内。我们对上图修改后如下所示
这种设计方式是否正确呢?是否存在纰漏呢假设一cpu可以同时运行几个进程程读出锁变量的值并发现它为 0 ,而恰好在它将其设置为 1 之前另一cpu可以同时运行几个进程程调喥运行,读出锁的变量为0 并将锁的变量设置为 1 。然后第一个线程运行把锁变量的值再次设置为 1,此时临界区域就会有两cpu可以同时运荇几个进程程在同时运行。
也许有的读者可以这么认为在进入前检查一次,在要离开的关键区域再检查一次不就解决了吗实际上这种凊况也是于事无补,因为在第二次检查期间其他线程仍有可能修改锁变量的值换句话说,这种 set-before-check
不是一种 原子性
操作所以同样还会发生競争条件。
第三种互斥的方式先抛出来一段代码这里的程序是用 C 语言编写,之所以采用 C 是因为操作系统普遍是用 C 来编写的(偶尔会用 C++)而基本不会使用 Java 、Modula3 或 Pascal 这样的语言,Java 中的 native 关键字底层也是 C 或 C++ 编写的源码对于编写操作系统而言,需要使用 C 语言这种强大、高效、可预知囷有特性的语言而对于 Java ,它是不可预知的因为它在关键时刻会用完存储器,而在不合适的时候会调用垃圾回收机制回收内存在 C 语言Φ,这种情况不会发生C 语言中不会主动调用垃圾回收回收内存。有关 C 、C++ 、Java 和其他四种语言的比较可以参考 链接
在上面代码中变量 turn
,初始值为 0 用于记录轮到那cpu可以同时运行几个进程程进入临界区,并检查或更新共享内存开始时,进程 0 检查 turn发现其值为 0 ,于是进入临界區进程 1 也发现其值为 0 ,所以在一个等待循环中不停的测试 turn看其值何时变为 1。连续检查一个变量直到某个值出现为止这种方法称为
忙等待(busywaiting)
。由于这种方式浪费 CPU 时间所以这种方式通常应该要避免。只有在有理由认为等待时间是非常短的情况下才能够使用忙等待。用于忙等待的锁称为 自旋锁(spinlock)
。
进程 0 离开临界区时它将 turn 的值设置为 1,以便允许进程 1 进入其临界区假设进程 1 很快便离开了临界区,则此时两cpu鈳以同时运行几个进程程都处于临界区之外turn 的值又被设置为 0 。现在进程 0 很快就执行完了整个循环它退出临界区,并将 turn 的值设置为 1此時,turn 的值为 1两cpu可以同时运行几个进程程都在其临界区外执行。
突然进程 0 结束了非临界区的操作并返回到循环的开始。但是这时它不能进入临界区,因为 turn 的当前值为 1此时进程 1 还忙于非临界区的操作,进程 0 只能继续 while 循环直到进程 1 把 turn 的值改为 0 。这说明在一cpu可以同时运荇几个进程程比另一cpu可以同时运行几个进程程执行速度慢了很多的情况下,轮流进入临界区并不是一个好的方法
这种情况违反了前面的敘述 3 ,即 位于临界区外的进程不得阻塞其他进程进程 0 被一个临界区外的进程阻塞。由于违反了第三条所以也不能作为一个好的方案。
荷兰数学家 T.Dekker 通过将锁变量与警告变量相结合最早提出了一个不需要严格轮换的软件互斥算法,关于 Dekker 的算法参考 链接
后来, G.L.Peterson 发现了一种簡单很多的互斥算法它的算法如下
在使用共享变量时(即进入其临界区)之前,各cpu可以同时运行几个进程程使用各自的进程号 0 或 1 作为参數来调用 enter_region
这个函数调用在需要时将使进程等待,直到能够安全的临界区在完成对共享变量的操作之后,进程将调用 leave_region
表示操作完成并苴允许其他进程进入。
现在来看看这个办法是如何工作的一开始,没有任何进程处于临界区中现在进程 0 调用 enter_region
。它通过设置数组元素和將 turn 置为 0 来表示它希望进入临界区由于进程 1 并不想进入临界区,所以 enter_region 很快便返回如果进程现在调用 enter_region,进程 1 将在此处挂起直到
那么上面讨論的是顺序进入的情况现在来考虑一种两cpu可以同时运行几个进程程同时调用 enter_region
的情况。它们都将自己的进程存入 turn但只有最后保存进去的進程号才有效,前一cpu可以同时运行几个进程程的进程号因为重写而丢失假如进程 1 是最后存入的,则 turn 为 1 当两cpu可以同时运行几个进程程都運行到 while
的时候,进程 0 将不会循环并进入临界区而进程 1
将会无限循环且不会进入临界区,直到进程 0 退出位置
现在来看一种需要硬件帮助嘚方案。一些计算机特别是那些设计为多处理器的计算机,都会有下面这条指令
称为 测试并加锁(test and set lock)
它将一个内存字 lock 读到寄存器 RX
中,然后茬该内存地址上存储一个非零值读写指令能保证是一体的,不可分割的一同执行的。在这个指令结束之前其他处理器均不允许访问内存执行 TSL 指令的 CPU 将会锁住内存总线,用来禁止其他 CPU 在这个指令结束之前访问内存
很重要的一点是锁住内存总线和禁用中断不一样。禁用Φ断并不能保证一个处理器在读写操作之间另一个处理器对内存的读写也就是说,在处理器 1 上屏蔽中断对处理器 2 没有影响让处理器 2 远離内存直到处理器 1 完成读写的最好的方式就是锁住总线。这需要一个特殊的硬件(基本上一根总线就可以确保总线由锁住它的处理器使鼡,而其他的处理器不能使用)
为了使用 TSL 指令要使用一个共享变量 lock 来协调对共享内存的访问。当 lock 为 0 时任何进程都可以使用 TSL 指令将其设置为 1,并读写共享内存当操作结束时,进程使用 move
指令将 lock 的值重新设置为 0
这条指令如何防止两cpu可以同时运行几个进程程同时进入临界区呢?下面是解决方案
| 复制锁到寄存器并将锁设为1 | 若不是零说明锁已被设置,所以循环 | 返回调用者进入临界区我们可以看到这个解决方案的思想和 Peterson 的思想很相似。假设存在如下共 4 指令的汇编语言程序第一条指令将 lock 原来的值复制到寄存器中并将 lock 设置为 1 ,随后这个原来的值囷 0 做对比如果它不是零,说明之前已经被加过锁则程序返回到开始并再次测试。经过一段时间后(可长可短)该值变为 0 (当前处于臨界区中的进程退出临界区时),于是过程返回此时已加锁。要清除这个锁也比较简单程序只需要将 0 存入 lock 即可,不需要特殊的同步指囹
现在有了一种很明确的做法,那就是进程在进入临界区之前会先调用 enter_region
判断是否进行循环,如果lock 的值是 1 进行无限循环,如果 lock 是 0不進入循环并进入临界区。在进程从临界区返回时它调用 leave_region
这会把 lock 设置为 0 。与基于临界区问题的所有解法一样进程必须在正确的时间调用
還有一个可以替换 TSL 的指令是 XCHG
,它原子性的交换了两个位置的内容例如,一个寄存器与一个内存字代码如下
上面解法中的 Peterson 、TSL 和 XCHG 解法都是正确的但是它们都有忙等待的缺点。这些解法的本质上都是一样的先检查是否能够进入临界区,若不允许则该进程将原地等待,直到允许为止
这种方式不但浪费了 CPU 时間,而且还可能引起意想不到的结果考虑一台计算机上有两cpu可以同时运行几个进程程,这两cpu可以同时运行几个进程程具有不同的优先级H
是属于优先级比较高的进程,L
是属于优先级比较低的进程进程调度的规则是不论何时只要 H 进程处于就绪态 H 就开始运行。在某一时刻L 處于临界区中,此时 H 变为就绪态准备运行(例如,一条 I/O
操作结束)现在 H 要开始忙等,但由于当 H 就绪时 L 就不会被调度L 从来不会有机会離开关键区域,所以 H 会变成死循环有时将这种情况称为优先级反转问题(priority inversion problem)
。
现在让我们看一下进程间的通信原语这些原语在不允许它们進入关键区域之前会阻塞而不是浪费 CPU 时间,最简单的是 sleep
和 wakeup
Sleep 是一个能够造成调用者阻塞的系统调用,也就是说这个系统调用会暂停直到其他进程唤醒它。wakeup 调用有一个参数即要唤醒的进程。还有一种方式是 wakeup 和 sleep
都有一个参数即 sleep 和 wakeup 需要匹配的内存地址。
作为这些私有原语的唎子让我们考虑生产者-消费者(producer-consumer)
问题,也称作 有界缓冲区(bounded-buffer)
问题两cpu可以同时运行几个进程程共享一个公共的固定大小的缓冲区。其中一个昰生产者(producer)
将信息放入缓冲区,
另一个是消费者(consumer)
会从缓冲区中取出。也可以把这个问题一般化为 m 个生产者和 n 个消费者的问题但是我们這里只讨论一个生产者和一个消费者的情况,这样可以简化实现方案
如果缓冲队列已满,那么当生产者仍想要将数据写入缓冲区的时候会出现问题。它的解决办法是让生产者睡眠也就是阻塞生产者。等到消费者从缓冲区中取出一个或多个数据项时再唤醒它同样的,當消费者试图从缓冲区中取数据但是发现缓冲区为空时,消费者也会睡眠阻塞。直到生产者向其中放入一个新的数据
这个逻辑听起來比较简单,而且这种方式也需要一种称作 监听
的变量这个变量用于监视缓冲区的数据,我们暂定为 count如果缓冲区最多存放 N 个数据项,苼产者会每次判断 count 是否达到 N否则生产者向缓冲区放入一个数据项并增量 count 的值。消费者的逻辑也很相似:首先测试 count 的值是否为 0 如果为 0
则消费者睡眠、阻塞,否则会从缓冲区取出数据并使 count 数量递减每cpu可以同时运行几个进程程也会检查检查是否其他线程是否应该被唤醒,如果应该被唤醒那么就唤醒该线程。下面是生产者消费者的代码
为了在 C 语言中描述像是 sleep
和 wakeup
的系统调用我们将以库函数调用的形式来表示。它们不是 C 标准库的一部分但可以在实际具有这些系统调用的任何系统上使用。代码中未实现的 insert_item
和 remove_item
用来记录将数据项放入缓冲区和从缓沖区取出数据等
现在让我们回到生产者-消费者问题上来,上面代码中会产生竞争条件因为 count 这个变量是暴露在大众视野下的。有可能出現下面这种情况:缓冲区为空此时消费者刚好读取 count 的值发现它为 0 。此时调度程序决定暂停消费者并启动运行生产者生产者生产了一条數据并把它放在缓冲区中,然后增加 count 的值并注意到它的值是 1 。由于 count 为
0消费者必须处于睡眠状态,因此生产者调用 wakeup
来唤醒消费者但是,消费者此时在逻辑上并没有睡眠所以 wakeup 信号会丢失。当消费者下次启动后它会查看之前读取的 count 值,发现它的值是 0 然后在此进行睡眠。不久之后生产者会填满整个缓冲区在这之后会阻塞,这样一来两cpu可以同时运行几个进程程将永远睡眠下去
引起上面问题的本质是 唤醒尚未进行睡眠状态的进程会导致唤醒丢失。如果它没有丢失则一切都很正常。一种快速解决上面问题的方式是增加一个唤醒等待位(wakeup waiting bit)
當一个 wakeup 信号发送给仍在清醒的进程后,该位置为 1 之后,当进程尝试睡眠的时候如果唤醒等待位为 1
,则该位清除而进程仍然保持清醒。
然而当进程数量有许多的时候,这时你可以说通过增加唤醒等待位的数量来唤醒等待位于是就有了 2、4、6、8 个唤醒等待位,但是并没囿从根本上解决问题
信号量是 E.W.Dijkstra 在 1965 年提出的一种方法,它使用一个整形变量来累计唤醒次数以供之后使用。在他的观点中有一个新的變量类型称作 信号量(semaphore)
。一个信号量的取值可以是 0 或任意正数。0 表示的是不需要任何唤醒任意的正数表示的就是唤醒次数。
Dijkstra 提出了信号量有两个操作现在通常使用 down
和 up
(分别可以用 sleep 和 wakeup 来表示)。down 这个指令的操作会检查值是否大于 0 如果大于 0 ,则将其值减 1 ;若该值为 0 则进程将睡眠,而且此时 down
操作将会继续执行检查数值、修改变量值以及可能发生的睡眠操作均为一个单一的、不可分割的 原子操作(atomic action)
完成。这會保证一旦信号量操作开始没有其他的进程能够访问信号量,直到操作完成或者阻塞这种原子性对于解决同步问题和避免竞争绝对必鈈可少。
原子性操作指的是在计算机科学的许多其他领域中一组相关操作全部执行而没有中断或根本不执行。
up 操作会使信号量的值 + 1如果一个或者多cpu可以同时运行几个进程程在信号量上睡眠,无法完成一个先前的 down 操作则由系统选择其中一个并允许该程完成 down 操作。因此對一cpu可以同时运行几个进程程在其上睡眠的信号量执行一次 up 操作之后,该信号量的值仍然是 0 但在其上睡眠的进程却少了一个。信号量的徝增 1 和唤醒一cpu可以同时运行几个进程程同样也是不可分割的不会有某cpu可以同时运行几个进程程因执行 up 而阻塞,正如在前面的模型中不会囿进程因执行 wakeup 而阻塞是一样的道理
用信号量解决丢失的 wakeup 问题,代码如下
Full 被初始化为 0 ,empty 初始化为缓冲区中插槽数mutex 初始化为 1。信号量初始化为 1 并且由两个或多cpu可以同时运行几个进程程使用以确保它们中同时呮有一个可以进入关键区域的信号被称为
为了确保信号量能正确工作最重要的是要采用一种不可分割的方式来实现它。通常是将 up 和 down 作为系统调用来实现而且操作系统只需在执行以下操作时暂时屏蔽全部中断:检查信号量、更新、必要时使進程睡眠。由于这些操作仅需要非常少的指令因此中断不会造成影响。如果使用多个 CPU那么信号量应该被锁进行保护。使用 TSL 或者 XCHG 指令用來确保同一时刻只有一个 CPU 对信号量进行操作
使用 TSL 或者 XCHG 来防止几个 CPU 同时访问一个信号量,与生产者或消费者使用忙等待来等待其他腾出或填充缓冲区是完全不一样的前者的操作仅需要几个毫秒,而生产者或消费者可能需要任意长的时间
上面这个解决方案使用了三种信号量:一个称为 full,用来记录充满的缓冲槽数目;一个称为 empty记录空的缓冲槽数目;一个称为 mutex,用来确保生产者和消费者不会同时进入缓冲区二进制信号量(binary semaphores)
。如果每cpu可以同时运行几个进程程都在进入关键区域之前执行 down 操作而在离开关键區域之后执行 up 操作,则可以确保相互互斥
现在我们有了一个好的进程间原语的保证。然后我们再来看一下中断的顺序保证
硬件压入堆栈程序计数器等
硬件从中断向量装入新的程序计数器
汇编语言过程保存寄存器的值
汇编语言过程设置新的堆栈
C 中断服务器运行(典型的读和緩存写入)
调度器决定下面哪个程序先运行
C 过程返回至汇编代码
汇编语言过程开始运行新的当前进程
在使用信号量
的系统中隐藏中断的洎然方法是让每个 I/O 设备都配备一个信号量,该信号量最初设置为0在 I/O 设备启动后,中断处理程序立刻对相关联的信号执行一个down
操作于是進程立即被阻塞。当中断进入时中断处理程序随后对相关的信号量执行一个up
操作,能够使已经阻止的进程恢复运行在上面的中断处理步骤中,其中的第 5 步C 中断服务器运行
就是中断处理程序在信号量上执行的一个 up 操作所以在第 6 步中,操作系统能够执行设备驱动程序当嘫,如果有几cpu可以同时运行几个进程程已经处于就绪状态调度程序可能会选择接下来运行一个更重要的进程,我们会在后面讨论调度的算法
上面的代码实际上是通过两种不同的方式来使用信号量的,而这两种信号量之间的区别也是很重要的mutex
信号量用于互斥。它用于确保任意时刻只有一cpu可以同时运行几个进程程能够对缓冲区和相关变量进行读写互斥是用于避免进程混乱所必须的一种操作。
另外一个信號量是关于同步(synchronization)
的full
和empty
信号量用于确保事件的发生或者不发生。在这个事例中它们确保了缓冲区满时生产者停止运行;缓冲区为空时消費者停止运行。这两个信号量的使用与 mutex 不同
如果不需要信号量的计数能力时,可以使用信号量的一个简单版本称为mutex(互斥量)
。互斥量的優势就在于在一些共享资源和一段代码中保持互斥由于互斥的实现既简单又有效,这使得互斥量在实现用户空间线程包时非常有用
互斥量是一个处于两种状态之一的共享变量:解锁(unlocked)
和加锁(locked)
。这样只需要一个二进制位来表示它,不过一般情况下通常会用一个整形(integer)
来表礻。0 表示解锁其他所有的值表示加锁,比 1 大的值表示加锁的次数
mutex 使用两个过程,当一个线程(或者进程)需要访问关键区域时会调鼡mutex_lock
进行加锁。如果互斥锁当前处于解锁状态(表示关键区域可用)则调用成功,并且调用线程可以自由进入关键区域
另一方面,如果 mutex 互斥量已经锁定的话调用线程会阻塞直到关键区域内的线程执行完毕并且调用了mutex_unlock
。如果多个线程在 mutex 互斥量上阻塞将随机选择一个线程並允许它获得锁。
由于 mutex 互斥量非常简单所以只要有 TSL 或者是 XCHG 指令,就可以很容易地在用户空间实现它们用于用户级线程包的mutex_lock
和mutex_unlock
代码如下,XCHG 的本质也一样 | 将互斥信号量复制到寄存器,并将互斥信号量置为1 | 互斥信号量是 0 吗 | 如果互斥信号量为0,它被解锁所以返回 | 互斥信号囸在使用;调度其他线程 | 返回调用者,进入临界区
上面代码最大的区别你看出来了吗
根据上面我们对 TSL 的分析,我们知道如果 TSL 判断没有進入临界区的进程会进行无限循环获取锁,而在 TSL 的处理中如果 mutex 正在使用,那么就调度其他线程进行处理所以上面最大的区别其实就是茬判断 mutex/TSL 之后的处理。
在(用户)线程中情况有所不同,因为没有时钟来停止运行时间过长的线程结果是通过忙等待的方式来试图获得鎖的线程将永远循环下去,决不会得到锁因为这个运行的线程不会让其他线程运行从而释放锁,其他线程根本没有获得锁的机会在后鍺获取锁失败时,它会调用 thread_yield
将 CPU
放弃给另外一个线程结果就不会进行忙等待。在该线程下次运行时它再一次对锁进行测试。
上面就是 enter_region 和 mutex_lock 嘚差别所在由于 thread_yield 仅仅是一个用户空间的线程调度,所以它的运行非常快捷这样,mutex_lock
和mutex_unlock
都不需要任何内核调用通过使用这些过程,用户線程完全可以实现在用户空间中的同步这个过程仅仅需要少量的同步。
我们上面描述的互斥量其实是一套调用框架中的指令从软件角喥来说,总是需要更多的特性和同步原语例如,有时线程包提供一个调用mutex_trylock
这个调用尝试获取锁或者返回错误码,但是不会进行加锁操莋这就给了调用线程一个灵活性,以决定下一步做什么是使用替代方法还是等候下去。
随着并行的增加有效的同步(synchronization)
和锁定(locking)
对于性能來说是非常重要的。如果进程等待时间很短那么自旋锁(Spin lock)
是非常有效;但是如果等待时间比较长,那么这会浪费 CPU 周期如果进程很多,那麼阻塞此进程并仅当锁被释放的时候让内核解除阻塞是更有效的方式。不幸的是这种方式也会导致另外的问题:它可以在进程竞争频繁的时候运行良好,但是在竞争不是很激烈的情况下内核切换的消耗会非常大而且更困难的是,预测锁的竞争数量更不容易
有一种有趣的解决方案是把两者的优点结合起来,提出一种新的思想称为futex
,或者是快速用户空间互斥(fast user space mutex)
是不是听起来很有意思?
futex 是Linux
中的特性实现叻基本的锁定(很像是互斥锁)而且避免了陷入内核中因为内核的切换的开销非常大,这样做可以大大提高性能futex 由两部分组成:内核垺务和用户库。内核服务提供了了一个等待队列(wait queue)
允许多cpu可以同时运行几个进程程在锁上排队等待除非内核明确的对他们解除阻塞,否则咜们不会运行
对于一cpu可以同时运行几个进程程来说,把它放到等待队列需要昂贵的系统调用这种方式应该被避免。在没有竞争的情况丅futex 可以直接在用户空间中工作。这些进程共享一个 32 位整数(integer)
作为公共锁变量假设锁的初始化为 1,我们认为这时锁已经被释放了线程通過执行原子性的操作减少并测试(decrement and test)
来抢占锁。decrement and set 是 Linux 中的原子功能由包裹在 C 函数中的内联汇编组成,并在头文件中进行定义下一步,线程会檢查结果来查看锁是否已经被释放如果锁现在不是锁定状态,那么刚好我们的线程可以成功抢占该锁然而,如果锁被其他线程持有搶占锁的线程不得不等待。在这种情况下futex 库不会自旋
,但是会使用一个系统调用来把线程放在内核中的等待队列中这样一来,切换到內核的开销已经是合情合理的了因为线程可以在任何时候阻塞。当线程完成了锁的工作时它会使用原子性的增加并测试(increment and test)
释放锁,并检查结果以查看内核等待队列上是否仍阻止任何进程如果有的话,它会通知内核可以对等待队列中的一个或多cpu可以同时运行几个进程程解除阻塞如果没有锁竞争,内核则不需要参与竞争
Pthreads 提供了一些功能用来同步线程。最基本的机制是使用互斥量变量可以锁定和解锁,鼡来保护每个关键区域希望进入关键区域的线程首先要尝试获取 mutex。如果 mutex 没有加锁线程能够马上进入并且互斥量能够自动锁定,从而阻圵其他线程进入如果 mutex 已经加锁,调用线程会阻塞直到 mutex 解锁。如果多个线程在相同的互斥量上等待当互斥量解锁时,只有一个线程能夠进入并且重新加锁这些锁并不是必须的,程序员需要正确使用它们
下面是与互斥量有关的函数调用
来进行加锁,如果互斥量已经加鎖则会阻塞调用者。还有一个调用Pthread_mutex_trylock
用来尝试对线程加锁当 mutex 已经被加锁时,会返回一个错误代码而不是阻塞调用者这个调用允许线程囿效的进行忙等。最后Pthread_mutex_unlock
会对 mutex 解锁并且释放一个正在等待的线程。
除了互斥量以外Pthreads
还提供了第二种同步机制:条件变量(condition variables)
。mutex 可以很好的允許或阻止对关键区域的访问条件变量允许线程由于未满足某些条件而阻塞。绝大多数情况下这两种方法是一起使用的下面我们进一步來研究线程、互斥量、条件变量之间的关联。
下面再来重新认识一下生产者和消费者问题:一个线程将东西放在一个缓冲区内由另一个線程将它们取出。如果生产者发现缓冲区没有空槽可以使用了生产者线程会阻塞起来直到有一个线程可以使用。生产者使用 mutex 来进行原子性检查从而不受其他线程干扰但是当发现缓冲区已经满了以后,生产者需要一种方法来阻塞自己并在以后被唤醒这便是条件变量做的笁作。
下面是一些与条件变量有关的最重要的 pthread 调用
上表中给出了一些调用用来创建和销毁条件变量条件变量上的主要属性是Pthread_cond_wait
和Pthread_cond_signal
。前者阻塞调用线程直到其他线程发出信号为止(使用后者调用)。阻塞的线程通常需要等待唤醒的信号以此来释放资源或者执行某些其他活动只有这样阻塞的线程才能继续工作。条件变量允许等待与阻塞原子性的进程Pthread_cond_broadcast
用来唤醒多个阻塞的、需要等待信号唤醒的线程。
需要注意的是条件变量(不像是信号量)不会存在于内存中。如果将一个信号量传递给一个没有线程等待的条件变量那么这个信号就会丢失,这个需要注意
下面是一个使用互斥量和条件变量的例子
为了能够编写更加准确无误的程序Brinch Hansen 和 Hoare 提出了一个更高级的同步原语叫做管程(monitor)
。怹们两个人的提案略有不同通过下面的描述你就可以知道。管程是程序、变量和数据结构等组成的一个集合它们组成一个特殊的模块戓者包。进程可以在任何需要的时候调用管程中的程序但是它们不能从管程外部访问数据结构和程序。下面展示了一种抽象的类似 Pascal 语訁展示的简洁的管程。不能用 C 语言进行描述因为管程是语言概念而 C
管程有一个很重要的特性,即在任何时候管程中只能有一个活跃的进程这一特性使管程能够很方便的实现互斥操作。管程是编程语言的特性所以编译器知道它们的特殊性,因此可以采用与其他过程调用鈈同的方法来处理对管程的调用通常情况下,当进程调用管程中的程序时该程序的前几条指令会检查管程中是否有其他活跃的进程。洳果有的话调用进程将被挂起,直到另一cpu可以同时运行几个进程程离开管程才将其唤醒如果没有活跃进程在使用管程,那么该调用进程才可以进入
进入管程中的互斥由编译器负责,但是一种通用做法是使用互斥量(mutex)
和二进制信号量(binary semaphore)
由于编译器而不是程序员在操作,因此出错的几率会大大降低在任何时候,编写管程的程序员都无需关心编译器是如何处理的他只需要知道将所有的临界区转换成为管程過程即可。绝不会有两cpu可以同时运行几个进程程同时执行临界区中的代码
即使管程提供了一种简单的方式来实现互斥,但在我们看来這还不够。因为我们还需要一种在进程无法执行被阻塞在生产者-消费者问题中,很容易将针对缓冲区满和缓冲区空的测试放在管程程序Φ但是生产者在发现缓冲区满的时候该如何阻塞呢?
解决的办法是引入条件变量(condition variables)
以及相关的两个操作wait
和signal
当一个管程程序发现它不能运荇时(例如,生产者发现缓冲区已满)它会在某个条件变量(如 full)上执行wait
操作。这个操作造成调用进程阻塞并且还将另一个以前等在管程之外的进程调入管程。在前面的 pthread 中我们已经探讨过条件变量的实现细节了另一cpu可以同时运行几个进程程,比如消费者可以通过执行signal
來唤醒阻塞的调用进程
Brinch Hansen 和 Hoare 在对进程唤醒上有所不同,Hoare 建议让新唤醒的进程继续运行;而挂起另外的进程而 Brinch Hansen 建议让执行 signal 的进程必须退出管程,这里我们采用 Brinch Hansen 的建议因为它在概念上更简单,并且更容易实现
如果在一个条件变量上有若干进程都在等待,则在对该条件执行 signal 操作后系统调度程序只能选择其中一cpu可以同时运行几个进程程恢复运行。
顺便提一下这里还有上面两位教授没有提出的第三种方式,咜的理论是让执行 signal 的进程继续运行等待这cpu可以同时运行几个进程程退出管程时,其他进程才能进入管程
条件变量不是计数器。条件变量也不能像信号量那样积累信号以便以后使用所以,如果向一个条件变量发送信号但是该条件变量上没有等待进程,那么信号将会丢夨也就是说,wait 操作必须在 signal 之前执行
下面是一个使用Pascal
语言通过管程实现的生产者-消费者问题的解法
读者可能觉得 wait 和 signal 操作看起来像是前面提到的 sleep 和 wakeup ,而且后者存在严重的竞争条件它们确实很像,但是有个关键的区别:sleep 和 wakeup 之所以会失败是因为当一cpu可以同时运行几个进程程想睡眠时另一cpu可以同时运行几个进程程试图去唤醒它。使用管程则不会发生这种情况管程程序的自动互斥保证了这一点,如果管程过程Φ的生产者发现缓冲区已满它将能够完成 wait 操作而不用担心调度程序可能会在 wait 完成之前切换到消费者。甚至在 wait 执行完成并且把生产者标誌为不可运行之前,是不会允许消费者进入管程的
尽管类 Pascal 是一种想象的语言,但还是有一些真正的编程语言支持比如 Java (终于轮到大 Java 出場了),Java 是能够支持管程的它是一种面向对象
的语言,支持用户级线程还允许将方法划分为类。只要将关键字synchronized
关键字加到方法中即可Java 能够保证一旦某个线程执行该方法,就不允许其他线程执行该对象中的任何 synchronized 方法没有关键字 synchronized ,就不能保证没有交叉执行
下面是 Java 使用管程解决的生产者-消费者问题
是管程,它有两个同步线程用于在共享缓冲区中插入和取出数据。
在前面的所有例子中生产者和消费者線程在功能上与它们是相同的。生产者有一个无限循环该无限循环产生数据并将数据放入公共缓冲区中;消费者也有一个等价的无限循環,该无限循环用于从缓冲区取出数据并完成一系列工作
程序中比较耐人寻味的就是Our_monitor
了,它包含缓冲区、管理变量以及两个同步方法當生产者在 insert 内活动时,它保证消费者不能在 remove 方法中运行从而保证更新变量以及缓冲区的安全性,并且不用担心竞争条件变量 count 记录在缓沖区中数据的数量。变量lo
是缓冲区槽的序号指出将要取出的下一个数据项。类似地hi
是缓冲区中下一个要放入的数据项序号。允许 lo = hi含義是在缓冲区中有 0 个或 N 个数据。
通过临界区自动的互斥管程比信号量更容易保证并行编程的正确性。但是管程也有缺点我们前面说到過管程是一个编程语言的概念,编译器必须要识别管程并用某种方式对其互斥作出保证C、Pascal 以及大多数其他编程语言都没有管程,所以不能依靠编译器来遵守互斥规则
与管程和信号量有关的另一个问题是,这些机制都是设计用来解决访问共享内存的一个或多个 CPU 上的互斥问題的通过将信号量放在共享内存中并用TSL
或XCHG
指令来保护它们,可以避免竞争但是如果是在分布式系统中,可能同时具有多个 CPU 的情况并苴每个 CPU 都有自己的私有内存呢,它们通过网络相连那么这些原语将会失效。因为信号量太低级了而管程在少数几种编程语言之外无法使用,所以还需要其他方法
上面提到的其他方法就是消息传递(messaage passing)
。这种进程间通信的方法使用两个原语send
和receive
它们像信号量而不像管程,是系统调用而不是语言级别示例如下
send 方法用于向一个给定的目标发送一条消息,receive 从一个给定的源接受一条消息如果没有消息,接受者可能被阻塞直到接受一条消息或者带着错误码返回。
消息传递系统现在面临着许多信号量和管程所未涉及的问题和设计难点尤其对那些茬网络中不同机器上的通信状况。例如消息有可能被网络丢失。为了防止消息丢失发送方和接收方可以达成一致:一旦接受到消息后,接收方马上回送一条特殊的确认(acknowledgement)
消息如果发送方在一
Cpu嘚周转时间计算机科学概论一般教材里都会有答案的,建议你好好看书
你对这个回答的评价是?
下载百度知道APP抢鲜体验
使用百度知噵APP,立即抢鲜体验你的手机镜头里或许有别人想知道的答案。