高級 IO

目錄

IO 簡介

五種 IO 模型

阻塞 IO

非阻塞 IO

信號驅動 IO

IO 多路轉接

異步 IO

IO 多路轉接 — select

詳解 select 系統調用

函數參數

函數返回值 

fd_set 結構

timeval 結構

select 執行過程

socket 就緒條件

讀就緒

寫就緒

異常就緒

select 的特點

select 的缺點

使用 select 的網絡服務器

IO 多路轉接 — poll

poll 函數接口

函數參數

函數返回值

pollfd 結構

poll 的優點

poll 的缺點

使用 poll 的網絡服務器

IO 多路轉接 — epoll

epoll 相關的三個系統調用:

創建 epoll 模型函數

epoll 的事件註冊函數

函數參數 

epoll_event 結構

收集 epoll 監控的已經就緒的事件的函數

函數參數

函數返回值

epoll 工作原理 

epoll 的優點

epoll 的工作方式

epoll 的使用場景

epoll 中的驚羣問題

epoll 服務器 


IO 簡介

所有的 I/O 過程可以理解爲等待和拷貝兩個階段,等待可以進行數據讀寫的條件,當等待的描述符就緒時進行數據的拷貝。一個低效的 I/O 大部分時間在等,在等待就緒條件和拷貝數據兩個階段等的比重大,高效的 I/O 大部分時間在拷貝數據,拷貝的比重相對更大。

五種 IO 模型

阻塞 IO

阻塞 I/O 就是在內核將數據準備好之前,系統調用會一直等待,直到條件就緒完成數據拷貝後再返回。所有的套接字,默認都是阻塞的。

非阻塞 IO

非阻塞 I/O 就是如果內核未將數據準備好時系統調用會直接返回,並且返回 EWOULDBLOCK 錯誤碼。

非阻塞 I/O 往往需要程序員以循環的方式反覆嘗試去讀寫,這個過程稱爲輪詢。這對於 CPU 來說是較大的浪費,一般只有特定場景下才使用。

信號驅動 IO

內核將數據準備好的時候,使用 SIGIO 信號通知應用程序進行 I/O 操作。

實現信號驅動 I/O 模型需要建立 SIGIO 的信號處理程序。用到的系統調用是:

#include <signal.h>

int sigaction(int signum, const struct sigaction *act,
              struct sigaction *oldact);

IO 多路轉接

I/O 多路轉接和阻塞 I/O 本質類似,都是先等待就緒條件再拷貝數據。實際上 I/O 多路轉接的核心在於能夠同時等待多個文件描述符的就緒狀態。

它是將串行化的等待並行化,某一時間段等待多個文件描述符的就緒狀態,等待的時間是重疊的,從而提高了整個 I/O 過程的效率。

我們後面要詳細說的 select、poll、epoll 都是在幫我們監控一定數量的文件描述符是否具備 I/O 條件,雖然他們的實現方式不一樣。I/O 多路轉接是高級 I/O 這塊最核心的東西啦沒有之一,我在參加阿里巴巴面試的時候面試官在這塊問的特別詳細。

異步 IO

前面的四種 I/O 模型各有區別,BUT! 最終完成數據拷貝的還是發起 I/O 操作者本身,這就是同步 I/O ,這個知識點同樣被問到過,需要我們對這點有清晰的認知。

異步 I/O 是別人幫你你至到完成數據拷貝時才通知你。

IO 多路轉接 — select

詳解 select 系統調用

select 系統調用是用來讓我們的程序監視多個文件描述符狀態變化的,程序會停在 select 這裏等待,直到被監視的文件描述符一個或多個發生了狀態改變。

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

函數參數

  1. nfds :是一個數字,這個數字的值是要監視的最大的文件描述符值再加 1

  2. readfds :對應於需要監視的可讀文件描述的集合

  3. writefds :對應於需要監視的可寫文件描述符的集合

  4. exceptfds :對應於異常文件描述符的集合

  5. timeout :是一個結構體,用來設置 select 的等待時間

函數返回值 

執行成功則返回監聽的那些文件描述符中已經就緒的文件描述符個數。

如果返回 0 代表在文件描述符狀態改變之前已經超過設置的 timeout 時間。

當有錯誤發生時則返回 -1,錯誤原因存於 errno,此時參數的值得變化是不可預測的。當文件描述符是無效的、該文件描述符已關閉、此調用被信號中斷、參數 nfds 爲負數、核心內存不足時可能會返回 -1。

fd_set 結構

/* The fd_set member is required to be an array of longs.  */

typedef long int __fd_mask;
   

/* fd_set for select and pselect.  */
typedef struct
{
    /* XPG4.2 requires this member name.  Otherwise avoid the name
       from the global namespace.  */
#ifdef __USE_XOPEN
    __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];// __FD_SETSIZE=1024 __NFDBITS=32
# define __FDS_BITS(set) ((set)->fds_bits)
#else
    __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;

這個結構就是一個數組,更嚴格的說,是一個“位圖”,使用位圖中的位來表示要監視的文件描述符。

可監控的文件描述符的個數取決於 sizeof(fd_set) 的值,我的 Centos7 上 sizeof(fd_set) = 128,每個比特位表示一個文件描述符,所以我的機器上支持的最大文件描述符時 128 * 8 = 1024 個。

這裏提供了一組操作 fd_set 的接口,來供我們使用,可很方便的操作位圖:

void FD_CLR(int fd, fd_set *set);  //用來清除描述符詞組 set 中相關 fd 的位
int  FD_ISSET(int fd, fd_set *set);  //用來測試描述符詞組 set 中 fd 的位是否爲真
void FD_SET(int fd, fd_set *set);  //用來設置描述符詞組 set 中 fd 的位
void FD_ZERO(fd_set *set);  //用來清除描述符詞組 set 的全部位

fd_set 的大小可以調整,可能會涉及到重新編譯內核。

timeval 結構

 用於描述一段時間長度,如果在這個時間內,需要監視的描述符沒有狀態改變,則函數直接返回 0.

select 執行過程

理解 select 模型的關鍵在於理解 fd_set 這個結構,而這個結構是一個數組,在 select 模型裏當作位圖來用。將需要監控的文件描述符添加到這個位圖中,內核通過輪詢的方式監控有無文件描述符就緒,當至少有一個文件描述符就緒時,內核將位圖中沒有就緒的文件描述符對應的位清楚掉,select 調用返回時,我們程序員用 array 數組作爲源數據用 FD_ISSET 接口一一篩選就緒的文件描述符,針對就緒的文件描述符進行數據拷貝。再次調用 select 時將需要監控的文件描述符重新一一添加到 fd_set 集合中。

socket 就緒條件

讀就緒

  • socket 內核中,接收緩衝區中的字節數大於等於低水平標記 SO_RCVLOWAT,此時可以無阻塞的讀該文件描述符,並且返回值大於 0
  • socket TCP 通信中,對端關閉連接,此時對該 socket 讀,返回 0
  • 監聽的 socket 上有新的連接請求
  • socket 上有未處理的錯誤

寫就緒

  • socket 內核中,發送緩衝區中的可用字節數(發送緩衝區的空閒位置大小)大於等於低水位標記 SO_SNDLOWAT,此時可以無阻塞的寫,並且返回值大於 0
  • socket 的寫操作被關閉(close 或 shutdown),對一個寫操作被關閉的 socket 進行寫操作,會觸發 SIGPIPE 信號
  • socket 使用非阻塞 connect 連接成功或失敗之後
  • socket 上有未讀取的錯誤

異常就緒

(先放這兒),以後再說

select 的特點

將文件描述符加入 select 監控集合的同時,再使用一個數組 array 保存放到 select 監控集中的 fd。一是用於在 select 返回後,array 作爲源數據使用 FD_ISSET 接口來一一篩選就緒的文件描述符;二是 select 返回後會把之前加入到監控集合的但無就緒的文件描述符清空,所以再次開始 select 之前需要從 array 取得文件描述符加入到監控集合中,掃描 array 的過程中找出值最大的文件描述符,用於設置 select 的第一個參數時用(maxfd+1)。

select 的缺點

  1. 監控的性能隨着文件描述符的增多而降低
  2. 監控的描述符是向 fd_set 這個結構中添加,fd_set 這個結構的大小決定了能同時監控的文件描述符的最大數量,即 select 能同時監控的描述符是有上限的。在我機器上是 1024 個,這個數量太小了
  3. 每次調用 select,都需要手動設置 fd 集合,從接口使用角度來說也非常不便
  4. 每次調用 select 都需要把 fd 集合從用戶態拷貝到內核態,這也是一部分開銷
  5. 調用 select 需要在內核通過輪詢的方式判斷有無文件描述符就緒,在 fd 很大時開銷也是比較大的

使用 select 的網絡服務器

IO 多路轉接 — poll

poll 函數接口

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

函數參數

  1. fds :是一個 poll 函數監聽的結構列表(數據結構:數組),其中的每一個元素,包含了三個部分內容:文件描述符、監聽事件集合、返回的事件集合
  2. nfds :fds 數組的大小
  3. timeout :表示 poll 函數的超時時間,單位是毫秒(ms)

函數返回值

  • 返回值小於 0,表示出錯
  • 返回值等於 0,表示 poll 函數等待超時
  • 返回值大於 0,表示監控的文件描述符中已經就緒的文件描述符數量

pollfd 結構

 struct pollfd 
{
    int   fd;         /* file descriptor */
    short events;     /* requested events */
    short revents;    /* returned events */
};

events 和 revents 的取值:取值全部是宏,只有其中一個是 1,其他全是 0 

這裏我簡單的列幾項:

  • POLLIN :數據(普通數據、優先數據)可讀
  • POLLOUT :數據(普通數據、優先數據)可寫
  • POLLPRI :高級優先數據可讀,比如 TCP 緊急數據
  • POLLHUP :掛起,比如管道的寫段被關閉後,讀端描述符上將受到 POLLHUP 事件

poll 的優點

不同於 select 使用三個位圖來表示三個 fdset 的方式,poll 使用一個 pollfd 的結構實現:輸入輸出參數分離

  1. poll 沒有最大數量限制(但 poll 的性能也會隨着監控文件描述符的增多而降低)
  2. pollfd 結構包含了要監視的 event 和發生的 event ,不再使用 select “參數-值”傳遞方式,接口使用比 select 更方便

poll 的缺點

poll 中監聽的文件描述符增多時:

  1. 和 select 一樣 poll 返回後需要用輪詢的方式來獲取就緒的文件描述符,當連接的大量客戶端在一時刻只有很少的處於就緒狀態,因此隨着監控的文件描述數量的增長,其效率也會下降
  2. 每次調用 poll 都需要把大量的 pollfd 結構從用戶態拷貝到內核態

使用 poll 的網絡服務器

IO 多路轉接 — epoll

epoll 幾乎具備了之前所說的一切優點,被公認爲 Linux2.6 下性能最好的 I/O 就緒通知方法。

epoll 相關的三個系統調用:

創建 epoll 模型函數

#include <sys/epoll.h>
int epoll_create(int size);
  •  返回值佔一個文件描述符
  • 自從 Linux2.6.8 之後,size 參數是被忽略的
  • 用完之後,必須調用 close 關閉

epoll 的事件註冊函數

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

函數參數 

  1. epfd :epoll 的句柄
  2. op :表示動作,用三個宏來表示
  3. fd :需要監聽的文件描述符
  4. event :告訴內核需要監聽什麼事

第二個參數的取值:

  •  EPOLL_CTL_ADD :註冊新的 fd 到 epfd 中
  •  EPOLL_CTL_MOD :修改已經註冊的 fd 的監聽事件
  • EPOLL_CTL_DEL :從 epfd 中刪除一個 fd

epoll_event 結構

typedef union epoll_data {
        void        *ptr;
        int          fd;
        uint32_t     u32;
        uint64_t     u64;
} epoll_data_t;

struct epoll_event {
        uint32_t     events;      /* Epoll events */
        epoll_data_t data;        /* User data variable */
};

events 可以是一下幾個宏的集合:

  • EPOLLIN :表示對應的文件描述符可以讀
  • EPOLLOUT :表示對應的文件描述符可以寫
  • EPOLLRDHUP :表示流式套接字對端關閉連接
  • EPOLLPRI :表示對應的文件描述符有緊急數據可讀(這裏應該表示有帶外數據到來)
  • EPOLLERR :表示對應的文件描述符發生錯誤
  • EPOLLHUP :表示對應的文件描述符被掛斷
  • EPOLLET :將 epoll 設爲邊緣觸發模式,這是相對於水平出發來說的
  • EPOLLONESHOT :只監聽一次事件,當監聽完這次事件之後,如果還需要監聽這個文件描述符的話,需要再次把這個文件描述符加入到 epoll 模型裏

收集 epoll 監控的已經就緒的事件的函數

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

函數參數

  1. epfd :epoll 的句柄
  2. events :epoll 將會把就緒的文件描述符及對應的監控事件結構賦值到 events 數組中(內核只負責把數據複製到這個 events 數組中,不會去幫我們在用戶態中分配內存)
  3. maxevents :表示 events 數組的大小
  4. timeout :表示超時時間,0 會立即返回,-1是永久阻塞

函數返回值

如果函數調用成功,返回對應 I/O 上已經準備好的文件描述符數目,如果返回0表示已超時,返回小於 0 表示函數調用失敗。

epoll 工作原理 

  • 當某一進程調用 epoll_craete 方法時,Linux 內核會創建一個 eventpoll 結構體,這個結構體當中有兩個成員與 epoll 的工作過程密切相關。
struct eventpoll{ 
     ....      
    /*紅⿊黑樹的根節點,這顆樹中存儲着所有添加到epoll中的需要監控的事件*/
    struct rb_root  rbr;      
    /*雙鏈表中則存放着將要通過epoll_wait返回給⽤用戶的滿⾜足條件的事件*/ 
    struct list_head rdlist;
    .... 
};
  • 每一個 epoll 對象都有一個獨立的 eventpoll 結構體,用於存放通過 epoll_ctl 方法向 epoll 對象中添加進來的事件
  • 這些事件都會掛載在紅黑樹中,因此,重複添加的事件可以通過紅黑樹高效的識別出來(紅黑樹的插入時間效率是 lgN)
  • 而所有添加到 epoll 中的事件都會與設備(網卡)驅動程序建立回調關係,也就是說,當有就緒事件時會調用這個回調方法
  • 這個回調方法在內核中叫 callback,它會將就緒的事件添加到 rdlist 雙鏈表中
  • 在 epoll 中,對於每一個事件,都會建立一個 epitem 結構體
struct epitem{
    struct rb_node  rbn;//紅⿊黑樹節點
    struct list_head    rdllink;//雙向鏈表節點      
    struct epoll_filefd  ffd;  //事件句柄信息      
    struct eventpoll *ep;    //指向其所屬的eventpoll對象      
    struct epoll_event event; //期待發⽣生的事件類型  
} 
  • 當調用 epoll_wait 檢查是否有事件發生時,只需要檢查 eventpoll 對象中的 rdlist 雙向鏈表中是否有 epitem 元素即可
  • 如果 rdlist 不爲空,則把就緒的事件複製到用戶態,同時將就緒事件的數目返回給用戶。這個操作的時間複雜度是 O(1)

總結一下,epoll 的使用過程就是三部曲:

  1. 調用 epoll_craete 創建一個 epoll 句柄
  2. 調用 epoll_ctl 將要監控的文件描述符進行註冊
  3. 調用 epoll_wait 等待文件描述符就緒

epoll 的優點

  1. 文件描述符數目無上限:通過 epoll_ctr 來註冊一個文件描述符,內核中使用紅黑樹的數據結構來管理所有需要監控的文件描述符。
  2. 基於事件的就緒通知方式:一旦被監聽的某個文件描述符就緒,內核會採用類似於 callback 的回調機制,迅速激活這個文件描述符,這樣隨着文件描述符數量的增加,也不會影響判定就緒的性能。
  3. 維護就緒隊列當文件描述符就緒,就會被放到內核中的一個就緒隊列中。這樣調用 epoll_wait 獲取就需文件描述符的時候,只要取到隊列中的元素即可,操作的事件複雜度是 O(1)
  4. 內存映射機制:內核直接將就緒隊列通過 mmap 的方式映射到用戶態,避免了拷貝內存這樣的額外性能開銷

epoll 的工作方式

epoll 有兩種工作方式:水平觸發(LT)和邊緣觸發(ET)

epoll 的使用場景

對於多連接,且多連接中只有一部分連接比較活躍時,比較適合用 epoll。

比如 APP 的入口服務器。

epoll 中的驚羣問題

(先放放)

epoll 服務器 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章