天天看點

進階 IO 模型之 kqueue 和 epoll

進階 IO 模型之 kqueue 和 epoll

簡介

任何一個程式都離不開 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 支援。

進階 IO 模型之 kqueue 和 epoll

繼續閱讀