I/O多路復(fù)用(I/O Multiplexing)

我們希望在一個或多個I/O條件就緒(即輸入已準(zhǔn)備好被讀取,或者描述符能夠接收更多輸出)時得到通知。此功能稱為I/O復(fù)用,由select和poll等函數(shù)支持。

通常情況下,I/O多路復(fù)用通常用于網(wǎng)絡(luò)應(yīng)用程序:

  • 當(dāng)客戶端處理多個描述符時(通常是交互式輸入和網(wǎng)絡(luò)套接字)
  • 當(dāng)客戶端同時處理多個套接字時(這是可能的,但很少見)
  • 如果TCP服務(wù)器同時處理偵聽套接字及其連接的套接字
  • 如果服務(wù)器同時處理TCP和UDP
  • 如果服務(wù)器處理多種服務(wù),并且可能處理多種協(xié)議

但是I/O復(fù)用不限于網(wǎng)絡(luò)編程。

Unix/Linux系統(tǒng)中的所有內(nèi)容都是文件。每個進程都有一個文件描述符,該描述符指向文件,套接字,設(shè)備和其他操作系統(tǒng)對象。

我們告訴內(nèi)核我們對哪些描述符感興趣(用于讀取,寫入或異常情況)以及等待多長時間。我們感興趣的描述符不限于套接字,還可以是任何的描述符。

I/O模型

Unix/Linux 下可供我們使用的五個I/O模型

  1. blocking I/O 【阻塞I/O】
  2. nonblocking I/O 【非阻塞I/O】
  3. I/O multiplexing (select and poll) 【多路復(fù)用I/O】
  4. signal driven I/O (SIGIO) 【信號驅(qū)動】
  5. asynchronous I/O (the POSIX aio_ functions) 【異步I/O,由POSIX規(guī)范定義的】

輸入操作通常有兩個不同的階段:

  1. 等待數(shù)據(jù)準(zhǔn)備就緒。這涉及等待數(shù)據(jù)到達網(wǎng)絡(luò)。數(shù)據(jù)包到達時,它將被復(fù)制到內(nèi)核中的緩沖區(qū)中。
  2. 將數(shù)據(jù)從內(nèi)核復(fù)制到進程。這意味著將(就緒)數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到我們的應(yīng)用程序緩沖區(qū)中

同步IO&異步IO

根據(jù) POSIX 定義:

  • 同步I/O操作會阻塞請求進程,直到I/O操作完成
  • 異步I/O操作不會導(dǎo)致阻塞請求進程

按照這種分類,上邊5種I/O模型中,只有AIO一種是異步的,其他都是同步的。
因為其中真正的IO操作(recvfrom 調(diào)用) 會阻塞進程,recvfrom 會阻塞等待內(nèi)核將數(shù)據(jù)從內(nèi)核空間復(fù)制到應(yīng)用進程空間, 當(dāng)賦值完成后, recvfrom 才返回。

但是從我們編程的角度來看,「I/O多路復(fù)用」和「信號驅(qū)動I/O」都不會導(dǎo)致我們的進程完全被阻塞,因為在多線程下,阻塞一個線程并不會導(dǎo)致整個進程被阻塞。

I/O多路復(fù)用實現(xiàn)方式

I/O多路復(fù)用(I/O multiplexing) 通過系統(tǒng)調(diào)用 select,poll,epoll 支持。

select/epoll的好處就在于單個進程就可以同時處理多個網(wǎng)絡(luò)連接的IO。它的基本原理就是select,poll,epoll這些個函數(shù)會不斷的輪詢所負(fù)責(zé)的所有socket,當(dāng)某個socket有數(shù)據(jù)到達了,就通知用戶進程。

本質(zhì)是通過系統(tǒng)函數(shù)select、poll、epool(模塊)來監(jiān)聽多個文件描述符(套接字描述符)。
無論epoll還是select都受限于系統(tǒng)中單個進程能夠打開的文件句柄數(shù)。

select/poll的優(yōu)勢并不是對于單個連接能處理得更快,而是在于能處理更多的連接。

一、select

select是個系統(tǒng)調(diào)用,提供了一種用于實現(xiàn)同步多路復(fù)用I/O的機制

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

對select()的調(diào)用將一直阻塞,直到給定的文件描述符準(zhǔn)備執(zhí)行I/O為止,或者直到經(jīng)過可選的指定超時為止。

監(jiān)視的文件描述符類型分為三種:

  • 監(jiān)視readfds集中列出的文件描述符,以查看是否有數(shù)據(jù)可讀取。
  • 監(jiān)視writefds集中列出的文件描述符,以查看寫操作是否將完成而不會阻塞。
  • 監(jiān)視exceptionfds集中的文件描述符,以查看是否發(fā)生了異?;驇鈹?shù)據(jù)是否可用(這些狀態(tài)僅適用于套接字)。

執(zhí)行select()成功返回后,將修改每個集合,以使其僅包含準(zhǔn)備好由該集合描述的I/O類型的文件描述符。

優(yōu)缺點

優(yōu)點:

select的主要優(yōu)點是它具有很高的可移植性-像OS一樣的每個UNIX都具有它

缺點:
  • select()使用fd_set來表示文件描述符的集合,而fd_set其實就是一個固定長度的位向量(bit vector),在Linux上,這個固定長度是FD_SETSIZE,其數(shù)值是1024。
    故select()監(jiān)聽的文件描述符總數(shù)必須小于1024。
  • 我們需要遍歷文件描述符以檢查它是否存在于select返回的集合中
注意事項:

每次select之前要重置每個入?yún)⒓系闹担ǚ祷貢r會被修改)。

二、poll

poll提供與相似的功能select。
與select()不同,因為select()具有效率低下的三個基于位掩碼的文件描述符集,poll()使用nfds pollfd結(jié)構(gòu)的單個數(shù)組。原型更簡單:

int poll (struct pollfd *fds, unsigned int nfds, int timeout);

pollfd結(jié)構(gòu)的事件和返回事件具有不同的字段,因此我們不必每次都構(gòu)建它

struct pollfd {
      int fd;
      short events; 
      short revents;
};

對于每個文件描述符,構(gòu)建一個類型為pollfd的對象,并填充所需的事件。poll返回后,檢查revents字段即可

就像我們對select所做的那樣,我們需要檢查每個pollfd對象以查看其文件描述符是否已準(zhǔn)備就緒,但是我們不需要在每次迭代時都構(gòu)建集合

優(yōu)缺點

優(yōu)點:

poll()系統(tǒng)調(diào)用將輸入(events字段)與輸出(revents字段)分開,從而允許入?yún)⒈恢貜?fù)使用而無需更改。

缺點:

由于某些Unix系統(tǒng)不支持poll(),因此select()具有更高的可移植性。

select 和 poll 的性能問題

使用select()或poll()監(jiān)聽大量的文件描述符時,往往會遭遇到性能問題。當(dāng)用戶每次調(diào)用select()或poll()時,內(nèi)核會對傳入的所有文件描述符都檢查一遍,并記錄其中有哪些文件描述符存在I/O就緒,這個操作的耗時將隨著文件描述符數(shù)量的增加而線性增長。

另一個重要因素也會影響select()和poll()的性能,例如用戶每次調(diào)用poll()時,都需要傳遞一個pollfd數(shù)組,而poll()會將這個數(shù)組從用戶空間拷貝到內(nèi)核空間,當(dāng)內(nèi)核對這個數(shù)組作了修改之后,poll()又會將這個數(shù)組從內(nèi)核空間拷貝到用戶空間。隨著pollfd數(shù)組長度的增加,每次拷貝的時間也會線性增長,一旦poll()需要監(jiān)聽大量的文件描述符,每次調(diào)用poll()時,這個拷貝操作將帶來不小的開銷。這個問題的根源在于select()和poll()的API設(shè)計不當(dāng),例如,對于應(yīng)用程序來說,它每次調(diào)用poll()所監(jiān)聽的文件描述符集合應(yīng)該是差不多的,所以我們不禁這樣想,如果內(nèi)核愿意提供一個數(shù)據(jù)結(jié)構(gòu),記錄程序所要監(jiān)聽的文件描述符集合,這樣每次用戶調(diào)用poll()時,poll()就不需要將pollfd數(shù)組拷貝來拷貝去了(沒錯,epoll 就是這樣解決的)。

三、epoll

epoll是為了解決select()和poll()中存在的問題

epoll是個模塊,由三個系統(tǒng)調(diào)用(epoll_create epoll_ctl epoll_wait)組成,Epoll 系統(tǒng)調(diào)用可幫助我們在內(nèi)核中創(chuàng)建和管理上下文。epoll使用紅黑樹(RB-tree)數(shù)據(jù)結(jié)構(gòu)來跟蹤當(dāng)前正在監(jiān)視的所有文件描述符。

我們將任務(wù)分為3個步驟:

  • 使用epoll_create在內(nèi)核中創(chuàng)建上下文
  • 使用epoll_ctl向/從上下文中添加和刪除文件描述符
  • 使用epoll_wait等待上下文中的事件

epoll_ctl()負(fù)責(zé)增加、刪除或修改紅黑樹上的節(jié)點,而epoll_wait()則負(fù)責(zé)返回雙向鏈表中就緒的文件描述符(及其事件)。

當(dāng)網(wǎng)卡收到一個 packet 的時候,會觸發(fā)一個硬件中斷,這導(dǎo)致內(nèi)核調(diào)用相應(yīng)的中斷 handler,從網(wǎng)卡中讀入數(shù)據(jù)放到協(xié)議棧,當(dāng)數(shù)據(jù)量滿足一定條件時,內(nèi)核將回調(diào)ep_poll_callback()這個方法,它負(fù)責(zé)把這個就緒的文件描述符添加到雙向鏈表中。這樣當(dāng)用戶調(diào)用epoll_wait()時,epoll_wait()所做的就只是檢查雙向鏈表是否為空,如果不為空,就把文件描述符和數(shù)量返回給用戶即可。

觸發(fā)模式

epoll提供邊沿觸發(fā)(Edge-Triggered)及狀態(tài)觸發(fā)(Level-Triggered)模式。

Level-Triggered 也翻譯成水平觸發(fā)、條件觸發(fā)

邊沿觸發(fā):
監(jiān)控對象的狀態(tài)發(fā)生改變時觸發(fā),此后如果狀態(tài)一直沒有發(fā)生變化應(yīng)用程序?qū)⒉辉偈盏酵ㄖ?/p>

狀態(tài)觸發(fā):
處于某種狀態(tài)下(如緩沖區(qū)可以讀)就一直觸發(fā)

如:
socket接收到緩存數(shù)據(jù)時,調(diào)用epoll_wait,上面兩種方法都將返回,表明存在要讀取的數(shù)據(jù)。
假設(shè)讀取器僅消耗了緩沖區(qū)中的部分?jǐn)?shù)據(jù)。
在狀態(tài)觸發(fā)模式下,epoll_wait只要管道的緩沖區(qū)包含要讀取的數(shù)據(jù),對epoll_wait的調(diào)用將立即返回;
但是,在邊沿觸發(fā)模式下,epoll_wait僅在將新數(shù)據(jù)寫入緩存區(qū)后才返回。

一般編程邏輯

  1. 【epoll_create】在內(nèi)核中創(chuàng)建epoll實例并返回一個epoll文件描述符(epfd)。
int epoll_create1(int flags);
  1. 【epoll_ctl】向epfd對應(yīng)的內(nèi)核epoll實例添加、修改或刪除對fd(File descriptor)上事件event的監(jiān)聽。類型可以為EPOLL_CTL_ADD, EPOLL_CTL_MOD, EPOLL_CTL_DEL分別對應(yīng)的是添加新的事件,修改文件描述符上監(jiān)聽的事件類型。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  1. 【epoll_wait】循環(huán)執(zhí)行epoll_wait,當(dāng)timeout 為0 時,epoll_wait 永遠(yuǎn)會立即返回。而timeout 為-1 時,epoll_wait 會一直阻塞直到任一已注冊的事件變?yōu)榫途w。當(dāng)timeout 為一正整數(shù)時,epoll 會阻塞直到計時timeout 毫秒終了或已注冊的事件變?yōu)榫途w。因為內(nèi)核調(diào)度延遲,阻塞的時間可能會略微超過timeout 毫秒。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

優(yōu)缺點

優(yōu)點:
  • 我們可以在等待時添加和刪除文件描述符
  • epoll_wait僅返回具有就緒文件描述符的對象
  • epoll具有更好的性能-(Epoll時間復(fù)雜度為O(1) 代替之前的O(n))
  • epoll可以表現(xiàn)為狀態(tài)觸發(fā)或邊緣觸發(fā)
缺點:

epoll是特定于Linux的,因此不可移植

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容