ZeroMQ源碼分析之Message

這篇博文主要分析了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.cppinit()的實現


int zmq::msg_t::init(){
    u.vsm.type =type_vsm;
    u.vsm.flags =0;
    u.vsm.size = 0;
    return 0;
}

可以大致看出就是初始化了消息的類型、標誌和消息內容大小。對應的看一下在msg.hpp是如何定義這個消息結構體的:

msg.hppmsg_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的長度,使得後面的typeflags在每個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結構的時候提到過,contentffn爲銷燬消息時使用的函數指針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;

}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章