一文看懂IO多路複用

本文首發在 技術成長之道 博客,訪問 hechen0.com 查看更多,或者微信搜索「技術成長之道」關注個人公衆號,或者掃描下方二維碼關注公衆號得到第一時間更新通知!html

微信

本文讓你理解

  1. 什麼是IO多路複用
  2. IO多路複用解決什麼問題
  3. 目前有哪些IO多路複用的方案
  4. 具體怎麼用
  5. 不一樣IO多路複用方案優缺點

1. 什麼是IO多路複用

一句話解釋:單線程或單進程同時監測若干個文件描述符是否能夠執行IO操做的能力。linux

2. 解決什麼問題

說在前頭

應用程序一般須要處理來自多條事件流中的事件,好比我如今用的電腦,須要同時處理鍵盤鼠標的輸入、中斷信號等等事件,再好比web服務器如nginx,須要同時處理來來自N個客戶端的事件。nginx

邏輯控制流在時間上的重疊叫作 併發git

而CPU單核在同一時刻只能作一件事情,一種解決辦法是對CPU進行時分複用(多個事件流將CPU切割成多個時間片,不一樣事件流的時間片交替進行)。在計算機系統中,咱們用線程或者進程來表示一條執行流,經過不一樣的線程或進程在操做系統內部的調度,來作到對CPU處理的時分複用。這樣多個事件流就能夠併發進行,不須要一個等待另外一個過久,在用戶看起來他們彷佛就是並行在作同樣。github

但凡事都是有成本的。線程/進程也同樣,有這麼幾個方面:web

  1. 線程/進程建立成本
  2. CPU切換不一樣線程/進程成本 Context Switch
  3. 多線程的資源競爭

有沒有一種能夠在單線程/進程中處理多個事件流的方法呢?一種答案就是IO多路複用。redis

所以IO多路複用解決的本質問題是在用更少的資源完成更多的事編程

爲了更全面的理解,先介紹下在Linux系統下全部IO模型。數組

I/O模型

目前Linux系統中提供了5種IO處理模型服務器

  1. 阻塞IO
  2. 非阻塞IO
  3. IO多路複用
  4. 信號驅動IO
  5. 異步IO

阻塞IO

這是最經常使用的簡單的IO模型。阻塞IO意味着當咱們發起一次IO操做後一直等待成功或失敗以後才返回,在這期間程序不能作其它的事情。阻塞IO操做只能對單個文件描述符進行操做,詳見readwrite

非阻塞IO

咱們在發起IO時,經過對文件描述符設置O_NONBLOCK flag來指定該文件描述符的IO操做爲非阻塞。非阻塞IO一般發生在一個for循環當中,由於每次進行IO操做時要麼IO操做成功,要麼當IO操做會阻塞時返回錯誤EWOULDBLOCK/EAGAIN,而後再根據須要進行下一次的for循環操做,這種相似輪詢的方式會浪費不少沒必要要的CPU資源,是一種糟糕的設計。和阻塞IO同樣,非阻塞IO也是經過調用read或writewrite來進行操做的,也只能對單個描述符進行操做。

IO多路複用

IO多路複用在Linux下包括了三種,selectpollepoll,抽象來看,他們功能是相似的,但具體細節各有不一樣:首先都會對一組文件描述符進行相關事件的註冊,而後阻塞等待某些事件的發生或等待超時。更多細節詳見下面的 "具體怎麼用"。IO多路複用均可以關注多個文件描述符,但對於這三種機制而言,不一樣數量級文件描述符對性能的影響是不一樣的,下面會詳細介紹。

信號驅動IO

信號驅動IO是利用信號機制,讓內核告知應用程序文件描述符的相關事件。這裏有一個信號驅動IO相關的例子

但信號驅動IO在網絡編程的時候一般不多用到,由於在網絡環境中,和socket相關的讀寫事件太多了,好比下面的事件都會致使SIGIO信號的產生:

  1. TCP鏈接創建
  2. 一方斷開TCP鏈接請求
  3. 斷開TCP鏈接請求完成
  4. TCP鏈接半關閉
  5. 數據到達TCP socket
  6. 數據已經發送出去(如:寫buffer有空餘空間)

上面全部的這些都會產生SIGIO信號,但咱們沒辦法在SIGIO對應的信號處理函數中區分上述不一樣的事件,SIGIO只應該在IO事件單一狀況下使用,好比說用來監聽端口的socket,由於只有客戶端發起新鏈接的時候纔會產生SIGIO信號。

異步IO

異步IO和信號驅動IO差很少,但它比信號驅動IO能夠多作一步:相比信號驅動IO須要在程序中完成數據從用戶態到內核態(或反方向)的拷貝,異步IO能夠把拷貝這一步也幫咱們完成以後才通知應用程序。咱們使用 aio_read 來讀,aio_write 寫。

同步IO vs 異步IO

  1. 同步IO指的是程序會一直阻塞到IO操做如read、write完成
  2. 異步IO指的是IO操做不會阻塞當前程序的繼續執行

因此根據這個定義,上面阻塞IO固然算是同步的IO,非阻塞IO也是同步IO,由於當文件操做符可用時咱們仍是須要阻塞的讀或寫,同理IO多路複用和信號驅動IO也是同步IO,只有異步IO是徹底完成了數據的拷貝以後才通知程序進行處理,沒有阻塞的數據讀寫過程。

3. 目前有哪些IO多路複用的方案

解決方案總覽

os 解決方案
Linux select、poll、epoll
MacOS/FreeBSD kqueue
Windows/Solaris IOCP

常見軟件的IO多路複用方案

軟件 解決方案
redis Linux下 epoll(level-triggered),沒有epoll用select
nginx Linux下 epoll(edge-triggered),沒有epoll用select

4. 具體怎麼用

我在工做中接觸的都是Linux系統的服務器,因此在這裏只介紹Linux系統的解決方案

select

相關函數定義以下

/* According to POSIX.1-2001, POSIX.1-2008 */
    #include <sys/select.h>

    /* According to earlier standards */
    #include <sys/time.h>
    #include <sys/types.h>
    #include <unistd.h>

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

    int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask);

    void FD_CLR(int fd, fd_set *set);
    int FD_ISSET(int fd, fd_set *set);
    void FD_SET(int fd, fd_set *set);
    void FD_ZERO(fd_set *set);

複製代碼

select的調用會阻塞到有文件描述符能夠進行IO操做或被信號打斷或者超時纔會返回。

select將監聽的文件描述符分爲三組,每一組監聽不一樣的須要進行的IO操做。readfds是須要進行讀操做的文件描述符,writefds是須要進行寫操做的文件描述符,exceptfds是須要進行異常事件處理的文件描述符。這三個參數能夠用NULL來表示對應的事件不須要監聽。

當select返回時,每組文件描述符會被select過濾,只留下能夠進行對應IO操做的文件描述符。

FD_xx系列的函數是用來操做文件描述符組和文件描述符的關係。

FD_ZERO用來清空文件描述符組。每次調用select前都須要清空一次。

fd_set writefds;
    FD_ZERO(&writefds)
複製代碼

FD_SET添加一個文件描述符到組中,FD_CLR對應將一個文件描述符移出組中

FD_SET(fd, &writefds);
    FD_CLR(fd, &writefds);
複製代碼

FD_ISSET檢測一個文件描述符是否在組中,咱們用這個來檢測一次select調用以後有哪些文件描述符能夠進行IO操做

if (FD_ISSET(fd, &readfds)){
        /* fd可讀 */
    }
複製代碼

select可同時監聽的文件描述符數量是經過FS_SETSIZE來限制的,在Linux系統中,該值爲1024,固然咱們能夠增大這個值,但隨着監聽的文件描述符數量增長,select的效率會下降,咱們會在『不一樣IO多路複用方案優缺點』一節中展開。

pselect和select大致上是同樣的,但有一些細節上的區別

打開連接查看完整的使用select的例子

poll

相關函數定義

#include <poll.h>

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

    #include <signal.h>
    #include <poll.h>

    int ppoll(struct pollfd *fds, nfds_t nfds, const struct timespec *tmo_p, const sigset_t *sigmask);

    struct pollfd {
        int fd; /* file descriptor */
        short events; /* requested events to watch */
        short revents; /* returned events witnessed */
    };
複製代碼

和select用三組文件描述符不一樣的是,poll只有一個pollfd數組,數組中的每一個元素都表示一個須要監聽IO操做事件的文件描述符。events參數是咱們須要關心的事件,revents是全部內核監測到的事件。合法的事件能夠參考這裏

打開連接查看完整的使用poll的例子

epoll

相關函數定義以下

#include <sys/epoll.h>

    int epoll_create(int size);
    int epoll_create1(int flags);

    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_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout, const sigset_t *sigmask);
複製代碼

epoll_create&epoll_create1用於建立一個epoll實例,而epoll_ctl用於往epoll實例中增刪改要監測的文件描述符,epoll_wait則用於阻塞的等待能夠執行IO操做的文件描述符直到超時。

打開連接查看完整的使用epoll的例子

level-triggered and edge-triggered

這兩種底層的事件通知機制一般被稱爲水平觸發和邊沿觸發,真是翻譯的詞不達意,若是我來翻譯,我會翻譯成:狀態持續通知和狀態變化通知。

這兩個概念來自電路,triggered表明電路激活,也就是有事件通知給程序,level-triggered表示只要有IO操做能夠進行好比某個文件描述符有數據可讀,每次調用epoll_wait都會返回以通知程序能夠進行IO操做,edge-triggered表示只有在文件描述符狀態發生變化時,調用epoll_wait纔會返回,若是第一次沒有所有讀完該文件描述符的數據並且沒有新數據寫入,再次調用epoll_wait都不會有通知給到程序,由於文件描述符的狀態沒有變化。

select和poll都是狀態持續通知的機制,且不可改變,只要文件描述符中有IO操做能夠進行,那麼select和poll都會返回以通知程序。而epoll兩種通知機制可選。

狀態變化通知(edge-triggered)模式下的epoll

在epoll狀態變化通知機制下,有一些的特殊的地方須要注意。考慮下面這個例子

  1. 服務端文件描述符rfd表明要執行read操做的TCP socket,rfd已被註冊到一個epoll實例中
  2. 客戶端向rfd寫了2kb數據
  3. 服務端調用epoll_wait返回,rfd可執行read操做
  4. 服務端從rfd中讀取了1kb數據
  5. 服務端又調用了一次epoll_wait

在第5步的epoll_wait調用不會返回,而對應的客戶端會由於服務端沒有返回對應的response而超時重試,緣由就是我上面所說的,epoll_wait只會在狀態變化時纔會通知程序進行處理。第3步epoll_wait會返回,是由於客戶端寫了數據,致使rfd狀態被改變了,第3步的epoll_wait已經消費了這個事件,因此第5步的epoll_wait不會返回。

咱們須要配合非阻塞IO來解決上面的問題:

  1. 對須要監聽的文件描述符加上非阻塞IO標識
  2. 只在read或者write返回EAGAIN或EWOULDBLOCK錯誤時,才調用epoll_wait等待下次狀態改變發生

經過上述方式,咱們能夠確保每次epoll_wait返回以後,咱們的文件描述符中沒有讀到一半或寫到一半的數據。

5. 不一樣IO多路複用方案優缺點

poll vs select

poll和select基本上是同樣的,poll相比select好在以下幾點:

  1. poll傳參對用戶更友好。好比不須要和select同樣計算不少奇怪的參數好比nfds(值最大的文件描述符+1),再好比不須要分開三組傳入參數。
  2. poll會比select性能稍好些,由於select是每一個bit位都檢測,假設有個值爲1000的文件描述符,select會從第一位開始檢測一直到第1000個bit位。但poll檢測的是一個數組。
  3. select的時間參數在返回的時候各個系統的處理方式不統一,若是但願程序可移植性更好,須要每次調用select都初始化時間參數。

而select比poll好在下面幾點

  1. 支持select的系統更多,兼容更強大,有一些unix系統不支持poll
  2. select提供精度更高(到microsecond)的超時時間,而poll只提供到毫秒的精度。

但整體而言 select和poll基本一致。

epoll vs poll&select

epoll優於select&poll在下面幾點:

  1. 在須要同時監聽的文件描述符數量增長時,select&poll是O(N)的複雜度,epoll是O(1),在N很小的狀況下,差距不會特別大,但若是N很大的前提下,一次O(N)的循環可要比O(1)慢不少,因此高性能的網絡服務器都會選擇epoll進行IO多路複用。
  2. epoll內部用一個文件描述符掛載須要監聽的文件描述符,這個epoll的文件描述符能夠在多個線程/進程共享,因此epoll的使用場景要比select&poll要多。

總結

本文從使用者的角度,從問題出發,介紹了多種IO多路複用方案,有任何問題歡迎在下方留言交流,或掃描二維碼/微信搜索『技術成長之道』關注公衆號後留言私信。

PS:代碼永遠是最正確的,man文檔其次,更多細節能夠多看代碼和文檔。

參考

  1. Linux 系統編程(第二版)
  2. UNIX網絡編程 : 第1卷:套接口API(第3版)
相關文章
相關標籤/搜索