本文首發在 技術成長之道 博客,訪問 hechen0.com 查看更多,或者微信搜索「技術成長之道」關注個人公衆號,或者掃描下方二維碼關注公衆號得到第一時間更新通知!html
一句話解釋:單線程或單進程同時監測若干個文件描述符是否能夠執行IO操做的能力。linux
應用程序一般須要處理來自多條事件流中的事件,好比我如今用的電腦,須要同時處理鍵盤鼠標的輸入、中斷信號等等事件,再好比web服務器如nginx,須要同時處理來來自N個客戶端的事件。nginx
邏輯控制流在時間上的重疊叫作 併發git
而CPU單核在同一時刻只能作一件事情,一種解決辦法是對CPU進行時分複用(多個事件流將CPU切割成多個時間片,不一樣事件流的時間片交替進行)。在計算機系統中,咱們用線程或者進程來表示一條執行流,經過不一樣的線程或進程在操做系統內部的調度,來作到對CPU處理的時分複用。這樣多個事件流就能夠併發進行,不須要一個等待另外一個過久,在用戶看起來他們彷佛就是並行在作同樣。github
但凡事都是有成本的。線程/進程也同樣,有這麼幾個方面:web
有沒有一種能夠在單線程/進程中處理多個事件流的方法呢?一種答案就是IO多路複用。redis
所以IO多路複用解決的本質問題是在用更少的資源完成更多的事。編程
爲了更全面的理解,先介紹下在Linux系統下全部IO模型。數組
目前Linux系統中提供了5種IO處理模型服務器
這是最經常使用的簡單的IO模型。阻塞IO意味着當咱們發起一次IO操做後一直等待成功或失敗以後才返回,在這期間程序不能作其它的事情。阻塞IO操做只能對單個文件描述符進行操做,詳見read或write。
咱們在發起IO時,經過對文件描述符設置O_NONBLOCK flag來指定該文件描述符的IO操做爲非阻塞。非阻塞IO一般發生在一個for循環當中,由於每次進行IO操做時要麼IO操做成功,要麼當IO操做會阻塞時返回錯誤EWOULDBLOCK/EAGAIN,而後再根據須要進行下一次的for循環操做,這種相似輪詢的方式會浪費不少沒必要要的CPU資源,是一種糟糕的設計。和阻塞IO同樣,非阻塞IO也是經過調用read或writewrite來進行操做的,也只能對單個描述符進行操做。
IO多路複用在Linux下包括了三種,select、poll、epoll,抽象來看,他們功能是相似的,但具體細節各有不一樣:首先都會對一組文件描述符進行相關事件的註冊,而後阻塞等待某些事件的發生或等待超時。更多細節詳見下面的 "具體怎麼用"。IO多路複用均可以關注多個文件描述符,但對於這三種機制而言,不一樣數量級文件描述符對性能的影響是不一樣的,下面會詳細介紹。
信號驅動IO是利用信號機制,讓內核告知應用程序文件描述符的相關事件。這裏有一個信號驅動IO相關的例子。
但信號驅動IO在網絡編程的時候一般不多用到,由於在網絡環境中,和socket相關的讀寫事件太多了,好比下面的事件都會致使SIGIO信號的產生:
上面全部的這些都會產生SIGIO信號,但咱們沒辦法在SIGIO對應的信號處理函數中區分上述不一樣的事件,SIGIO只應該在IO事件單一狀況下使用,好比說用來監聽端口的socket,由於只有客戶端發起新鏈接的時候纔會產生SIGIO信號。
異步IO和信號驅動IO差很少,但它比信號驅動IO能夠多作一步:相比信號驅動IO須要在程序中完成數據從用戶態到內核態(或反方向)的拷貝,異步IO能夠把拷貝這一步也幫咱們完成以後才通知應用程序。咱們使用 aio_read 來讀,aio_write 寫。
同步IO vs 異步IO
- 同步IO指的是程序會一直阻塞到IO操做如read、write完成
- 異步IO指的是IO操做不會阻塞當前程序的繼續執行
因此根據這個定義,上面阻塞IO固然算是同步的IO,非阻塞IO也是同步IO,由於當文件操做符可用時咱們仍是須要阻塞的讀或寫,同理IO多路複用和信號驅動IO也是同步IO,只有異步IO是徹底完成了數據的拷貝以後才通知程序進行處理,沒有阻塞的數據讀寫過程。
os | 解決方案 |
---|---|
Linux | select、poll、epoll |
MacOS/FreeBSD | kqueue |
Windows/Solaris | IOCP |
軟件 | 解決方案 |
---|---|
redis | Linux下 epoll(level-triggered),沒有epoll用select |
nginx | Linux下 epoll(edge-triggered),沒有epoll用select |
我在工做中接觸的都是Linux系統的服務器,因此在這裏只介紹Linux系統的解決方案
相關函數定義以下
/* 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大致上是同樣的,但有一些細節上的區別。
相關函數定義
#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是全部內核監測到的事件。合法的事件能夠參考這裏。
相關函數定義以下
#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操做的文件描述符直到超時。
這兩種底層的事件通知機制一般被稱爲水平觸發和邊沿觸發,真是翻譯的詞不達意,若是我來翻譯,我會翻譯成:狀態持續通知和狀態變化通知。
這兩個概念來自電路,triggered表明電路激活,也就是有事件通知給程序,level-triggered表示只要有IO操做能夠進行好比某個文件描述符有數據可讀,每次調用epoll_wait都會返回以通知程序能夠進行IO操做,edge-triggered表示只有在文件描述符狀態發生變化時,調用epoll_wait纔會返回,若是第一次沒有所有讀完該文件描述符的數據並且沒有新數據寫入,再次調用epoll_wait都不會有通知給到程序,由於文件描述符的狀態沒有變化。
select和poll都是狀態持續通知的機制,且不可改變,只要文件描述符中有IO操做能夠進行,那麼select和poll都會返回以通知程序。而epoll兩種通知機制可選。
在epoll狀態變化通知機制下,有一些的特殊的地方須要注意。考慮下面這個例子
在第5步的epoll_wait調用不會返回,而對應的客戶端會由於服務端沒有返回對應的response而超時重試,緣由就是我上面所說的,epoll_wait只會在狀態變化時纔會通知程序進行處理。第3步epoll_wait會返回,是由於客戶端寫了數據,致使rfd狀態被改變了,第3步的epoll_wait已經消費了這個事件,因此第5步的epoll_wait不會返回。
咱們須要配合非阻塞IO來解決上面的問題:
經過上述方式,咱們能夠確保每次epoll_wait返回以後,咱們的文件描述符中沒有讀到一半或寫到一半的數據。
poll和select基本上是同樣的,poll相比select好在以下幾點:
而select比poll好在下面幾點
但整體而言 select和poll基本一致。
epoll優於select&poll在下面幾點:
本文從使用者的角度,從問題出發,介紹了多種IO多路複用方案,有任何問題歡迎在下方留言交流,或掃描二維碼/微信搜索『技術成長之道』關注公衆號後留言私信。
PS:代碼永遠是最正確的,man文檔其次,更多細節能夠多看代碼和文檔。
參考