這篇博文主要分析了ZeroMQ源碼中和message相關的消息機制,大致分析了ZeroMQ消息的基本結構,創建過程以及銷燬過程。分析了ZeroMQ是如何區別對待長消息和短消息,並用指針計數的方法做到到零拷貝。其中涉及到union關鍵字的使用,強制類型轉換和placement new的用法等編程技巧,可供學習參考。本人水平有限,歡迎讀者指正。
在實際項目中使用ZeroMQ創建消息時的代碼通常如下:
zmq_msg_t msgName; //第1行
zmq_msg_init(&msgName); //第2行
這兩條代碼做了什麼呢?
首先對第1行代碼,在zmq.h中有如下定義:
typedef struct zmq_msg_t {unsigned char _ [32];} zmq_msg_t;
what?消息體就這樣定義?也許這不是它的真面目。
看第2行代碼,在zmq.cpp找到zmq_msg_init的實現方式。
int zmq_msg_init(zmq_msg_t *msg_){
return((zmq::msg_t*) msg_)->init();
}
暈,居然強制類型轉換,把原本指向32字節空間的zmq_msg_t類型指針轉換成了指向msg_t類型的指針。
現在來看一下這個神祕的msg_t,也就是ZeroMQ的消息類。
首先看msg.cpp中init()的實現
int zmq::msg_t::init(){
u.vsm.type =type_vsm;
u.vsm.flags =0;
u.vsm.size = 0;
return 0;
}
可以大致看出就是初始化了消息的類型、標誌和消息內容大小。對應的看一下在msg.hpp是如何定義這個消息結構體的:
在msg.hpp的msg_t類中有:
union {
struct {
unsigned char unused [max_vsm_size + 1];
unsigned char type;
unsigned char flags;
} base;
struct {
unsigned char data [max_vsm_size];
unsigned char size;
unsigned char type;
unsigned char flags;
} vsm;
struct {
content_t *content;
unsigned char unused [max_vsm_size + 1 - sizeof (content_t*)];
unsigned char type;
unsigned char flags;
} lmsg;
struct {
void* data;
size_t size;
unsigned char unused
[max_vsm_size + 1 - sizeof (void*) - sizeof (size_t)];
unsigned char type;
unsigned char flags;
} cmsg;
struct {
unsigned char unused [max_vsm_size + 1];
unsigned char type;
unsigned char flags;
} delimiter;
} u;
可見這裏利用union來壓縮空間。union維護足夠的空間來置放多個數據成員中的“一種”,而不是爲每一個數據成員配置空間,在union中所有的數據成員共用一個空間,同一時間只能儲存其中一個數據成員,所有的數據成員具有相同的起始地址。
從這裏可以看出ZeroMQ的幾種消息類型:vsm (verysmall message?), lmsg (long message?), cmsg (constant message) 和delimiter 。
每個struct人爲地控制爲等長,其中unused數組就是用來控制每個struct的長度,使得後面的type和flags在每個struct中的存儲位置是一樣的。這樣就可以做到,無論該消息是vsm或者lmsg或其他類型,只要調用u.base.type就能獲取到這個消息的類型了。
enum {max_vsm_size =29};
通過vsm類型和lmsg類型的對比可以知道,ZeroMQ對短消息和長消息是區別對待的。對於短的消息,即不超過29字節的消息,直接複製賦值;而對於長消息,則需要在內存中分配空間,如下面代碼所示:
//初始化消息大小
int zmq::msg_t::init_size (size_t size_)
{
if (size_ <= max_vsm_size) {
//當消息爲小消息時
u.vsm.type = type_vsm;
u.vsm.flags = 0;
u.vsm.size = (unsigned char) size_;
}
else {
u.lmsg.type = type_lmsg;
u.lmsg.flags = 0;
u.lmsg.content =
(content_t*) malloc (sizeof (content_t) + size_);
//消息爲長消息,需要分配內存空間
if (unlikely (!u.lmsg.content)) {
errno = ENOMEM;
return -1;
}
u.lmsg.content->data = u.lmsg.content + 1;
//指向在內存空間中分配的消息內容的地址
u.lmsg.content->size = size_;
u.lmsg.content->ffn = NULL;
u.lmsg.content->hint = NULL;
new (&u.lmsg.content->refcnt) zmq::atomic_counter_t ();
}
return 0;
}
//初始化消息內容
int zmq::msg_t::init_data (void *data_, size_t size_, msg_free_fn *ffn_,
void *hint_)
{
// If data is NULL and size is not 0, a segfault
// would occur once the data is accessed
assert (data_ != NULL || size_ == 0);
// Initialize constant message if there's no need to deallocate
if(ffn_ == NULL) {
//如果銷燬函數爲空,則該消息爲常量消息
u.cmsg.type = type_cmsg;
u.cmsg.flags = 0;
u.cmsg.data = data_;
u.cmsg.size = size_;
}
else {
u.lmsg.type = type_lmsg;
u.lmsg.flags = 0;
u.lmsg.content = (content_t*) malloc (sizeof (content_t));
if (!u.lmsg.content) {
errno = ENOMEM;
return -1;
}
u.lmsg.content->data = data_;
u.lmsg.content->size = size_;
u.lmsg.content->ffn = ffn_;
u.lmsg.content->hint = hint_;
new (&u.lmsg.content->refcnt) zmq::atomic_counter_t ();
//placement new 的用法,後面說明
}
return 0;
}
這裏有必要看一下上面出現的content的結構:
struct content_t
{
void *data;
size_t size;
msg_free_fn *ffn;
void *hint;
zmq::atomic_counter_t refcnt;
};
其中ffn爲銷燬消息時使用的函數指針。而refcnt則是該消息被共享次數的計數器。當該計算器計數爲0,即該消息以及沒有被使用時,則該消息銷燬。當需要拷貝長消息時,只要把指針指向長消息內容即可,然後計數器加一,這便實現了ZeroMQ所謂的零拷貝。
在上面msg_t::init_data()中出現了這麼一行:
new(&u.lmsg.content->refcnt) zmq::atomic_counter_t ();
這裏使用了placement new的寫法。placement new是用來實現定位構造的,也就是在取得了一塊可以容納指定類型對象的內存後,在這塊內存上構造一個對象。對new的深入瞭解,可以參考這個博客:http://blog.csdn.net/songthin/article/details/1703966。
再回過頭來看最開頭的地方,好像還有一個問題沒解決:
typedef structzmq_msg_t {unsigned char _ [32];} zmq_msg_t;
int zmq_msg_init (zmq_msg_t *msg_)
{
return ((zmq::msg_t*) msg_)->init ();
}
這裏做的強制類型轉換,把原本指向32字節空間的zmq_msg_t類型指針轉換成了指向msg_t類型的指針,爲什麼是32位呢,通過下面代碼,對消息結構進行字節計算,不難發現每個消息結構就是佔了32個字節的。只不過長消息中使用了指針指向了用於存儲長消息數據的內存空間而已。所以不要被外表所矇騙,要看到內在,才知道她的心是怎樣的。
struct {
unsigned char data [max_vsm_size];
unsigned char size;
unsigned char type;
unsigned char flags;
} vsm;
struct {
content_t *content;
unsigned char unused [max_vsm_size + 1 - sizeof (content_t*)];
unsigned char type;
unsigned char flags;
} lmsg;
enum {max_vsm_size =29};
上面簡單分析了消息的創建過程,下面接着來分析一下消息的銷燬過程,這裏主要指的是對於長消息的銷燬,因爲只有長消息需要動態內存分配。
前面分析content結構的時候提到過,content中ffn爲銷燬消息時使用的函數指針,而refcnt則是該消息被共享次數的計數器,當該計算器計數爲0,即該消息以及沒有被使用時,則該消息銷燬。
長消息的創建過程大致如下:
1. 設置長消息標誌位;
2.動態分配一塊內存給長消息的content;
3.content中的data指向在內存空間中分配的消息內容的地址;
4.設置content銷燬消息用的銷燬函數;
5. 設置計數器。
如果是長消息拷貝,直接使content指針指向同一塊內存區域即可,同時區域內的計數器加一。
長消息的銷燬過程只有逆向創建過程即可,見如下代碼(附帶註釋):
int zmq::msg_t::close ()
{
if (u.base.type == type_lmsg) {
// 如果content不是共享的,或者它是共享的但它的指向計數以及降爲零,則釋放它
if (!(u.lmsg.flags & msg_t::shared) ||
!u.lmsg.content->refcnt.sub (1)) {
// 1. 使用 "placement new"操作去初始化這個指向計數器的,所以要調用對應的顯式的析構函數
u.lmsg.content->refcnt.~atomic_counter_t ();
// 2. 調用content中的銷燬函數銷燬指向的內存數據
if (u.lmsg.content->ffn)
u.lmsg.content->ffn (u.lmsg.content->data,
u.lmsg.content->hint);
// 3. 釋放content
free (u.lmsg.content);
}
}
// 使該消息失效
u.base.type = 0;
return 0;
}