mailbox是zmq中线程之间及socket和线程之间的通信机制。
mailbox类中包含了cpipe类及signaler类,其中cpipe类负责发送给mailbox的命令的存储,读写等操作;signaler类负责命令写入到cpipe之后写线程通过调用send()接口给读线程发送信号看,读线程调用signaler类的recv()函数及wait()函数执行接收信号的操作。
cpipe类实现了一个管道的功能,通过其内部的f/w/r/c变量实现了支持一个读线程一个写线程同时在管道上进行读写操作而不会引起并发问题,其中c变量为读写数据之间的栅栏。
cpipe类的内部使用了yqueue_t类来实现消息的存储,yquque_t类是一个高效的内存分配的queue,它在每次都分配N个命令大小的内存空间,而且在内存释放的时候并不真正的立即释放内存,而是持有一块真正的内存块以备下次分配时使用。yqueue_t减少了执行内存分配的频率,提高了queue的效率。
cpipe和yqueue_t类都使用到了atomic_ptr_t类,这个类实现了一个原子性的指针操作,在cpipe中的c变量就是一个这种类型的指针,atomic_ptr_t类型保证了指针赋值操作等的线程安全性。
具体这写类的分析参见本博客的相关文档。
本文将基于源代码对mailbox的实现原理简要分析。
- mailbox模块中内部由两个模块组成:
- cpipe模块,此模块存储mailbox需要发送及接收的命令。
- signaler模块,mailbox在把命令放置到了cpipe中之后,通过signaler模块发送消息给写线程或者读线程,通知消息可操作。
class i_mailbox
{
public:
virtual ~i_mailbox () {}
virtual void send (const command_t &cmd_) = 0;
virtual int recv (command_t *cmd_, int timeout_) = 0;
};
i_mailbox作为mailbox的接口类规定了mailbox需要实现的借口。此类为虚拟基类,因为有纯虚函数所以不能实例化,send和recv函数为纯虚函数,在派生类中必须要实现。
//mailbox定义,继承了i_mailbox接口类,对外提供send和recv接口实现。
class mailbox_t : public i_mailbox {
public:
//获得读文件描述符。
fd_t get_fd () const;
//发送命令。
void send (const command_t &cmd_);
//接收命令。
int recv (command_t *cmd_, int timeout_);
private:
//cpipe实际上是一个ypipe_t类型的队列,实现了单一读线程/写线程的无锁访问。
//command_pipe_granularity值为16,此值代表了ypipe_t的底层实现中一次实际的内存分配操作会分配
//16*command_t大小的内存空间。
typedef ypipe_t cpipe_t;
cpipe_t cpipe; //ypipe_t类型的对象,用于存储命令。
signaler_t signaler; //用于写线程给读线程发送信号通知命令到达。
mutex_t sync; //用于同步cpipe及signaler的操作。
}
//发送命令到线程的mailbox。
//在实际使用的过程中其实是首先找到目的mailbox对象,然后调用此对象的send函数,把消息放入到这个对象的queue中。并不是从一个mailbox能够发送消息到另外一个mailbox。send的含义更像是把消息放入到函数所属对象的queue中。
//参数:
// cmd_,要发送的命令。
void zmq::mailbox_t::send (const command_t &cmd_)
{
sync.lock (); //加锁。
cpipe.write (cmd_, false); //写消息到管道。
const bool ok = cpipe.flush (); //使消息对读线程可见。
sync.unlock (); //解锁。
if (!ok) //如果发送成功,那么发送信号通知命令处理线程。
signaler.send ();
}
//接受命令。
//参数:
// cmd_,要接收的命令的buffer。
// timeout_,等待超时时间。
int zmq::mailbox_t::recv (command_t *cmd_, int timeout_)
{
//先尝试直接读取命令,如果读到了命令则直接返回。
//如果读取失败说明当前mailbox中没有未处理的命令,那么把状态设置为不活跃。
// Try to get the command straight away.
if (active) {
if (cpipe.read (cmd_))
return 0;
// If there are no more commands available, switch into passive state.
active = false;
}
//等待信号,如果有信号到达说明有命令到达了mailbox。(函数会阻塞在此)
// Wait for signal from the command sender.
int rc = signaler.wait (timeout_);
if (rc == -1) {
errno_assert (errno == EAGAIN || errno == EINTR);
return -1;
}
//收到信号说明有命令要处理。此时把mailbox状态设置为活跃,
//然后读取命令到指定buffer中。
// Receive the signal.
rc = signaler.recv_failable ();
if (rc == -1) {
errno_assert (errno == EAGAIN);
return -1;
}
// Switch into active state.
active = true;
// Get a command.
const bool ok = cpipe.read (cmd_);
zmq_assert (ok);
return 0;
}
//析构函数。
//此处用了一个空行为锁保证了不会在其他线程send()的时候释放对象,**是一个好的方法。**
//其他线程send()的时候会加锁,因此析构函数会阻塞在lock()操作,只有等到没有send()操作的时候
//析构函数才能继续进行下去。
zmq::mailbox_t::~mailbox_t ()
{
// TODO: Retrieve and deallocate commands inside the cpipe.
// Work around problem that other threads might still be in our
// send() method, by waiting on the mutex before disappearing.
sync.lock ();
sync.unlock ();
}
class signaler_t {
public:
signaler_t (); //构造函数。
~signaler_t (); //析构函数。
fd_t get_fd () const; //获得读描述符。
void send (); //发送信号。
int wait (int timeout_); //等待信号。
void recv (); //接收信号,如果出错程序退出。
int recv_failable (); //接收信号,如果出错程序退出。
private:
// Creates a pair of file descriptors that will be used
// to pass the signals.
static int make_fdpair (fd_t *r_, fd_t *w_); //创建r/w描述符。
// Underlying write & read file descriptor
// Will be -1 if we exceeded number of available handles
fd_t w; //w描述符,用于发送信号。
fd_t r; //r描述符,用于接收信号。
//声明类的拷贝构造函数及拷贝赋值操作符为私有。
// Disable copying of signaler_t object.
signaler_t (const signaler_t&);
const signaler_t &operator = (const signaler_t&);
}
//构造函数。
zmq::signaler_t::signaler_t ()
{
// Create the socketpair for signaling.
if (make_fdpair (&r, &w) == 0) {
unblock_socket (w);
unblock_socket (r);
}
#ifdef HAVE_FORK
pid = getpid ();
#endif
}
//发信号。
void zmq::signaler_t::send ()
{
#if defined HAVE_FORK
if (unlikely (pid != getpid ())) {
//printf("Child process %d signaler_t::send returning without sending #1\n", getpid());
return; // do not send anything in forked child context
}
#endif
#if defined ZMQ_HAVE_EVENTFD
const uint64_t inc = 1;
ssize_t sz = write (w, &inc, sizeof (inc)); //向eventfd写输入,默认不使用eventfd。
errno_assert (sz == sizeof (inc));
#else
unsigned char dummy = 0;
while (true) {
ssize_t nbytes = ::send (w, &dummy, sizeof (dummy), 0); //发数据到w描述符。
if (unlikely (nbytes == -1 && errno == EINTR))
continue;
#if defined(HAVE_FORK)
if (unlikely (pid != getpid ())) {
//printf("Child process %d signaler_t::send returning without sending #2\n", getpid());
errno = EINTR;
break;
}
#endif
zmq_assert (nbytes == sizeof dummy);
break;
}
#endif
}
//在读描述符上等待,使用poll或者select。
//参数:
// timeout_,等待超时的时间。
//返回值:
// 错误返回-1;正确返回0。
int zmq::signaler_t::wait (int timeout_)
{
....
}
//收信号,如果发生错误程序退出。
void zmq::signaler_t::recv ()
{
// Attempt to read a signal.
#if defined ZMQ_HAVE_EVENTFD
uint64_t dummy;
ssize_t sz = read (r, &dummy, sizeof (dummy)); //从socket中读。
errno_assert (sz == sizeof (dummy));
// If we accidentally grabbed the next signal(s) along with the current
// one, return it back to the eventfd object.
//一个信号只增加1,如果为>1的数字说明收到了多个信号,这样需要减去1,把剩余值重新存储进去。
if (unlikely (dummy > 1)) {
const uint64_t inc = dummy - 1;
ssize_t sz2 = write (w, &inc, sizeof (inc)); //重新写回值。
errno_assert (sz2 == sizeof (inc));
return;
}
zmq_assert (dummy == 1);
#else
unsigned char dummy;
ssize_t nbytes = ::recv (r, &dummy, sizeof (dummy), 0); //从socket中收信号。
errno_assert (nbytes >= 0);
zmq_assert (nbytes == sizeof (dummy));
zmq_assert (dummy == 0);
#endif
}
//接收信号,如果发生错误返回-1,运行正常返回0。
//与recv()的区别就是recv()不会返回值,错误发生程序直接退出;此函数会返回错误值,而不会导致程序退出。
int zmq::signaler_t::recv_failable ()
{
....
}
//生成用于发送信号的socket对。
// Returns -1 if we could not make the socket pair successfully
int zmq::signaler_t::make_fdpair (fd_t *r_, fd_t *w_)
{
//通过eventfd实现线程间通信。
#if defined ZMQ_HAVE_EVENTFD
int flags = 0;
#if defined ZMQ_HAVE_EVENTFD_CLOEXEC
// Setting this option result in sane behaviour when exec() functions
// are used. Old sockets are closed and don't block TCP ports, avoid
// leaks, etc.
flags |= EFD_CLOEXEC;
#endif
fd_t fd = eventfd (0, flags);
if (fd == -1) {
errno_assert (errno == ENFILE || errno == EMFILE);
*w_ = *r_ = -1;
return -1;
}
else {
*w_ = *r_ = fd;
return 0;
}
//如果不支持eventfd则采用如下方法。
#else
// All other implementations support socketpair()
int sv [2];
int type = SOCK_STREAM;
// Setting this option result in sane behaviour when exec() functions
// are used. Old sockets are closed and don't block TCP ports, avoid
// leaks, etc.
#if defined ZMQ_HAVE_SOCK_CLOEXEC
type |= SOCK_CLOEXEC;
#endif
//创建一个socket对。
int rc = socketpair (AF_UNIX, type, 0, sv);
if (rc == -1) {
errno_assert (errno == ENFILE || errno == EMFILE);
*w_ = *r_ = -1;
return -1;
}
else {
// If there's no SOCK_CLOEXEC, let's try the second best option. Note that
// race condition can cause socket not to be closed (if fork happens
// between socket creation and this point).
#if !defined ZMQ_HAVE_SOCK_CLOEXEC && defined FD_CLOEXEC
rc = fcntl (sv [0], F_SETFD, FD_CLOEXEC); //设置为非阻塞。
errno_assert (rc != -1);
rc = fcntl (sv [1], F_SETFD, FD_CLOEXEC); //设置为非阻塞。
errno_assert (rc != -1);
#endif
*w_ = sv [0]; //0用于写线程。
*r_ = sv [1]; //1用于读线程。
return 0;
}
#endif