zeromq的内部架构

从代码的行数来看,代码库并不复杂(目前有10000行);但zeromq要考虑大量不同的组合,因此很复杂。比如说,要在超过10个OS的不同版本上运行;要运行在许多不同的指令体系结构,从ARMItanium;由不同的编译器编译,从gccMSVCSunStudio;可以与20多种不同语言绑定进行交互;可以使用不同的底层传输协议,不同的进程间消息传递机制并支持可靠多播;支持不同的消息模式:远程过程调用,数据分发,并行流水线等;每个socket可以连接或被连接到对等socket,或者同时建立双向连接;两个节点之间的通信失败可能是由于底层连接的中断也可能是暂时性的网络问题等等。所有的这些选项是相互正交的,需要考虑到上千种可能的组合。

在对代码做任何更改之前要小心仔细的了解代码在做什么和为什么要这么做。

全局状态

libzmq没有使用全局变量,而是由使用者负责显示的创建全局状态。包含全局状态的对象被称为“上下文(context)”。从使用者的角度来看,context或多或少像是共zmq socket使用的I/O线程池;在libzmq的观点来看,context只是一个存储libzmq所需要的全局状态的对象。例如,进程内部使用的可用端点(endpoint)列表(已经关闭的,但仍然逗留在内存中的zmq socket列表,之所以逗留在内存中是由于在上下文中有需要发出的消息)就存储在上下文中。

上下文由ctx_t实现。

并发模型

使用自己独特的消息传递方式来实现并发性和可扩展性。好处:在多线程环境中,zmq的使用者不必使用互斥锁,条件变量或信号等用来同步并行处理的东西;每个对象只会在自己所在的线程上执行,其他线程只能通过发送消息(以下称为“命令”,以区别于使用者发送给zmq的消息)与对象进行交互而不能直接使用对象(这就是为什么不需要互斥体的原因),同时对象间也可以相互发送命令进行交互(实际上线程在zmq中也是一种对象)。

对象由类object_t定义,类中定义了发送命令处理命令 的接口。如果想要在自定义对象之间传递命令,对象类只需要简单的派生自object_t,并定义处理命令的程序就可以了。
命令由类command_t实现,command_t中定义了所有可用的命令。比如有一个带有单参数“linger"”term“(销毁)命令,以参数linger=100发送term命令到对象p中,调用方法:
           send_term(p, 100);
另一方面,如果你想给对象定义一个处理”term“命令的程序,可以这样做:
          void my_object_t :: process_term(int linger)
     {
                   // 处理动作在这里实现
         }
需要注意的是,以上实现只有当应用类派生自object_t类才有效。

对于大多数命令,zmq可以保证命令在传递过程中目标对象不会消失,也就是说,命令可以保证投递。(关于如何保证,见后文关于对象树模型 的相关描述,对象树模型将异步对象绑定至树状的层次结构。)

不过,对于少数跨越对象树的命令需要额外的操作来实现保证投递。对于这类命令,发送者在向接收者发送命令之前需要调用接收方的inc_seqnum()方法来获取这一保证。inc_seqnum()增加接收方中的计数sent_seqnum.当接收方处理命令时,会增加另一个计数processed_seqnum。在接收方要销毁是,如果发现processed_seqnum小于sent_seqnum,就说明有正在传送而没有处理的命令,这时接收方就不会继续执行销毁动作。销毁操作的逻辑对object_town_t来说是透明的,命令发送方和接收方只需要发送和接收命令,而无需关心命令的具体序列号seqnum

备注:事实上,有些数据被限制在临界区中,使用临界区遵循以下两个规则:
  1. 在任何时候,数据对每个线程都是可访问的。(例如,前面提到的同一进程内的端点列表)
  2. 在消息传递过程中,不应使用临界区中的数据。

线程模型


操作系统来看zmq只有两种线程:应用线程和I/O线程。应用线程在zmq外部创建,访问zmqAPI ;I/O线程在zmq内部创建,用于在后台发送和接收消息。thread_t是系统级线程的抽象,可以以OS无关的方式创建线程。

而从zmq的观点来看,线程只是一个拥有邮箱(mailbox_t)的对象。邮箱存储发送给居住在当前程序上所有对象的信件(命令commond_t),所有这些对象公用线程上的邮箱。线程从邮箱中按需获取命令并交给其上的对象进行处理。

目前,zmq内部使用两种不同类型的线程(拥有邮箱的对象):I/O线程(io_thread_t)socket(socket_base_t)
I/O线程:每个I/O线程与一个系统级线程一 一对应。I/O线程运行在自己的系统线程上,并且拥有独立的获取命令的邮箱。
socket:在某种程度上显得复杂一些。每个zmq socket拥有自己的接受命令的邮箱,因此socket可被zmq视为分离线程。而实际上,一个应用程序线程可以创建多个套接字,也就是说多个zmq socket被映射到同一个系统线程。更加复杂的是,zmq socket可以在系统线程之间迁移。例如,java语言绑定可以在单线程中使用zmq socket,而当线程结束时,zmq socket会传递给垃圾回收线程,并在垃圾回收线程上销毁。

I/O线程


I/O线程(io_thread_t)zmq异步处理网络I/O的后台线程。它的实现非常简洁。io_thread_t实现继承object_t,并实现io_poll_events接口,其内部包含一个邮箱(mailbox_t)和一个poller对象(poller_t)继承object_t使得io_thread_t能够发送和接收command(如stop命令,当收到该命令时,I/O线程将被终止)。io_poll_events接口定义了文件描述符和计时器事件就绪时的回调处理函数(in_event/out_event/timer_event)io_thread_t实现此接口(in_event)来处理mailbox的事件。当mailbox_t事件触发时,I/O线程从mailbox中获取命令,并让命令的接收者进行处理。

mailbox_t用来存储发送给任何居住在io_thread_t上的object_t的命令,每个io_thread_t上有多个对象,这些对象公用同一个邮箱,邮箱的收件人就是对象。mailbox_t本质上是一个具有就绪通知功能的存储命令的队列。就绪通知机制由singaler_t提供的文件描述符实现。队列是由ypipe_t实现的无锁无溢出队列。

poller_t是从不同操作系统提供的时间通知机制中抽象出来的概念,用来通知描述符和计时器事件,poller_t通过typedef定义为操作系统首选的通知机制(select_t/poll_t/epoll_t等)。所有运行在io_thread_t上的对象都继承自辅助类io_object_t,该类实现了向io_thread_t注册/删除文件描述符(add_fd/rm_fd)和计时器(add_timer/cancel_timer)事件的功能,同时io_thread_t还继承了i_poll_events接口来实现时间回调功能。


对象树


zmq库的内部,对象在大多数情况下被组织成树状层次结构。树的根节点只能是供应用使用的zmq socket (socket_base_t)



树中的每个对象可以处在不同的线程上。想要将子节点束缚在根节点对应的线程上是不可行的,因为根节点(zmq socket)处于应用程序线程上,而其他节点则处在I/O线程上:

对象树模型产生的主要目的是为了实现一致性的销毁机制(对象销毁)。经验做法是在对象销毁之前,向其所有子对象发送销毁请求命令,当收到所有孩子对销毁请求的确认时才会真会被销毁。由于命令处理是按序的,销毁请求和销毁确认机制能够刷新在对象之间送中的命令。这就是为什么大多数命令(在对象树内传递的命令)不要使用命令序列号(见下文)来保证当有正在传送的消息时对象不会销毁的原因。

当孩子对象决定销毁自身而父对象没有向它发出销毁请求时,销毁过程会变得更加复杂,例如TCP连接断开时会话对象会销毁。我们必须仔细处理由父对象向子对象发起销毁请求,子对象自身发起销毁请求,一起二者同时发生时的情况。最后发现只要子对象销毁自身时向其父对象发出销毁子对象的请求就可以同一处理以上三种情况。下面是以上三种情况的时序图。与销毁有关的三种命令:父对象要求子对象销毁的term命令,子对象向父对象的销毁确认term_ack命令,子对象想销毁自身时向父对象发送的term_req命令。


在上图的最后一种情况下,term_req命令被父对象简单地丢弃。因为父对象已经向子对象发出了销毁请求(term),所以重新发送没有任何意义。如果父对象发送两次term请求,第二次请求到达时孩子已被释放,将会造成segmentation fault错误或覆盖现有的内存。

对象树机制通过类own_t来实现,own_t定义了对象树中的节点。own_t继承自object_t使得对象树中的每一个节点可以发送和接收命令(在销毁阶段需要接收和发送命令)。

需要注意的是,并不是每个对象都在对象树中。有些对象可以发送和接收命令,但不属于对象树(如管道端点)。

回收线程

上一小节描述了与销毁相关的机制。而销毁任何一个指定的对象(包括socket)消耗的时间是不确定的。然而,我们希望close有类似于POSIX的行为:当关闭TCP套接字时,即使在后台还有没有完全发出的数据,调用也会立即返回。所以,应用程序线程调用close时,zmq应该关闭对应的套接字。但是,我们不能依赖应用程序来完成socket子对象的销毁(销毁可能需要多次命令交互)。同时应用线程调用zmq_close以后不会再继续使用该socket,甚至可能永远不会再调用zmq库函数。因此,zmq socket应该从应用线程迁移到一个工作线程来处理销毁的逻辑。一个可能的解决方案是将socket迁移到某个后台的I/O线程上去,然而zmq可以初始化为具有0个I/O线程(适用于只在进程间通信的情况),因此,我们需要一个专门的回收线程来执行销毁任务。回收线程由类reaper_t实现 socket通过(send_reap)向回收线程发送回收命令,回收线程收到命令后会将socket从应用线程迁移到回收线程上,这样socket就可以在回收线程上处理命令(term/term_ack),直到socket的所有子对象都成功销毁时,socket就会在回收线程上销毁。实际上,回收线程只是待回收对象驻留的线程,对象的处理逻辑仍然由对象自身处理。








评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值