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 
 

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