Linux 下的五種 I/O 模型

總的來說,阻塞 IO 就是 JDK 里的 BIO 編程,IO 復(fù)用就是 JDK 里的 NIO 編程,Linux 下異
步 IO 的實(shí)現(xiàn)建立在 epoll 之上,是個(gè)偽異步實(shí)現(xiàn),而且相比 IO 復(fù)用,沒有體現(xiàn)出性能優(yōu)勢,
使用不廣。非阻塞 IO 使用輪詢模式,會(huì)不斷檢測是否有數(shù)據(jù)到達(dá),大量的占用 CPU 的時(shí)間,
是絕不被推薦的模型。信號(hào)驅(qū)動(dòng) IO 需要在網(wǎng)絡(luò)通信時(shí)額外安裝信號(hào)處理函數(shù),使用也不廣
泛。

阻塞 IO 模型

I/O 復(fù)用模型
比較上面兩張圖,IO 復(fù)用需要使用兩個(gè)系統(tǒng)調(diào)用(select 和 recvfrom),而 blocking IO 只
調(diào)用了一個(gè)系統(tǒng)調(diào)用(recvfrom)。但是,用 select 的優(yōu)勢在于它可以同時(shí)處理多個(gè) connection。
所以,如果處理的連接數(shù)不是很高的話,使用 select/epoll 的 web server 不一定比使用
multi-threading + blocking IO 的 web server 性能更好,可能延遲還更大。select/epoll 的優(yōu)勢
并不是對(duì)于單個(gè)連接能處理得更快,而是在于能處理更多的連接。
Linux 下的 IO 復(fù)用編程
select,poll,epoll 都是 IO 多路復(fù)用的機(jī)制。I/O 多路復(fù)用就是通過一種機(jī)制,一個(gè)進(jìn)
程可以監(jiān)視多個(gè)描述符,一旦某個(gè)描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序
進(jìn)行相應(yīng)的讀寫操作。但 select,poll,epoll 本質(zhì)上都是同步 I/O,因?yàn)樗麄兌夹枰谧x寫事
件就緒后自己負(fù)責(zé)進(jìn)行讀寫,并等待讀寫完成。
select int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval
*timeout);
select 函數(shù)監(jiān)視的文件描述符分 3 類,分別是 writefds、readfds、和 exceptfds。調(diào)用后
select 函數(shù)會(huì)阻塞,直到有描述副就緒(有數(shù)據(jù) 可讀、可寫、或者有 except),或者超時(shí)
(timeout 指定等待時(shí)間,如果立即返回設(shè)為 null 即可),函數(shù)返回。當(dāng) select 函數(shù)返回后,
可以 通過遍歷 fdset,來找到就緒的描述符。
select 目前幾乎在所有的平臺(tái)上支持,其良好跨平臺(tái)支持也是它的一個(gè)優(yōu)點(diǎn)。select 的
一 個(gè)缺點(diǎn)在于單個(gè)進(jìn)程能夠監(jiān)視的文件描述符的數(shù)量存在最大限制,在Linux上一般為1024,
可以通過修改宏定義甚至重新編譯內(nèi)核的方式提升這一限制,但是這樣也會(huì)造成效率的降低。
poll
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
不同與 select 使用三個(gè)位圖來表示三個(gè) fdset 的方式,poll 使用一個(gè) pollfd 的指針實(shí)現(xiàn)。
pollfd 結(jié)構(gòu)包含了要監(jiān)視的 event 和發(fā)生的 event,不再使用 select“參數(shù)-值”傳遞的方
式。同時(shí),pollfd 并沒有最大數(shù)量限制(但是數(shù)量過大后性能也是會(huì)下降)。 和 select 函數(shù)
一樣,poll 返回后,需要輪詢 pollfd 來獲取就緒的描述符。
epoll
epoll 是在 2.6 內(nèi)核中提出的,是之前的 select 和 poll 的增強(qiáng)版本。相對(duì)于 select 和 poll
來說,可以看到 epoll 做了更細(xì)致的分解,包含了三個(gè)方法,使用上更加靈活。
int epoll_create(int size);//創(chuàng)建
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//注冊(cè)事件
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);//等待感興趣的事件跟nio的選擇器功能相似
select、poll、epoll 的比較
select,poll,epoll 都是 操作系統(tǒng)實(shí)現(xiàn) IO 多路復(fù)用的機(jī)制。 我們知道,I/O 多路復(fù)用
就通過一種機(jī)制,可以監(jiān)視多個(gè)描述符,一旦某個(gè)描述符就緒(一般是讀就緒或者寫就緒),
能夠通知程序進(jìn)行相應(yīng)的讀寫操作。那么這三種機(jī)制有什么區(qū)別呢
1、支持一個(gè)進(jìn)程所能打開的最大連接數(shù)
select 底層基于數(shù)組(連接數(shù)有限),默認(rèn)1024個(gè)
單個(gè)進(jìn)程所能打開的最大連接數(shù)有 FD_SETSIZE 宏定義,其大小是 32
個(gè)整數(shù)的大?。ㄔ?32 位的機(jī)器上,大小就是 32*32,同理 64 位機(jī)器上
FD_SETSIZE 為 32*64),當(dāng)然我們可以對(duì)進(jìn)行修改,然后重新編譯內(nèi)核,
但是性能可能會(huì)受到影響
poll 底層基于鏈表
poll 本質(zhì)上和 select 沒有區(qū)別,但是它沒有最大連接數(shù)的限制,原因
是它是基于鏈表來存儲(chǔ)的
epoll
雖然連接數(shù)基本上只受限于機(jī)器的內(nèi)存大小
2、FD 劇增后帶來的 IO 效率問題
select
因?yàn)槊看握{(diào)用時(shí)都會(huì)對(duì)連接進(jìn)行線性遍歷,所以隨著 FD 的增加會(huì)造
成遍歷速度慢的“線性下降性能問題”。
poll 同select一樣
epoll 回調(diào)函數(shù)實(shí)現(xiàn)
因?yàn)?epoll 內(nèi)核中實(shí)現(xiàn)是根據(jù)每個(gè) fd 上的 callback 函數(shù)來實(shí)現(xiàn)的,只
有活躍的 socket 才會(huì)主動(dòng)調(diào)用 callback,所以在活躍 socket 較少的情況下,
使用 epoll 沒有前面兩者的線性下降的性能問題,但是所有 socket 都很活
躍的情況下,可能會(huì)有性能問題
3、 消息傳遞方式
select/poll 內(nèi)核需要將消息傳遞到用戶空間,都需要內(nèi)核拷貝動(dòng)作
epoll
epoll 通過內(nèi)核和用戶空間共享一塊內(nèi)存來實(shí)現(xiàn)的。
==============================================
從網(wǎng)卡接收數(shù)據(jù)說起
網(wǎng)卡收到網(wǎng)線傳來的數(shù)據(jù);經(jīng)過硬件電路的傳輸;最終將數(shù)據(jù)寫入到內(nèi)存中的某個(gè)地址
上。這個(gè)過程涉及到 DMA 傳輸、IO 通路選擇等硬件有關(guān)的知識(shí),但我們只需知道:網(wǎng)卡會(huì)
把接收到的數(shù)據(jù)寫入內(nèi)存。操作系統(tǒng)就可以去讀取它們。
如何知道接收了數(shù)據(jù)?
CPU 和操作系統(tǒng)如何知道網(wǎng)絡(luò)上有數(shù)據(jù)要接收?使用中斷機(jī)制。
進(jìn)程阻塞
cpu調(diào)度工作隊(duì)列上的進(jìn)程,當(dāng)cpu時(shí)間片切換后將進(jìn)程的引用指向socket的等待隊(duì)列列表中

同時(shí)監(jiān)視多個(gè) socket 的簡單方法

epoll 的原理和流程
當(dāng)某個(gè)進(jìn)程調(diào)用 epoll_create 方法時(shí),內(nèi)核會(huì)創(chuàng)建一個(gè) eventpoll 對(duì)象(也就是程序中
epfd 所代表的對(duì)象)。eventpoll 對(duì)象也是文件系統(tǒng)中的一員,和 socket 一樣,它也會(huì)有等
待隊(duì)列。
創(chuàng)建 epoll 對(duì)象后,可以用 epoll_ctl 添加或刪除所要監(jiān)聽的 socket。以添加 socket 為例,
如下圖,如果通過 epoll_ctl 添加 sock1、sock2 和 sock3 的監(jiān)視,內(nèi)核會(huì)將 eventpoll 添加到
這三個(gè) socket 的等待隊(duì)列中。

當(dāng) socket 收到數(shù)據(jù)后,中斷程序會(huì)操作 eventpoll 對(duì)象,而不是直接操作進(jìn)程。中斷程
序會(huì)給 eventpoll 的“就緒列表”添加 socket 引用。如下圖展示的是 sock2 和 sock3 收到數(shù)
據(jù)后,中斷程序讓 rdlist 引用這兩個(gè) socket。

eventpoll 對(duì)象相當(dāng)于是 socket 和進(jìn)程之間的中介,socket 的數(shù)據(jù)接收并不直接影響進(jìn)
程,而是通過改變 eventpoll 的就緒列表來改變進(jìn)程狀態(tài)。
當(dāng)程序執(zhí)行到 epoll_wait 時(shí),如果 rdlist 已經(jīng)引用了 socket,那么 epoll_wait 直接返回,
如果 rdlist 為空,阻塞進(jìn)程。
假設(shè)計(jì)算機(jī)中正在運(yùn)行進(jìn)程 A 和進(jìn)程 B,在某時(shí)刻進(jìn)程 A 運(yùn)行到了 epoll_wait 語句。如
下圖所示,內(nèi)核會(huì)將進(jìn)程 A 放入 eventpoll 的等待隊(duì)列中,阻塞進(jìn)程。

當(dāng) socket 接收到數(shù)據(jù),中斷程序一方面修改 rdlist,另一方面喚醒 eventpoll 等待隊(duì)列中
的進(jìn)程,進(jìn)程 A 再次進(jìn)入運(yùn)行狀態(tài)。也因?yàn)?rdlist 的存在,進(jìn)程 A 可以知道哪些 socket 發(fā)生
了變化。
數(shù)據(jù)結(jié)構(gòu)
epoll 使用雙向鏈表來實(shí)現(xiàn)就緒隊(duì)列(rdlist等待隊(duì)列)
紅黑樹存儲(chǔ) epoll_ctl 傳入的soket
總結(jié)
當(dāng)某一進(jìn)程調(diào)用 epoll_create 方法時(shí),Linux 內(nèi)核會(huì)創(chuàng)建一個(gè) eventpoll 結(jié)構(gòu)體,在內(nèi)核
cache 里建了個(gè)紅黑樹用于存儲(chǔ)以后 epoll_ctl 傳來的 socket 外,還會(huì)再建立一個(gè) rdllist 雙向
鏈表,用于存儲(chǔ)準(zhǔn)備就緒的事件,當(dāng) epoll_wait 調(diào)用時(shí),僅僅觀察這個(gè) rdllist 雙向鏈表里有
沒有數(shù)據(jù)即可。有數(shù)據(jù)就返回,沒有數(shù)據(jù)就 sleep,等到 timeout 時(shí)間到后即使鏈表沒數(shù)據(jù)
也返回。
同時(shí),所有添加到 epoll 中的事件都會(huì)與設(shè)備(如網(wǎng)卡)驅(qū)動(dòng)程序建立回調(diào)關(guān)系,也就是
說相應(yīng)事件的發(fā)生時(shí)會(huì)調(diào)用這里的回調(diào)方法。這個(gè)回調(diào)方法在內(nèi)核中叫做 ep_poll_callback,
它會(huì)把這樣的事件放到上面的 rdllist 雙向鏈表中。
當(dāng)調(diào)用 epoll_wait 檢查是否有發(fā)生事件的連接時(shí),只是檢查 eventpoll 對(duì)象中的 rdllist
雙向鏈表是否有 epitem 元素而已,如果 rdllist 鏈表不為空,則這里的事件復(fù)制到用戶態(tài)內(nèi)
存(使用共享內(nèi)存提高效率)中,同時(shí)將事件數(shù)量返回給用戶。因此 epoll_waitx 效率非常
高,可以輕易地處理百萬級(jí)別的并發(fā)連接。