淺談 non-blocking I/O Multiplexing + poll/epoll 的正確使用
在前面的文章中曾經粗略講過poll,那時是用阻塞IO實現,在傳送和接收資料量都較小情況下和網路狀況良好的情況下是基本沒有問題的,read 不會只接收部分資料,write 也不會一直阻塞。但實際上poll IO複用經常是跟非阻塞IO一起使用的,想想如果現在核心接收緩衝區一點資料沒有,read 阻塞了,或者核心傳送緩衝區不夠空間存放資料,write 阻塞了,那整個事件迴圈就會延遲響應,比如現在又有一個新連線connect上來了,也不能很快回到迴圈去accept 它。
在前面的文章中也曾粗略講過epoll,使用的是ET 邊沿觸發模式,每次accept 返回需要將conn 設定為非阻塞,ET模式可能存在的問題是有可能只讀取了部分資料,剩下的epoll_wait 就再也不會返回可讀事件了。
這篇文章來談談如何正確使用non-blocking I/O Multiplexing + poll/epoll。
1、首先來回顧下poll / epoll 函式的原型
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
int epoll_create(int size); //size 並不代表能夠容納的事件個數
int epoll_create1(int flags); // EPOLL_CLOEXEC
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);
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 */
};
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <sys/socket.h>
int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);
SOCK_CLOEXEC Set the close-on-exec (FD_CLOEXEC) flag on the new file descriptor. See the description of the O_CLOEXEC flag in open(2) for reasons why this may be useful.
注意,這兩個標誌是設定accept 回來的conn 標誌的,當然也可以使用fcntl (F_SETFL / F_SETFD) 設定,但少了兩次系統呼叫,可以稍微提高點效能。
7、poll 的處理流程和存在的問題
存在的問題和解決辦法:
(1)、read 可能一次並沒有把connfd 所對應的接收緩衝區(核心)的資料都讀完(粘包問題),那麼connfd 下次仍然是活躍的
應該把讀到的資料儲存在connfd 的應用層接收緩衝區,每次都追加在末尾。需要處理協議以區分每條訊息的邊界
(2)、write 可能一次並不能把所有資料都寫到傳送緩衝區(核心),所以應該有一個應用層傳送緩衝區,將未傳送完的資料新增到應用層傳送緩衝區,關注connfd 的POLLOUT 事件。POLLOUT事件到來,則取出應用層傳送緩衝區資料傳送write,如果應用層傳送緩衝區資料傳送完畢,則取消關注POLLOUT事件。
POLLOUT 事件觸發條件:connfd的傳送緩衝區(核心)不滿(可以容納資料)
注:connfd 的接收緩衝區(核心)資料被接收後會被清空,當發出資料段後接收到對方的ACK段後,傳送緩衝區(核心)資料段會被清空。write只是將應用層傳送緩衝區資料拷貝到connfd 對應的核心傳送緩衝區就返回;read 只是從connfd對應的核心接收緩衝區資料拷貝到應用層接收緩衝區就返回。
9、epoll 的兩種模式處理流程和存在的問題
核心中的某個socket接收緩衝區 為空 低電平核心中的某個socket接收緩衝區 不為空 高電平
核心中的某個socket傳送緩衝區 不滿 高電平核心中的某個socket傳送緩衝區 滿 低電平
Edge-Triggered:
與poll相容LT模式不會發生漏掉事件的BUG,但POLLOUT事件不能一開始就關注,否則會出現busy loop(即暫時還沒有資料需要寫入,但一旦連線建立,核心傳送緩衝區為空會一直觸發POLLOUT事件),而應該在write無法完全寫入核心緩衝區的時候才關注,將未寫入核心緩衝區的資料新增到應用層output buffer,直到應用層output buffer寫完,停止關注POLLOUT事件。讀寫的時候不必等候EAGAIN,可以節省系統呼叫次數,降低延遲。(注:如果用ET模式,讀的時候讀到EAGAIN,寫的時候直到output buffer寫完或者寫到EAGAIN)
注:在使用 ET 模式時,可以寫得更嚴謹,即將 listenfd 設定為非阻塞,如果accpet 呼叫有返回,除了建立當前這個連線外,不能馬上就回到 epoll_wait ,還需要繼續迴圈accpet,直到返回-1 且errno == EAGAIN 才退出。程式碼示例如下:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
if(ev.events & EPOLLIN)
{ do { struct sockaddr_in stSockAddr; socklen_t iSockAddrSize = sizeof(sockaddr_in); int iRetCode = accept(listenfd, (struct sockaddr *) &stSockAddr, iSockAddrSize); if (iRetCode > 0) { // ...建立連線 // 新增事件關注 } else { //直到發生EAGAIN才不繼續accept if(errno == EAGAIN) { break; } } } while(true); // ... 其他 EPOLLIN 事件 } |
10、accept(2)返回EMFILE的處理(檔案描述符已經用完)
(1)、調高程式檔案描述符數目
(2)、死等
(3)、退出程式
(4)、關閉監聽套接字。那什麼時候重新開啟呢?
(5)、如果是epoll模型,可以改用edge trigger。問題是如果漏掉了一次accept(2),程式再也不會收到新連線(沒有狀態變化)
(6)、準備一個空閒的檔案描述符。遇到這種情況,先關閉這個空閒檔案,獲得一個檔案描述符名額;再accept(2)拿到socket連線的檔案描述符;隨後立刻close(2),這樣就優雅地斷開了與客戶端的連線;最後重新開啟空閒檔案,把“坑”填上,以備再次出現這種情況時使用。
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
int idlefd = open("/dev/null", O_RDONLY | O_CLOEXEC);
connfd = accept4(listenfd, (struct sockaddr *)&peeraddr, &peerlen, SOCK_NONBLOCK | SOCK_CLOEXEC); /* if (connfd == -1) ERR_EXIT("accept4"); */ if (connfd == -1) { if (errno == EMFILE) { close(idlefd); idlefd = accept(listenfd, NULL, NULL); close(idlefd); idlefd = open("/dev/null", O_RDONLY | O_CLOEXEC); continue; } else ERR_EXIT("accept4"); } |
相關文章
- I/O Mutiplexing poll 和 epoll
- 伺服器程式設計——I/O複用(select、poll、epoll)伺服器程式設計
- I/O多路複用技術(multiplexing)
- 流?I/O 操作?阻塞?epoll?
- Epoll多路I/O複用技術
- Epoll程式設計-I/O多路複用程式設計
- IO模式 select、poll、epoll模式
- IO多路複用——深入淺出理解select、poll、epoll的實現
- select、poll、epoll之間的區別
- java 淺析I/O模型Java模型
- 聊聊select, poll 和 epoll_waitAI
- 淺談Elasticsearch的AAA (I)Elasticsearch
- Java NIO:淺析I/O模型Java模型
- java的nio之:淺析I/O模型Java模型
- 談如何正確理解 IP 資料的覆蓋率,兼談正確率~
- select、poll、epoll之間的區別總結[整理]
- [譯]如何在 Google I/O 2018 以正確的姿勢觀看 FlutterGoFlutter
- 淺談正則的基礎
- Linux IO模式及 select、poll、epoll詳解Linux模式
- 【雜談】Java I/O的底層實現Java
- linux系統下poll和epoll核心原始碼剖析Linux原始碼
- PHP Opcache 的正確使用PHPopcache
- 淺談js中的正規表示式JS
- python淺談正則的常用方法Python
- Veritas Quick I/O and Cached Quick I/OUI
- 淺談JavaScript正規表示式JavaScript
- java的I/OJava
- 深度解讀 | 透過FD耗盡實驗談談使用HttpClient的正確姿勢HTTPclient
- Redis的正確使用姿勢Redis
- 使用正確的工具(轉載)
- 正確高效使用 GoogleGo
- 正確使用rman crosscheckROS
- 計算機I/O與I/O模型計算機模型
- I/O埠和I/O記憶體記憶體
- 淺談程式設計正規化程式設計
- 談談對不同I/O模型的理解 (阻塞/非阻塞IO,同步/非同步IO)模型非同步
- 淺談如何確定伺服器的數量伺服器
- 淺談系統的不確定性與穩定性