现代操作系统-多处理机系统

769 阅读42分钟

多处理机系统

简介

多处理机系统的目的在于集中不同CPU,主机,甚至不同系统的个人PC来实现统一的工作。目前有三个可用的模型,用于实现协作式计算类型。分别是多处理机,多计算机和分布式系统。

多处理机

这一模型有一个很常见的使用场景,就是多核CPU,如果是这样的话那么这里的共享存储器就是L3缓存,这么说就清晰了,所以这里的处理机更像是一个CPU核心或一个CPU(单核的)。

多计算机

这个更多是在一个房间里,用专用高速网络,连接多个独立的计算机,但是这些计算机没有键盘显示器显卡等,仅把它们串在一起做协作运算。

分布式系统

这个旨在通过普通网络连接全球的计算机,这些计算机可能完全不同,创建一个中间件统一它们做协作运算。

多处理机(multi-processor)

这个系统唯一特别的性质是,CPU对共享存储器写入某个值,然后读回来另一个值,因为可能另一个CPU修改了它。这种性质构成了处理器间通信的基础:一个CPU向存储器写数据而另一个从中读取。

多处理机硬件

访问存储器。如果对于CPU来说,读出每个存储器的速度一样快,那就是UMA(统一存储器访问)。否则就是NUMA(非一致存储器访问)。这些CPU共享同一个地址空间(仅对于公共部分),这和是UMA还是NUMA无关。

  1. 基于总线的UMA多处理机体系结构

对于基于此模式的访问,CPU每次检查总线是否忙碌,如果不是,则把所需要的字的地址放在总线上,发出信号,等到总线给予回复;如果总现在忙,就等待。如图a所示,但是缺点是当CPU很多时(比如AMD 3990X)大多数的CPU就在空闲等待。

解决方案是为每个CPU添加一个高速缓存(L1),如图b,这样的话许多读操作就可以通过每个CPU的高速缓存来实现。为了更好的性能,每次读取总线的某个字时,它所在的整个块都被引入到高速缓存里。

每个高速缓存块都被标记为只读,或者读写(此时此高速缓存行仅为当前CPU独有)。那么怎么处理对于存在于多个CPU的高速缓存行呢?那就是高速缓存一致性协议,也是Intel在自家处理器使用的:每当一个CPU试图写一个同样存在于其他CPU高速缓存的某一块时,总线检测到写,通知其他CPU,让所有拥有此块但未修改的(也就是“干净”的)CPU丢弃它;让所有已修改的CPU把它写回到存储器(实现一致性),并让写者之后去存储器拿到这个块,那么此时写者拿到了最新的此块,而其他CPU也不会拿到过期的块。

当然了,还有一种优化方案,此时每个CPU除了高速缓存外,还有一个小的私有存储区,用专用高速总线连接,如图c,用来放非共享数据,比如程序代码,字符串,常量,其他只读数据,栈和局部变量,而共享存储器只用来放共享变量。

  1. 使用交叉开关的UMA多处理机

对于使用总线的方式,即使总线和高速缓冲区再优秀,还是把CPU的数量限制在16-32。所以我们需要一种新的方法,交叉开关很好地满足了这一需求。

每一个CPU连接一个存储器,并且可以切换到另一个存储器,对于n*n的排列,仅需简单的设置某一横向开关和另一竖向开关的打开与关闭就能实现任意CPU与任意存储器连接,这就和8皇后问题一样。

交叉开关的优点是它是非阻塞的,可以不受某一已存在的连接的限制。而缺点是开关节点数随CPU数呈平方级增长。这对于中等级别是可以接受的,对于更大规模的系统来说是不可接受的。

  1. 使用多级交换网络的UMA多处理机

对于一个交换开关,它可以把输入重新从任意一个输出输出来。比如A->X或A->Y;B->X或B->Y。而每次CPU传输的是一个消息块,它包含四个属性,module指出了目标存储器,address指明在模块中的地址,opcode表明是读还是写,最后可选的value包含实际操作的值。交换开关检查model来确认是发送给X还是发送给Y。

这种2*2的交换开关可用于构建大型网络。其中一种就是简单经济的omega网络,如下所示,对于n个CPU和n个存储器,需要log2(n)级,每级n/2个开关,总数为(n/2)log2(n)个开关,当n很大时,这比交叉开关好一些。

在消息经过交换网络后,模块号左边的位就不需要了,可以用来记录入线编号,这样应答消息可以找到回去的路。

omega网络是一种阻塞网络,所以对于同时访问同一个存储器的CPU而言,就会有一个需要等待,冲突可以发生在线上,也可以发生在存储器里,或者发生在开关中。

对于连续地字被放在不同的模块里称为交叉存储系统,这样可以把并行效率最大化,因为一般对于存储器的引用是连续编址的,或者设计多条从CPU到存储器的线路,使得非阻塞成为可能。

  1. NUMA多处理机

因为交叉开关和交换网络多处理机比较昂贵,所以规模还是不够大,在此,引入了NUMA这种机制。它有一个典型的特征就是,访问本地存储器会比访问远程存储器块(废话)。每个CPU都有自己的存储器,除了自己访问,也可供其他CPU访问。所有NUMA机器都具有以下关键特性:

1. 具有对所有CPU都可见的单个地址空间。
2. 通过LOAD和STORE指令访问远程存储器。
3. 访问远程存储器慢于访问本地存储器。

对远程存储器的访问时间不被隐藏时(因为没有高速缓存),称为NC-NUMA;有一致性高速缓存时,系统成为CC-NUMA。

目前构造CC-NUMA最常见的方式是基于目录的多处理机,通过一个数据库来记录高速缓存行的位置及其状态。

来看一个可能的例子,一个256节点的系统,每个节点包含一个CPURAM,每个RAM有16MB,整个系统存储器加一块有4GB(2^32),被划分成2^26个64字节大小的高速缓存行。全存储器被静态地在节点间分配,每个节点16MB,就是每个节点的RAM,节点0是0-16M,节点1是16M-32M,以此类推。节点通过互联网络彼此连接。每个节点还有记录16MB存储器的2^18个64字节的目录项。也就是每个节点都有一个目录,且目录项个数和高速缓存行个数一致,因为每行都要一个目录项来记录,在这里是2^18。

假设CPU20发处一条LOAD指令,然后CPU20把此指令交给自己的MMU,MMU翻译得到b形式的地址,假设它发现这个字在节点36,那么它就把请求消息通过互联网络发送到该高速缓存行的主节点36上,并询问数据库(目录)行4是否缓存,如果是,高速缓存在哪?假设此时是c所示的情况。请求到达36后,检索目录硬件,每一个目录项都是一个高速缓存行。此时看到行4未被缓存,于是从节点36的RAM中取出第4行,送回到节点20并更新目录项4,指出其目前被缓存在节点20。

假设此时CPU20又有一个请求访问节点36的第2行,得到其目前在82行,此时硬件更新目录项2为20,发送消息给82让它把该行发送给20并使自己的缓存无效。

这个形象化后就像每次引用后都会把引用节点的值带跑了,然后存在自己这里,下次再被其他节点引用然后转移到那里去,高速缓存行好像跑来跑去似的。

对于STORE的话,MMU解析正确的地址,把数据送到目标节点,目标节点在自己的RAM里存储,并更新对应的目录项为未缓存,这样可以保证再次引用得到的是新的值。

  1. 多和芯片

当某个CPU修改了内存中的某个字,特殊的硬件电路会使其他CPU的高速缓存中的该字原子性地删除来实现一致性,这个过程称为窥探

  1. 众核芯片

就是CPU核心数很多,比如AMD 3990X 64核128线程。

  1. 异构多核

在一块芯片上封装了不同类型的处理器的系统称为异构多核处理器。

  1. 在多核上编程

这比较难...确实如此。

多处理机操作系统类型(多处理机软件)

  1. 每个CPU都有自己的OS

这么做允许多个CPU共享操作系统代码,而且只需要提供数据的私有副本,由于每个CPU共享的是OS的不可写部分,再加上每个CPU都有自己的OS数据副本,所以看起来就是每个CPU一个OS。

这一机制相比较于多计算机系统,它允许多个机器共享一套磁盘和其他I/O设备。

该设计固然不错,但是它存在四个潜在的问题:

第一是,在一个进程进行系统调用时,该系统调用是在本机的CPU上进行处理的。

二是,这里没有进程共享,每个进程都在自己的OS和CPU上,因此可能发生某一CPU吃满而另一空闲。

第三,无法共享内存页面,所以可能会出现某个CPU不停地调整页面而另一个在旁边守着空内存看戏地情况。

第四个,也是最坏的情况,每个系统进行自己的磁盘缓冲区维护工作,然后他们分别写入磁盘,嚯!不一致现象就这么出现了。

综上所述,这种模型实际上很少使用。

  1. 主从多处理机

在这里,有一个主CPU,所有的系统调用都定位到主CPU上,而且进程,OS数据表等由一个CPU控制,就不会出现不一致以及同步竞争问题,当某个CPU空闲,就向主CPU申请一个进程运行。但是,这种模型加剧了主CPU的负担。主CPU逐渐成为性能瓶颈。所以也不怎么用。

  1. 对称多处理机

对称多处理机消除了主从机的不对称性。

对于这种设计,所有CPU共享OS,以及OS里面的进程。然后把整个OS当成一个临界区,进行加锁,操作,释放锁。这叫大内核锁(BKL)。但是吧,如果每次在OS操作会花费很久的话,这可能造成后面CPU的长队列等待,所以也不是什么好想法,遂直接把OS切分成不同的临界区。这样的话就可以做到进行不同任务的CPU并行运行,当然也可以做到某些数据被多CPU共享(如果共享它们不会造成麻烦的话),不过记录临界区状态的临界表只能被一个CPU访问。

现在大多数多处理机都采用这种方式,这就使得OS编写更加困难,因为此时要仔细考量临界区分割问题,以及死锁等复杂问题。

多处理机同步

在多处理机中,禁用中断仅仅作用于当前CPU,而不会影响其他CPU的行为。因此,必须采用一种合适的互斥信号量协议,使所有的CPU可以按照此协议互斥地工作。

所有互斥访问的核心都是一条特殊指令,该指令允许检测一个存储器字并以一种不可见的方式写入它。

为了实现互斥访问,可以使用TSL指令。TSL指令必须首先锁住总线,然后进行对存储器的读写,最后释放总线。对总线加锁的典型做法是,先使用通常的总线协议请求总线,并申明(assert)已拥有某些特定的总线线路,在这两个操作完成,只要保持这条总线的拥有权,其他CPU就没法访问,这样就完成了对总线加锁的过程。

不过,TSL是自旋锁(就是在得不到锁时会不停地循环获取锁),这就浪费了CPU以及加剧了总线的负担(因为它一直问总线:我能加锁了吗?能了吗?能吗?...),进而影响其他CPU地工作(因为总线带宽被拉低了)。

或许高速缓存可以解决这一问题,但事实并非如此。某个请求锁的CPU试图获取锁,读到了lock的值,如果此时锁在别的CPU手上,那么这个CPU把lock放入自己的高速缓存,然后不再请求,每次循环自己的高速缓存(或不循环)就可以了,直到那个有锁的CPU释放锁。怎么释放呢?它把lock对应的高速缓存设为0并释放锁,此时由于高速缓存一致性协议的存在,所有拥有lock高速缓存的CPU都丢掉自己的高速缓存,再次去读取lock的值,然后判断是否获得锁。如果获得了,就把内存里的lock字和自己的高速缓存设为1(因为一致性协议会从它这读值给其他请求锁的,所以设为1就告诉其他CPU锁不可用),就开始接下来的工作。

这看似没问题,可是由于缓存优化,每次都会读取lock字周围的块,所以最终读取的是64/32位的块。因为TSL是一个写指令,所以每次有CPU请求锁时,都会触发高速缓存一致性协议,使得锁拥有者的高速缓存块失效,然后从内存位请求者请求一个私有的,唯一的副本,如果锁拥有者触发了锁字的邻接字,就又会触发从内存取块到高速缓存里,这样包含锁的块就在锁的拥有者和请求者之间来回跑,这就造成了更大的总线流量。

解决方案是,先设置一个读请求,然后每次尝试读取,如果读取成功,再施加TSL指令,否则只是读指令,这让多CPU共享同一个高速缓存块成为了可能,这个优化减少了高速缓存块在CPU之间的移动复制。在持有锁的CPU释放了锁后,其他CPU发现锁可用,然后施加TSL指令,注意,此时即使有多个CPU发现了锁可用,最后也只会有一个CPU成功获得锁。

另一个很好的减少总线流量的方法是使用以太网的二进制指数回退算法。此时不是采用轮询方式,而是采用延时查询锁的方式,如果不成功,延时加倍,直至某个最大值。

一个更好的想法是,为每个请求锁的CPU一个私有锁,然后把它们附在等待链表的末尾,当某个持有实际锁的CPU完成时,释放该实际锁并释放链表首的CPU的私有锁,让首CPU进行操作,然后循环往复。

自旋与切换

对于CPU等待锁的时间,到底是进行不停地轮询呢?还是切换其他进程运行呢?这是一个值得考究的问题。

一般而言,这取决于获得锁地时间,如果切换进程地耗时远大于取得锁的时间的话,可以考虑使用自旋,如果进程切换更快的话,那么毫无疑问选择切换到另一个进程并运行。

在这里,有一种称为事后算法的方法,根据获得锁与进程切换的时间进行分析,得出具体哪种方法更好。

在这里有一个研究模型可供参考:一个未能获得互斥信号量的进程首先自旋一段时间,如果时间超过某个阀值,切换,该阀值是一个定值,也可以是一个动态改变的值,这取决于先前观察到的等待互斥信号量的历史信息。

多处理机调度

线程分类

对于用户线程,内核眼中只有用户进程,所以是没法精确调度的;对于单线程进程和用户空间线程,调度单位都是进程;但是对于内核线程,内核忽略它是哪个进程的然后进行任意的调度。

有些线程是协作关系,有些线程是...没有关系。对于协作的线程,可以安排它们并行,并安排彼此间通信。

  1. 分时(time-sharing)

在这里的分时调度类似于普通的进程调度,包括使用时间片的轮转调度,带有优先级的优先级等级调度。所以可以很容易看到,这适用于线程无关的情况,如果是这样的话,以这种方式调度是明智的选择并且很容易高效的实现。

不过这存在两个缺点,一是随着CPU数量的增加所引起的对调度数据结构的潜在竞争;二是当线程由于I/O阻塞所引起的上下文切换的开销。

还有一种情况,假如某个持有互斥量的线程被挂起,那么另一个进程启动并试图获取此互斥量,它就会立马被阻塞。为了避免这样的情况,可以使用一种称为智能调度的方法,如果一个线程获取了信号量,它就会设置一个进程范围内的标志,表明它获得了信号量,智能调度会尽量不挂起它,甚至过多的延长它的时间片直到它完成临界区的工作并释放锁。

对于实际的调度,还有一种情况,某个线程在它上次运行过的CPU里运行效果会更好,因为可能还留有缓存等,所以对于这个线程的调度可以考虑这个问题。这是亲和调度,它尽量让某个线程运行在上次运行过的CPU上。创建这种亲和力的一种途径是两级调度算法。每次位线程分配CPU时,考察哪个CPU比较闲,这种把线程分给CPU的工作在算法的顶层运行,其结果是每个CPU获得了自己的线程集。线程的实际调度工作在算法的底层进行。如果某个CPU把自己的线程集跑完了,它会找另一个CPU要线程跑。两级调度还使得CPU对就绪线程数据结构的竞争减到了最小。

  1. 空间共享(space-sharing)

在多个CPU上同时运行多个线程称为空间共享。对于空间调度,每次检查是否有同线程数量一样多的CPU存在,如果存在,每个进程一个CPU,开始运行,否则等待,直到满足此条件。CPU们被划分成不同的分区,用来满足不同的进程。另一种方式是主动的管理线程的并行度,或者说,当可用CPU数不足时,主动削减需要运行的线程数。

  1. 群调度(gang-scheduling)

空间共享的缺点在于,如果有线程发生了阻塞,那么其所在的CPU就被白白的浪费了,而分时的缺点在于不好实现线程间通信(因为这需要同时运行线程)。如下所示:

所以提出了群调度,每次一起运行一批线程,它们的起始时间终止时间相同,且尽量让这些线程属于同一进程,然后在有空闲CPU时补上其他进程的线程。

多计算机(multi-computor)

由于多处理机构造困难,造价高昂,所以为了进一步提升CPU数量,引入了多计算机。

多计算机容易构造,因为其基本部件只是一个配有高性能网络接口卡的PC裸机,没有键盘,鼠标,显示器。当然,获得高性能的关键是巧妙地设计互联网络以及网络接口卡。

来看几种网络连接方式:

多计算机硬件

一个多计算机系统的基本节点包含一个CPU,存储器,一个网络接口,有时还有一个硬盘。

  1. 互联技术

在每个节点,有一个网络接口用于把各个节点连接在一起,就上图的连接方式来进行分析。

a和b是简单常见的结构。c中的网格,是一种商业中常用的二维设计,这个系统比较简单易扩展,不过它有一个参数,称为直径,即任意两点间的最长路径。该值随节点数的平方根增加。d是其变种之一,双凸面,这种网络容错能力更强,直径也更小。e表示的立方体,f表示的超立方体。现代多计算机更多采用多立方体结构。

在多计算机中,可采用两种交换机制。在第一种机制里,每个消息被分解成有最大长度限制的块,成为包,该交换机制称为存储转发包交换。包从源节点出发,一步一步向前传递,缓冲,最后到达目的节点。包交换机制存在时延问题,解决措施之一是,把包切分成更小的包,然后只要包的第一个单元到达一个交换机,传输就开始而不必等下一个。

另一种交换方式是电路交换。它包括由第一个交换机建立的,通过所有交换机最终到达目的地的传输路径,此路径一旦建立就会一直保持,比特流会源源不断地,快速地通过路径到达目的地,中间的交换机没有缓冲。

电路交换的变种之一是虫孔路由。它把每个包拆分成子包,并允许第一个子包在整个路径还没有建立之前就开始流动。

  1. 网络接口

一般来说,网络接口板上都有一个存储进出包的RAM,因为许多互联网络是同步的,所以一旦一个包的传送开始,比特流必须以恒定的速率连续进行。而在主RAM中,可能还有其它的应用在使用RAM,这就可能导致速率不一致,而使用接口板专用的RAM就可以解决这个问题。同样的问题也发生在读取数据时,因为比特流一般是以很高的速率到达,所以在接口板缓冲一下是一个不错的选择。

接口板上还可以有一个或多个DMA通道,甚至一个完整的CPU(也可能不止一个)。DMA通道可以在接口板和主RAM之间以非常快的速率复制包,因为可以一次性传输多个字。接口板上的CPU功能日益强大,可以进行加密/解密操作,压缩/解压缩等,如果是多CPU还要进行同步,避免竞争条件。

最后,这种一层一层复制数据的跨层复制是安全的,但是不一定高效。

低层通信软件

对于网络通信来说,过多的复制操作是高性能的杀手,所以应尽可能减少复制的发生。有不少多计算机会把接口板映射到用户空间。这种情况不需要内核的参与,但是却引来了两个问题。第一个问题是,映射到虚拟空间的话,假设A进程使用,但是B的消息到达,C进程想发送消息,这应该怎么处理?二是内核有时也要访问网络。假设此时接口板在用户空间,内核消息到达,怎么处理?

对于第一个问题的解决方案,可以把接口板映射到某一个进程里,但是由于存在分时调度,某个刚刚获得接口板的进程被切换,另一需要接口板的进程上场,然后阻塞?所以对于这种方案,只有在每个节点只有一个用户进程时才能工作,否则需要专门的预防机制(比如把接口板上的RAM的不同部分映射到不同的进程)。

第二个问题的解决方案可以是,用两个接口板,一个给用户空间,一个给内核,简单粗暴。

不过,较新的网络接口板都有任务队列,而且有多个队列,可以虚拟化为多个虚拟端口,所以它们可以有效地支持多用户。这对虚拟机也很有用。

  1. 节点至网络接口通信

此时需要考虑下一个问题,就是怎么把包送到接口板上。最快的方法是直接使用板上的DMA芯片把消息从主RAM复制到板上。但是这存在一个问题,就是DMA一般使用物理地址,并且独立于CPU运行,除非存在I/O MMU,否则很难解决。对于这个问题,细分一下,还可以这么描述:用户进程不知道物理地址,只知道虚拟地址,通过系统调用来转换是不现实的,因为映射到用户空间的意图就是减少系统调用;另一个问题是DMA正在读取某个页面时,OS进行了页面置换?或者正在写入然后发生了页面置换??这更严重。

解决上述问题的方法,可以是采取一类钉住和释放的系统调用。不过,这也会加剧OS负担,尤其在包被分成很小的块时,同时这也会增加软件的复杂性。

  1. 远程直接内存访问

就是直接用某个机子访问另一个远程机器的内存,但是不是很现实。

用户层通信软件

操作系统提供了进程间发送和接收消息的系统调用,其实现过程对用户进程是隐藏的,库过程使得这些调用对用户进程可用。所以,对于计算机之间的远程通信,可以把它们封装的尽可能像过程调用那样。下面来讨论这两种方法。

  1. 发送和接收

在最简单的情形下,所提供的通信服务调用可以减少到只有两个库过程调用。一个用于发送消息,一个用于接收。

比方说,发送可以是:send(dest, &mptr);

而接收消息可能是:receive(addr, &mptr);

进程使用这两个方法进行远程通信,dest指出目标进程,addr指出了接收者监听的地址。

  1. 阻塞调用和非阻塞调用

上述的调用是阻塞调用,就是调用send时,必须等消息发送完成才能进行下一步,receive也是,必须等到接收完成才能继续。

相对应的调用时非阻塞调用。在调用完send/receive后,控制权返回,进程继续自己的事,不过非阻塞调用的缺点往往很明显,发送进程由于不知道什么时候发送结束,所以并不知道什么时候才能重用缓冲区,而不再次使用缓冲区又是不可能的。在此有三种解决方案。

第一种是每次把数据复制进内核,然后让内核处理,此时调用的进程可以返回了,但是这就造成了额外的复制,降低了系统性能。

第二种是在消息发送完毕,中断进程,告诉它已发送。但是这会加剧编程负担,并且可能需要处理竞争条件,所以不是很现实的方法。

第三种是让缓冲区进行写时复制,在消息被发送之前,标记缓冲区为只读,然后每次进程想要再次写入缓冲区,就会触发复制(假如此时还没发送消息)。但是对临近的写操作也会触发复制,所以这可能需要把缓冲区孤立在自己的页面上。

自此我们看到,在发送端的选择是:

1. 阻塞发送(CPU在发送期间无事可做)。
2. 带有复制操作的非阻塞发送(复制本身是一种性能浪费)。
3. 带有中断操作的非阻塞发送(编程困难)。
4. 写时复制(最终可能也会需要额外的复制)。

在正常条件下,第一种是最好的选择,尤其是在多线程协作时,因为其他线程依旧可以继续其他工作。

说完了发送,来看看接收,对于receive,阻塞调用也是一个不错的方法,而非阻塞只是通知内核缓冲区所在的位置,然后立刻返回。可以使用中断来通知进程消息到达,但是中断方式编程困难,速度很慢,于是可以采用一个方法poll来轮询进来的消息。该方法报告是否有消息正在等待,进程可以调用get_message()方法来获取第一个到达的消息。当然也可以把poll放在合适的代码里,不过,掌握以怎样的频度使用poll是有技巧的。

另一种选择就是弹出式线程,这个线程运行一个预定义的过程,其参数是一个指向进来消息的指针,在处理完这个消息后,该线程直接退出并被自动销毁。或者消息自身可以带有该处理程序的句柄,这样当消息到达时,就可以用很少的指令调用处理程序,因此避免了复制,处理程序从接口板取到消息然后即时处理。

远程过程调用(RPC)

对于多计算机通信,send/receive本质上还是I/O调用,所以应该寻求一种其他的方式,于是引入了远程过程调用,它允许计算机A调用计算机B的程序,此时计算机B的程序被执行,计算机A的程序挂起,可以在过程调用的参数中传递信息,也可以在调用结果中返回信息。

远程调用背后的思想是尽可能使远程调用像本地调用。一般情况下,称调用者为客户机,被调用者为服务器要调用一个远程过程,客户程序必须必须绑定在客户端存根上,它是一个小型的库过程,它在客户机地址空间代表一个服务器过程。类似的,服务器程序也绑定在服务器存根上,这些过程隐藏了这样一个事实,就是客户机到服务器的过程调用并不是本地调用。

RPC调用过程简述如下:客户机调用客户端存根,这是一个本地调用,其参数以通常方式压入栈内;然后是客户端把参数打包成一条消息,进行系统调用来发出该消息,打包消息的过程称为编排;接下来是内核把该消息发送给服务器;再之是服务器内核把接收来的消息传送给服务器端存根;最后。服务器端存根调用服务器过程。应答则是反过来。

在这里,客户机程序只对客户端存根进行正常的本地调用,客户端存根与服务器过程同名。

通常,对本地过程调用传递指针没什么问题,但是对于RPC来说,这是不可能的,因为服务器客户机不在一个虚拟地址空间里。如果一定要用指针传参,那么对于基本类型以及确定类型是可以的,但是对于图像等复杂数据结构可能就不太行,所以需要对远程调用的参数作出限制。

第二个问题是,参数类型并不是总能推导出的。第三个问题是,对于一些弱类型语言,没法指出矢量类型大小,比如C语言的数组的大小就没法表示。第四个问题和全局变量有关,客户机程序和客户端存根可能使用全局变量来通信,但是在远程上,这是不可能的。

分布式共享存储器

不同的CPU之间共享他们各自的RAM,对于某个地址的操作,如果不在自己的RAM里,会陷入OS,OS为CPU取回页面,建立映射关系,重新执行导致陷入的指令。

在这里需要先声明一个概念,那就是这些CPU共享的虚拟地址空间可能不如所有的RAM加起来大,也就是每个节点的RAM只有一部分被用作虚拟地址空间的物理映射地址。因为本意是共享而不是扩大空间。

  1. 复制

对于系统的一个改进是复制那些只读页面,比方说程序代码,只读常量或其他只读数据结构。它可以明显的提升性能。

当然也可以复制读写页面,但是必须保证在某个页面在被修改后保持其副本的一致性。

  1. 伪共享

当决定复制页面时,问题出来了,因为每次都会复制一块内存,所以复制多大合适?因为网络传输时间最长的地方是一般是启动连接时,所以传输大块和小块差不多。选择大块时,会造成网络长期占用,阻塞其他进程引起的故障,同时CPU引用有一个局限性,就是它很可能会再次引用同一个内存块的字,所以大块内存优势也在此;小块内存可能就会增加传输次数。

过大的页面引发了另一个问题,伪共享。有两个变量A和B,进程1使用A,进行读写,进程2使用B,共享包含这两个变量的块后,如果两个进程频繁对各自的变量进行读写操作,那么会造成内存块在两个节点间来回移动(因为同步问题)。

  1. 实现顺序一致性

对于复制可写页面,然后某个CPU修改了页面内容,维护其他副本一致性的方法和高速缓存一致性协议差不多:在进行写之前,先向所有持有该页面副本的CPU发送一条消息,告诉它们解除映射并丢弃该页面,然后进行写。当然,也可进行在部分虚拟地址空间上加锁,然后在空间内进行写操作,在锁被释放时,产生的修改传播到其他副本上,只要某一时刻只有一个CPU锁住页面,就可以保持一致性。最后一种可能的方法是,写之前复制一下当前内容,写完了对比刚刚保存的原本内容,产生一个修改列表,然后送往获得锁的CPU更新它的对应的副本的值。

多计算机调度

在这里,可以采用很多多处理机的调度策略,但是对于群调度,更重要的是有一个初始的协议决定哪个进程在那个时间槽中运行以及用于协调时间槽起点的某种方法。更多的策略见负载平衡。

负载平衡

在这里需要明确一件事情,一旦某个进程指定给了某个节点,就可以使用本地化调度算法,所以把进程指定给节点的算法就显得尤为重要。

  1. 图论确定算法

因为进程之间需要通信,所以可以以这个为出发点设计——减少节点之间的网络流量。

设计一个模型,圆代表进程,直线代表他们有通信,直线上的数字代表二者通信的平均负载。所以此算法的目的在于把整个图划分成n个独立的区域,使得区域与区域之间的流量和最小,如果是分割的话,就是分割线穿过的线的值加起来最小。这就是算法目标。

  1. 发布者发起的分布式启发算法

如果某个CPU负载很多,它会主动探查其他节点是否负载不重可以替它接几个进程,但是在整个系统负载都很重的情况下,在进行探查,无疑是雪上加霜,而且几乎没有哪个CPU的进程可以被卸载,所以这不是一个特别好的方法。

  1. 接收者发起的分布式启发算法

这次换成空闲的CPU主动找事干,它会探查其他CPU是否过载,然后要进程来运行,这种方法的优点是,不会在系统过载时再次增加系统负担。

当然,那这两种算法组合起来也是可能的。同时也可以设置一个历史记录,记录哪个CPU更可能过载,以更好地解决它的负担。

分布式系统(distributed-system)

简介

多处理机时在一个机箱里,多计算机在一个房间里,而分布式系统,那就厉害了,在全球。它的每个节点可能都不一样,每个结点的OS,CPU,内存,文件系统,会不一样,是否有硬盘,键盘鼠标都不好说。

在操作系统顶层添加一层软件,用来统一分布式系统,这层软件就是中间件。从某种意义上来说,中间件就是分布式系统的操作系统。

网络硬件

  1. 以太网

作为连接多个计算的网络而存在,通过把网线插入计算机网络接口即可实现。

  1. 因特网

在更大范围内完成计算机之间的连接。在Internet上,所有的通信都是以包的形式传送的。

网络服务和协议

  1. 网络服务

对于网络服务提供的方式,一般有两种:面向连接的服务和无连接服务。前者是对电话系统的模仿,后者是对邮政系统的模仿,不在乎应答,只管推送,每个包都有完整的目的地地址,独立于其他包而存在。

每种服务都可以用服务质量来描述,有些服务从来不丢数据,比方说通过接收者返回一个特别的确认包来实现。虽然这降低了传送的速度以及引入了过载和延迟的问题。

可靠的,面向连接的服务有两种很轻微的变种:消息序列和字节流。前者消息由边界,每次发送一个边界之内的消息,比如传送文件,短信等;后者的消息组成一个字节流,不存在边界问题,这可以用于ssh远程控制。

而对于某些应用而言,延迟是不可接受的,比方说语音频通话,毕竟,宁愿马赛克也不能音画不同步。

当然,并不是所有的应用都需要连接,如果只是测试网络,发送单个包(通常这个包具有高可达性)就好了,这是数据报服务。如果要求得到回复,那么可以使用确认数据报服务,它要求在发送一个数据包后得到一个回复。

最后还有一种,请求-应答服务,它在发送一个请求的数据报后,应答里包含着回复。

  1. 网络协议

网络协议,用于定义什么消息可以发送以及怎么响应这些消息。现代所有的网络都使用协议栈来把不同的协议一层层叠加起来使用。

大部分分布式系统都是用Internet作为基础,因此这些系统使用的关键协议是两种主要的Internet协议:IP和TCP。

IP是一种数据报协议,发送者可以向网络上发出长达64KB的数据报,并期望它能到达。IP数据报是非应答的,所以为了可靠的通信,通常在IP层之上使用另一种协议:TCP。TCP协议使用IP来提供面向连接的数据流。远程通信技术对于用户进程是隐藏的,它们看到的只是可靠的进程间通信,就像UNIX管道一样。

基于文档的中间件

对于基于文档的中间件,用一个著名的例子阐述:Web。

Web背后的逻辑很简单,它指出每个计算机持有一个或多个文档,称为Web页面(现在来看应该是HTML页面)。每个页面可以包含文字,图像,声音,甚至指向别的页面的超链接。当用户使用Web浏览器访问远程计算机的某个页面时,Web浏览器发起请求,然后把请求返回的页面呈现给用户。

每个Web页面都有一个唯一的地址,称为URL(统一资源定位器),其形式为:protocol://ip:port/filename。

整个系统按照如下方式结合在一起:Web根本上是一个客户机-服务器系统,用户是客户端,而Web站点则是服务器。

基于文件系统的中间件

阴藏在Web背后的思想是:使一个分布式系统看起来像一个巨大的,超链接的集合。另一种处理方式则是使一个分布式系统看起来像一个大型文件系统。

分布式系统采用一个文件系统意味着只存在一个全局文件系统,全世界的用户都能读写他们各自被授权的文件。通过一个进程把数据写入文件而另一个读出文件即可实现进程间通信。不过这引发了一些与分布性有关的新问题,如下所述:

  1. 传输模式

既然是文件系统,就必然涉及到文件的处理。对于文件的处理无非就是读和写,那么怎么读写整个系统里的文件呢?有两个可用方法,一个是上传/下载模式,一个是远程访问模式。前者每次客户机打算对服务器文件操作时,都会把文件复制到本地,然后操作,再把更新过的文件写回到服务器;而在后者,客户机远程操作服务器上的文件。

不过上传/下载模式要求客户机空间足够,当有多个用户时,还有考虑并发访问问题。

  1. 目录层次

如果采用文件系统,那么另一个需要考虑的方式是文件的组织形式,也就是文件目录。这就产生一个问题:是否所有的用户都拥有该目录层次的相同视图。

如果都一致,那么要求再客户机1的某个路径对于客户机2也必须可用;若采用不同的视图,则可能会发生同一路径下文件不存在的现象,不过这样的好处是灵活且可以直接实现。

  1. 命名透明性

对于基于文件系统的分布式系统,文件的命名也是一个值得考究的问题,在这里命名的主要问题是,它不是完全透明的。(在这里解释一下透明性,就是在PC里,每个文件都有路径名,此时它是透明的,因为知道它的绝对位置,以及它是在确定机器上的,也就是当前PC,但是在分布式系统不一定,因为不同的节点甚至系统都不一样,就很难做到刚刚的完全确定,这就是不透明的命名)

对于透明性,这里有两种类型,一种是位置透明性,其含义是路径名没有隐含文件所在的(物理)位置信息。所以该服务器可以随便跑,而不用在意它到底是在东京还是新泽西或者是成都。

如果在整个分布式系统中,随意在服务器之间移动某个文件但文件名称(包含路径的名称)不会改变,那么称该文件具有位置独立性

最后来总结一下在分布式系统里处理文件和目录命名的方式通常有以下3种:

1. 机器+路径名,如/machine/path。
2. 将远程文件系统安装在本地文件层次中。
3. 在所有机器上看起来都相同的单一名字空间。
  1. 文件共享的语义

既然是多用户,就不可避免地会发生多个用户对服务器上的同一个文件进行操作而引发的竞争问题。事实上,系统强制所有的系统调用有序,并且所有的处理器都能看到同样的顺序,我们将这种模型称为顺序一致性

如果只有一个文件服务器且客户机不缓存,那么还是可以实现的,不过实际情况是客户机不缓存的话,所有请求打到服务器,会造成服务器性能堪忧,所以客户机必须缓存,但是这样就会造成客户机1对文件A的修改是本地化的,这样另一个请求文件A的客户机2拿到的就是过时的,进而造成不一致。解决措施可以是让每次修改立马同步到服务器,这样后面请求同一文件的客户机可以拿到最新的副本。不过这很低效。

另一个方法是只有在文件关闭时,才把更改同步到服务器上,这个就是上传/下载模式,这种同步语义得到了广泛地实现,即会话语义。

另一种可行的方法是,进行上传/下载模式时,对文件加锁,以避免不一致的文件。

基于对象的中间件

此中间件使用一种称为ORB的对象实现,这种对象无关于语言,它含有域,以及访问域的方法。要调用一个对象中的方法,客户机需要先获得对该对象的引用。ORB被放置在客户机和服务器之间,客户机像客户端ORB传递参数,客户机的ORB与服务器的ORB取得联系,执行远程调用,整个过程类似RPC。

基于协作的中间件

  1. Linda

对于Linda来说,整个系统共用一个元组空间,节点与节点之间通过元组空间通信,元组空间是一个元组的集合,元组是一系列数据的集合,有点像JSON字符串,里面用','分隔出一个一个的数据域。

每个节点都可以在元组空间里添加或者移除元组,out操作可以添加元组,in操作获取元组,匹配方式是内容匹配,只有元组内容一致才可以。元组元素支持表达式,变量和常量。in操作如果匹配到元组就把它移出元组空间并返回元组。当然了,还有read操作,它不会把元组移出,eval进行元组计算,并把计算结果放入元组空间。

  1. 发布/订阅

这种方式类似Linda的元组,生产者会产生一个新的消息并在网络上广播,这个过程称为发布,而消费者可以订阅特定的主题。系统会提供守护线程监听消息类型,如果是某个消费者感兴趣的就调用这个消费者进行处理。

为了实现更加稳定的系统,可以使用一个数据库来记录历史消息。