`
xitong
  • 浏览: 6178613 次
文章分类
社区版块
存档分类
最新评论

UNIX的流模式影响以及和套接字的PK

 
阅读更多

分层驱动的设计在windows上应用很多,但是实际上却是从unix开始的,早在unix为前途纷争的时代,那时还没有tcp/ip,有人就提出“将一个流压入另一个流”来进行堆栈式的流组合,用以适应多个不同的协议,所谓流,用OO语言讲就是一个对象,包含一个或者多个模块,每个模块拥有两个队列(输入队列和输出队列)以及一套方法,比如“流压入”,“数据放入”,“取出”,“服务”等,一个流的最下端模块压入另一个流的最上端模块时候就需要调用“另一个流”的压入方法用以衔接,数据通过一个流时就要调用该流的服务方法,数据在一个流中的流动就是依次经过各个模块的队列的过程,每个模块都可以对数据实行任意策略化的加工,流模式的IO要比单纯的设备驱动程序更有吸引力,更能体现出“协议”的含义,协议无非就是一套多数人都遵循的规则,其行为无非就是对数据进行加工,将一个协议实现成一个流模块是十分不错的想法,这样协议本身就能和实现IO的具体设备分离开来,一个协议流模块可以压入多个设备的IO驱动流模块之上,在unix上,一个流的一个端点是文件描述符,另一个端点则是设备驱动,也即具体设备,如果想领略一下unix的流的风采,请参见unix的源代码,不方便的话请看linux的终端驱动程序,也可以参考我前面关于终端的文章,此中所谓的线路规程就是一个协议流模块,它可以压在不同的设备驱动程序之上,也可以直接压在一对队列上,如此可以方便实现成对的伪终端,实际上,这世上本来就没有什么网络协议,最初的网络就是终端和主机之间的连接,因此网络协议也是从终端和主机的连接规则中而来,不过当时的所谓终端协议仍然不是对等的通信协议,终端最初只有简单的处理能力,随着计算机科学的发展,最初的真实终端逐渐成为了客户机,终端也退化成了客户机上的一个程序,不对等的通信协议逐渐演化为队列的协议,这就是后来的tcp/ip协议族,此时,终端几乎全部退化为了本机的虚拟终端--键盘/显示器组合(数据直接在用户空间和内核间传输),或者网络伪终端--ssh或者telnet等应用层协议连接的双方终端对(数据需要先进入应用层再返回内核),不管哪种情况,都从流模型中大大受益,用户空间的sshd等进程读写的ptmx和内核空间的键盘/显示器驱动截然不同,但是它们都统一到了流模块队列,因此相同的线路规程可以毫无疑问地压入它们,达到了相同的效果,网络协议最初多数为了实现远程终端仿真(telnet),随后终于脱离了终端的影子,http,ftp,ssl等由运而生,网络开始大行其道,用户不再仅仅希望远程主机帮忙执行自己需要的程序,然后把结果显示给自己--终端的方式,而是希望和远程机器对等的交换数据,仅仅是数据本身--真正的网络。
如果世上就一个协议,那么一个流驱动硬编码即可,正是因为有很多不同层次的协议,才需要将一些基本的东西抽象出来各自独自成为一个流,然后通过组合这些流来实现不同的协议,当时抽象出来的这些形成各个独立流的基础设施实际上和后来的OSI模型以及tcp/ip的各个层次逻辑很类似,只是没有后者表述的更清楚罢了,不光是对于网络,对于任何设备驱动程序,unix都建议采用这种堆栈式的流堆积的形式,堆栈的每个层次表达了设备逻辑上的一个独立的功能,这种思想就是分层驱动的思想,又因为unix将所有的IO元素抽象成了文件,因此这堆栈中众多层次的每一个层次或者多个层次组合可以单独是一个文件也可以将整个堆栈看成一个文件,层次的逻辑控制通过ioctl来完成,这种方式和windows如今的驱动方式是一致的,可是对于网络驱动来说,由于存在大量协议,用这种纵向的组合就比较复杂和繁琐了,需要大量的层次,对每个层次的控制也不是一般管理员和开发者所能胜任的,于是需要用一种分块的控制方式来管理网络通信,对于每一个块再进入分级或者上述纵向的管理控制,这样在每一个小块中,层次就清晰了,控制也简单了,少了很多不需要却又避不开的层次,于是BSD套接字就产生了,套接字采用了三个约束来产生一个本地可以用于通信的唯一文件描述符,这三个约束处于不同的层次,第一个约束是“域”,比如PF_INET(vpv4),PF_INET6(ipv6),PF_PACKET等等,通过指定域,可以选择一个协议族,在每一个“域”下面存在很多的“类型”,比如SOCK_STREAM,SOCK_DGRAM,通过指定类型,可以选择一个协议族下特定的协议类型,该类型约束了通信的语义,但是却没有给出通信的语法,接下来在特定的协议类型下选择一个特定的协议,比如TCP,UDP之类的,通信语法也就确定了,这样就唯一的确定了一个套接字,套接字并不对数据传输语义层次下面的层次给与关注,也就是说一般套接字并不关注链路层和物理层的任何语义和语法,这也是套接字的魅力之一,同时成了战胜流模型的重量级筹码,套接字十分清楚自己的职责是通信,因此它清晰地分割了通信业务和承载业务,也就是什么是通信双方关注的,什么是数据承载信道关注的或者说是机器关注的,套接字从而也就不再关注底层信道的控制,于是三个参数即可确定一个套接字描述符,对于流模型来讲,由于流只是一个加一个堆叠的,数量和性质都不固定,因此很难区分通信业务和承载业务,最终不得不全包,顺堆栈而下一直管到物理链路。上述的内容都是tcp/ip以后的事情,在套接字开始设计的时候还没有那么多协议呢,因此可以看出套接字还有一个优势,那就是可扩展性。
总之,套接字通过一个类似树形(由于很多domain可以共用很多type,其实是一个有环图)的模型来实现通信节点设施的创建,树上节点虽多,可某一节点的孩子却有限,而流模型则是一个链表,虽然各个流也组成一张图,但是节点之间出了层次关系外没有横向的任何关系,某一个层次的某一个节点的孩子是其下层的所有节点,必须搞清楚所有这一切流的性质后才能创建一个通信设施,很复杂,很难控制,并且此时文件描述符也不再是简单统一的代名词,而成了杂糅和陷阱的替代。最终套接字完胜流模型,被包括windows在内的各大操作系统所接受...
windows不仅仅接受了套接字,还接受了流模型的思想,并且是在网络通信这个领域接受了该思想,因此windows的套接字实现得近乎完美而不像其它的套接字实现那么极端,windows不仅仅实现了bsd的套接字,还提供了类似流模式的SPI用以支持LSP,它为应用厂商提供了控制套接字的接口,注意,这个接口不是为应用直接开发者提供的,应用开发者不是嫌流模式太麻烦吗?那么就给他们一个简单的公认的套接字接口好了,具体接口下面是什么,还是流模式的,不嫌麻烦的人可以用SPI提供的WSC系列函数任意的堆叠自己的逻辑从而实现对通信流的控制,由于微软一向善于接口设计,所以SPI使用起来也不是很麻烦的,WSCEnumProtocols枚举出所有的协议,然后你可以自己定义一个WSAPROTOCOL_INFOW,用WSCInstallProvider将之安装,WSAPROTOCOL_INFOW中本身就包含了一个链表用于实现流堆栈,链表的每个元素都是一个WSAPROTOCOL_INFOW,其中可以通过WSCGetProviderPath得到该PROTOCOL具体实现逻辑的dll,该dll中有一个WSPStartup方法,内中可以实现send/recv/connect/accept等逻辑,每当该dll处理数据完毕后,可以调用该dll对应的WSAPROTOCOL_INFOW的链表的下一个WSAPROTOCOL_INFOW的dll实现的对应方法来继续将数据往下传递。可以用WSCWriteProviderOrder来实现任意顺序的“流堆叠”(其实是一个协议实现链表的形成)。可见LSP的逻辑和古老的unix流模型是一致的,利用SPI可以实现表示层,会话层等可有可无的逻辑层次。即使到了内核模式,NDIS驱动也是分层的,和LSP是很类似的,只是它除了实现那些可有可无的诸如过滤或者加解密机制之外还要实现标准的协议栈,协议栈呢?也是分层的!看来分层是本质,而套接字只是一个便于使用的接口。
windows实现了分层的本质,并且其LSP和NDIS驱动都和流模式很类似,但是别的操作系统呢?是不是就完全抛弃了古老unix的分层流呢?非也!Mac没怎么接触过,除了windows用得最广泛的就是linux和各种版本的unix了,由于这些系统并无心统一设计策略化的操作系统级别的接口,因此你必须从各个开源代码中寻找分层流的影子,OpenSSL就是一个例子,看一下BIO机制,是不是和windows的LSP有异曲同工之妙,并且和古老的unix流更加接近一些。实际上好的东西永远都不会被抛弃,当初unix抛弃流模型仅仅是对于网络通信而言的,并且只是在接口的意义上更加偏向套接字而已。
总的来说,流模型比套接字更加灵活,但是套接字却在灵活性和可用性之间达到了一个完美的平衡,流模式主要优势是协议模块对不同驱动程序复用,但是没有涉及如何去实现这种复用,也就是过于灵活,管理能力不足,这样虽然协议模块可以复用,但是另一部分--设备控制部分就过于复杂,反观套接字,不能随意组合domain,type和protocol,起码你会事先阅读一下规范那些可以组合,控制部分也由ioctl或者sockopt统一控制,参数不是很多,很容易学习和使用,孰优孰劣,实则unix也不是极端之辈,再好的程序员也需要权衡。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics