skynet 源碼閱讀筆記

前言:

這已經不是我第一次閱讀skynet的源碼了,以前每次都是走馬觀花的看了自己關心的部分內容。對其內部的結構和流程只是有個大概的瞭解。最近又開始研究使用skynet這次我下定決定系統化的閱讀一遍源碼,將源碼的大部分的內容加上方便理解和查看的註釋(我拉了一個分支專門寫註釋:https://github.com/xzben/skynet.git),另外也計劃將整理框架的流程細節總結下。方便使用中減少不必要的採坑和細節小錯誤。

一、整體結構

1、線程模型,skynet 包含了 一個網絡線程,一個定時器線程,一個檢測線程(用於檢測任務線程是否有卡死的情況),若干(可配置)工作任務線程。

2、skynet 中沒有使用鎖的,大部分是使用原子操作來控制多線程的競爭問題。另外在結構設計上skynet本身就避免了很多的線程競爭問題,比如工作線程通過使用二級隊列並限制一個服務的消息隊列只會出現在一個線程中處理,比如網絡模塊通過管道的方式進行網絡的事件控制操作。

3、任務隊列模型爲雙極隊列模式。我們框架中每個邏輯單元(我們稱之爲 “服務” service)都有一個自己的專屬消息隊列(這個隊列就是第二級隊列),全局有一個大的消息隊列,裏面存放當前有消息需要處理的二級隊列。

我們工作線程的循環工作就是從全局隊列中讀取二級隊列出來,然後消耗二級隊列的消息,每次只會處理二級隊列中一定數量的消息(具體數量和線程配置的權重有關,但是最多一次消耗當前隊列的一般消息)就將二級隊列塞入全局隊列尾部讓出工作線程,防止一個服務任務太多餓死其它服務。

這裏還有一個很特殊的地方就是我們全局消息隊列中的二級隊列不會同時出現兩次,也就是不會同時不會有兩個線程會處理同一個服務的消息。這個點很重要,那就是這個設計能夠保證我們每個服務的消息處理都是單線程也就是寫邏輯的時候單服務不需要考慮統一服務中的資源存在多線程競爭問題。這是個很棒的設計能讓業務層寫邏輯拋開多線程的煩惱。

4、網絡層skynet 採用的單線程的模型,linux 下用epoll驅動網絡事件。

這裏需要特殊指出的是網絡層由於數據結構的限制只支持(1<<16 = 65536)個連接。如果你要增加需要改下 MAX_SOCKET_P 這個宏定義。

另外網絡的所有操作都是網絡線程操作處理的,也就是網絡線程不僅處理網絡的io事件監聽處理,還同時具有操作相關監聽操作的處理,而不是放在邏輯線程處理。大部分的框架的網絡事件的操作都是放在工作線程來處理的。那麼就會引入鎖這種東西增加了很大的複雜度。邏輯上就沒那麼的清晰簡單了。skynet 通過創建一個 管道 的讀寫通道外部邏輯線程發送操作命令,網絡線程讀取操作命令並執行。完美的別開了網絡管理結構的多線程競爭問題。

5、定時器功能,skynet單獨配置了一個線程來做定時器的任務這樣我們的定時器的時間準確性要高很多,唯一的延遲只會消耗在定時到達後消息從產生到被收到這一派發過程的延遲了,但是這種延遲基本可以忽略了。

6、監視線程,skynet 爲了達到監控 業務線程是否會出現死循環類的卡死現象專門配置一個線程坐死循環檢測,原理就是工作線程每次處理一個消息都回升級一次線程的工作版本號,而監視線程每隔五秒中對比一次每個工作線程的工作版本號,如果五秒鐘版本號沒變則可能陷入死循環卡死狀態,則輸出報錯日誌警告。

到此skynet的整體大框架就講述完畢了,核心的內容就是這麼多。剩下的逐步解析下各個比較重要的模塊的細節

二、各個核心模塊的深入解析

1、邏輯單元管理模塊(skynet_context)  skynet-src/skynet_server.c

在skynet中一個邏輯單元管理模塊 數據結構上就是一個 skynet_context,數據結構如下

struct skynet_context {
	void * instance; //當前服務對象指針
	struct skynet_module * mod; //服務所屬模塊
	uint32_t handle; //服務id
	int ref; //引用數量
	char result[32];  // 用於操作 服務時的cmd命令存儲返回值用
	void * cb_ud; // 服務消息處理回調函數的 user data
	skynet_cb cb; // 服務消息處理回調函數
	int session_id;//服務的消息會話id
	struct message_queue *queue; //服務的消息隊列
	bool init;   //服務對象是否已經初始化過
 	bool endless; // 服務是否進入死循環卡死狀態

	CHECKCALLING_DECL
};

通過我們查看上面的數據結構註釋我們可以指定,其實一個邏輯模塊的組成還是比較簡單的,主要是一個 專屬的消息隊列,一個消息消耗執行的回調函數,另外就是模塊的標識id,控制內存釋放的引用計數器,剩下的 比較特殊的屬性是比較有特色的一些內容,就是我們的邏輯單元模塊的實現是可以動態定製一個skynet_module,也就是使用者可以根據自己的情況很方便的定製自己的邏輯單元模塊,將其編譯成動態庫並導出三個關鍵接口就行了。skynet_module 的結構如下:

struct skynet_module {
	const char * name;  //庫的名字
	void * module;   // 動態庫的 打開句柄
	skynet_dl_create create;  // 庫對外暴露的 create 接口
	skynet_dl_init init;     // 庫對外暴露的 init 接口
	skynet_dl_release release;// 庫對外暴露的 release 接口
};

就像上述的結構體一樣我們只要定製我們自己的模塊,只需要定製一個 動態庫,並導出  create, init,release 單個關鍵接口,然後init內部設置消息監聽 回調函數 skynet_cb 就能成爲一個skynet的邏輯服,正常接收到消息做自己的邏輯處理。簡單的動態庫的實現可以參考  service-src/service_logger.c 文件這是一個最精簡的模塊了。

2、邏輯模塊的管理器( handle_storage )skynet-src/skynet_handle.c

首先我們看下結構體

struct handle_storage {
    //讀寫鎖,這裏的鎖使用的是原子操作實現,而非我們常見的線程鎖之類的
	struct rwlock lock;
    
    //當前進程的節點id,主要是 harbor 分佈式架構中使用的,
    //如果使用 cluster 模式這個值都是0,我們服務的handle 值高兩個字節存儲的就是 harbor值
	uint32_t harbor;	
    // 用於尋找空閒服務存儲槽位的index值
	uint32_t handle_index; 
     // 當前可用存儲服務的數組大小
	int slot_size;        
    // 存儲服務的指針數組
	struct skynet_context ** slot; 
	//當前存儲服務handle 映射名字的 數組容量
	int name_cap;   
    // 當前存儲的映射數據數量
	int name_count;
    // 存儲映射關係的數據,這個存儲結構存儲的方案使用的是按名字的字符順序
    // 從小到大的方式存儲的。主要是爲了快速查找名字所對應的handle
	struct handle_name *name; 
};

其實這個管理模塊的功能很簡單,就是存儲服務,和服務handle別名name的映射關係。

3、外部自定義服務模塊管理(modules) skynet-src/skynet_module.c

前面提到了skynet允許用戶自定自己的邏輯服務模塊,只需要編譯好自己的模塊動態,並在動態庫導出create init release 接口並在init的時候設置好skynet_cb 就可以使用。具體 modules 模塊的管理機制就是我們這裏要介紹的。首先看下管理模塊的數據結構

struct modules {
	int count;  //當前擁有的模塊數量
	int lock;   //數據結構控制讀取的鎖,這裏用原子操作控制的,所以int 能當鎖控制
	const char * path; // 模塊動態庫的搜索匹配路徑配置,有點類似 lua 的 package.cpath 類似的配置
	struct skynet_module m[MAX_MODULE_TYPE]; //存儲模塊的數組,限定大小爲32,如果需要擴展修改宏 MAX_MODULE_TYPE 就行了
};

skynet 現在目前代碼限制的第三方模塊大小爲 32,如果需要擴展更多自行修改宏定義就行了。這裏的管理模塊採用的是惰性加載的模式,也就是隻有當第一次使用這個模塊的時候纔會去加載對應的模塊,並解析模塊的的必要接口存儲起來。

4、網絡模塊   (socket_server) skynet-src/skynet_server.c skynet-src/skynet_socket.c

skynet 網絡模塊,主要有幾個特點,1、網絡模塊的邏輯驅動只有一個線程運行 2、網絡事件linux 使用epoll,apple 使用kqueue 3、網絡事件操控是通過 pip 管道從邏輯線程發送給網絡線程運行的,這樣避免了對網絡管理結構體的多線程處理問題。4、網絡中的socket 套接字都是用非阻塞模式的。這樣能夠更大程度的利用單線程的性能,避免不必要的IO阻塞等待時間。5、網絡模塊限定的管理數組大小爲(1<<16 = 65536)個連接,如果需要增加請自行改宏定義 MAX_SOCKET 6、網絡層支持發送消息的高、低兩種優先級。 7、所有的連接socket 在走完基本流程時,還需要業務層自己主動open或start 啓動正式使用否則網絡層是沒有處理這些socket的網絡事件的,這樣做就相當於可以給業務層去對連接的一個過濾和驗證過程。 8、網絡模塊最終收到的消息發送給註冊的業務邏輯服時他的消息都是 socket_message結構體,網絡層是沒有處理粘包問題的。網絡層只是單純的不斷接收buffer然後發給關心的業務層。業務層接收到網絡消息後需要用  netpack(lualib-src/lua-netpack) 插件工具使用 netpack.filter 接口去處理粘包然後將通信的數據流拆分成一個一個獨立的網絡消息buffer。netpack.filter 接口返回 queue,buffer 的lightuserdaba, size 三個返回值,queue 是一個buffer的隊列,用於中轉不完整的buffer流。 

5、lua服務的核心模塊 (snlua) server-src/server_snlua.c

前面我們的基礎框架其實沒有具體限定我們業務邏輯模塊具體是什麼結構的。只是簡單的提供了模塊管理結構和消息分發的工作線程。而我們接觸到底skynet 服務大多數是lua寫的,這裏其實就是通過 這個框架自帶的外部庫 snlua 實現的。按雲鳳的說法其實我們具體的業務服務用什麼語言都是沒有關係的只要你自己寫好這個業務擴展的模塊就行了。那麼不管你用啥都行。因爲本身skynet的核心並不關心 你的邏輯用什麼做它只是幫我們管理好網絡事件,任務消息分發執行,剩下的用我們自定義的服務擴展模塊做就行了。比如框架自帶的lua模塊 snlua。

其實我們查看snlua的代碼也是很簡單的,它主要的工作就是提供一個lua虛擬機,並且在init 設置了一個一次性的skynet_cb 用於啓動加載lua對應的服務文件和給這個lua虛擬機設置我們再啓動配置中配置的 lua服務查找配置路徑和預加載前綴文件LUA_PRELOAD(就是所有服務加載前都會先加載這個文件)。

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