咱們知道,計算機的硬件資源由操做系統管理、調度,咱們的應用程序運行在操做系統之上,咱們的程序運行須要訪問計算機上的資源(如讀取文件,接收網絡請求),操做系統有內核空間和用戶空間之分,因此數據讀取,先由內核讀取數據到內核緩衝區,而後纔會從操做系統的內核空間拷貝到用戶空間,這個就是緩存I/O,又被稱做標準I/O。html
幾種常見的IO模式:阻塞I/O、非阻塞I/O、I/O多路複用node
一、阻塞I/Olinux
用戶進程向內核發起I/O系統調用,內核去準備所需的數據,直到數據都準備好了(須要一段時間)返回給用戶進程,在這期間,用戶進程一直處於阻塞狀態,拿到所需數據,纔會繼續向下執行。git
二、非阻塞I/Ogithub
用戶進程向內核發起I/O系統調用,內核發現數據還沒準備好,當即返回error,用戶進程拿到error,能夠再次向內核發起請求,直到獲取所需數據web
三、I/O多路複用api
下面詳細介紹數組
定義:I/O multiplexing allows us to simultaneously monitor multiple file descriptors to see if I/O is possible on any of them.緩存
傳統的阻塞I/O模型能夠知足大部分的應用程序使用場景,但有的時候,一些應用程序會同時須要以下特性:服務器
好比一個web服務器,可能同時打開了幾千個鏈接,每accept一個鏈接,操做系統產生一個fd(文件描述符),咱們的服務器須要監聽這些文件描述符,當客戶端發來新的數據的時候,咱們須要處理請求數據,並給出響應,正常的實現方式可能以下:
connections = [fd1, fd2, fdn]; for (c in connections) { if (hasNewInput(x)) { processInput(x); } }
這樣實現的問題是,咱們本身維護着全部的鏈接,須要主動的去輪尋判斷I/O是否已經ready以便進行讀寫,但事實上並非全部鏈接都是活躍狀態(創建的鏈接並無數據交互),若是咱們輪尋的頻率很低,那用戶獲取響應的時間可能長的沒法忍受,若是輪尋的頻率很是的高,則會浪費CPU的時間。
因此若是能夠將主動遍歷全部鏈接,判斷每個的IO狀態,改成將這些文件描述符(fd)交給內核去監視,而後當一個或多個文件描述符IO ready的時候,內核來告訴咱們,這樣效率就大大提升了,所以咱們須要IO多路複用。
IO多路複用的實現比較廣泛的有兩個:select和poll,相比較poll,select更爲廣泛,最先與BSD socket api一塊兒出現,已被歸入SUSV3標準規範,epoll是隻屬於Linux的特性,最先的API出如今Linux2.6。
名字叫I/O多路複用,所謂的複用,複用的是同一個進程(線程),也就是在同一個進程中「併發「的完成多個文件描述符的I/O。
2.一、select
select系統調用會阻塞,直到一個或多個文件描述符I/O ready
1>定義:
#include <sys/time.h> /* For portability */ #include <sys/select.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
2>參數:
3>返回值:
4>小示例
#include <stdio.h> #include <string.h> #include <sys/time.h> #include <sys/select.h> int main(int argc, char *argv[]) { fd_set readfds, writefds; int ready, nfds, fd, numRead, j; struct timeval timeout; struct timeval *pto; char buf[10]; //判斷參數是否包含短斜線,包含timeout設置NULL if (strcmp(argv[1], "-") == 0) { pto = NULL; } else { pto = &timeout; pto.tv_sec = atoi(argv[1]); pto.tv_usec = 0; } //準備監聽文件描述符集合 nfds = 0; //用select 提供的宏初始化fd_set FD_ZERO(&readfds); FD_ZERO(&writefds); for (j = 2; j < argc; j++) { numRead = sscanf(argv[j], "%d%2[rw]", &fd, buf); if (numRead != 2) { printf("error"); return 0; } if (fd >= nfds) { nfds += 1; } //判斷是監聽讀仍是寫 if (strchr(buf, 'r') != NULL) { FD_SET(fd, &readfds); } if (strchr(buf, 'w') != NULL) { FD_SET(fd, &writefds); } } //調用select函數 ready = select(nfds, &readfds, &writefds, NULL, pto); if (ready == -1) { printf("select error\n"); return 0; } for (fd = 0; fd < nfds; fd++) { //打印ready 狀態 printf("%d: %s%s\n", fd, FD_ISSET(fd, &readfds) ? "r" : "", FD_ISSET(fd, &writefds) ? "w" : ""); } return 0; }
使用文件描述符0 也就是標準輸入測試,首先編譯一下這段代碼gcc -o select select.c
運行./select 10 0r 表明select阻塞最多10秒超時返回,監聽標準輸入的 讀ready狀態,會發現程序處於阻塞狀態,直到10秒後select返回,打印 0:,也就是沒有ready
運行./select - 0r 表明select不會超時直到 標準輸入 讀ready,程序會一直掛起,直到咱們敲回車,返回0:r
運行./select 5 0r 1w 表明最多5秒超時,同時監聽標準輸入的讀狀態,和標準輸出的寫狀態,返回0: 1:w
2.二、poll
poll跟select機制基本同樣,不一樣點在於select將監聽的文件描述符分開到3個集合中,poll只需一個文件描述符列表
1>定義
#include <poll.h> int poll(struct pollfd fds[], nfds_t nfds, int timeout);
2>參數
struct pollfd { int fd; /* File descriptor */ short events; /* Requested events bit mask */ short revents; /* Returned events bit mask */ };
3>返回值
一、每次調用poll()或select(),內核必須檢查傳過來的全部文件描述符,當文件描述符超過必定數量後,光是檢查這一步就已經很是耗時了
二、每次調用poll()或select(),須要從用戶態初始化文件描述符的數據結構,而後傳遞到內核態,在內核態檢查IO狀態,然以將狀態更新到文件描述符數據結構,再返回這個數據結構,數據需不斷的在用戶態和內核態間拷貝,一樣當文件描述符數量很大時,很是耗費CPU時間
三、每次調用完poll()或select(),還要遍歷返回的全部文件描述符,判斷狀態,浪費時間
解決以上問題的關鍵是:1)不要每次都在內核態和用戶態複製這些監聽的文件描述符數據 2)只返回IO ready 的文件描述符,不要只標識狀態,本身還須要再遍歷一遍,所以,Linux給了咱們epoll。
Linux的epoll,也是I/O多路複用的一種實現方式,一樣是同時監聽多個文件描述符的I/O狀態,他有以下幾個優點:
不像select和poll直接就是系統調用的函數,epoll由3個系統調用函數組成
1.一、epoll_create
經過調用epoll_create,建立一個新的epoll內核數據結構實例,返回該實例的文件描述符,而後,調用進程可使用這個文件描述符向epoll實例添加、刪除或修改它想要監視的其餘文件描述符。
#include <sys/epoll.h> int epoll_create(int size);
1.二、epoll_ctl
進程能夠經過調用epoll_ctl向epoll實例添加它但願監視的文件描述符,全部這些文件描述符由epoll實例維護在interest list中。當被監視的文件描述符爲I/O作好準備時,他們就進入到ready list,ready list是interest list的一個子集
定義:
#include <sys/epoll.h> int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
參數:
epoll_event結構:
struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ };
events:
事件的成員是位掩碼,好比fd是一個socket,咱們可能但願監視他,以獲取套接字緩衝區上的新數據(使用EPOLLIN),若是但願事件的通知機制使用邊緣觸發的方式,可使用EPOLLET。也就是讀操做就緒,使用邊緣觸發的通知方式,須要這樣指定events:EPOLLIN | EPOLLET
全部能夠指定的events見http://man7.org/linux/man-pages/man2/epoll_ctl.2.html
data:
epoll_data_t 是一個union,能夠存儲一些數據,當該文件描述符ready的時候,返回給調用監聽的進程
typedef union epoll_data { void *ptr; /* Pointer to user-defined data */ int fd; /* File descriptor */ uint32_t u32; /* 32-bit integer */ uint64_t u64; /* 64-bit integer */ } epoll_data_t;
返回值:返回0執行成功,-1發生錯誤
1.三、epoll_wait
經過調用epoll_wait,能夠獲取以前監聽的全部事件(interest list)中I/O準備好的事件(ready list)
定義:
#include <sys/epoll.h> int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);
參數:
2.一、先弄清文件描述符和打開文件的關係
爲了弄清楚打開文件、文件描述符和epoll之間的關係,咱們先看一下Linux操做系統中文件描述符(file descriptors)、打開文件描述(open file description)、和系統級文件inode表之間的關係,如圖所示:
內核維護了3個數據結構:
圖中能夠看出:
2.二、再看epoll_create
當調用了epoll_create,事實上內核在inode-table建立了一個新的inode(也就是epoll實例),以及在file description table中添加一條打開文件描述記錄,而且返回給調用者一個文件描述符。
接下來咱們就可使用這個文件描述符,向epoll實例中添加須要監聽的文件描述符,當調用epoll_ctl添加一個文件描述符到epoll實例的監聽列表(interest list)的時候,事實上真正添加的不是文件描述符,而是其對應的文件描述(file description table)。基於這一點,結合上面文件描述符和打開文件關係的圖:
假如進程A調用epoll_create,返回文件描述符fd8。
2.三、epoll爲何性能高於poll和select?
回顧上述poll()和select()存在的問題,能夠看到epoll恰好解決了這些問題:
監聽不一樣數量文件描述符,執行100000次狀態檢查select、poll、epoll性能對比:
3.一、文件描述符準備就緒通知的兩種模型:
3.二、不一樣的通知模型如何影響咱們的程序設計?
水平觸發
邊緣觸發:
注:對於邊緣觸發,當觸發通知時,咱們並不知道有多少IO可用(例若有多少字節能夠讀),因此通常使用邊緣觸發通知方式要遵循以下規則:
- 接收到事件通知後,程序應該儘量多的執行IO(讀寫),由於僅僅通知這一次,不讀取完畢,數據可能就丟失了
- 爲了不IO阻塞,每一個被監視的文件描述符應該是非阻塞模式打開的,而後收到事件通知後,重複的執行IO直到返回錯誤信息
接下來的聊天室的實例,咱們使用邊緣觸發的方式
瞭解完epoll的基礎和原理,使用一個精簡版的聊天室的小實例來鞏固學習。
主要思路:一個服務端,能夠接收多個客戶端的鏈接,客戶端發送的消息會同步到全部聊天室內的客戶端
第一步:服務端建立socket服務
第二步:建立epoll實例,將socket服務fd加入interest list
第三步:循環調用epoll_wait看是否有ready的文件描述符,若是有而且是咱們的socket服務fd,說明是有新的客戶端鏈接加入,保存客戶端fd,並將fd加入interest list;若是非socket 服務fd,說明是客戶端有新消息發送至服務端,接收消息而後廣播給保存的全部客戶端fd
第一步:建立socket鏈接服務端
第二步:建立管道,以便獲取客戶端輸入
第三步:建立epoll實例,並把socket fd和管道fd加入interest list
第四步:fork子進程
第五步:父進程調用epoll_wait獲取ready list,若是fd是服務端sockt,則直接打印廣播消息;若是fd是管道則爲用戶輸入,讀取管道中的消息,發到服務端
運行如圖:
源碼放到了Githubhttps://github.com/bigbignerd/epollchat
一、《The linux programming interface》
二、Medium: The method to epoll's madness
若有表述錯誤之處,歡迎指正!