Redis事件驅動庫《轉》

2022-03-16 08:15:29 字數 3576 閱讀 5898

本文**:

事件驅動的程式設計方式已經很普及了,原因自然是網際網路的疾速膨脹,現在要寫個伺服器不用事件驅動,出門都不好意思跟人打招呼。但是實現事件庫並不是那 麼容易,首先它與人們亦步亦趨的思考方式有點兒衝突,其次事件庫的底層實現必須平台相關,如linux使用epoll,freebsd使用kqueue。

事件驅動庫是很多系統軟體的基礎設施,如lighttpd、nodejs使用了libev,memcached使用了libevent,nginx 和redis自己實現了一套。通用的事件庫一般比較複雜,有很多我們並不需要的功能,而自己寫乙個又費時間,可能boss不會同意。本文將分析乙個小巧的 事件庫,它內置於redis。如果你事先沒看redis事件庫的原始碼(ae.h ae.c ae_epoll.c),讀起來可能會有困難。

乙個事件庫的必要組成元素有哪些呢:

1)事件,一般由外在因素觸發,比如有網路資料到達;

2)事件處理函式,事情發生以後要靠它處理;

3)事件與處理函式之間的對映關係,將上述兩種概念聯絡起來;

4)迴圈監控,基於事件驅動的程式一般主體是個迴圈,在每一遍迴圈中檢查發生了哪些事兒,然後呼叫相應的處理函式;

事件:作業系統產生的事件包括檔案的讀寫,網路介面的讀寫,作業系統訊號,超時事件。前兩個linux統一為檔案描述符,訊號事件

redis沒有在事件庫中實現(確實沒這個必要),超時事件redis只向作業系統借了個獲取系統時間的系統呼叫。於是庫中事件只有兩種:檔案讀寫和超

時。struct aefileevent ; 

struct aetimeevent ; 

「將發生的檔案事件儲存在fireevent中」這個過程主要是由作業系統幫你完成的,不是在主迴圈內實現的,你要是能在使用者程式中完成那就是見著鬼了。  

基本資訊就介紹到這兒了,我不會深入到瑣碎的**細節,而是以問答的方式展現有意義的細節。讀者要先讀原始碼,並時刻對比下面這張圖:

q1:redis是先處理fileevent,還是先處理timeevent?

從aeprocessevents函式可以看出是先處理fileevent。

q2:我看了下aeprocessevents函式,確實是先fileevent,再timeevent。但是在處理fileevent前,還有一長串**,那是幹什麼用的?

呵呵,這個說起來比較煩,我現在不知道怎麼表達,你問些其他的問題吧,說不定等下就說清楚了。

q3:如果我要延遲5秒輸出「hello, world」,該怎麼辦?

/* 先定義好超時處理函式 */

int print5(struct aeeventloop *loop, long long id, void *clientdata)

int main(void)

q4:如果我要每隔5秒輸出「hello, world」,又該怎麼辦呢?

這個好辦,把q3的**原樣拷來,只做一處修改:print5函式不返回-1,而返回5000

其實返回-1代表刪掉對應的超時事件,這樣只會列印一次「hello, world」;若是返回正整數n,則代表n毫秒後再觸發該事件,於是就會迴圈列印。注意千萬不能返回0或小於-1的負數,否則會陷入死迴圈,這也算是個瑕疵吧。 

q5:redis是怎麼處理超時事件的?

從圖中可看出timeevent被組織為乙個單向鍊錶,表頭指標timeeventhead儲存在核心資料結構aeeventloop中。

aemain函式在每一輪迴圈中都會遍歷該鍊錶,針對每個timeevent,先呼叫gettimeofday獲取系統當前時間,如果它比

timeevent中的時間要小,則說明timeevent還沒觸發,應繼續前進,否則說明timeevent已經觸發了,立即呼叫超時處理函式,接下來

根據處理函式的返回值分兩種情況討論:

1)若處理函式返回-1,那麼把這個timeevent刪掉。

2)否則,根據返回值修改當前的timeevent。比如返回5000,這個timeevent就會在5秒後再次被觸發。

好了,這個timeevent已經搞定了,按理說應該繼續前進處理下乙個timeevent了,但是且慢,由於情況1)我們不能由當前結點到達下一結點,於是作者就又從表頭開始遍歷。

這個演算法不怎麼好,假如我從表頭開始遍歷,碰到第100個結點時才發現該呼叫處理函式了,這樣處理完之後,又得從表頭開始遍歷,前面的很多結點都做了無用的重複計算。作者也承認這個演算法不怎麼好,應該改進。

q6:你已經說過在每一輪主迴圈中,是先處理fileevent,再處理timeevent,如此往復。有沒有可能這一輪的timeevent永遠處理不完,從而導致後來發生的fileevent得不到處理?

有可能,如果timeevent的處理函式返回0或者除 -1以外的負數,那麼會再次無休止地呼叫這個處理函式。

q7:如果每個timeevent函式都會呼叫aecreatetimeevent函式,那麼會不會導致和q6一樣的問題?

不會,

由aecreatetimeevent函式創造的timeevent都不會在此輪得到處理,而是會在下一輪處理完fileevent後再處理。這個功能是

由struct aeeventloop中的timeeventnextid成員完成的,具體怎麼實現的請讀者看原始碼,很簡單。

q8:redis是怎麼處理fileevent的?

fileevent和timeevent的處理方式差別很大,使用者程式不可能去遍歷檔案描述符,而是在迴圈中呼叫epoll_wait系統呼叫。這個系統呼叫是阻塞式的,直到發現有檔案事件觸發才會返回到使用者空間,進而處理fileevent。

決這個問題有賴於epoll_wait函式可以接受乙個引數,用來確定最長等待時間,如果在這段時間一直沒有檔案事件觸發,epoll_wait不會傻傻

間設為最長等待時間。epoll_wait在這段時間內都沒有等到檔案事件觸發就會返回到使用者空間,繼而執行後面的事件處理流程。確定最長等待時間的**

在檔案事件處理之前,現在你不會有q2的疑問了。

q10:上面的回答似乎令人信服,但是有一種特殊情況,如果timeevent鍊錶為空,你如何確定最長等待時間?

這確實是個好問題。其實

在主迴圈啟動前我們要決定設不設ae_dont_wait這個標誌。當碰到timeevent為空的情況時,

如果設定了ae_dont_wait,epoll_wait會立即返回,不再等待檔案事件;如果沒設此標誌,epoll_wait會永遠等待,直到有檔案

事件觸發為止。

總結:如果把上面10個問題搞清楚了,那麼對redis事件庫已經很了解了。其實redis對事件庫的功能要求很簡單,完全不需要libevent和libev中的各種複雜功能,事實上很多時候我們也並不需要多強大的庫,或許下次你可以把redis的事件庫運用到你的作品中去。

移植時要注意以下幾個問題:

1)ae.實現主要的邏輯;ae_select.cae_epoll.c,ae_kqueue.c分別是三種底層實現,根據你的系統選其一即可

2)上面那些檔案依賴了config.h zmalloc.,移植的話要對上面的檔案做些修改。 

自己編寫事件庫時應注意以下問題:

1)檔案讀寫和時間超時是兩種不同的事件。我們要根據應用情況決定是處理一種,還是兩者包辦,甚至有沒有必要處理訊號事件。

2)防止飢餓現象,q6,q7,q9,q10都是關於飢餓現象的。

《Redis官方教程》 事件庫

讓我們通過一系列q a來弄明白。q 你期望網路伺服器都做些什麼事情?a 在它監聽的埠上等待連線的到來,然後接收 accpet 它們。q 呼叫accept會產生乙個描述符,我該怎麼處理它?a 先儲存這個描述符,然後對它進行非阻塞 non blocking 的讀寫 read write 操作。q 為什麼...

redis0 1原始碼解析之事件驅動

redis的事件驅動模組負責處理檔案和定時器兩種任務。下面是幾個函式指標 typedef void aefileproc struct aeeventloop eventloop,int fd,void clientdata,int mask typedef intaetimeproc struct...

驅動python python實現事件驅動

eventmanager事件管理類實現,大概就百來行 左右。encoding utf 8 系統模組 from queue import queue,empty from threading import class eventmanager def init self 初始化事件管理器 事件物件列表...