先介紹eventfd
1 #include<sys/eventfd.h> 2 int eventfd(unsigned int initval, int flags);
使用這個函數來創建一個事件對象,linux線程間通信為了提高效率,大多使用異步通信,采用事件監聽和回調函數的方式來實現高效的任務處理方式(雖然會將邏輯變得復雜)。
linux內核會為這個事件對象維護一個64位的計數器(uint64_t).並在初始化時用傳進去的initval來初始化這個計數器,然后返回一個文件描述符來代表這個事件對象。
第二個參數是描述這個事件對象的屬性,可以設置為EFD_NONBLOCK , EFD_CLOEXEC;前面的是設置對象為非阻塞狀態,如果沒有設置為非阻塞狀態,read系統調用來讀這個計數器,且計數器的值為0時,就會一直阻塞在read系統調用上,反之如果設置了該標志位,就會返回EAGAIN錯誤。后面的EFD_CLOEXEC功能是在程序調用exec()函數族加載其他程序時自動關閉當前已有的文件描述符(具體為什么暫不解釋)。
通過此函數得到的對象既然是一個計數器,我們就可以對它進行讀和寫:
使用write將緩沖區寫入的8字節整形值加到內核計數器上。
使用read將內核計數的8字節值讀取到緩沖區中,並把計數器重設為0,如果buffer的長度小於8字節則read會失敗,錯誤碼設為EINVAl。
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
再介紹epoll,忍不住的可以直接向下翻
epoll是對select,poll這種IO多路轉接方式的改進
接口: int epoll_create(int intsize);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
int epoll_wait(int epfd, struct epoll_event* events,int maxevents, int timeout);
工作模式:
水平觸發:缺省的工作方式,並且同時支持block和no-blocksocket,在這種做法中,內核告訴你一個文件描述符是否就緒了,然后你可以對這個就緒的fd進行IO操作。如果你不作任何操作,內核還是會繼續通知你的,所以,這種模式編程出錯誤可能性要小一點。傳統的select/poll都是這種模型的代表
邊緣觸發:高速工作方式,只支持no-blocksocket。在這種模式下,當描述符從未就緒變為就緒時,內核通過epoll告訴你。然后它會假設你知道文件描述符已經就緒,並且不會再為那個文件描述符發送更多的就緒通知,直到你做了某些操作導致那個文件描述符不再為就緒狀態了
用途:使用epoll_wait對某個文件描述符進行事件監聽,監聽到事件后會返回相關的結構體,得到其中有事件到來的fd,使用對應的回調函數(手動實現fd到回調函數的映射)來處理該fd上的事件:讀數據或者寫數據之類的。
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
下面進入使用場景:
原始做法:(會有bug,下面分析)
初始化:先生成一個eventfd,初始化計數器為0,此eventfd可以通過一些方法在下面兩個線程間共享
線程A:處理一些來自外部的請求,每處理完一個請求后會向eventfd的計數器中寫入處理的結果,是一個整型值,然后接着處理下一個請求。
線程B:對eventfd進行Epoll監聽,回調函數的功能是對eventfd的計數器讀數據出來並將結果進行分發。
用例1:外部單個客戶端每隔1秒向線程A發送一個請求。
用例1結果:線程A正確處理請求,並將結果寫入eventfd中,線程B及時從eventfd中讀取出請求處理結果,並正確分發給其他線程。
用例2:外部單個客戶端連續向線程A發送多個請求。
用例2結果:線程A正確處理請求,並正確地將結果寫入eventfd中,但在一定概率的情況下,線程B從eventfd中讀到的結果不是線程A一次寫入的結果,而是多次寫入的結果。因此不能正確的分發請求。線程B中epoll捕捉到的事件次數小於線程A寫入產生的事件數量。
用例3:外部多個客戶端同時向線程A發送一個請求
用例3結果:線程A正確處理請求,並正確的將結果寫入eventfd中,在很大的概率情況下,線程B中eventfd中讀到的結果不是線程A一次寫入的結果,而是多次寫入的結果。因此,也不能正確的分發請求。線程B中epoll捕捉到的事件次數小於線程A寫入產生的事件數量。
BUG分析:在這個場景中,線程A和線程B分別相當於生產者和消費者,只從原始生產者消費者模型上看並沒有問題,滿足數據為空時讀不到數據,數據滿時寫不進數據(read,write的功能),但是在當前場景中,加了一個特別的要求:每次寫入的數據應該可以被獨立識別而不是累加,每次寫入的事件也應該被epoll獨立的捕捉到。因此,需要對事件和數據各自進行序列化上的拆分。
改進做法:
初始化:先生成一個eventfd,初始化計數器為1,再生成一個空隊列Q和互斥鎖,此eventfd,隊列Q和互斥鎖可以通過一些方法在下面兩個線程間共享,
線程A:處理一些來自外部的請求,每處理完一個請求后會從eventfd的計數器read數據,加1之后再write,將處理結果寫入到隊列末尾,然后接着處理下一個請求。
線程B:對eventfd進行Epoll監聽,回調函數的功能是對eventfd的計數器read數據出來然后判斷,如果大於1就自減1然后從隊列頭部取出數據,並將結果進行分發
,最后再寫入新的計數器數據。如果等於1那么就直接返回,代表沒有新的數據到來。
用例1,2,3在此環境下均可正常跑通。
回過頭來分析原始做法的fatal error在哪:
作為生產者的線程A沒有向線程B解釋自己向eventfd中寫入了多少個數據,產生了多少次事件。
作為消費者的線程B一次read就把eventfd中所有的數據當做一個數據讀了出來,卻沒有相關依據來對讀出來的數據做拆分。
作為通信工具的eventfd只能將數據進行累加,起到計數器的作用而不能存儲實際數據。
作為消息監聽的epoll在水平觸發模式下只能通知是否有事件而不能通知有多少事件,在邊緣觸發下不能保留每次事件的產生都能及時被消費者捕獲到。
因此,改進做法是將事件的多少通過計數器來表達,將實際傳輸的數據通過FIFO隊列來傳達。
Happy Ending Every Day.