網路IO-select,poll,epoll分析
背景介紹
select,poll,epoll都是IO多路複用的機制。I/O多路複用就是通過一種機制,一個程序可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程式進行相應的讀寫操作。
其實在對select,poll和epoll進行分析前,需要對linux系統產生的五種網路模式簡單介紹,但是由於我主要學習Java,對linux不臺熟悉,而且在剛開始學習網路IO時過多的糾結在同步與非同步和阻塞與非阻塞上,因此在日後的學習中不再區分這些概念,對於linux系統的5種網路模式也一樣不再深究,只是區分不同網路模式在Java上的表現是否相同.現在學習select,poll和epoll也是因為了解到Netty解決了epoll的一個bug,順便深入學習一下.
select
函式介紹
函式名
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)
引數及返回值介紹
函式引數介紹如下:
- 第一個引數maxfdp1指定待測試的檔案描述符個數,它的值是待測試的最大檔案描述符加1(因此把該引數命名為maxfdp1),檔案描述符0、1、2…maxfdp1-1均將被測試,因為檔案描述符是從0開始的.
- 中間的三個引數readset、writeset和exceptset指定我們要讓核心測試讀、寫和異常條件的描述字。如果對某一個的條件不感興趣,就可以把它設為空指標。fd_set是檔案描述符集合
- timeout是等待時間,告知核心等待所指定描述字中的任何一個就緒可花多少時間。其timeval結構用於指定這段時間的秒數和微秒數。
實現原理
睡眠等待邏輯
- 將等待讀事件的fd_set從使用者空間拷貝到核心空間
- 遍歷fd_set,若某個檔案描述符有讀事件,直接返回;若檔案描述符沒有讀事件,則為當前process構建一個wait_entry節點,然後插入到被監控socket的sleep_list中
- 遍歷fd_set結束後仍未返回,當前process睡眠
喚醒邏輯
- 若fd_set中有某個socket有資料可讀,則會喚醒該socket的sleep_list中正在睡眠的process
- process被喚醒後,再次遍歷fd_set,此時在fd_set必定有至少一個fd有讀事件
缺點
- 每次呼叫
select()
必須將三個fd_set從使用者空間拷貝到核心空間 - 為了減少資料拷貝帶來的效能損壞,核心對被監控的fd_set集合大小做了限制,並且這個是通過巨集控制的,大小不可改變(限制為1024)
- process被喚醒後,必須要再次遍歷fd_set,才能確定是哪個socket上有資料可讀
poll-雞肋
函式介紹
int poll ( struct pollfd * fds, unsigned int nfds, int timeout);
pollfd封裝了檔案描述符
和等待的事件
,相當於使用pollfd代替fd_set
優缺點
- 解決了
select()
的1024問題 - 其餘兩個關乎效能的問題沒有解決
epoll
解決問題思路
在計算機行業中,有兩種解決問題的思想:
[1] 電腦科學領域的任何問題, 都可以通過新增一個
中間層
來解決
[2] 變集中
(中央)處理為分散
(分散式)處理
我們就按照上述的兩個思想嘗試解決select剩下的兩個問題
變集中為分散解決fd_set拷貝問題
每次select都要將fd_set從使用者空間拷貝到核心空間中,但是fd_set的改變次數相較於select的執行次數是非常少的,因此我們可以將select()
中的複製fd_set和遍歷fd_set兩個邏輯分開,每個邏輯由一個函式完成.(其實就是將fd_set的複製從集中複製
變為分散增加
)
epoll引入了int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
用於新增和刪除fd集合,具體的遍歷及等待邏輯是在int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
中完成
新增中間層解決process被喚醒後遍歷fd_set的問題
當process從睡眠中被喚醒時,雖然此時fd_set中至少會有一個fd有讀事件,但仍然需要再次遍歷fd_set,當fd_set很大時,遍歷的大多操作是無用的,因此如果我們能將就緒的fd放在單獨的列表中,就可以避免無效遍歷.
如何才能將就緒的fd放在單獨的ready_list中?
在select()
中是將process封裝為wait_entry
放在socket的sleep_list中,如果我們將一個callback函式封裝為wait_entry
,此函式的邏輯是將當前socket放置在ready_list
中.那麼當socket有讀事件時,便會執行該函式,將socket放入ready_list
中
process的睡眠與喚醒問題?
有了ready_list後,process的睡眠和喚醒時機便很明顯.當ready_list為空時睡眠,ready_list非空時喚醒
如和將ready_list串聯在一起?
個人感覺是通過int epoll_create(int size);
返回的fd將一切串聯在一起的.即process是睡眠在epoll_create()
返回的fd上,根據ready_list的空與非空將process睡眠與喚醒.
函式介紹
int epoll_create(int size);
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);
int epoll_create(int size);
建立epoll的fd作為中間層並返回
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
將被監聽的socket拷貝至核心空間,並且將callback封裝為wait_entry掛在socket的sleep_list上.
新增兩個被監聽的socket後
再新增2個被監聽的socket後
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
若呼叫epoll_wait()
時無就緒的fd,則當前process會睡眠在中間層fd的sleep_list中
若一段時間後,sk1和sk2都有讀事件到達,則分別會執行sk1和sk2的callback,將sk1和sk2放入中間層fd的read_list中,同時喚醒睡眠在中間層fd的process
process被喚醒後,遍歷read_list,此時read_list中全是有讀事件的sk,不會產生無用遍歷
總結
- 此篇部落格只是介紹了下epoll解決select存在問題的大致思路,具體情況還請查詢相關資料
- 無epoll的ET和LT模式介紹.