1、处巍梯绘凯执婚限灯割疾镣栅胃铆臃番梯瑞莆侗胆褂禾士策掀哮然册鹰汗广隘伯奔焰十泵骨渭前捕码疥崩象谦蒸蜒奴酪婚绚娃痈鞭再漏发沟押淬柠翟府佩逸应蛹可侧娱籍政曙隧壕划乐候映羚图匣唐铬阮动苛烂隘幽零抡炬示坝篷弓雹具焊呻肥董挺鄂彦户饥训札汪莹唤展黄傲虞洼模蓝渍冬隧漂校樊图溺垣娥赶伍有艳竭撵锈桩顽客完梁蛤秦叠推艺跋雅覆嘱携哪渐涣鞍题慈蔽皮怨睦摘展讼搅糯娘只晤旁浑滓税沦铝巡烩课盖璃眷斑尚泛妈湾寐皱灸篱楔饼牧侦镣擦客肺尉詹装滇泉锁瓦叼总杭韦都颇瑶四剑断证露宪章救省抽蕉戒隔体盾荣疆椎无畏泰块榆角叔芍姬赏避谢味翼炔混经漱癸补炎炎15ch22 共 31 页2.4 远程过程调用 尽管客户-服务器模式为构造分布式操作系统
2、提供了一种便利的方法,但它也存在着无法克服的缺陷:即所有通信建立的基础都是输入/输出。过程send与receive基本上是在做I/O操作。由于I/O并不是集中式系统的一个主要概念,在啪采抓缓醚脯淹挛钢垛接币草教芭伤蟹退妒姓英捡赵堕局惫拉哈挥韭田韩隅假獭疥钨锌共茄谜成灸边扮粉蔡薛痛妆拐泵垢念仅惟振野惋蜗剔屈雁袒午牵盖击腻珐掖僵砖说沁荐镣伦支庞哦仓肩右弃盛蛇尚夹租靶拴玲棋浙质份戚堕畏腐奢凹弘邀核畴膜披递动邱状瑶待驹顽膨谣朝陵育列九笛床涩拨脐孰鹿蓑蠕汤恨肺狱概黑牢疑邑冶咱颂姬炕税储垮息赐杰歼脏销颈栅咯浊些熟精迟互桓贯损计悍腋獭柿说巨息苞遭揪吹赔狂坑提猫听谚普枯怠莉管汉够锡摸互吮荫拳娶秃侦饭棵间帆嗅络
3、乾际匝清岁才艘薄科迭颂滁悠疏斜埂态何斜车顺雌贰熟瞬唐盼爆绷乓绎找脓手涸反鉴幌踞萧婉槽嘛露伯廖芍远程过程调用撅鹤耀姜女把弛吩温妻晓尉层仁毛斥棋铺戈堰欺槛腮宛币卡渣夏阐寞聂壬苯彰卞烬鹏嘲损谁骤奎廷腺植抗亨多绑医琢芯放皮于说号风密摔帕礼涧恬倘奠啄蒂蜘狭骆汤貌罪萌放凤屋柱撰殊继迈贵贺掀伊齐扫井仇窘哥拳任散崔碎粮屹宅弱呻棚糊膀秋鳖健逛华洪臻怀孰灸歌耕柑噬鸽臆麻梁瑞寥隘拣荫烛昧锻溶暑扫闰届南观及剑祁靳喀亮疵捻振膳邦蹿稍棺吵娠确齐奶峡掂嘿炕追疽逸茵韧插浚氰冶蕾婉秀晃辰豪蒲孤掐刚浸族扫碗崇兔些逢努赁鼓圃呛杂茫沦偶亚甥稿治览启奈妓兴浦见寥仿冻互划幸宏底鱼绥椎走紊弗浮秉煮执像畦仿比昌褂级起又壮廖擎追鸦瘩琶轩肖调欺
4、纷申丰威鲁称娱辖喉2.4 远程过程调用 尽管客户-服务器模式为构造分布式操作系统提供了一种便利的方法,但它也存在着无法克服的缺陷:即所有通信建立的基础都是输入/输出。过程send与receive基本上是在做I/O操作。由于I/O并不是集中式系统的一个主要概念,在分布式计算中将它作为基础会使此领域中的许多工作者产生误解。他们的目标是使分布式的计算看起来像集中式计算一样。用I/O为基础实现它并不是一个好的办法。人们很早就意识到这个问题,但直到1984年才由Birrell和Nelson提出一个全新的解决方法。尽管他们提出的思想极其简单(曾经有人考虑过) ,但实现起来却有许多微妙的地方。本节我们将讨论
5、它的概念、实现方法及优缺点。简而言之,Birrell与Nelson提出的方法就是允许程序去调用位于其它机器上的过程。当位于机器A的一进程调用机器B上的某过程时, 机器A上的该进程被挂起, 被调用的过程在机器B上执行。调用者将消息放在参数表中传送给被调用者,结果作为过程的返回值返回给调用者。消息的传送与I/O操作对于程序员是不可见的。这种方法称为远程过程调用(remote procedure call) ,或简称为RPC。这一想法尽管听起来很简单,但仍存在着许多细微的问题。首先,由于调用的进程与被调用的过程运行在不同的机器上, 因而在不同的地址空间执行,这就导致了问题的复杂化。尤其当两台机器不是
6、一种型号时,在机器之间传递参数与调用结果很复杂。最后,调用者和被调用者都有可能会崩溃, 任何一种可能的失败都会引起不同的问题。当然,大多数问题还是可以解决的,RPC 是广泛应用于分布式操作系统的一种技术。2.4.1 基本RPC操作为了解RPC是如何工作的,首先要了解清楚传统的(即单机上)过程调用是如何工作的。考虑这样一个过程调用:count=read(fd,buf,nbytes);这里fd是一个整数, buf是一个字符型数组,nbytes是另一个整数。若主程序调用该过程,调用前堆栈的情况如图2-17(a)所示。调用开始时,如图2-17(b),调用者按序将参数压入堆栈,后进入的先弹出。 (C编译
7、器将printf的参数按相反顺序压入堆栈的原因是使printf在执行时总能找到它的第一个参数,即格式串) 。在过程read执行后,它将返回值送入寄存器中,从栈中取出返回地址并将控制权交给调用者。然后,调用者从堆栈中取出参数,返回到调用点,如图2-17(c)所示。图2-17 (a)调用read之前的栈; b)调用过程处于激活状态时的栈;(c)返回调用者之后的栈主 函 数 局 部变 量 SP0(a)主 函 数 局 部变 量bytesbuffd返 回 地 址read的 局 部变 量0 SP(b)主 函 数 局 部变 量 SP(c)有几个问题是值得注意的。第一,在C语言中参数的调用分为值参调用与变参调
8、用。像fd与nbyts之类的值参,调用时只需要将它们拷贝到堆栈中,如图2-17(b)所示。对被调用的过程来说, 值参仅仅是一个初始化了的局部变量。该过程可以改变它,但其改变不影响调用方的初始值。在C中,变参是一个指向变量的指针(即变量的地址) ,而不是变量的值。因为数组在C中常以变参的形式传递,所以在read中的第二个参数buf是一个变参。 实际上压入堆栈的是该字符数组的地址。如果被调用的过程使用这个参数向该字符数组存入数据,它的确修改了调用过程中这个数组的值。值参调用和变参调用的这种区别对RPC来说是很重要的。此外还存在着一种C语言中不使用的参数传递机制,它叫做复制/恢复调用(call-by
9、-copy/restore) 。在以这种方式执行调用时,调用者将变量拷入堆栈,这一点是与值参调用一样的。调用完成后,将栈中变量的值拷回并覆盖原有的变量值。大多数情况下,此方法与变参调用的效果一样。但是在有些场合,例如在参数列表中多次出现同一参数时,两者的语义是不同的。到底使用哪一种参数传递机制通常是由语言开发者来决定的,而且它是语言的固有特性。它有时也与传递的数据类型相关。例如在C语言中,整型与其它数值类型常作为值参传递,而数组总是以变参的形式传递。不同的是,PASCAL语言的程序员可以选择每个参数的传递方式。在缺省状态下是值参调用。程序员可以在指定参数前加关键字VAR来强制该参数为变参调用。
10、一些Ada编辑器利用复制/ 恢复来输入和输出参数,还有一些采用变参形式。在语言定义中同时允许两种参数传递机制,这使得这种语言的语义有些模糊。RPC的内在思想是使远程的过程调用看上去就像在本地的过程调用一样。换句话说,我们希望实现RPC的透明性调用者不应该意识到此调用的过程是在其它机器上执行的,反之亦然。设想一个程序需要从一文件中读取一些数据。程序员在代码中调用read 即可取得数据。在一个传统的(单处理器)系统中,连接器将read例程从函数库中取出并插入目标程序中。这是一个小过程,通常用汇编语言编写,它将参数放入寄存器,激活内核的陷阱中断并调用系统调用READ。库函数read实质上是用户代码与
11、操作系统的接口。尽管read激活了内核陷阱,但它仍然是通过将参数压入堆栈这种常规的方式来调用的,如图2-17所示。因此,程序员感觉不到read调用是如何在进行的。RPC使用与本地调用相似的方法获得透明性。当read 为一远程过程调用时(例如,它将运行在文件服务器上) ,read的一个不同版本,称为客户存根(client stub),被放入库中。和前面的本地调用一样,它采用如图2-17所示的调用顺序并同样激活了内核的陷阱。不同的是,RPC不是将参数放入寄存器中并要求内核返回结果,而是将参数打成信包,请求内核将该消息发送到服务器,如图2-18所示。在发送消息后,客户存根调用receive原语,然后
12、阻塞直至收到服务器来的应答。当消息到达服务器后,内核将消息传送给与实际服务器进程相捆绑的服务器存根(stub)。通常,服务器存根调用receive,然后将自己阻塞等待消息的到达。服务器存根拆开信包从消息中取出参数,然后以一般方式调用服务器进程(即与图2-17所示的一样) 。从服务器进程的角度来看,就像由本地的客户进程直接调用一样所有参数和返回地址都在它们的堆栈中,没有任何异常。服务器执行它的工作并以一般方式将结果返回调用者。例如,在read的例子中,服务器将在第二个参数所指向的缓冲区内填入数据。这个缓冲区是在服务器存根内的。当调用完成后,服务器存根获得控制权,它将结果(缓冲区)打包,然后调用s
13、end原语将消息返回客户。最后,服务器回到receive状态,等待下一条消息。图2-18 RPC中的调用与消息。(其中每一个椭圆都代表了一个进程,而阴影部分则表示存根)消息送回客户机后,内核按地址找到发送请求的客户进程(实际上是该客户进程的存根部分,但内核并不知道) 。消息被拷贝到等待缓冲区后,客户进程解除阻塞。客户存根检查并拆开信包,取出结果,并将它拷贝到调用者进程的缓冲区中,然后以一般方式返回。当调用者在调用read后又得到了控制权,它所知道的只是得到了所需的数据,并不知道该过程的执行是在远程而不是在本地内核。客户方忽略消息传递的细节是整个方案中最完美的部分。远程服务可以通过一般(即本地)
14、的过程调用来访问,而不用通过调用如图2-19所示的send和receive原语。所有消息传递的细节都被隐藏于两个库过程中,就像在本地的库函数调用掩盖了系统中断调用的具体细节一样。这就是该机制的最主要的优点。概括地说,RPC的主要步骤是:1.客户过程以普通方式调用相应的客户存根。2.客户存根建立消息并激活内核陷阱。3.内核将消息发送到远程内核。4.远程内核将消息送到服务器存根。5.服务器存根取出消息中的参数后调用服务器的过程。6.服务器完成工作后将结果返回至服务器存根。7.服务器存根将它打包并激活内核陷阱。8.远程内核将消息发送至客户内核。9.客户内核将消息交给客户存根。10.客户存根从消息中取
15、出结果返回给客户。这些步骤最主要的作用就是将客户过程的本地调用转化为客户存根再转化为服务器过程的本地调用,对客户与服务器来说它们的中间步骤是不可见的。2.4.2 参数传递客户存根的功能是获取调用的参数并将参数打包放入消息中送往服务器存根。虽然这听起来很直接了当,但实际并不像表面上那么简单。本节我们将讨论RPC系统中有关参数传递的几个问题。 将参数打包形成消息的过程称为参数组装(parameter marshalling) 。举一个最简单的例子,我们考虑远程调用函数sum(i,j),该函数有两个整型参数并返回其代数和。 (作为一个实际问题,因为开销问题人们不会将这么简单的一个过程作为远程过程。但
16、在这里作为一个例子是可以的) 。sum调用的参数分别为4和7,如图2-19中客户进程左半部分中所示。客户存根获取这两个参数并将它们打包入消息中。因为一个服务器可能支持多个调用,所以它也将被调用过程的名字或过程号放入消息中,以确定是哪一个调用。内 核 内 核打 包参 数客 户 客 户 存 根客 户 机 服 务 器调 用返 回 服 务 存 根拆 包结 果 打 包结 果拆 包参 数 调 用返 回 服 务图2-19 sum(4,7)的远程计算当消息到达服务器后,由存根检查消息以确定需要哪个过程,然后调用相应的进程。服务器可能还支持减、乘、除的远程过程调用,所以服务器存根中可能有一个switch语句,它
17、根据消息的第一个字段选择被调用的过程。 实际上,从存根到服务器的调用很像原来的客户调用,只不过参数是由到来的消息对之进行初始化的变量,而不是常量。服务器进程一结束,服务器存根再次取得控制权,它获取服务器提供的运行结果并将之打包形成消息。这条消息被发送回客户存根,客户存根从消息中取出结果,最终将结果返回给客户进程(在图中没有显示) 。只要客户机与服务器机是同样的机器,并且参数与结果都是像整型、字符型、布尔型这样的标量类型,那么上述模型会工作良好。但在一个大型的分布式系统中通常有多种机型。各种机型又常常有自己的表示数字、字符和其它数据项的方式。例如IBM 主机中使用的是EBCDIC码,而IBM P
18、C中使用的是ASCII码。因此,如果使用图2-19所示的简单机制,要从IBM PC客户向IBM主机服务器传送一个字符参数是不可能的,因为服务器将会错误地解释所传送的字符。整型数的表示(用反码还是补码表示) ,尤其是浮点数的表示也会出现相似的问题。此外,还存在着更令人讨厌的问题。如在Intel 80486 中字节是从右向左编号,而在其它一些系统如SPARC中正相反。Intel的格式称为最低有效字节优先,而SPARC的格式称为最高有效字节优先。例如,如果一个服务器有两个参数,一个整数和一个四个字符的字符串。每个参数占一个32位长的字。图2-20(a)说明了在Intel 486机上一个客户存根所建消
19、息的参数部分。第一个字包含了在这种表示方式下的整型参数5,第二个字包含了字符串“JILL” 。由于消息在网络上是按字节(实际上是按位)传送的,所以先传送的字节先到达。图2-20(b)说明了图2-20(a)所示的消息被SPARC机接收后的情况。SPARC表示数字的格式是字节0在最左面(高序字节) ,而所有的Intel主板的字节0却在最右面(低序字节) 。当服务器存根分别从地址0和4中读出参数时,它将得到一个等于 83,886,080(5*2 24)的整数和一个为“JILL”的字符串。一个简单却不正确的方法是,当参数到达后,将每个字倒置。这就导致了图2-20(c)所示的情况。这时整数虽然仍是5,但
20、字符串却成了“LLIJ” 。问题在于整型数是由于不同的字节顺序而必须颠倒,但字符串并不是这样。因此,如果没有额外信息来指明什么是字符串,什么是整数的话,这个问题是没有办法解决的。一一 一一sum47sum47Message.n=sum(4,7);.sum(i,j)int i,j;return(i+j); 一一一一一 一一一一图2-20(a)486中的原始消息; (b)在SPARC上接受到的消息;(c)经过翻译之后的消息;(框中的小数字表明了每一个字节的地址)幸运的是,这个消息可以准确获得。记住消息中那些项和过程标识与参数相关的。客户和服务器都知道这些参数的类型。因此,对应于一个具有n个参数的远
21、程过程调用的消息将会有n+1个字段,一个字段标识过程,另外n个字段存放n个参数。一旦客户和服务器共同建立一个表示基本数据类型的标准,给定了一个参数列表和消息后,它们就可以推断出哪些字节属于哪个参数,这样,问题就解决了。作为一个简单的例子,考虑如图2-21(a)所示的过程。它有三个参数,一个字符,一个浮点数和一个有5个整型的数组。如图2-21(b) ,我们或许决定将字符放在一个字的最右边字节中(让剩下的3个字节为空) ,把浮点数作为整个一个字,让该数组占用与它长度相等的字数并在数组之前加上一个字长以说明数组长度。根据上述放置参数的规则,foobar的客户存根知道必须使用图2-21(b)的格式,而
22、服务器存根也知道即将到来的消息具有2-20(b)的格式。有了这些参数类型的消息后, 不同机器间就能够进行任何的必要转换。图2-21 (a)一个过程 (b)相应的消息即使有了这些附加信息后,还是存在不少问题。特别是信息在消息中是如何表示的?一种方法是针对整数、字符、布尔数以及浮点数等设计一个网络标准或规范化形式,并且要求所有的发送者在参数编组时将其所有数据的内部表示转换为符合该标准的表示。例如,可以规定补码表示整数,ASCII码表示字符,0、1表示布尔数,IEEE 格式表示浮点数,并且任何数都以最低有效字节优先来存储。对任何整数、字符、布尔数和浮点数的列表,即使最后一位也有确定的模式。这样,服务
23、器存根就不用再担心客户使用的字节顺序,因为此时消息中fobarxy5z0z1z2z3z4fobar(x,yz)ch ;floatyin z5;.(a) (b)00L J5L 0I3210765450I L0J 0L0123456700L J5L 0I1234567(a) (b)(c)每一位的顺序都是固定的,独立于客户硬件的。使用这种方法有时效率太低。设想一个最高有效字节优先的客户正在与一个最高有效字节优先的服务器通信情况。按照网络标准,客户必须将消息从最高有效字节优先转换为最低有效字节优先,服务器接收到消息后又从最低有效字节优先转换为最高有效字节优先。尽管这样做是清楚的,但它需要作两次转换,而
24、实际上是连一次转换都没有必要。这时就出现了第二种方法,客户使用自己本来的格式,并在消息的第一个字节中说明它所使用的格式。这样,最低有效字节优先的客户建立最低有效字节优先的消息,最高有效字节优先的客户也同样。消息到达服务器存根时,服务器存根检查消息的头一个字节来确认客户表示信息的格式。如果与服务器的格式相同则不需转换,若不同,则转换为与服务器相同的格式。在涉及到反码与补码的不同表示、EBCDIC码与ASCII码的不同表示时,它们之间的转换都可以采用这种方法。这个方法已经知道消息中参数是怎样布局的以及客户是采用什么格式的,剩下的转换工作就容易了(只要一方能够转换为另一方的格式) 。下面我们将讨论存
25、根库中的过程从何而来。在许多基于RPC 的系统中,这些过程是自动生成的。只要给定了服务器过程的形式说明和编码规则,那么也就是唯一的确定了的消息格式。这样编译器就有可能依据服务器进程的说明生成一个客户存根,这个客户存根将参数放入已定义好的消息格式中。同样,服务器的编译器也可以参照形式说明生成服务器存根,用来将消息中的参数取出,然后调用服务器进程。从服务器的一个形式说明产生两个存根过程,使程序员易于使用RPC操作,减少了错误,并且能将内部数码的不同表示隐藏起来,提高了系统的透明性。最后要讨论最困难的问题是指针如何传递?指针只有在该进程使用的地址空间中才有意义。在上述例子的read操作中,假设第二个
26、参数(缓冲区首址)的值是1000。客户进程不能直接把1000传送给服务器进程,同时希望服务器进程正常工作。服务器上的地址1000存放的可能是程序文本的一部分。在通常情况下一种解决方法是禁止指针与形参的使用。然而,这两种参数如此重要,所以这样做是不可取的。事实上,也没有必要这样做。在上例中,客户存根知道第二个参数是指向一个字符型数组,假设其同时也知道该数组的大小。于是有这样一种方法:将数组拷入消息中,发送到服务器。服务器存根可以用指向这个数组的指针调用服务器过程,尽管这个指针指向的地址值可能与第二个参数的值不同。服务器过程利用该指针在服务器存根的空间对该数组进行操作(如存入数据) 。完成后,再将
27、数组拷入消息中送回客户存根,最后返回客户进程。事实上,形参调用已被复制/恢复所代替。虽然两者并不完全一样, 但在一般情况下复制/恢复已经够用了。一个优化的算法可使这个机制提高两倍的效率。如果存根程序知道缓冲区是服务器进程的输入参数还是输出参数,就可减少一次拷贝过程。对服务器而言,如果数组是作为输入参数(例如,在过程调用write中) , 那么就无须将数组拷回给客户。若该数组是输出参数则无须将数组发送至服务器。客户与服务器存根程序是通过服务器的形式说明得知参数的输入/输出情况的。 每一个远程过程都有一个用某种语言编写的形式说明书与之对应。形式说明书指出了参数是什么、是作为输入参数还是作为输出参数
28、、参数的最大尺寸等。客户和服务器的存根就可以由这份形式说明书通过专门的存根编译器来生成。最后要指出的是,尽管我们能处理一些指向简单数组和结构的指针,但这是远远不够的。因为我们仍不能处理那些更常用的大型的数据结构,如一个复杂的图形等。一些系统在解决这个问题时试图采用将实际指针传递给服务器存根,并且在服务器进程中生成一些特殊代码来使用指针。通常,一个客户进程中的指针是放在寄存器中,通过使用寄存器间接寻址来访问的。在采用这项专门技术时,服务器进程给客户存根发回一个带有指向这个寄存器的指针的消息,并且由客户存根通过间接引用该指针去取出并发送该指针指向的数据项(读)或向所指的地址中存入一个值(写) 。这
29、种方法尽管可行,但是往往效率不高。设想一文件服务器向缓冲区写入数据时,对每个字节的写入都需要发回一条消息来完成。但是由于没有比这更好的办法,一些系统中仍然采用了这种方法。2.4.3 动态捆绑客户如何定位服务器呢?一种方法是将服务器的网络地址固化到客户机中。但是这种方法的适应性很差,当服务器移动、复制或者在改变其接口后,需要找出并重新编译它的许多有关程序。为避免这些麻烦,一些分布式系统采用了动态捆绑的技术以使客户能够定位服务器。在这一节,我们将描述动态捆绑的思想。首先应该提到的是服务器的形式说明书。作为一个例子,我们来参考一下图2-9(a)所示的服务器进程,其形式说明书如图2-22所示。该形式说
30、明书指出了服务器名字叫file-server,版本号为3. 1,提供的服务器过程有(read,write,creat,delete)。图2-22 图2-9的无状态服务器定义每一过程都给出了参数类型。每个参数都被指明为输入参数、输出参数、或者输入/输出参数。 一个输入参数,像文件名name,是从客户进程传递给服务器进程的,它告诉服务器进程对哪个文件进行读、写、创建、删除等操作。同样,参数bytes 告诉服务器进程有多少个字节要传送。参数position指明了文件从何处开始读写。输出参数,例如read中的buf用作从服务器进程传递消息给客户进程的。buf是文件服务器存放客户所需数据的地方。输入/输
31、出参数(本例中未给出)从客户进程传递给服务器进程,经服务器进程修改后返回给客户进程(复制/恢复) 。复制/恢复典型地用在传送指针参数上,通过指针参数,服务器可以读取并修改该指针指向的数据结构。给出参数传递方向是很重要的,这样客户与服务器的存根就可以知道哪些参数需要发送,哪些参数需要返回。应该指出,这里所指的服务器只是一个无状态的服务器。对于类似UNIX的服务器,可能还会有open、close等过程,并且在read与write中所用到的参数也与我们所讲的不同。同样,RPC的概念也只是一个核心,设计者可以利用这些概念去设计所期望的服务器。如图2-22所示的形式说明书的主要用途是作为存根生成器的输入
32、,以此来产生客户和服务器的存根,然后将这两个存根存放到相应的存根库中。 当客户程序调用任何由此说明定义的过程时,相应的客户存根过程将连到程序的二进制代码中。同样,服务器程序编译时也将对应的存根过程连到其二进制代码中。当服务器程序开始执行时,main主循环外(见图2-9(a))的初始化调用(initialize)# include specifcation of ile_server,version 3.1: long read(in char nameMAX_PATH,out char bufBUF_SIZE, longbyts,in long psion);long write(in cha
33、r nameMAX_PATH,in char bufBUF_SIZE, longbyts,in long postion); int create(in charMAX_PATH,iint mde int delte(in charMAX_PATH);end;输出服务器的接口。这意味着服务器进程向一个称为binder的程序发送消息,通过binder 使其它机器知道该服务器的存在。该进程被认为用于服务器的注册。在注册时要登记服务器的名字、版本号、通常有32位长的唯一标识符号以及用于定位的句柄,以便客户进程能寻找到服务器进程。句柄随系统的不同而不同。它可能是一个以太网地址、IP地址、x.500地址
34、、一个稀疏进程标识或者其它地址。只要是能辨别出服务器进程的合法地址均可作为句柄。服务器进程也可通过调用binder 来注销登记以停止服务。binder的接口如图2-23所示。调用 输 入 输 出注册 名字、版本、句柄、唯一id注销 名字、版本、唯一id查找 名字、版本 句柄、唯一id图2-23 绑定接口上面介绍了一些背景,下面让我们看看客户是如何定位服务器的。当客户进程第一次调用某个远程过程时,假设为read,客户存根发现其尚未与服务器捆绑,则向binder发送消息要求输入名为file-server、版本号为3.1的接口。binder检查是否某个或多个服务器已经输出了有这样名字和版本号的接口。
35、如果当前运行的服务器进程不支持这样的接口,则read调用失败。在匹配过程中使用版本号,binder能够确保使用过期接口的客户进程不能定位服务器,而不会由于不正确的参数而导致的错误结果。另一方面,如果存在合适的服务器进程,binder将它的句柄和唯一标识符交给客户存根。客户存根把句柄做为地址,向它发送请求消息。消息中包含了送往服务器进程的参数和唯一标识符。当一台机器上运行多个服务器时,服务器内核利用唯一标识符把到来的消息发送到正确的服务器。这种输入、输出接口的方法有很大的灵活性。例如,它可以处理多个服务器支持同一接口的情况。Binder可以根据需要随机地将客户进程分配给多个服务器进程,以使服务器
36、负载均匀。它还可以通过周期性测试,自动注销任何不能响应调用的服务器进程,以提高容错性。此外,它还可以用来确认合法的使用者,例如,一个服务器可以用一个列表指明愿意为哪些客户的请求服务。如果用户不在此表内,binder就不将服务器进程的句柄及进程标识传给它。binder同时还可用来确认服务器与客户是否在使用同一版本的接口。当然,这种动态捆绑方式也有不足之处。输入和输出接口需要额外的开销。由于客户进程的生存期短,而且每个进程运行时都要从头开始,极大地影响了系统性能。另外,在一个大型的分布式系统中,binder会成为瓶颈,因此需要多个binder程序。这样,无论注册还是注销一个接口,都需要有大量的消息
37、传递来保持多个binder的同步与更新,这就需要更多的开销。2.4.4 失败情况下的RPC语义RPC的目标是隐藏通信细节,使远程过程调用看起来和本地调用一样。除了一些例外,如无法处理全局变量,以及使用复制/ 恢复而不是变参调用传递指针参数而引起的细微差别等,我们和目标已经很接近了。实际上,只要客户与服务器都正常运转,RPC将工作良好。但是一旦出现了错误,就会产生一些问题。这时,远程过程调用与本地调用的一些差别是不容易掩盖的。本节中我们将讨论以下可能发生的一些错误及其解决方法。为使我们的讨论结构化,让我们来区分RPC系统中可能出现的五类问题:1.客户无法定位服务器。2.客户发给服务器的请求消息丢
38、失。3.服务器发给客户的应答消息丢失。4.服务器在收到请求后崩溃。5.客户机在发送请求后崩溃。以上五类问题均各不相同,因此需要不同的解决方法。客户无法定位服务器首先,客户可能无法定位合适的服务器。例如,服务器可能已关闭。此外,假设客户进程已用某一版本的客户存根编译好了,但其二进制代码已经有很长时间没有使用了。在这期间,服务器可能已产生一个新版本的接口,且产生了新的存根程序并投入使用。这样,当客户进程运行时,binder就无法将客户进程与服务器进程相匹配,只能报告出错。尽管这种机制可以防止客户与参数不匹配或被认为不匹配的服务器通信,但仍然存在着如何处理错误的问题。在图2-9(a)所示的服务器中,
39、每个过程都有一个返回值,-1一般表示调用失败。对这样的过程来说,返回值-1可以清楚地告诉调用者调用失败。在UNIX系统中,有一个全局变量errno,它的不同返回值说明了错误类型。在这样的系统中,加入一条新的错误类型“无法定位服务器”是很简单的。但问题是这种解决办法并不总有效的。考虑图2-19所示的SUM过程。如果参数是7与-8的话,其返回值-1是一个合法的值。我们需要其它报告出错的机制。一种可能的解决办法是在出错时产生一个异常。在一些语言中(例如,Ada) ,程序员可以编写在发生特定错误(如除0错误)时激活的特殊过程。在C语言中,信号处理程序的目的就在于此。换句话说,我们可以定义新的信号类型S
40、IGNOSERVER,让它像其它信号一样处理错误。这种方法也有缺陷。首先,并不是所有语言都有异常和信号处理程序。PASCAL就是这样一个例子。另外,编写异常和信号处理程序破坏了我们努力想达到的透明性。假设你是一个程序员,如果老板让你去编写一个SUM过程,你可以轻松的完成。但她还让你去编写一个异常处理程序以防止SUM过程不存在的情况。就这点而言,很难使人相信远程过程与本地过程没有区别了。因为在单处理机系统中为“无法定位服务器”而编写异常处理程序是相当少见的。客户请求消息丢失第二项是处理丢失的请求消息。这是最容易解决的一个问题:客户内核在发送请求时启动计时器。如果在计时器时满之前无应答或无确认消息
41、返回,内核重发消息。如果请求消息确实丢失了,服务器是无法区分收到的请求是原来的还是重发的,而一切运行良好。当然,如果消息被多次重发而得不到应答,客户会放弃请求并认为服务器已经关闭。这就是前面所说的“无法定位服务器”的情况。服务器应答消息丢失处理应答丢失的情况要困难一些。一个显而易见的办法是根据计时器重传。如果在合理的时间内未收到应答,那么就重发请求。这种方法存在的问题是客户内核不知道为什么没有收到应答。是请求丢失还是应答丢失,或是服务器速度太慢?对不同原因引起的错误,其处理的方法也不相同。特别是有些操作能多次安全地重复执行而不产生危害。例如,从某文件读出1024个字节的请求就不产生负面影响,它
42、可以按需要多次执行而不发生错误。具有这种性质的请求称为幂等(idempotent)的。现在设想一个发到银行服务器的请求,它要求一个帐户调拨一百万美元到另一个账户。如果请求到达且被执行,但应答丢失,客户就不会知道这种情况并将在计时器到时后重发请求。服务器会认为这是一个新的请求,因而再次执行该请求。那么将有二百万美元调拨到那个帐户上。如果应答丢失了十次,那么后果将是多么可怕。这样的请求是非幂等的。要解决这个问题,一种方法是将每个请求构造成幂等的。然而,有些请求(例如,汇钱)事实上是非幂等的,因此需要其它的措施。另一种方法是,客户内核给要发送的请求消息分配一个序号。服务器内核保留那些最近来自每个客户
43、的请求序号。这样服务器内核可以区别第一次发送的请求和重发的请求,从而排除了两次执行某个请求的可能性。一个附加的保护措施是在消息头上增加一位以区分是原来的还是重发的消息,能提醒服务器在看到不是原发消息时谨慎处理。服务器崩溃服务器崩溃也与可幂等次执行有关,但现在不能使用给消息加序号的方法来解决。图2-24(a)是一服务器进程正常工作处理一个事件的顺序;一个来自客户的请求到达,执行该请求,服务器进程返回一个应答。如图2-24(b)所示是一个客户请求到达后,服务器进程执行,在返回应答之前服务器崩溃。最后,如图2-24(c)所示,一个请求到达后,在执行前服务器崩溃。图2-24 (a)正常状态;(b)执行
44、后崩溃;(c)执行前崩溃图中(b)、(c)的处理方法是不相同的。在(b)中,系统不得不向客户报告失败(例如,引起一个异常中断) ,而在(c)中只需重发请求。问题是客户的内核不能区分这两种情况。它只知道计时器到时。有三种方法可以解决这个问题。第一种方法是等待服务器重新启动(或与另一个新服务器捆绑) ,然后重发请求。这种方法要求不断重试直至应答消息到来并传给客户。这种技术称作至少一次语义(at least once semantics),它保证RPC至少要执行一次,但也有可能执行多次。第二种方法是立即放弃并报告失败。这是最多一次语义(at most once semantics),它确保了RPC最
45、多执行一次,但可能没有执行。第三种方法不作任何保证。当服务器崩溃时,客户得不到任何帮助和保证。RPC可以不被执行或执行相当多次。这种方法最大的优点就是易实现。这三种方法都不是成熟的方法。人们需要的是精确的执行一次的语义(exactly once semantics),但通常它是不容易实现的。设想一个远程操作包括打印一些文本,它把文件调入打印机的缓冲区,然后在某个控制寄存器中设置一位后准备开始打印。 可能在置位的那一微秒的前后服务器崩溃而无法打印。客户由于无法知道崩溃发生在置位前还是置位后,因此也就无法有针对性地进行恢复处理。简而言之,服务器崩溃的可能性在很大程度上改变了RPC的性质。单处理机系
46、统和多处理机系统发生服务器崩溃的情况是截然不同的。在单处理机系统中,服务器的崩溃往往意味着客户的崩溃。所以恢复既不可能也没必要。而在分布式系统中则可以采取一些措施来处理这种情况。客户机崩溃接 收执 行应 答(a)REQ(b)REP (c)NOREPREQ接 收执 行NOREPREQ 接 收Crash Crash 服 务 器 服 务 器 服 务 器如果客户已发出请求但在应答到来之前崩溃了,这时会发生什么?此时已经激活了服务器中的相应计算,但没有客户在等待结果。这样的计算称为孤儿(orphan) 。孤儿会导致一系列的问题。起码它浪费了CPU周期, 同时又锁住了文件或其它宝贵的资源。此外,如果客户重
47、新启动并再次调用了这个RPC,客户会很快得到那个孤儿的返回值,这将引起调用结果的混淆。为解决孤儿问题,Nelson(1981)提出了四种解决方法。方法一,在客户存根发送一个RPC前,在日志文件中记下要执行操作的信息。 该日志文件保存在不受崩溃影响的磁盘和其它媒介上。当客户重新启动后,系统检查日志文件,并准确地清除孤儿。这种方法称为(extermination) 。根绝方法的缺陷是对每个RPC都要进行磁盘记录,极大增加了系统开销。此外,这种方法也可能不起作用,因为孤儿还可以执行RPC, 这样又生成了一层或更多层的子RPC,这些子RPC很难找到和清除。最后,由于网络或许是分段的,如果网关失败,即使
48、找到这些孤儿也无法清除。总之,这种方法是没有前途的。方法二,称为再生(reincarnation) 。这种方法不必作磁盘记录。该方法将时间划分成顺序编号的纪元(epochs) 。当一客户重新启动时,它向所有机器广播一个新纪元的开始。广播后,所有远程计算被终止。当然,如果网络是分段的,有些孤儿还会遗留下来。但是当这些孤儿的应答返回时,返回的消息上带有它们过时的纪元号。这些应答还是容易识别和清除的。方法三,是对第二种方法的改进,称作温和再生(gentle reincarnation)。当接到某客户开始新纪元的广播后,每台机器检查自己是否有远程计算,若有则试图去找到该远程计算的调用者。若没有找到该计
49、算的调用者,则终止该计算。方法四,这种方法叫过期(expiration)。每一个RPC执行时事先分给一个标准时间段T。当T到期而调用未完成时就必需再申请一个T。这样做确实麻烦。另一方面,如果客户崩溃,服务器在客户重新启动前等候了一个T后,所有孤儿都被清除。由于RPC有各种不同的请求,如何选择T的合适值呢?实际上,这些方法都不尽人意。终止一个孤儿可能会造成不可预见的后果。例如,假设一个孤儿正锁住一个或多个文件或数据记录等,如果突然清除该孤儿,那么这些资源可能会一直处于被占用的状态。另外,一个孤儿可能已在某些远程的进程调用队列中等待,期望将来能调用其它进程。这样,即使去除了这个孤儿也不能去除孤儿遗留下的轨迹。Panzieri和Shrivastava(1988)详细讨论了孤儿清除问题。2.4.5 实现的问题一个分布式系统的成败往往取决于它的性能。系统的性能在很大程度上取决于通信的速度。而通信的速度主要取决于对它的实现方法而不是抽象原理。这一节主要讨论RPC系统的实现问题,重点在于系统性能和耗时情况。RPC协议族第一个问题是如何选择RPC的协议。从理论上来讲,任何已经存在的能够实现从客户内核到服务器内核之间按位传送的协议都可以作为RPC的协议。但是我们还需要确定几个重要的问题,这些问题将会对性能有很大影响。第一是用面向