本文我们讲一下zeromq的线/进程间通信方式。
在zeromq源代码分析1中我们分析了zeromq的基本工作流程。我们了解到了zeromq的线/进程间通信的方式为消息传递。
zeromq中的各线程间通信的消息称之为command_t类型:
// This structure defines the commands that can be sent between threads.
struct command_t
{
// Object to process the command.
class object_t *destination;
enum type_t
{
stop,
plug,
own,
attach,
bind,
activate_reader,
activate_writer,
pipe_term,
pipe_term_ack,
term_req,
term,
term_ack,
reap,
reaped,
done
} type;
union {
// Sent to I/O thread to let it know that it should
// terminate itself.
struct {
} stop;
// Sent to I/O object to make it register with its I/O thread.
struct {
} plug;
// Sent to socket to let it know about the newly created object.
struct {
class own_t *object;
} own;
// Attach the engine to the session. If engine is NULL, it informs
// session that the connection have failed.
struct {
struct i_engine *engine;
unsigned char peer_identity_size;
unsigned char *peer_identity;
} attach;
// Sent from session to socket to establish pipe(s) between them.
// Caller have used inc_seqnum beforehand sending the command.
struct {
class reader_t *in_pipe;
class writer_t *out_pipe;
unsigned char peer_identity_size;
unsigned char *peer_identity;
} bind;
// Sent by pipe writer to inform dormant pipe reader that there
// are messages in the pipe.
struct {
} activate_reader;
// Sent by pipe reader to inform pipe writer about how many
// messages it has read so far.
struct {
uint64_t msgs_read;
} activate_writer;
// Sent by pipe reader to pipe writer to ask it to terminate
// its end of the pipe.
struct {
} pipe_term;
// Pipe writer acknowledges pipe_term command.
struct {
} pipe_term_ack;
// Sent by I/O object ot the socket to request the shutdown of
// the I/O object.
struct {
class own_t *object;
} term_req;
// Sent by socket to I/O object to start its shutdown.
struct {
int linger;
} term;
// Sent by I/O object to the socket to acknowledge it has
// shut down.
struct {
} term_ack;
// Transfers the ownership of the closed socket
// to the reaper thread.
struct {
class socket_base_t *socket;
} reap;
// Closed socket notifies the reaper that it's already deallocated.
struct {
} reaped;
// Sent by reaper thread to the term thread when all the sockets
// are successfully deallocated.
struct {
} done;
} args;
};
由此可见一个command_t主要包括destination,type,args,其中destination表明要传递命令消息的目的对象,type和args则是命令消息的类型和参数。
而发送消息的代码如下:
// Derived object can use these functions to send commands
// to other objects.
void send_stop ();
void send_plug (class own_t *destination_,
bool inc_seqnum_ = true);
void send_own (class own_t *destination_,
class own_t *object_);
void send_attach (class session_t *destination_,
struct i_engine *engine_, const blob_t &peer_identity_,
bool inc_seqnum_ = true);
void send_bind (class own_t *destination_,
class reader_t *in_pipe_, class writer_t *out_pipe_,
const blob_t &peer_identity_, bool inc_seqnum_ = true);
void send_activate_reader (class reader_t *destination_);
void send_activate_writer (class writer_t *destination_,
uint64_t msgs_read_);
void send_pipe_term (class writer_t *destination_);
void send_pipe_term_ack (class reader_t *destination_);
void send_term_req (class own_t *destination_,
class own_t *object_);
void send_term (class own_t *destination_, int linger_);
void send_term_ack (class own_t *destination_);
void send_reap (class socket_base_t *socket_);
void send_reaped ();
void send_done ();
void zmq::object_t::send_command (command_t &cmd_)
{
ctx->send_command (cmd_.destination->get_tid (), cmd_);
}
void zmq::ctx_t::send_command (uint32_t tid_, const command_t &command_)
{
slots [tid_]->send (command_);
}
因为zeromq采用的模式基本上就是一个object和一个io_thread绑定起来,所以这里的意思就是我们将command发送给目的对象的io_thread的mailbox中去。
如果对ctx::slots不太懂,请回到zeromq源代码分析1并且结合源代码理解。
下面我们对mailbox来一个深入分析:
zmq::mailbox_t::mailbox_t ()
{
#ifdef PIPE_BUF
// Make sure that command can be written to the socket in atomic fashion.
// If this wasn't guaranteed, commands from different threads would be
// interleaved.
zmq_assert (sizeof (command_t) <= PIPE_BUF);
#endif
// Create the socketpair for signaling.
int rc = make_socketpair (&r, &w);
errno_assert (rc == 0);
// Set the writer to non-blocking mode.
int flags = fcntl (w, F_GETFL, 0);
errno_assert (flags >= 0);
rc = fcntl (w, F_SETFL, flags | O_NONBLOCK);
errno_assert (rc == 0);
#ifndef MSG_DONTWAIT
// Set the reader to non-blocking mode.
flags = fcntl (r, F_GETFL, 0);
errno_assert (flags >= 0);
rc = fcntl (r, F_SETFL, flags | O_NONBLOCK);
errno_assert (rc == 0);
#endif
}
该函数创建了socketpair,并且设置了writer和reader都为非阻塞模式。
而socketpair在UNIX上是有原生的实现的,在windows下面通过自己创建一个一对socket连接,客户端当reader,服务端当writer。
void zmq::mailbox_t::send (const command_t &cmd_)
{
// Attempt to write an entire command without blocking.
ssize_t nbytes;
do {
nbytes = ::send (w, &cmd_, sizeof (command_t), 0);
} while (nbytes == -1 && errno == EINTR);
// Attempt to increase mailbox SNDBUF if the send failed.
if (nbytes == -1 && errno == EAGAIN) {
int old_sndbuf, new_sndbuf;
socklen_t sndbuf_size = sizeof old_sndbuf;
// Retrieve current send buffer size.
int rc = getsockopt (w, SOL_SOCKET, SO_SNDBUF, &old_sndbuf,
&sndbuf_size);
errno_assert (rc == 0);
new_sndbuf = old_sndbuf * 2;
// Double the new send buffer size.
rc = setsockopt (w, SOL_SOCKET, SO_SNDBUF, &new_sndbuf, sndbuf_size);
errno_assert (rc == 0);
// Verify that the OS actually honored the request.
rc = getsockopt (w, SOL_SOCKET, SO_SNDBUF, &new_sndbuf, &sndbuf_size);
errno_assert (rc == 0);
zmq_assert (new_sndbuf > old_sndbuf);
// Retry the sending operation; at this point it must succeed.
do {
nbytes = ::send (w, &cmd_, sizeof (command_t), 0);
} while (nbytes == -1 && errno == EINTR);
}
errno_assert (nbytes != -1);
// This should never happen as we've already checked that command size is
// less than PIPE_BUF.
zmq_assert (nbytes == sizeof (command_t));
}
发送command,这边利用writer句柄调用send(4) syscall进行发送command。先尝试写,如果被中断则继续循环写。如果返回错误EAGAIN,由于是非阻塞模式写我们就会增加SND_BUF继续写,最后验证一下是否成功。
int zmq::mailbox_t::recv (command_t *cmd_, bool block_)
{
#ifdef MSG_DONTWAIT
// Attempt to read an entire command. Returns EAGAIN if non-blocking
// mode is requested and a command is not available.
ssize_t nbytes = ::recv (r, cmd_, sizeof (command_t),
block_ ? 0 : MSG_DONTWAIT);
if (nbytes == -1 && (errno == EAGAIN || errno == EINTR))
return -1;
#else
// If required, set the reader to blocking mode.
if (block_) {
int flags = fcntl (r, F_GETFL, 0);
errno_assert (flags >= 0);
int rc = fcntl (r, F_SETFL, flags & ~O_NONBLOCK);
errno_assert (rc == 0);
}
// Attempt to read an entire command. Returns EAGAIN if non-blocking
// and a command is not available. Save value of errno if we wish to pass
// it to caller.
int err = 0;
ssize_t nbytes = ::recv (r, cmd_, sizeof (command_t), 0);
if (nbytes == -1 && (errno == EAGAIN || errno == EINTR))
err = errno;
// Re-set the reader to non-blocking mode.
if (block_) {
int flags = fcntl (r, F_GETFL, 0);
errno_assert (flags >= 0);
int rc = fcntl (r, F_SETFL, flags | O_NONBLOCK);
errno_assert (rc == 0);
}
// If the recv failed, return with the saved errno if set.
if (err != 0) {
errno = err;
return -1;
}
#endif
// Sanity check for success.
errno_assert (nbytes != -1);
// Check whether we haven't got half of command.
zmq_assert (nbytes == sizeof (command_t));
return 0;
ssize_t nbytes = ::recv (r, cmd_, sizeof (command_t), 0);
是因为对于线程间的通信木有字节序不同的问题。
下面我们来讲一下接收的函数调用过程。
由于我们在zeromq源代码分析1中已经说明io_thread在构造函数中会将自己的mailbox的句柄加入到poller中,设置事件处理器就是自身的函数in_event()/out_event(),并且激活读事件。因此当poller轮询到mailbox的读事件,也就是有人发消息给该io_thread的mailbox时候,就能通过in_event()函数去recv消息。
void zmq::io_thread_t::in_event ()
{
// TODO: Do we want to limit number of commands I/O thread can
// process in a single go?
while (true) {
// Get the next command. If there is none, exit.
command_t cmd;
int rc = mailbox.recv (&cmd, false);
if (rc != 0 && errno == EINTR)
continue;
if (rc != 0 && errno == EAGAIN)
break;
errno_assert (rc == 0);
// Process the command.
cmd.destination->process_command (cmd);
}
}
// These handlers can be overloaded by the derived objects. They are
// called when command arrives from another thread.
virtual void process_stop ();
virtual void process_plug ();
virtual void process_own (class own_t *object_);
virtual void process_attach (struct i_engine *engine_,
const blob_t &peer_identity_);
virtual void process_bind (class reader_t *in_pipe_,
class writer_t *out_pipe_, const blob_t &peer_identity_);
virtual void process_activate_reader ();
virtual void process_activate_writer (uint64_t msgs_read_);
virtual void process_pipe_term ();
virtual void process_pipe_term_ack ();
virtual void process_term_req (class own_t *object_);
virtual void process_term (int linger_);
virtual void process_term_ack ();
virtual void process_reap (class socket_base_t *socket_);
virtual void process_reaped ();
void zmq::object_t::process_command (command_t &cmd_)
{
switch (cmd_.type) {
case command_t::activate_reader:
process_activate_reader ();
break;
case command_t::activate_writer:
process_activate_writer (cmd_.args.activate_writer.msgs_read);
break;
case command_t::stop:
process_stop ();
break;
case command_t::plug:
process_plug ();
process_seqnum ();
return;
...
下一篇文章会介绍zmq_msg_t以及相关的函数,敬请期待。希望有兴趣的朋友可以和我联系,一起学习。 [email protected]