zmq的内部结构

介绍:
本文介绍了ZMQ的一些概述,但不会涉及到一些细节,因为随着版本的更新,实现的细节也不一样,而且很多代码是为了兼容不同的操作系统和编译器的,如果需要知道其中的细节,还是要看源码。

全局状态
在库里使用全局变量看起来是一件搬起石头砸自己的脚的事情。一开始一切正常,直到一个全局变量被可执行文件链接两次(见下图),就会发生一些奇怪的错误和崩溃。
arch1.png
为了防止这种事情发生,zmq没有使用全局变量,相反,用户需要自己显示的创建全局状态。包含全局状态的对象叫"上下文(context)",而从用户的角度看“上下文”很像是一种给套接字用的线程池,从zmq的角度看,上下文只是一个存储全局状态的对象。比如,一个可用端的列表存储在上下文中,这些套接字实际已经关闭了,但是由于有未发送的消息,这些数据被上下文存在内存中。上下文的实现在class ctx_t中。

并发模型:
zmq的并发模型说起来可能有点混乱,因为,我们吃自己的狗粮(??),并使用消息传递实现并发和伸缩性。因此,尽管ZMQ是一个多线程程序,但是你也找不到里面有互斥锁,条件变量,和信号量去处理同步互斥。相反,每个对象都在自己的线程内存货,不会有其他线程对他处理,不同的线程之间通过发送命令的方式来通信,已区分用户级的信息,同样的,对象之间也可以通过发命令的方式来对话。

从用户的角度看,两个对象之间传递消息很简单。这两个对象只需要继承”object_t“就可以发送消息(commands)了。对应的命令实现可以在command.hpp中查看。可以通过send_term方法来发送一个内部命令

send_term (p, 100);

 如果你想对对应的command定义不同的处理要这么做:

void my_object_t::process_term (int linger)
{
    //  Implement your action here.
}

 

然而需要注意的是,只能在object_t的派生类中做这些操作。

对于大部分命令来说,发送的时候,要保证这些对象不会消失。当然也有几个命令是例外,对这些命令来说,发送方需要在目的对象调用sent_seqnum(会让目的同步计数器+1)方法前调用inc_seqnum方法。当目标对象收到了命令,会增加另一个计数器(processed_seqnum),当这个对象生命周期结束,他知道不能完成processed_seqnum 少于set_seqnum计时器。比如仍在传输中被传递到该对象的命令。整个过程都是透明的,在object_t和own_t对象的实现中,他们只是关注收发,并不关注顺序。

线程模型:
zmq有两种线程,一种是工作线程,一种是普通线程。普通线程在外部创建,用于访问API,IO线程在内部创建,用于收发消息。tread_t提供了线程的创建,屏蔽了操作系统的细节。

I/O threads:
I/O线程是由MQ异步处理网络流量所使用的后台线程。实现比较简洁。io_thread_t类继承了thread_t类,thread_t提供了屏蔽套作系统细节的线程API的一个简单的相容性包装。它还来继承了object_t使得它能够发送和接收commads。

此外,每个I/O线程拥有一个poller(事件轮训器)。poller(poller_t)是一个抽象的概念,在不同的操作系统有不同的实现,封装了如select_t,poll_t,epoll_t等,主要是用来做事件轮训。

还有一个简单的辅助对象叫io_object,他提供了注册fd,移除fd,添加事件,移除事件,登记定时器,移除定时器等操作。
io1.png

Object tree

zmq的内部对象的关系像一个树型结构,根节点是sokcet
objtree1.png

每个对象都可能生存在不同的线程中,但是不会和他的祖先节点生存在同一个线程中,跟节点(socket)存活在应用线程中,其他存货在IO线程中。

objtree2.png

对象书存在的理由是提供了明确的关闭机制。经验是,当这个对象要求对所有子对象发送关闭的请求时需要在自己关闭之前被确认。值得注意的是,关闭和确认请求的交换--这两个命令都有效的刷新了两个对象之间命令传递的时间。这也就是为什么大部分命令不需要用命令计数器的原因,因为这样可以保证对象不会被销毁。
当一个对象不问父母就进行自我关闭时,这种情况会比较复杂,比如有一个会话对象在TCP链接断开后关闭自己。我们需要考虑到他的父对象是否终止。
实时证明,所有的情况都可以被要求父对象关闭它的自我终结来解决。下图是所有场景的序列图。子对象确认其终端通过发送term_ack到父对象。如果子对象想自我销毁,会请求父对象发送关闭term_req命令。


注意,在最后一种情况下,在发送term后,未收到term_ack的情况下,term_req会被partent忽略。
对象机制在own_t中实现,own_t是从object_t中派生的,因此对象书中的每个对象都可以发送和接收命令。

The reaper thread 

上述机制有个特殊的问题。关闭任何特定对象时会消耗任意的时间。然而,我们希望让zmq_close有类似POSIX一样的行为:你可以关闭tcp socket,立即返回,即时还有数据还没有收到。
所以,在调用zmq_close的关闭socket的应用线程应该初始化。但是我们不能简单依靠和子对象握手,这个线程可能已经参与了完全不同的事情,甚至可能不会再调用zmq库,因此,这个socket应该迁移到一个和应用线程的工作线程,这样就可以代替应用线程握手了。
一个符合逻辑的办法就是把套接字迁移到I/O线程,但是,zmq可初始化空的IO线程,因此我们需要一个专门的线程来完成这个任务,这个就是reaper thread。它的实现在reaper_t中,专门负责销毁套接字。

Message
我们需要知道zmq的消息是怎么工作的。zmq对消息的要求比较复杂,它的复杂在于,既要求高效,不占空间,又需要携带更多的信息,具体要求如下:
1,对于非常小的消息,复制这条消息比在堆上面共享消息要高效。因此,这些消息没有关联缓存而是直接存在zmq_msg结构里的,这堆性能有极大的提升,因为避免了很多内存的申请和释放。
2,当用inproc传输数据时,数据不能被拷贝。因此,buffer发送一个线程,应该在其他线程中会搜销毁。
3,消息应该支持引用计数。因此,如果一个消息被发布到多个不同的tcp链接上,所有的io线程应该访问同一个缓存,而不是都去拷贝一份。
4,用户也应该使用同样的技巧,用缓存避免拷贝。
5,用户能够发送特点情况下被应用申请的缓存缓存,而不需要复制数据。这对于大量数据下的应用十分重要。
为了实现这些目标,zmq的message设计成了下图的样子:



对于非常小的消息(vsm),buffer是zmq_msg_t的一部分,他分配在栈空间上,不需要调用maclloc等方法申请,这样就比较高效。vsm_size是消息的长度。vsm_data缓冲区的大小由ZMQ_MAX_SIZE定义,通常是30,当然你可以改变它的值。
对于不适合VSM的消息,我们在堆上面申请一块空间,并用zmq_msg_t结构指向它。
在堆上面分配的结构是msg_conten_t,他包含,地址,大小,用于释放他的函数指针,以及一些提示。
这个缓冲区可以被多个zmq_msg_t共享,因为他有引用计数的功能,当没有的zmq_msg_t指向它的时候,它会自动销毁。



从上图可以看出,为了减少内存分配,消息数据和其他数据会存在同一个内存块中。
请注意,用户可以访问引用计数器。调用,zqm_msg_copy不会物理的复制缓存,而是会创建一个新的zmq_msg_t的结构体,指向同一个缓存,最后,如果缓冲区的消息由用户提供,那么不能将缓冲区的数据放在同一个内存块中,会单独分配一个内存块。




消息调度
zmq中使用了多种调度算法,但是他们都在一个pipe上面工作。这些pipe是动态的,可以发送和接收消息,有些是被动的,只能接收消息,不能发送消息。

翻译自原文:http://zeromq.org/whitepapers:architecture 
 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值