1579864397535共31页12.4远程过程调用尽管客户-服务器模式为构造分布式操作系统提供了一种便利的方法,但它也存在着无法克服的缺陷:即所有通信建立的基础都是输入/输出。过程send与receive基本上是在做I/O操作。由于I/O并不是集中式系统的一个主要概念,在分布式计算中将它作为基础会使此领域中的许多工作者产生误解。他们的目标是使分布式的计算看起来像集中式计算一样。用I/O为基础实现它并不是一个好的办法。人们很早就意识到这个问题,但直到1984年才由Birrell和Nelson提出一个全新的解决方法。尽管他们提出的思想极其简单(曾经有人考虑过),但实现起来却有许多微妙的地方。本节我们将讨论它的概念、实现方法及优缺点。简而言之,Birrell与Nelson提出的方法就是允许程序去调用位于其它机器上的过程。当位于机器A的一进程调用机器B上的某过程时,机器A上的该进程被挂起,被调用的过程在机器B上执行。调用者将消息放在参数表中传送给被调用者,结果作为过程的返回值返回给调用者。消息的传送与I/O操作对于程序员是不可见的。这种方法称为远程过程调用(remoteprocedurecall),或简称为RPC。这一想法尽管听起来很简单,但仍存在着许多细微的问题。首先,由于调用的进程与被调用的过程运行在不同的机器上,因而在不同的地址空间执行,这就导致了问题的复杂化。尤其当两台机器不是一种型号时,在机器之间传递参数与调用结果很复杂。最后,调用者和被调用者都有可能会崩溃,任何一种可能的失败都会引起不同的问题。当然,大多数问题还是可以解决的,RPC是广泛应用于分布式操作系统的一种技术。2.4.1基本RPC操作为了解RPC是如何工作的,首先要了解清楚传统的(即单机上)过程调用是如何工作的。考虑这样一个过程调用:count=read(fd,buf,nbytes);这里fd是一个整数,buf是一个字符型数组,nbytes是另一个整数。若主程序调用该过程,调用前堆栈的情况如图2-17(a)所示。调用开始时,如图2-17(b),调用者按序将参数压入堆栈,后进入的先弹出。(C编译器将printf的参数按相反顺序压入堆栈的原因是使printf在执行时总能找到它的第一个参数,即格式串)。在过程read执行后,它将返回值送入寄存器中,从栈中取出返回地址并将控制权交给调用者。然后,调用者从堆栈中取出参数,返回到调用点,如图2-17(c)所示。图2-17(a)调用read之前的栈;b)调用过程处于激活状态时的栈;(c)返回调用者之后的栈有几个问题是值得注意的。第一,在C语言中参数的调用分为值参调用与变参调用。像fd主函数局部变量SP0(a)主函数局部变量bytesbuffd返回地址read的局部变量0SP(b)主函数局部变量SP(c)1579864397535共31页2与nbyts之类的值参,调用时只需要将它们拷贝到堆栈中,如图2-17(b)所示。对被调用的过程来说,值参仅仅是一个初始化了的局部变量。该过程可以改变它,但其改变不影响调用方的初始值。在C中,变参是一个指向变量的指针(即变量的地址),而不是变量的值。因为数组在C中常以变参的形式传递,所以在read中的第二个参数buf是一个变参。实际上压入堆栈的是该字符数组的地址。如果被调用的过程使用这个参数向该字符数组存入数据,它的确修改了调用过程中这个数组的值。值参调用和变参调用的这种区别对RPC来说是很重要的。此外还存在着一种C语言中不使用的参数传递机制,它叫做复制/恢复调用(call-by-copy/restore)。在以这种方式执行调用时,调用者将变量拷入堆栈,这一点是与值参调用一样的。调用完成后,将栈中变量的值拷回并覆盖原有的变量值。大多数情况下,此方法与变参调用的效果一样。但是在有些场合,例如在参数列表中多次出现同一参数时,两者的语义是不同的。到底使用哪一种参数传递机制通常是由语言开发者来决定的,而且它是语言的固有特性。它有时也与传递的数据类型相关。例如在C语言中,整型与其它数值类型常作为值参传递,而数组总是以变参的形式传递。不同的是,PASCAL语言的程序员可以选择每个参数的传递方式。在缺省状态下是值参调用。程序员可以在指定参数前加关键字VAR来强制该参数为变参调用。一些Ada编辑器利用复制/恢复来输入和输出参数,还有一些采用变参形式。在语言定义中同时允许两种参数传递机制,这使得这种语言的语义有些模糊。RPC的内在思想是使远程的过程调用看上去就像在本地的过程调用一样。换句话说,我们希望实现RPC的透明性—调用者不应该意识到此调用的过程是在其它机器上执行的,反之亦然。设想一个程序需要从一文件中读取一些数据。程序员在代码中调用read即可取得数据。在一个传统的(单处理器)系统中,连接器将read例程从函数库中取出并插入目标程序中。这是一个小过程,通常用汇编语言编写,它将参数放入寄存器,激活内核的陷阱中断并调用系统调用READ。库函数read实质上是用户代码与操作系统的接口。尽管read激活了内核陷阱,但它仍然是通过将参数压入堆栈这种常规的方式来调用的,如图2-17所示。因此,程序员感觉不到read调用是如何在进行的。RPC使用与本地调用相似的方法获得透明性。当read为一远程过程调用时(例如,它将运行在文件服务器上),read的一个不同版本,称为客户存根(clientstub),被放入库中。和前面的本地调用一样,它采用如图2-17所示的调用顺序并同样激活了内核的陷阱。不同的是,RPC不是将参数放入寄存器中并要求内核返回结果,而是将参数打成信包,请求内核将该消息发送到服务器,如图2-18所示。在发送消息后,客户存根调用receive原语,然后阻塞直至收到服务器来的应答。当消息到达服务器后,内核将消息传送给与实际服务器进程相捆绑的服务器存根(stub)。通常,服务器存根调用receive,然后将自己阻塞等待消息的到达。服务器存根拆开信包从消息中取出参数,然后以一般方式调用服务器进程(即与图2-17所示的一样)。从服务器进程的角度来看,就像由本地的客户进程直接调用一样——所有参数和返回地址都在它们的堆栈中,没有任何异常。服务器执行它的工作并以一般方式将结果返回调用者。例如,在read的例子中,服务器将在第二个参数所指向的缓冲区内填入数据。这个缓冲区是在服务器存根内的。当调用完成后,服务器存根获得控制权,它将结果(缓冲区)打包,然后调用send原语将消息返回客户。最后,服务器回到receive状态,等待下一条消息。1579864397535共31页3图2-18RPC中的调用与消息。(其中每一个椭圆都代表了一个进程,而阴影部分则表示存根)消息送回客户机后,内核按地址找到发送请求的客户进程(实际上是该客户进程的存根部分,但内核并不知道)。消息被拷贝到等待缓冲区后,客户进程解除阻塞。客户存根检查并拆开信包,取出结果,并将它拷贝到调用者进程的缓冲区中,然后以一般方式返回。当调用者在调用read后又得到了控制权,它所知道的只是得到了所需的数据,并不知道该过程的执行是在远程而不是在本地内核。客户方忽略消息传递的细节是整个方案中最完美的部分。远程服务可以通过一般(即本地)的过程调用来访问,而不用通过调用如图2-19所示的send和receive原语。所有消息传递的细节都被隐藏于两个库过程中,就像在本地的库函数调用掩盖了系统中断调用的具体细节一样。这就是该机制的最主要的优点。概括地说,RPC的主要步骤是:1.客户过程以普通方式调用相应的客户存根。2.客户存根建立消息并激活内核陷阱。3.内核将消息发送到远程内核。4.远程内核将消息送到服务器存根。5.服务器存根取出消息中的参数后调用服务器的过程。6.服务器完成工作后将结果返回至服务器存根。7.服务器存根将它打包并激活内核陷阱。8.远程内核将消息发送至客户内核。9.客户内核将消息交给客户存根。10.客户存根从消息中取出结果返回给客户。这些步骤最主要的作用就是将客户过程的本地调用转化为客户存根再转化为服务器过程的本地调用,对客户与服务器来说它们的中间步骤是不可见的。2.4.2参数传递客户存根的功能是获取调用的参数并将参数打包放入消息中送往服务器存根。虽然这听起来很直接了当,但实际并不像表面上那么简单。本节我们将讨论RPC系统中有关参数传递的几个问题。将参数打包形成消息的过程称为参数组装(parametermarshalling)。举一个最简单的例子,我们考虑远程调用函数sum(i,j),该函数有两个整型参数并返回其代数和。(作为一个实际问题,因为开销问题人们不会将这么简单的一个过程作为远程过程。但在这里作为一个例子是可以的)。sum调用的参数分别为4和7,如图2-19中客户进程左半部分中所示。客户存根获取这两个参数并将它们打包入消息中。因为一个服务器可能支持多个调用,所以它也将被调用过程的名字或过程号放入消息中,以确定是哪一个调用。内核内核打包参数客户客户存根客户机服务器调用返回服务存根拆包结果打包结果拆包参数调用返回服务1579864397535共31页4图2-19sum(4,7)的远程计算当消息到达服务器后,由存根检查消息以确定需要哪个过程,然后调用相应的进程。服务器可能还支持减、乘、除的远程过程调用,所以服务器存根中可能有一个switch语句,它根据消息的第一个字段选择被调用的过程。实际上,从存根到服务器的调用很像原来的客户调用,只不过参数是由到来的消息对之进行初始化的变量,而不是常量。服务器进程一结束,服务器存根再次取得控制权,它获取服务器提供的运行结果并将之打包形成消息。这条消息被发送回客户存根,客户存根从消息中取出结果,最终将结果返回给客户进程(在图中没有显示)。只要客户机与服务器机是同样的机器,并且参数与结果都是像整型、字符型、布尔型这样的标量类型,那么上述模型会工作良好。但在一个大型的分布式系统中通常有多种机型。各种机型又常常有自己的表示数字、字符和其它数据项的方式。例如IBM主机中使用的是EBCDIC码,而IBMPC中使用的是ASCII码。因此,如果使用图2-19所示的简单机制,要从IBMPC客户向IBM主机服务器传送一个字符参数是不可能的,因为服务器将会错误地解释所传送的字符。整型数的表示(用反码还是补码表示),尤其是浮点数的表示也会出现相似的问题。此外,还存在着更令人讨厌的问题。如在Intel80486中字节是从右向左编号,而在其它一些系统如SPARC中正相反。Intel的格式称为最低有效字节优先,而SPARC的格式称为最高有效字节优先。例如,如果一个服务器有两个参数,一个整数和一个四个字符的字符串。每个参数占一个32位长的字。图2-20(a)说明了在Intel486机上一个客户存根所建消息的参数部分。第一个字包含了在这种表示方式下的整型参数5,第二个字包含了字符串“JILL”。由于消息在网络上是按字节(实际上是按位)传送的,所以先传送的字节先到达。图2-20(b)说明了图2-20(a)所示的消息被SPARC机接收后的情况。SPARC表示数字的格式是字节0在最左面(高序字节),而所有的Intel主板的字节0却在最右面(低序字节)。当服务器存根分别从地址0和4中读出参数时,它将得到一个等于83,886,080(5*224)的整数和一个为“JILL”的字符串。一个简单却不正确的方法是,当参数到达后,将每个字倒置。这就导致了图2-20(c)所示的情况。这时整数虽然仍是5,但字符串却成了“LLIJ”。问题在于整型数是由于不同的字节顺序而必须颠倒,但字符串并不是这样。因此,如果没有额外信息来指明什么是字符串,什么是整数的话,这个问题是没有办法解决的。内核内核sum47sum47Message...n=sum(4,7);...sum(i,j)inti,j;{return(i+j);}存根客户机服务器机1579864397535共31页5图2-20(a)486中的原始消息;(b)在SPARC上接受到的消息;(c)经过翻译之后的消息;(框中的小数字表明了