高階IO模型之kqueue和epoll

flydean發表於2022-03-26

簡介

任何一個程式都離不開IO,有些是很明顯的IO,比如檔案的讀寫,也有一些是不明顯的IO,比如網路資料的傳輸等。那麼這些IO都有那些模式呢?我們在使用中應該如何選擇呢?高階的IO模型kqueue和epoll是怎麼工作的呢?一起來看看吧。

block IO和nonblocking IO

大家先來了解一下IO模型中最簡單的兩個模型:阻塞IO和非阻塞IO。

比如我們有多個執行緒要從一個Socket server中讀取資料,那麼這個讀取過程其實可以分成兩個部分,第一部分是等待socket的資料準備完畢,第二部分是讀取對應的資料進行業務處理。對於阻塞IO來說,它的工作流程是這樣的:

  1. 一個執行緒等待socket通道資料準備完畢。
  2. 當資料準備完畢之後,執行緒進行程式處理。
  3. 其他執行緒等待第一個執行緒結束之後,繼續上述流程。

為什麼叫做阻塞IO呢?這是因為當一個執行緒正在執行的過程中,其他執行緒只能等待,也就是說這個IO被阻塞了。

什麼叫做非阻塞IO呢?

還是上面的例子,如果在非阻塞IO中它的工作流程是這樣的:

  1. 一個執行緒嘗試讀取socket的資料。
  2. 如果socket中資料沒有準備好,那麼立即返回。
  3. 執行緒繼續嘗試讀取socket的資料。
  4. 如果socket中的資料準備好了,那麼這個執行緒繼續執行後續的程式處理步驟。

為什麼叫做非阻塞IO呢?這是因為執行緒如果查詢到socket沒有資料,就會立刻返回。並不會將這個socket的IO操作阻塞。

從上面的分析可以看到,雖然非阻塞IO不會阻塞Socket,但是因為它會一直輪詢Socket,所以並不會釋放Socket。

IO多路複用和select

IO多路複用有很多種模型,select是最為常見的一種。實時不管是netty還是JAVA的NIO使用的都是select模型。

select模型是怎麼工作的呢?

事實上select模型和非阻塞IO有點相似,不同的是select模型中有一個單獨的執行緒專門用來檢查socket中的資料是否就緒。如果發現資料已經就緒,select可以通過之前註冊的事件處理器,選擇通知具體的某一個資料處理執行緒。

這樣的好處是雖然select這個執行緒本身是阻塞的,但是其他用來真正處理資料的執行緒卻是非阻塞的。並且一個select執行緒其實可以用來監控多個socket連線,從而提高了IO的處理效率,因此select模型被應用在多個場合中。

為了更加詳細的瞭解select的原理,我們來看一下unix下的select方法:

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout);

先來解釋一下這幾個引數的含義,我們知道unix系統中,一切的物件都是檔案,所以這裡的fd表示的就是file descriptor ,也就是檔案描述符。

fds表示的是 file descriptor sets,也就是檔案描述符集合。

nfds是一個整數值,表示的是檔案描述符集合中最大值+1.

readfds是要檢查的檔案讀取的描述符集合。

writefds是要檢查的檔案寫入的描述符集合。

errorfds是要檢查的檔案異常描述符集合。

timeout是超時時間,表示的是等待選擇完成的最大間隔。

其工作原理是輪詢所有的file descriptors,然後找到要監控的那些檔案描述符,

poll

poll和select類很類似,只是描述fd集合的方式不同. poll主要是用在POSIX系統中。

epoll

實時上,select和poll雖然都是多路複用IO,但是他們都有些缺點。而epoll和kqueue就是對他們的優化。

epoll是linux系統中的系統命令,可以將其看做是event poll。首次是在linux核心的2.5.44版本引入的。

主要用來監控多個file descriptors其中的IO是否ready。

對於傳統的select和poll來說,因為需要不斷的遍歷所有的file descriptors,所以每一次的select的執行效率是O(n) ,但是對於epoll來說,這個時間可以提升到O(1)。

這是因為epoll會在具體的監控事件發生的時候觸發通知,所以不需要使用像select這樣的輪詢,其效率會更高。

epoll 使用紅黑樹 (RB-tree) 資料結構來跟蹤當前正在監視的所有檔案描述符。

epoll有三個api函式:

int epoll_create1(int flags);

用來建立一個epoll物件,並且返回它的file descriptor。傳入的flags可以用來控制epoll的表現。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

這個方法用來對epoll進行控制,可以用來監控具體哪些file descriptor和哪些事件。

這裡的op可以是ADD, MODIFY 或者 DELETE。

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

epoll_wait用來監聽使用epoll_ctl方法註冊的事件。

epoll提供了兩種觸發模式,分別是 edge-triggered 和 level-triggered。

如果一個使用epoll註冊的pipe收到了資料,那麼呼叫epoll_wait將會返回,表示存在要讀取的資料。但是在level-triggered模式下,只要管道的緩衝區包含要讀取的資料,對 epoll_wait的呼叫將立即返回。但是在level-triggered模式下,epoll_wait 只會在新資料寫入管道後返回。

kqueue

kqueue和epoll一樣,都是用來替換select和poll的。不同的是kqueue被用在FreeBSD,NetBSD, OpenBSD, DragonFly BSD, 和 macOS中。

kqueue 不僅能夠處理檔案描述符事件,還可以用於各種其他通知,例如檔案修改監視、訊號、非同步 I/O 事件 (AIO)、子程式狀態更改監視和支援納秒級解析度的計時器,此外kqueue提供了一種方式除了核心提供的事件之外,還可以使用使用者定義的事件。

kqueue提供了兩個API,第一個是構建kqueue:

int kqueue(void);

第二個是建立kevent:

int kevent(int kq, const struct kevent *changelist, int nchanges, struct kevent *eventlist, int nevents, const struct timespec *timeout);

kevent中的第一個引數是要註冊的kqueue,changelist是要監視的事件列表,nchanges表示要監聽事件的長度,eventlist是kevent返回的事件列表,nevents表示要返回事件列表的長度,最後一個引數是timeout。

除此之外,kqueue還有一個用來初始化kevent結構體的EV_SET巨集:

EV_SET(&kev, ident, filter, flags, fflags, data, udata);

epoll和kqueue的優勢

epoll和kqueue之所以比select和poll更加高階, 是因為他們充分利用作業系統底層的功能,對於作業系統來說,資料什麼時候ready是肯定知道的,通過向作業系統註冊對應的事件,可以避免select的輪詢操作,提升操作效率。

要注意的是,epoll和kqueue需要底層作業系統的支援,在使用的時候一定要注意對應的native libraries支援。

本文已收錄於 http://www.flydean.com/14-kqueue-epoll/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!

相關文章