分类: 系统运维
2008-05-07 10:29:01
图 OSI的七层参考模型
图 七层模型的实现结构
设计一个良好的网络操作系统取决于如何选配一个合理的协议套(protocol suite)。下面我们以4.2 BSD UNIX(内含TCP/IP和NFS)为例(图)说明一个实际网络操作系统的内部结构。
图 4.2BSD UNIX网络操作系统结构
4.2 BSD UNIX的NOS实现分为内核和核外两部分:内核部分与UNIX操作系统本身结合在一起,以管态运行;核外部分由一些标准的系统例程(各种标准的网络服务器,如名字服务器YP等)和各种用户应用程序组成。
核外与内核的交互作用通过NFS(即网络文件系统)接口和socket接口实现。scoket可以由TCP/IP协议由其他传输协议提供。
NFS负责所有的本地和远程文件(或文件系统)操作。通过NFS接口的屏蔽作用,用户感受不到本地文件操作和远程文件操作有何区别。
4.2 BSD的硬件接口部分支持链路驱动器和以太网(或其他物理网络技术,如令牌总线、令牌环等)控制器。网络收发器属于硬件部分。图中VFS即虚拟文件系统(Virtual File System),指通过MOUNT协议实现的由本地和远程文件系统组成的统一文件系统。
TCP(Transport Control Protocol)传输控制协议是面向连接的。UDP(User Datagram Protocol)用户数据报协议是无连接的。IP无连接数据报传送协议处在通信子网的最高层。RPC(Remote Procedure Call)远程过程调用,XDR(eXternal Data Representation) 外部数据表示,解决异种机数据格式不同所引起的问题。socket编程界面由4.2 BSD UNIX首先提出,目的是解决网间网进程通信问题。socket是面向客户/服务器模型而设计的,由一组系统调用组成的。
基本通信技术
通信技术是网络操作系统的低层实现技术,是支持通信管理和资源共享的基础。通信技术有send/receive原语,远程过程调用RPC,socket系统调用。
send/receive原语
在集中式系统中,我们在进程管理一章中曾介绍过send和receive。在网络环境下,这一对原语是在2台计算机上执行,有一个相互配合的问题。这与它们在集中式环境下是不同的。此外,两种环境下它们所带的参数也不同。
1、带有检查应答信号的send/receive原语
不带有检查应答信号的send原语在执行时,只是把信息发出去,而不能保证该信息一定能被对方进程所接受。如果信息在传输过程中丢失(集中式环境下没有丢失,因为消息不经过电缆等传输介质),或者接受方进程由于某种原因没有收到该信息,则造成send原语的运行出错,因此可靠性较差。图是带有检查应答信号的send原语的流程图。由图可以看出,在发出消息一段时间以后,如果没有收到接受方进程送回的应答信号,则重新发送信息。因此当send 的原语结束时,所发出的消息一定已被接受方接受了。等待时间T值的设定是影响通信效率的重要因素。环形网络因有固定的最长网络传输时间,T值是不难确定的,但是总线类型的网络因没有固定最长网络传输时间,则T值的确定比较困难。
带有检查应答信号的receive原语在接受信息后,必须自动返回应答信号。
带有检查应答信号的send/receive原语使通信的可靠性大增,但需要的系统开销较大,在通信距离短而且故障率低的网络中,往往不使用这种原语。
图 带有检查应答信号的send原语执行流程图
2、带锁的sendw/receivew原语
当用户程序执行sendw原语时,网络控制部件锁住信息发送区直到消息离开信息发送区。当被发送的消息进入对方信息接收区时也被锁住直到receivew把消息移出接收区。因为sendw不需要等待接收方的回答信号,所以返回用户程序的时间快于send原语,但通信的可靠性不及send。
3、带缓冲区的sendb/receiveb原语
在设计通信原语时,对于是否要为传送信息设置缓冲区以及在哪里设置缓冲区,有多种选择。最简单的选择是不设缓冲区,如上述的send/receive。也可在接受进程内设置多个缓冲区用以存放接收信息。发送进程在执行sendb原语后不必被封锁,可以继续运行用户程序,但当接收缓冲区全满时,发送进程必须等待。
远程过程调用
传统程序设计语言中的“过程调用”概念是不难理解的。远程过程调用RPC是把过程调用的概念加以扩充后引入网络环境中的一种形式。RPC的形式和行为与传统的过程调用的形式和行为极其相似,主要的差别在于被调用的过程代码实际运行在与调用者站(结)点不同的另一站(结)点上 。因此,需要设计相应的软件来实现两者之间的连接和信息沟通。
图 主机A调用远地主机B上的过程
RPC机制的实质是实现网络七层协议中会话层的功能──在两个试图进行通信的站点之间建立一条逻辑信道(即进行会话连接),并利用这个信道交换信息,不再使用时,负责释放所建立的连接。
RPC的通信模型是基于client/server进程间相互通信模型的一种同步通信形式,它对client提供了远程服务的过程抽象,其底层消息传递操作对client是透明的。在RPC中,client是请求服务的调用者(caller),server是执行client的请求而被调用的程序(callee)。
1、RPC的实现技术
图中表明,每个远程过程由若干成分组成:调用者(caller)或用户(user),调用代码段,以及被调用者(callee)或server,被调用代码段。这些都可用常规的程序设计语言编写,不需要利用特别的设施,就象它们在同一站点上执行一样。另一些成分是与调用者相关的stub,与被调用者相关的stub及RPC runtime子程序,后者可在系统中所有站点上运行。过程调用的主要工作环节如下:
图 RPC的实现概况
详细说明
①调用者用通常方式调用对应stub中的一个过程;
②这个stub过程把有关的参数组装成一个消息包或一组消息包,以形成一条消息。运行此过程的那个站点的“地址”和那个站点上指称此过程的“标识符”都应包含在这条消息中;
③将这消息发送给对应的RPC runtime子程序,该子程序再转至指定的站点。
④在接收此消息时,远程runtime子程序引用与被调用者对应的stub中的一个子程序,并让它来处理这条消息;
⑤被调用者对应的stub中的这个子程序拆卸有关的参数并用通常的过程调用方式调用所需的过程。
⑥返回调用结果,整个远程过程调用以与调用者对应的stub程序执行“return”语句返回到用户而终止。
2、RPC的语义
本地调用和远程调用之间存在许多不同多之处。如果远程调用是在两种异型机器间进行,这就存在数据表示问题,例如,这两类机器的字长可能不同,解决这一问题的方法之一是它在传递数据之前,让RPC机制将有关的数据转换成一种统一格式,接收点在接收数据时,再把它们转换成本地所允许的数据格式。
另一问题是如何解释指针。在不具有共享地址空间的情况下,RPC不允许在网络范围内传递指针。因此,在RPC中是不可能用“reference方式”传递参数的。
更严重的问题是调用者和被调用者都可能在调用期间发生故障,而且经常是被调用者故障,留下调用者挂起。如果发生这种情况,调用者可能不得不夭折,这在本地调用中是决不会出现的。
不同的RPC实现方案定义的这种效果或RPC语义是有差别的,几种常用的定义RPC语义的规则是:
①last-of-many 对执行一个远程过程调用而言,被调用的过程可能执行若干次,但规定其最后一次执行的结果作为返回结果;
②at-most-once 若调用者收到了回复消息,则称被调用的过程正确地完成了它的一次(仅仅一次)执行。如果调用者没收到回复消息,或者,如果调用者在获得回复消息之前发生故障,那么,这时的调用效果就看作是根本就没有执行相应的过程。
③at-least-once 在站点正常的情况下,远程过程至少执行一次,回复消息可能返回一次或多次。在站点故障时,就不能保证远程过程是否已被执行或曾返回任何回复消息。
④exactly-once 若server正常,则远程过程恰好执行一次,并返回一个调用结果。
RPC也有一些不同的形式。例如可以允许异步远程过程调用,因此,调用者和被调用者可以并行执行,调用者负责稍后某一时刻执行一个所谓的“回合”来获取调用结果。
SOCKET 系统调用
socket(套接字)技术在Windows系列的联网中的重要性越来越大。最初socket是为支持TCP/IP协议而开发的,现在它已被认为是开发非RPC Windows网络应用程序的最好途径。
socket的英文原义是“孔”或“插座”。在这里作为4BDS UNIX的进程通信机制,取后一种意义。socket非常类似于电话插座。以一个国家级电话网为例。电话的通话双方相当于相互通信的2个进程,区号是它的网络地址;区内一个单位的交换机相当于一台主机,主机分配给每个用户的局内号码相当于socket号。任何用户在通话之前,首先要占有一部电话机,相当于申请一个socket;同时要知道对方的号码,相当于对方有一个固定的socket。然后向对方拨号呼叫,相当于发出连接请求(假如对方不在同一区内,还要拨对方区号,相当于给出网络地址)。对方假如在场并空闲(相当于通信的另一主机开机且可以接受连接请求),拿起电话话筒,双方就可以正式通话,相当于连接成功。双方通话的过程,是一方向电话机发出信号和对方从电话机接收信号的过程,相当于向socket发送数据和从socket接收数据。通话结束后,一方挂起电话机相当于关闭socket,撤消连接。
在电话系统中,一般用户只能感受到本地电话机和对方电话号码的存在,建立通话的过程,话音传输的过程以及整个电话系统的技术细节对他都是透明的,这也与socket机制非常相似。socket利用网间网通信设施实现进程通信,但它对通信设施的细节毫不关心,只要通信设施能提供足够的通信能力,它就满足了。
至此,我们对socket进行了直观的描述。抽象出来,socket实质上提供了进程通信的端点。进程通信之前,双方首先必须各自创建一个端点,否则是没有办法建立联系并相互通信的。正如打电话之前,双方必须各自拥有一台电话机一样。在网间网内部,每一个socket用一个半相关描述;
(协议,本地地址,本地端口)
一个完整的socket有一个本地唯一的socket号,由操作系统分配。
最重要的是,socket 是面向客户/服务器模型而设计的,针对客户和服务器程序提供不同的socket 系统调用。客户随机申请一个socket (相当于一个想打电话的人可以在任何一台入网电话上拨号呼叫),系统为之分配一个socket号;服务器拥有全局公认的 socket ,任何客户都可以向它发出连接请求和信息请求(相当于一个被呼叫的电话拥有一个呼叫方知道的电话号码)。
socket利用客户/服务器模式巧妙地解决了进程之间建立通信连接的问题。服务器socket 半相关为全局所公认非常重要。读者不妨考虑一下,两个完全随机的用户进程之间如何建立通信?假如通信双方没有任何一方的socket 固定,就好比打电话的双方彼此不知道对方的电话号码,要通话是不可能的。
1、socket的功能
socket 的功能由6个主要的系统调用来体现,下面分别介绍。
1)创建socket
应用程序在使用socket之前,首先必须拥有一个socket。格式如下:
sockid=socket(af, type, protocol)
af为地址族,指出本socket 所用的地址类型。当前UNIX系统支持的地址包括:
AF_UNIX: UNIX内部地址 AF_INET: TCP/IP地址
AF_NS: Xerox NS地址
AF_IMPLINK: ARPANET IMP(接口报文处理机)地址
AF_APPLETALK: Apple 公司Appletalk地址
等等。与地址族等同的一个概念叫作域(domain),AF_INET等同于Internet域。
最重要的是,地址族与协议族是一一对应的。比如AF_INET地址族对应于TCP/IP协议族,AF_NS地址族对应于XNS协议族。事实上,在UNIX系统中,“AF(Address Family)”与“PF(Protocol Family)”是一回事,即AF_UNIX与PF_UNIX完全一样。
type指创建socket的应用程序所希望的通信服务类型。同一协议族可能提供多种服务类型,比如TCP/IP协议族提供的虚电路与数据报就是两种不同的通信服务类型。socket支持下列几种通信服务类型(对应用程序来说即socket 类型):
sock_STREAM: 流socket
sock_DGRAM: 数据报socket
sock_RAW: 原始socket
sock_SEQPACKET: 定序分组socket
sock_RDM: 可靠发送的消息
其中sock_RAW是低于传输层的低级协议或物理网络提供的socket类型,只有特权程序才能使用:sock_RDM(Reliable Delivery Message)尚未实现;sock_STREAM中的流(stream)是BSD术语,指可靠的面向连接传输的数据流。
protocol指出本socket请求所希望的协议。提出该参数的原因是前面的2个参数对于描述协议类型还不够充分,socket特别提供这个域,让程序员直接了当地指出具体协议。
可见,至少在TCP/IP协议族和XNS协议族中,协议族与socket类型基本上可以唯一地确定一个协议。当存在一一对应关系时,在socket()调用的protocol参数位置可以置0。
返回值sockid是一个整数,即socket 号。创建一个socket 实际上是向系统申请一个属于自己的socket号。申请socket号的操作比一般人想象的要复杂。socket()一共有3个参数。
综上所述,socket()系统调用实际上指定了相关五元组中的“协议”这一元。其他四元(本地地址、本地端口、远地地址、远地端口)将通过下面介绍的系统调用给出。一旦成功地给相关的全部五个元,一个完整的socket连接就可以建立起来。
2)指定本地地址
bind()将本地socket 地址(包括本地主机地址和本地端口)与所创建的socket 号联系起来,即将本地socket地址赋予socket ,以指定本地半相关。bind()的作用相当于给socket命名,其调用格式为:
bind(sockid, localaddr, addrlen)
sockid是一个已命名socket的socket号。
总的来说,socket地址数据结构包括两大部分:地址类型和协议地址。网络协议地址又包括主机地址和端口号。各种socket地址结构的长度相差太大,因而在bind()调用中,必须指定socket地址的长度,尤其是对变长的UNIX地址,更是必须指定其长度。
3)建立socket连接
connect()用于建立连接。这里的连接有2层含义:第1层是指两个socket之间的沟通;第2层指传输层连接(比如TCP连接)。
connect()的调用格式如下:
connect(sockid, destaddr, addrlen)
其中参数如下:
①sockid是欲建立连接的本地socket号。
②destaddr是一个指向对方socket地址(信宿地址)结构的指针。
③addrlen指出对方socket地址长度
假如连接失败,connect()返回出错代码。
accept()用于面向连接的服务器,其调用格式为:
newsock=accept(sockid, clientaddr, paddrlen)
newsock是由accept()调用返回的一个新的socket号,用于服务器处理并发请求,对应于一个从服务器。比如并发服务器方式中,主服务器fork一个从服务器,从服务器利用此新socket回答accept()所接收的客户请求。
clientaddr指向一个初始值为空的结构,paddrlen的初始值为0,二者用于存放客户的地址信息。accept()调用后,服务器等待从编号为sockid的socket上接收客户连接请求并通过clientaddr获取客户地址结构,通过paddrlen指针获取地址结构长度。连接请求是由客户的connect()调用发出的。
其中参数意义如下:
①sockid,本地socket号。
②clientaddr,指向客户socket地址结构的指针。
③paddrlen,客户socket地址长度。
4)listen()调用
以上讨论了socket(), bind(), connect()和accept()。通过它们,可以建立一个完全的五元相关,即在欲相互通信的两个进程之间建立联系。
假如抛开客户/服务器模型,上述socket系统调用的使用是非常灵活的。唯一的原则是进程通信之前,必须建立完整的相关(面向连接通信),或半相关(无连接通信)。或者说得更简单一点,只要能保证信息分组正确地传到信宿端,并能正确地收到传往自己的信息分组,任何调用方式都是允许的。
listen()调用用于面向连接服务器,表明它愿意接收连接。listen()在accept()之前使用,其格式为:
listen(sockid, quelen)
其中:
①sockid,本地socket号,服务器愿意从它上面接收请求。
②quelen,请求队列长度。listen()以此参数限制队列长度,目前允许的最大值为5。
5)发送数据──write()、writev()、send()、sendto()、 sendmsg()
用于socket数据发送的系统调用一共有5个,其中前3个用于面向连接传输,后2个用于无连接传输。面向连接的调用可以不指定信宿地址,而无连接调用必须指定。假如无连接socket的双方均调用过connect(),可以认为是建立了有连接的socket,也可以用面向连接调用发送数据。
3个面向连接调用的格式大致相同:
write(sockid, buff, bufflen); 缓冲发送
writev(sockid, iovector, vectorlen); 集中发送
send(sockid, buff, bufflen, flags); 可控缓冲发送
flags,传输控制标志,这是send()与write()的真正区别所在。send()的flags可取下列两个值之一:
MSG_OOB:带外数据发送。
MSG_DONTROUTE:寻径控制选项,在信宿地址的网络号部分指定数据发送须经过的网络接口,使其不经过本地寻径机制而直接发送出去。程序员借助此选项可编制网络调试软件。
①sockid,本地socket号
②buff,一个指向发送缓冲区的指针。
③bufflen,发送缓冲区大小。
④iovector ,指向I/O 向量表的指针。
⑤vectorlen,I/O向量表的大小。
图 iovec类型格式
所谓集中发送(gather write)是一种区别于缓冲发送的 socket发送方式。缓冲发送在发送之前,要将数据搜集拢来,存入一个连续地址缓冲区中;集中发送不这样,它可以一次发送若干分散的数据块。I/O向量就是一个指向数据块的指针(32比特指针),所有待发送数据块指针组合起来构成I/O 向量表。I/O向量表数据结构类型为iovec,其结构如图所示。
6)接收数据——read()、readv()、recvfrom()、recvmsg()
接收数据系统调用与发送数据系统调用是一一对应的。可控接收的flags与可控发送的flags略有不同,其可选值为:
MSG_OOB:带外数据接收。
MSG_PEEK:允许调用者从socket中读出下一输入数据块拷贝,而不将它从socket删除,以供提前查阅。一般情况下,如果从socket中读取一数据块,调用返回后,socket将不再保存该数据块。
接收数据系统调用的参数与发送数据系统调用的参数的最大区别在于,前者的bufflen是一个指针,其所指单元初值为欲读数据长度,调用后的值是实际读出的值。recvfrom()和recvmsg()的地址参数为指向信源socket地址的指针,信源地址的初始值为空。
2、客户/服务器模型的socket实现框架
我们讨论如何利用socket实现客户与服务器之间的相互作用以及客户和服务器程序本身。由于篇幅有限,这里只给出一些概括性的框架。
1)客户/服务器模型时序图
同步是客户/服务器模型实现中的一个重要问题。socket系统调用很好地解决了这个问题。
|
|
面向连接客户/服务器模型时序图是面向连接客户/服务器模型的典型时序图。该图说明,服务器必须首先启动,直到它执行完socket()调用,进入等待状态后,方能接受客户请求,假如客户先启动,则connect()将返回出错代码,连接不成功。
无连接客户/服务器模型时序图是无连接客户/服务器模型时序图。无连接服务器也必须是首先启动的,否则客户服务请求传不到服务器进程。注意到与面向连接方式不同,无连接客户一般不调用connect(),所以在发送数据之前,客户与服务器之间尚未建立完整相关(即便调用connect()也不起作用)。那么无连接socket是如何实现彼此的识别的?在服务器一端,无连接服务器通过socket()和bind()建立了本地半相关;在客户一端,无连接客户特别调用bind(),也建立了一个本地半相关。在传输数据之前,无连接的2个端点已建立起来,分别以一个本地socket号标识。然后在发送数据时,发方指定本地socket号和信宿端socket地址,于是,一个完整相关在数据收发过程中动态地建立起来,实现了无连接客户和服务器彼此的识别。
2)服务器框架
服务器为重复服务器和并发服务器。前者面向短时间能处理完的请求,由服务器亲自处理各请求;后者面向不知要花多长时间才能处理完的请求,每接到一个请求,fork一个子进程响应它,自己退回等待状态,等待下一请求。
面向连接的accept()调用为实现并发服务器提供了极大的方便,因为它要返回一个新socket号,如图所示。
图 利用accept()建立并发服务器
3)服务器socket地址的确定
在客户/服务器模型中,所有的相互作用都是客户首先发起的(如连接请求、服务请求等),因此客户必须要知道服务器的socket地址(包括服务器主机地址和服务器端口号)。服务器地址是公认的但这并不意味着每个程序员要记住所有服务器的公认地址并在编程中将它们一一填入相应数据结构中。socket提供2个系统调用,分别用于获取服务器端口号和主机地址。其中: port=getservbyname(servtype, proto)用于为获取某种协议(proto)提供的某种服务(servtype)的公认端口号(port)。由于不同协议可以提供同一类服务,所以getservbyname()必须指定协议类型。
另外,客户调用服务器之前,可以在命令行中给出服务器所在主机域名,根据域名可以获取服务器主机地址,系统调用为: hp=gethostbyname(host) 其中host可以是服务器主机域名,返回值hp是一个指向主机地址结构(内含主机网络地址)的指针。
服务器本身在调用bind()之前也必须知道自己的socket地址,与客户一样,它可以调用getservbyname()获取自己的公认端口号,再调用gethostname(localhost, namelen) 获取本地主机域名,最后调用 gethostbyname(localhost)获取本地主机网络地址。
与此类似,无连接客户可以用上述方法获取本地socket地址。面向连接客户不必显式地做上述工作,connect()系统调用会自动地为其socket选择并赋予一个合适的名字。
3、关于socket的进一步讨论
回顾并发服务器的实现,我们发现,在并发服务器中,一个半相关对应于若干个socket号,这是怎么回事?
事实上,这并不冲突。因为各客户半相关是不相同的,因此整个相关也是不相同的。不同的从服务器socket号虽然对应于本地半相关,但却是对整个相关的本地标识。至此,可以回答一个问题:我们知道,一个进程总是对应于一个本地端口,为什么不用端口而用socket来描述相互通信的进程?原因之一是同一端口可以被若干进程(如并发服务器)所共享。原因之二是不同协议提供独立分配的端口号(如TCP和UDP),同一端口号可能指不同的协议。更重要的是端口只是网络进程描述的一个层次,它由传输协议提供而与网络地址(如IP地址)无关。为了完整地描述网络进程,人们引入了socket的概念。一个本地socket号完整地描述了本地进程及与之通信的远地进程。socket语义具有网络一致性。